2openGL shader shader analysis & triangle filling

Source code is below. Document query > docs.gl

Result display: use your own shader and print error description

This article mainly adds a shader written by myself based on the previous part of the code, that is, a shader. The two most commonly used shaders are vertex shader and fragment shader, namely vertex shader and fragment shader.

Get a rough idea:

  • A shader is just a program
  • If you want to draw a triangle with three vertices, then the vertex shader is called three times
  • If you want to color the triangle, that is, rasterize it, then the fragment shader will be called many times, thousands of times, once for each pixel.
  • openGL is a state machine, just like there are many switches. It does not mean that as soon as you change the code, it will be executed immediately and affect the subsequent code.

The added shader is mainly implemented through the function CreateShader. This time, we mainly improve our analysis capabilities by checking the documentation.

/*For convenience, write it as a function*/
static unsigned int CompileShader(unsigned int type, const std::string & amp; source) {<!-- -->
    unsigned int id = glCreateShader(type);
    const char* src = source.c_str(); /*or write & amp;source[0]*/
    glShaderSource(id, 1, & amp;src, nullptr);
    glCompileShader(id);

    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, & amp;result);
    if (result == GL_FALSE) {<!-- -->
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, & amp;length);
        // char message[length]; /*Here you will find that stack allocation cannot be done because of the variable length, but you still have to do it*/
        char* message = (char*)alloca(length * sizeof(char));
        glGetShaderInfoLog(id, length, & length, message);
        std::cout << "Failed to compile " <<
            (type == GL_VERTEX_SHADER ? "vertex":"fragment" )<< "shader! Please locate this line" << std::endl;
        std::cout << message << std::endl;
        glDeleteShader(id);
        return 0;
    }

    return id;
}

/* Use static because you don’t want it to leak to other translation units?
Using string is not the best choice, but it is relatively safe. Int type - the unique identifier of the shader, an ID*/
static unsigned int CreateShader(const std::string & amp; vertexShader, const std::string & amp; fragmentShader) {<!-- -->
    /*Use unsigned because this is the parameter it accepts,
    Or you can use GLuint, but the author doesn't like this because it uses multiple image api*/
    unsigned int program = glCreateProgram();
    unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);
    glValidateProgram(program);

    glDeleteShader(vs);
    glDeleteShader(fs);

    return program;
}

Start analyzing some code:

1. Let’s look at CreateShader and CompileShader first, both are static

Here is the following explanation on the use of the static keyword:

  • C++ functions are non-static by default, which means they can be called from external functions/files.

  • If defined as static, this function is only visible and used in the compilation unit (in the source file) where it is located, and cannot be accessed externally.

  • In OpenGL, usually a file will contain multiple shader programs, and we need a function to return the ID of a specific program.

  • If static is not added, this function will be shared by all programs and conflicts may occur.

  • After adding static, this function will only be visible to the objects in this file and will not affect other files.

  • In other words, not adding static may cause the function definition to be repeated in other translation units (files), resulting in naming conflicts.

  • Adding static can prevent the function from being exposed to other files. It only serves the purpose of program creation within this file and does not affect other programs.

So in general, static is used to encapsulate functions in this file to avoid potential bugs caused by cross-file calls.

You asked a good question. I just said in my explanation:

“In OpenGL, usually a file will contain multiple shader programs, and we need a function to return the ID of a specific program.”

Here we need to further explain why:

  • In OpenGL, a shader program can contain multiple shaders (vertex shader + fragment shader, etc.).

  • If a file defines multiple such programs, it is necessary to distinguish and return the unique ID of each program.

  • If the function is defined as non-static, when multiple programs call this function at the same time:

    • Because the function name is repeatedly defined, compilation will report an error.

    • Or the program defined later will overwrite the ID saved by the previous program, causing ID confusion.

  • After using static, the functions called by each program are actually copies of different functions.

  • In this way, the ID obtained by each program is the unique identifier corresponding to the shader program itself.

So a non-static definition here may result in:

  • Compile Error
  • ID management error
  • The program does not correctly identify the respective shader

Static can avoid this problem and ensure the definition of the local nature of the function.

2. Analyze functions according to documents, such as glShaderSource

View the documentation: https://docs.gl/gl4/glShaderSource


The four parameters are

Function description:
glShaderSource sets the source code in shader to the source code in the array of strings specified by string. Any source code previously stored in the shader object is completely replaced. The number of strings in the array is specified by count. If length is NULL, each string is assumed to be null terminated. If length is a value other than NULL, it points to an array containing a string length for each of the corresponding elements of string. Each element in the length array may contain the length of the corresponding string (the null character is not counted as part of the string length) or a value less than 0 to indicate that the string is null terminated. The source code strings are not scanned or parsed at this time; they are simply copied into the specified shader object.

It will become clearer after reading this.

The first parameter, GLuint type, determines which shader it is.
You raise an important question.

