Imagine a colorful triangle with color pulled out as a texture. What kind of data would be needed to draw that triangle.
- World coordinates, that is, where should each pixel be, like if you put a triangle in a rectangular coordinate system, what are the coordinates of each position
- Texture coordinates, the color coordinates that each pixel coordinate should display, that is, the color of each point in the triangle.
Or more abstractly, have you ever played with one of those graphic stickers? If you want to attach a pattern to a suitable model, then you need to align the sticker (texture coordinates) with the model (similar coordinates) one by one, so that we can achieve the desired effect.
The world coordinates
As you can tell by the name, this is the coordinates of OpenGL’s own world, it’s a normalized coordinate system, ranging from -1 to 1, with the origin in the middle.
Texture coordinates
This is somewhat inconsistent on each platform. In Windows, the lower left corner is (0.0) and the upper right corner is (1.1), while in android, it is the upper left (0,0) and the lower right corner is (1,1). In this paper, Windows prevails
shader
Opengl deals directly with the GPU, and most graphics cards today have thousands of little processing cores that run their own little programs on the GPU for each stage to quickly process your data in the graphics rendering pipeline. These little programs are called shaders.
Some shaders allow developers to configure themselves, which allows us to replace the default shader with a shader we wrote ourselves. This gives us more control over specific parts of the graphics rendering pipeline, and because they run on the GPU, they save us valuable CPU time. OpenGL Shading is written in OpenGL Shading Language (GLSL)
Below, you’ll see an abstract representation of each stage of a graphics rendering pipeline. Note that the blue part represents the part where we can inject custom shaders.
Through step by step input and output, finally render the vertex data.
Vertex input
Before we can start drawing, we have to enter some vertex data into OpenGL. OpenGL is a 3D graphics library, so all coordinates we specify in OpenGL are 3D coordinates (x, y, and z). OpenGL doesn’t simply convert all 3D coordinates into 2D pixels on the screen; OpenGL only processes 3D coordinates if they are in the range of -1.0 to 1.0 on all three axes (x, y, and Z). All Coordinates within the range of so-called Normalized Device Coordinates will eventually appear on the screen (Coordinates outside this range will not be displayed).
Since we want to render a triangle, we specify a total of three vertices, each with a 3D position. We’ll define them as a float array in the form of normalized device coordinates (the visible region of OpenGL).
What’s left unsaid is that all of OpenGL’s graphics are made up of triangles. For example, a quadrilateral is made up of two triangles, and other more complex graphics can be divided into large and small triangles.
GLfloat are [] = {/ / vertex/color/f 0.5, 0.5 f, f, 0.0 1.0 f, f 0.0, 0.0, f - 0.5 f to 0.5 f to 0.0 f to 0.0 f to 1.0 f to 0.0 f to 0.0 f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f};Copy the code
Since we are drawing a plane graph, the z-axis here can be set to 0, similar to the figure below. The corresponding color coordinate here is RGB, showing red/green/blue at the three vertices
Once such vertex data is defined, we send it as input to the first processing phase of the graphics rendering pipeline: the vertex shader. It will create memory on the GPU to store our vertex data, configure how OpenGL interprets this memory, and specify how it is sent to the graphics card. The vertex shader then processes the number of vertices we specify in memory.
We manage this memory through Vertex Buffer Objects (VBO), which store a large number of vertices in GPU memory (commonly referred to as video memory). The advantage of using these buffer objects is that we can send a large number of data to the graphics card at once, rather than each vertex once. Sending data from the CPU to the graphics card is relatively slow, so whenever possible we try to send as much data as possible at once. Vertex shaders can access vertices almost immediately after data is sent to the graphics card’s memory, which is a very fast process.
VBO (Vertex buffer object)
The vertex buffer object was the first OpenGL object to appear. Just like any other object in OpenGL, this buffer has a unique ID, so we can use the glGenBuffers function and a buffer ID to generate a VBO object:
unsigned int VBO;
glGenBuffers(1, &VBO);
Copy the code
OpenGL has many buffer object types, and the buffer type for vertex buffer objects is GL_ARRAY_BUFFER. OpenGL allows us to bind multiple buffers simultaneously, as long as they are different buffer types. We can bind the newly created buffer to the GL_ARRAY_BUFFER target using the glBindBuffer function:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
Copy the code
From this point on, any buffer calls we use (on the GL_ARRAY_BUFFER target) will be used to configure the currently bound buffer (VBO). We can then call the glBufferData function, which copies the previously defined vertex data into the buffer’s memory:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
Copy the code
GlBufferData is a function specifically used to copy user-defined data into the current binding buffer. Its first argument is the type of the target buffer: the vertex buffer object is currently bound to the GL_ARRAY_BUFFER target. The second parameter specifies the size, in bytes, of the data transferred; Just use a simple sizeof to figure out the sizeof the vertex data. The third parameter is the actual data we want to send.
The fourth parameter specifies how we want the graphics card to manage the given data. It comes in three forms:
- GL_STATIC_DRAW: Data changes little or nothing.
- GL_DYNAMIC_DRAW: The data will change a lot.
- GL_STREAM_DRAW: Data changes each time it is drawn.
The triangle’s position data does not change and remains the same for each render call, so its best use type is GL_STATIC_DRAW. If, say, the data in a buffer is going to change frequently, the type used is GL_DYNAMIC_DRAW or GL_STREAM_DRAW, which ensures that the graphics card keeps the data in a part of memory that can be written to quickly.
VAO(Vertex Array object)
A Vertex Array Object (VAO) can be bound like a Vertex buffer Object, and any subsequent calls to Vertex properties are stored in this VAO. The advantage of this is that when configuring the vertex property Pointers, you only need to make those calls once, and then bind the corresponding VAO when drawing the object. This makes it very easy to switch between different vertex data and property configurations by binding different VAO. All the states you just set will be stored in the VAO
A vertex array object stores the following:
- GlEnableVertexAttribArray and glDisableVertexAttribArray calls.
- Vertex property configuration set via glVertexAttribPointer.
- The vertex buffer object associated with the vertex property is called via glVertexAttribPointer.
It’s a little bit harder to understand here, but you can look at it a little bit more, you can see it a little bit more, you can understand it over time, you don’t want to rush it, it’s like one set, multiple calls.
Linked vertex properties
We’ve stuffed all the vertices in, but how does the shader know that the first three are vertex coordinates and the last three are color texture coordinates? So we need to tell the pipeline how the vertex data is parsed
You can use the glVertexAttribPointer function to tell OpenGL how to parse vertex data (applied to each vertex property)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
Copy the code
The glVertexAttribPointer function takes a lot of arguments and I’ll go through them one by one
- The first parameter specifies the vertex properties we want to configure, as we’ll see in the shader code later.
- The second parameter specifies the size of the vertex attribute. The vertex property is one
vec3
It’s made up of three values, so it has size 3. - The third parameter specifies the type of data, in this case GL_FLOAT.
- The next parameter defines whether we want data to be normalized. If we set it to GL_TRUE, all data will be mapped between 0 (-1 for signed data) and 1. Let’s set it to GL_FALSE.
- The fifth parameter, called the Stride, tells us the spacing between successive sets of vertex attributes. Because the next group position data in 6
float
After that, we set the step size to6 * sizeof(float)
). . - The last parameter is of type
void*
So we need to do this weird cast. It represents the Offset of the starting position of the position data in the buffer, so the Offset at the vertex coordinates is 0, and the Offset at the texture coordinates is (GLvoid*)(3 * sizeof(GLfloat)).
Thus inform the line, how to parse the vertex data, then use the glEnableVertexAttribArray, position to vertex attribute value as a parameter, enable the vertex attribute, now we code should be like this
GLfloat are [] = {/ / vertex/color/f 0.5, 0.5 f, f, 0.0 1.0 f, f 0.0, 0.0, f - 0.5 f to 0.5 f to 0.0 f to 0.0 f to 1.0 f to 0.0 f to 0.0 f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f}; // Create the vertex buffer object GLuint VBO; glGenBuffers(1, &VBO); // Create the vertex array object GLuint VAO; glGenVertexArrays(1, &VAO); // 1.t bind vertex data glBindVertexArray(VAO); GlBindBuffer (GL_ARRAY_BUFFER, VBO); GlBufferData (GL_ARRAY_BUFFER, sizeof(vertices), vertices, vertices, vertices, vertices, vertices); GL_STATIC_DRAW); // // 3. Tell the pipeline how to parse vertex data glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0); / / enabled vertex attribute data 0 glEnableVertexAttribArray (0); GlVertexAttribPointer (1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat))); glEnableVertexAttribArray(1); //4. Unbind the VAO glBindVertexArray(0); glDisableVertexAttribArray(0);Copy the code
Vertex shader
The first thing we need to do is write the vertex shader in GLSL(OpenGL Shading Language) and then compile this shader so we can use it in our program. Below you will see a very basic GLSL vertex shader source code:
#version 330 core layout(location = 0) in vec3 position; layout(location = 1) in vec3 color; out vec3 ourColor; Void main() {gl_Position = vec4(position, 1.0f); ourColor = color; };Copy the code
First, the first line declares the version, in this case v3.3.
Layout (Location = 0) sets the location value of the input variable, which is the first parameter of glVertexAttribPointer. Layout (location =1) is the texture coordinate
The in/out keyword represents input, output, and this will convert the ourColor output to the input of the next shader
Fragment shader
const GLchar* fragmentShaderSource = #version 330 core in vec3 ourColor; out vec4 color; Void main() {color = vec4(ourColor, 1.0f); };Copy the code
In gets the vertex shader, passes the ourColor, and finally generates the final texture color through VEC4 (ourColor, 1.0f), and passes the color to the next process through out.
Join shader
A Shader Program Object is a version of a Shader that has been merged and eventually linked. To use the shaders we just compiled we must Link them to a shader program object and then activate the shader program when rendering the object. The shader of the activated shader program will be used when we send the render call.
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
Copy the code
The result is a program object. We can activate this program object by calling glUseProgram with the program object we just created as an argument:
The complete code
#include <iostream> #include <string> // Glad #define STB_IMAGE_IMPLEMENTATION #include <glad/glad.h> // GLFW #include <GLFW/glfw3.h> using namespace std; void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode); const GLchar* vertexShaderSource = "#version 330 core\n\ layout(location = 0) in vec3 position; \n\ layout(location = 1) in vec3 color; \n\ out vec3 ourColor; \n\ void main()\n\ {\n\ gl_Position = vec4(position, 1.0f); \n\ ourColor = color; \n\ }\n\0"; const GLchar* fragmentShaderSource = "#version 330 core\n \ in vec3 ourColor; \n\ out vec4 color; \n \ void main()\n \ {\n \ color = vec4(ourColor, 1.0f); \n \ }\n\0"; int main() { //GLFW glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", Nullptr, nullptr); if (window == nullptr) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } //glad glfwMakeContextCurrent(window); if (! gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return ; // GLuint vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader); GLint success; GLchar infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (! success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; } // GLuint fragmentShader; fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader); glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); if (! success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGEMENT::COMPILATION_FAILED\n" << infoLog << std::endl; } // create shaderProgram GLuint shaderProgram; shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if (! success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "ERROR::PROGRAM::LINK_FAILED\n" << infoLog << std::endl; } glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // glfwSetKeyCallback(window, key_callback); GLfloat are [] = {/ / vertex/color/f 0.5, 0.5 f, f, 0.0 1.0 f, f 0.0, 0.0, f - 0.5 f to 0.5 f to 0.0 f to 0.0 f to 1.0 f to 0.0 f to 0.0 f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f}; // Create the vertex buffer object GLuint VBO; glGenBuffers(1, &VBO); // Create the vertex array object GLuint VAO; glGenVertexArrays(1, &VAO); // 1.t bind vertex data glBindVertexArray(VAO); GlBindBuffer (GL_ARRAY_BUFFER, VBO); // Pass vertex data into a buffer object (& type of incoming buffer data & size in bytes & incoming data & how the graphics card manages incoming data) /* * GL_STATIC_DRAW: These data are mostly unchanged or rarely changed. GL_DYNAMIC_DRAW: These data may change frequently. GL_STREAM_DRAW: This data is changed each time it is drawn. */ glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // // 3. Tell the pipeline how the vertex data is parsed /* The attribute index in the shader program. */ glVertexAttribPointer(0, 3, GL_FLOAT, */ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0); / / enabled vertex attribute data 0 glEnableVertexAttribArray (0); GlVertexAttribPointer (1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat))); glEnableVertexAttribArray(1); //4. Unbind the VAO glBindVertexArray(0); glDisableVertexAttribArray(0); while (! GlfwWindowShouldClose (window)) {glfwPollEvents(); // Clear the background glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Render program object glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); GlfwSwapBuffers (window); glFlush(); } glfwTerminate(); cout << "Success!" << endl; system("pause"); return 0; } void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) { // When a user presses the escape key, we set the WindowShouldClose property to true, // closing the application if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) glfwSetWindowShouldClose(window, GL_TRUE); }Copy the code
And that’s the end result
conclusion
- Opengl processes data in a pipeline mode, which actually refers to the process of a bunch of raw graphics data passing through a pipeline, through various changes in the process and finally appear on the screen
- Use VBO/VAO to manage vertex objects
- Opengl uses vertex coordinates and texture coordinates to eventually populate the render data.
- Understand the basic rules of the GLSL shader language
- Initialize the OpenGL program, and compile, link vertex shaders and fragment shaders, and finally use.
extension
- Change the vertex coordinates to rotate the triangle 90 degrees
- Dynamically change the texture coordinate color value so that the triangle color is always changing.
Refer to the article
The shader language GLSL
LearnOpenGL-CN