Learn code address OpenGL(ES) Learn one: Prepare OpenGL(ES) Learn two: Draw a triangle

Just like learning to program Hello World, drawing a triangle is almost a must.

The rendering pipeline

Pipeline I understand the translation is “pipeline”, not “pipeline”. Because it means to work like an assembly line, the process of turning the input data into pixels on the screen step by step. The word “pipeline” is so easy to think of as a pipe that it’s wrong. “Assembly line” is more noticeable as a process at heart, a process rather than a… The pipe?

  • For any model, whether 3D or 2D, it starts with point data. For example, a square is the coordinate of 4 angles, and a cube is the coordinate of 8 points. A complex character in a game is actually a lot of points spliced together. Search the picture of “3D model” and you will have an intuitive feeling. So the whole process inputs Vertex Data, called Vertex Data.
  • What is finally presented to the user is the image on the display screen, and the image is composed of pixels, so the output is the color of each pixel. The whole process is: how do you get the coordinate data into the correct pixel color on the screen?
  • This is a pretty intuitive picture. And the blue part is what modern OpenGL allows us to write and participate in. A shader, or “shader,” is a subroutine in a process that handles a task at a given stage, just as there are many different machines and people on an assembly line that do part of the work.
  • The Vertex shader is the first shader that handles input Vertex data, such as coordinate transformations
  • Geometry shader and Tesselation shader are not necessary.
  • Fragment shader now accepts a Fragment instead of a vertex, which corresponds to a pixel. This stage is mainly to calculate the color, such as light calculation: when there are N light sources, what color is the fragment, the color of the light, the color of the object itself, the orientation of the fragment, etc.

So to draw a triangle, you need to provide three points of data and write the Vertex shader and fragment shader.

Load the shader

Shader is a subroutine that has its own language, GLSL, and also needs to be compiled to use it. GLSL is similar to c, such as the vertex shader needed to draw a triangle:

const GLchar *vertexShaderSource =
"#version 330 core \n\ layout (location = 0) in vec3 position; \n\ void main(){\n\ gl_Position = vec4(position, 1.0f); \n\ void main(){\n\ gl_Position = vec4(position, 1.0f); \n\ } \n\ ";
Copy the code

Ignore the “\n\” for now, this is just for multi-line input strings, and the shader content starts with #version.

With the shader code in hand, the next step is to load the shader cost into its compiler:

GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, 0);
glCompileShader(vertexShader);
Copy the code

Generate a Shader object with glCreateShader, feed the shader code to the glShaderSource, and compile the shader: glCompileShader.

If the shader code is written incorrectly, an error will be reported after compilation, so you need to check whether the shader is compiled:

GLint succeed;
GLchar infoLog[256];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &succeed);
if(! succeed) { glGetShaderInfoLog(vertexShader, sizeof(infoLog),NULL, infoLog); std::cout<<"compile vertex shader error: "<< infoLog << std::endl;
    return- 1; }Copy the code

It is also common to use glGetShaderiv as a function to get the compiled state of the shader:

  • Iv stands for int value, the suffix used to distinguish the type of returned or passed value
  • There is then an argument to fetch the value, in this case GL_COMPILE_STATUS, to fetch the compile status of the shader.

If the compile status is failed, get the log information and see what went wrong: glGetShaderInfoLog.

Fragment Shader loads in the same way:

const GLchar *fragmentShaderSource =
"#version 330 core \n\ out vec4 color; \ n \ void main () {\ n \ color = vec4 (1.0 f to 0.0 f to 0.0 f to 1.0 f); \n\ } \n\ ";
Copy the code
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, 0);
glCompileShader(fragmentShader);
    
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &succeed);
if(! succeed) { glGetShaderInfoLog(fragmentShader, sizeof(infoLog),NULL, infoLog); std::cout<<"compile fragment shader error: "<< infoLog << std::endl;
    return- 1; }Copy the code

program

After the shader is loaded, connect different shaders together to test whether they can be used together. These Shders also need to generate executables for the rendering process.

So you need a shader container, or manager, program.

program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
Copy the code

Create a program and use glAttachShader to bind all shaders used together to the program. And finally, glLinkProgram is used to program the link.

Finally, when rendering, use glUseProgram(Program); Specify the program to use.

Prepare data: VBO and VAO

With the Shader in place, the logic to process the process is ready; what is missing is the data.

VBO is a vertex buffer object that stores vertex data. What is a Buffer Object?

Buffer Objects are OpenGL Objects that store an array of unformatted memory allocated by the OpenGL context (aka: the GPU).

It is a piece of memory applied for in the GPU for storing data. Before, there was the direct mode: every time you drew, you had to commit the data. This was later developed as a “vertex array”, where the data was stored in computer memory and indexed as it was drawn. As for the current Buffer object, data is stored on the GPU end, which further speeds up data transmission.

  1. A buffer object

    Copy the code

GLuint VBO; glGenBuffers(1, &VBO);

2. Bind: glBindBuffer(GL_ARRAY_BUFFER, VBO);Copy the code
After the VBO is generated, it is no different from any other Buffer object, so all that remains to be done is who will use the data and how. GlBindBuffer is the problem of specifying who will use it. Use 'GL_ARRAY_BUFFER' to indicate that this buffer is used to store vertex attribute data.Copy the code