The shader id in OpenGL (such as the id returned by glCreateShader) may not be unique if not managed.

For example:

  • Multiple shader objects are created during program running, and IDs may overlap and conflict.

  • Shader IDs may also be the same between different programs.

Therefore, in order to ensure the uniqueness of the shader id, some additional processing needs to be done:

  1. Use a static counter to maintain shader ID allocation, and add 1 to each ID allocation.

  2. Bind the id to the shader object and use the object pointer as the unique identifier.

  3. Extract the id generation code into a separate function to control uniqueness.

  4. Design a namespace for programs, and define ids independently for each program.

  5. Use the object-oriented shader class and manage the id as an object attribute.

  6. Wait for other methods.

Even if the OpenGL ids themselves are not unique, we can manage the ids through programming to ensure that they are unique during the running of the program.

This requires considering additional ID management mechanisms instead of simply relying on OpenGL native IDs.

The second parameter counts, the number of shaders. The shader exists in the form of a string. The content of vertexShader is the source code of vertexShader.

The pointer passed in as the third parameter is

 std::string vertexShader =
        "#version 330 core\\
"
        "\\
"
        "layout(location = 0) in vec4 position;"
        "\\
"
        "void main()"
        "{\\
"
        " gl_Position = position;\\
"
        "}\\
";

    std::string fragmentShader =
        "#version 330 core\\
"
        "\\
"
        "layout(location = 0) out vec4 color;"
        "\\
"
        "void main()"
        "{\\
"
        " color = vec4(1.0, 0.0, 0.0, 1.0);\\
"
        "}\\
";

The fourth parameter is an array to calculate the length of each shader.

3. Error printing, just look at the code

glGetShaderInfoLog(id, length, & amp;length, message);

openGL provides the interface.

Source code:

#inclu
de 
#include 

#include 
#include 


 

/*For convenience, write it as a function*/
static unsigned int CompileShader(unsigned int type, const std::string & amp; source) {<!-- -->
    unsigned int id = glCreateShader(type);
    const char* src = source.c_str(); /*or write & amp;source[0]*/
    glShaderSource(id, 1, & amp;src, nullptr);
    glCompileShader(id);

    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, & amp;result);
    if (result == GL_FALSE) {<!-- -->
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, & amp;length);
        // char message[length]; /*Here you will find that stack allocation cannot be done because of the variable length, but you still have to do it*/
        char* message = (char*)alloca(length * sizeof(char));
        glGetShaderInfoLog(id, length, & length, message);
        std::cout << "Failed to compile " <<
            (type == GL_VERTEX_SHADER ? "vertex":"fragment" )<< "shader! Please locate this line" << std::endl;
        std::cout << message << std::endl;
        glDeleteShader(id);
        return 0;
    }

    return id;
}

/* Use static because you don’t want it to leak to other translation units?
Using string is not the best choice, but it is relatively safe. Int type - the unique identifier of the shader, an ID*/
static unsigned int CreateShader(const std::string & amp; vertexShader, const std::string & amp; fragmentShader) {<!-- -->
    /*Use unsigned because this is the parameter it accepts,
    Or you can use GLuint, but the author doesn't like this because it uses multiple image api*/
    unsigned int program = glCreateProgram();
    unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);
    glValidateProgram(program);

    glDeleteShader(vs);
    glDeleteShader(fs);

    return program;
}