What are the vertex properties? Layout (location = 0) in vec3 position; The vertex coordinate position is one of the attributes. The shader works with the VBO, so the position data is read from the buffer object.

  1. The input data

    Copy the code

F GLfloat are [] = {0.5, 0.3 f, f 0.0, 0.5, f to 0.3 f, f 0.0, 0.0 f, 0.8 f, 0.0 f}; glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

GlBufferData creates and initializes data for the buffer object corresponding to the first parameter. Since GL_ARRAY_BUFFER was previously bound to VBO, VBO entered vertices data. GlVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GL_FLOAT), vertices); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GL_FLOAT), vertices); glEnableVertexAttribArray(0); ` ` ` default data is inaccessible to vertex attribute, use ` glEnableVertexAttribArray ` open, parameter is the shader code the positions of the attributes, because ` layout (location = 0)invec3 position; ', the location of the position property is set to 0, so this is the position read capability. Void glVertexAttribPointer(GLuint index, GLint size, GLenum?) void glVertexAttribPointer(GLuint index, GLint size, GLenum?type, GLboolean normalized, GLsizei stride, const GLvoid * pointer); * index specifies which attribute this describes, passing 0 to describe how the position attribute reads data. * size is the size of each read *typeIs the data type, which together with size determines how much memory is read at a time. Here we pass in 3 and GL_FLOAT, which is the position of each vertex that reads 3 floats. * normalized indicates whether data needs to be normalized. Normalize maps values to [-1,1] or [0,1]. GL_FALSE. * Stride has a lot of vertices to read from here, and after reading one, where to read from next, the stride is the distance to jump. For example: 12, 34, 56, 78, after reading the memory at position 3, if the stride is set to 4, jump to 7 and start reading the next data. Because one vertex has three floats and is immediately adjacent to the next, we pass in 3*sizeof(GL_FLOAT). And if you pass in a zero, that's fine, because when you pass in a zero, you read the last one, and you read the next one from the end. * pointer This specifies the offset from which to start reading. For example, if 12, 34, 56, 78, 12 and 56 store the data of attribute 1, and 34 and 78 store the data of attribute 2, then the start position of attribute 2 is not the beginning of the buffer object, which has an offset. After the above sequence of operations, the vertex property knows where to read data (VBO) and how to read data, and the data is entered into the VBO. 5. Finally, there is Vertex Array Object. The above steps are repeated each time an object is drawn, in addition to vertex data and possibly index data. VAO packages these states together (which properties can read data, how those properties are read, what index data is read, and so on). To draw, call 'glBindVertexArray(VAO1); 'Then all states associated with VAO1 will be enabled if' glBindVertexArray(VAO2) is then called; 'you can immediately switch to all data of VAO2 again. It should be designed for easy coding. With VAO, you can put all of the above data processing into the preparation phase, before the rendering cycle, rather than every cycle. In the render loop, only the 'glBindVertexArray' switches the required VAO. In the preparation phase, which states will be bound by VAO?Copy the code

glBindVertexArray(VAO1); GlBindVertexArray (VAO2); GlBindVertexArray (0);

All operations performed in the position of data processing 1 will be bound to VAO1, and all operations performed in the position of data processing 2 will be bound to VAO2, that is, which VAO is bound to, that is, to whom it is applied.### render loop

Copy the code

glUseProgram(program); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3);

GlUseProgram Uses program and enables all shader associated with program. GlBindVertexArray Enables all data associated with VAO and read methods. Data and logic are there, glDrawArrays are drawn. Using GL_TRIANGLES, say, allows you to specify what types of primitives to draw. All complex objects are made up of basic shapes, GL_POINTS, GL_LINES, etc. 0 and 3 specify the data range of points to be used for drawing. Since we're only drawing one triangle, we can use three points.# # # back to the shader

vertex shader

Copy the code

#version 330 core layout (location = 0) in vec3 position; Void main(){gl_Position = vec4(position, 1.0f); }


* `#version 330 Core 'declares the version, in this case 3.3, core means to use core profile. The other is compatibility. Compatibility is compatibility mode, it keeps the previous functions, and core gets rid of those functions that have been disabled. So let's go straight to Core Profile.
* `layout (location = 0) invec3 position; Declare a variable of type VEC3. Vec is short for vector. Vec3 is a 3-element vector, such as RGB and XYZ coordinates. Layout and Location are used to specify the location of this property and are used in conjunction with VBO data reading. * Main is the main function that does vertex processing. Gl_Position is the default variable used to output vertex data to the next stage. Here, the main function simply changes vec3 to vec4. Fragment shaderCopy the code

#version 330 core out vec4 color; Void main(){color = vec4(1.0f, 0.0f, 0.0f, 1.0f); }

* # version ditto* Color defines a color, vec4 contains rgba4 components, and the out keyword is used to indicate that this variable is output to the next stage. If you useinIt was input from the previous phase. Fragment Shader outputs the first out VEC4 assigned as the pixel color.### OpenGL ESIn terms of drawing a triangle, there is basically no difference, except that there are two differences in shader code: * Core is changed to ES in the version statement, and the version is changed to 300. * ES because it is designed for embedded devices, it probably has more stringent tests on memory and performance, requiring the accuracy of execution data. There are two ways to declare precision: * Declare a default precision: 'precision mediumpfloat; 'All usedfloatAll are of medium accuracy. * Or specify precision for a variable: 'out mediump vec4 color; `Copy the code