int main(void)
{
    GLFWwindow* window;

    /* Initialize the library */
    if (!glfwInit())
        return -1;

    //if (glewInit() != GLEW_OK)/*glew document, an error will be reported here, because context is needed, and the context is behind*/
    // std::cout << "ERROR!-1" << std::endl;

    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);

    if (glewInit() != GLEW_OK)/*No error will be reported here*/
        std::cout << "ERROR!-2" << std::endl;

    std::cout << glGetString(GL_VERSION) << std::endl;

    float positions[6] = {
        -0.5f, 0.5f,
        0.0f, 0.0f,
        0.5f, 0.5f
    };

    /*
    This code creates and initializes the Vertex Buffer Object (VBO for short).

VBO is a very important concept in OpenGL and is used to render vertex data efficiently.

What this code does is:

glGenBuffers generates a new VBO, and the ID is saved in the buffer variable.

glBindBuffer binds this VBO to the GL_ARRAY_BUFFER target.

glBufferData fills the actual vertex data into the bound VBO.

Go through these three steps:

We get a VBO object that can store vertex data

Subsequent draw calls only need to specify this VBO to load vertex data.

The tutorial emphasizes VBO because:

It is more efficient than sending the vertices directly

Draw calls no longer need to send the same vertices repeatedly every frame

Improve rendering performance

So to summarize, VBO can efficiently draw complex vertex data to the graphics card, which is an important concept of OpenGL.



glGenBuffers(1, & amp;buffer);
The function of glGenBuffers is to generate the ID number of the VBO object.

The first parameter 1 indicates the number of VBOs to be generated. Only 1 is generated here.

The second parameter &buffer is used to return the generated VBO ID number.

glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBindBuffer is used to bind the VBO object to the specified target.

The first parameter GL_ARRAY_BUFFER indicates that the target to be bound is the vertex attribute array buffer.

GL_ARRAY_BUFFER specifies that vertex attribute data such as position, color, etc. will be saved.

The second parameter buffer is the VBO ID generated by glGenBuffers previously.

So to summarize:

glGenBuffers generates 1 VBO object and gets the ID number

glBindBuffer binds this VBO to the attribute buffer target as the storage object for subsequent vertex data.




The function of glBufferData is to fill the actual vertex data into the previously bound VBO object.

Parameter Description:

GL_ARRAY_BUFFER: Specifies that the operation target is the vertex attribute buffer (consistent with glBindBuffer)

6 * sizeof(float): data size, here the positions array has 6 float numbers

positions: array pointer, providing the actual data source

GL_STATIC_DRAW: Data usage pattern

GL_STATIC_DRAW: Data will not or rarely change
GL_DYNAMIC_DRAW: Data may be modified
GL_STREAM_DRAW: The data will change every time it is drawn.
Its function is:

Allocate memory of the specified size to the currently bound VBO object

Copy the contents of the positions array to the VBO object memory

In GL_STATIC_DRAW mode, the graphics card knows how to optimally allocate memory

In this way, the vertex data in the positions array is uploaded to the VBO object in the GPU.

OpenGL then reads the vertex data through the VBO object for drawing.

*/
    unsigned int buffer;
    glGenBuffers(1, & amp;buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW);

    glEnableVertexAttribArray(0);
    /*index-There is only one attribute, fill in 0
    size-two numbers represent a point, fill in 2
    stripe - number of bytes between vertices
    pointer-offset




    Okay, let's use an example to explain the meaning of the parameters of glVertexAttribPointer:

Suppose we have a VBO that stores three three-dimensional vertex data. Each vertex consists of (x, y, z), and each element type is float.

Then the data is arranged in the VBO as follows:

VBO address | data
0|x1
4 | y1\
8|z1
12|x2
16|y2
20|z2
24|x3
28|y3
32|z3

Now we need to tell OpenGL how to parse this data:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 12, 0);

- 0: The attribute is location data
- 3: Each position consists of 3 floats, (x, y, z)
- GL_FLOAT: The data type is float
- 12: The interval from the current attribute to the next attribute, that is, a vertex requires 12 bytes
- 0: The starting position of this attribute is the beginning of the VBO

This way OpenGL knows:

- Read 3 floats from the VBO starting address as the position of the first vertex
- Read another 3 floats at an offset of 12 bytes to the next vertex

The last parameter 0 tells OpenGL what the starting reading offset of the attribute is.





    
    Okay, let’s use an example to illustrate this situation in detail:

Suppose we have a VBO to store vertex data. Each vertex contains two attributes: position and color.

The data is arranged inside the VBO as:

position x | position y | position z | color r | color g | color b

So for the first vertex, its layout within the VBO is:

VBO address | data
0 | position x\
4 | position y
8 | position z
12 | color r
16 | color g
20 | color b

At this point, we set the pointers of the position attribute and color attribute:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, 0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, 12);

can be seen:

- The position attribute is read starting from byte 0
- Color attributes are read starting from 12 bytes (to make room for position data)

This is why the offset of the position attribute cannot be written as 0, and a non-zero offset needs to be specified to make room for the color attribute storage space.

In this way, the two separate but co-located VBO data can be correctly parsed. */
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);/* (const void)*8/

    /*Start using shaders here*/
    std::string vertexShader =
        "#version 330 core\\
"
        "\\
"
        "layout(location = 0) in vec4 position;"
        "\\
"
        "void main()"
        "{\\
"
        " gl_Position = position;\\
"
        "}\\
";

    std::string fragmentShader =
        "#version 330 core\\
"
        "\\
"
        "layout(location = 0) out vec4 color;"
        "\\
"
        "void main()"
        "{\\
"
        " color = vec4(1.0, 0.0, 0.0, 1.0);\\
"
        "}\\
";

    unsigned int shader = CreateShader(vertexShader, fragmentShader);
    glUseProgram(shader);

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT);

        glDrawArrays(GL_TRIANGLES, 0, 3);
        // glDrawElements(GL_TRIANGLES, )

    /* glBegin(GL_TRIANGLES);
        glVertex2f(-0.5f, 0.5f);
        glVertex2f(0.0f, 0.0f);
        glVertex2f(0.5f, 0.5f);
        glEnd();*/

        /* Swap front and back buffers */
        glfwSwapBuffers(window);

        /* Poll for and process events */
        glfwPollEvents();
    }

    glDeleteProgram(shader);

    glfwTerminate();
    return 0;
}