Series Article Directory
- LearnOpenGL Notes - Getting Started 01 OpenGL
- LearnOpenGL Notes - Getting Started 02 Create Window
- LearnOpenGL Notes - Getting Started 03 Hello, Windows
Article directory
foreword
- Original Link: Hello, Triangle
- The code of this article: 2_1_hello_triangle.cpp
This article is more difficult, and the learning curve suddenly becomes steeper. But it doesn't matter, I will explain my understanding from the perspective of a beginner to help you learn VAO, VBO, EBO, Shader and other concepts. First of all, the full text is still summarized in the form of a list of knowledge points.
hello triangle
- Remember three words first
- Vertex Array Object: Vertex Array Object, VAO
- Vertex buffer object: Vertex Buffer Object, VBO
- Element Buffer Object: Element Buffer Object, EBO or Index Buffer Object, Index Buffer Object, IBO
- Everything in OpenGL is in 3D space, while the screen is 2D. The process of converting 3D coordinates to 2D coordinates is performed by OpenGL's graphics rendering pipeline
- The graphics rendering pipeline is divided into two main parts
- Convert 3D coordinates to 2D coordinates
- 2D coordinates converted to actual colored pixels
- The graphics rendering pipeline can be divided into several stages, each stage will take the output of the previous stage as input. These stages are easily executed in parallel. The core of the graphics card runs small programs at each stage, these small programs are called shaders (Shader)
- Shaders in some stages can be written by developers. OpenGL shaders are written in the OpenGL Shading Language (GLSL)
- The figure below is an abstract representation of each stage of the rendering pipeline. The blue part can enter a custom shader
- The first part of the pipeline is the Vertex Shader, which takes a vertex as input. The role is to convert 3D coordinates into another 3D coordinates (explained later), and the vertex shader allows us to perform some basic processing on vertex attributes.
- The primitive assembly (Primitive Assembly) stage takes as input all the vertices output by the vertex shader (if it is GL_POINTS, then it is a vertex), and assembles all points into the shape of the specified primitive; in this section, it is a triangle.
- The output of the primitive assembly stage is passed to the Geometry Shader. A geometry shader takes as input a collection of vertices in the form of primitives, and it can generate other shapes by constructing new (or other) primitives by generating new vertices. In the example, it generates another triangle.
- The output of the geometry shader will be passed to the rasterization stage (Rasterization Stage), where it will map the primitive to the corresponding pixel on the final screen, and generate a fragment (Fragment) for the fragment shader (Fragment Shader). Clipping is performed before the fragment shader runs. Cropping discards all pixels outside your view to improve performance.
- The main purpose of the fragment shader is to calculate the final color of a pixel, and this is where all the advanced OpenGL effects come from. Typically, a fragment shader contains 3D scene data (such as lighting, shadows, light colors, etc.), which can be used to calculate the final pixel color.
- The final object will be passed to the final stage, which we call Alpha testing and blending (Blending) stage. This stage detects the corresponding depth (and template (Stencil)) values of the fragment (described later), and uses them to determine whether this pixel is in front of or behind other objects, and decides whether it should be discarded. This stage also checks the alpha value (the alpha value defines the transparency of an object) and blends the object (Blend). So, even if the output color of a pixel is calculated in the fragment shader, the final pixel color may be completely different when rendering multiple triangles.
- In modern OpenGL we have to define at least one vertex shader and one fragment shader.
vertex input
- Drawing graphics requires vertices. In OpenGL, vertices use 3D coordinates, namely x, y and z, and the range is between [-1, 1]. We call this range Normalized Coordinates . Points outside the range will not be displayed.
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
- Normalized device coordinates are converted to screen space coordinates through
glViewport
the function to change the viewport. vertices
The points in are now in memory (your code memory), we need to send them to the vertex shader. The vertex shader allocates a space in video memory to store these points. At the same time you have to tell OpenGL how to interpret the data.- Vertex Buffer Objects (VBO) are responsible for managing this video memory.
unsigned int VBO;
glGenBuffers(1, &VBO);
glGenBuffers
Create the VBO usingglBindBuffer(GL_ARRAY_BUFFER, VBO);
Bind this VBO to a vertex buffer object in the OpenGL Context.glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
Copy data from memory to video memory- GL_STATIC_DRAW : The data will not or will hardly change.
- GL_DYNAMIC_DRAW: The data will be changed a lot.
- GL_STREAM_DRAW : The data changes each time it is drawn.
Vertex Shader (Vertex Shader)
- Modern OpenGL requires us to set up at least one vertex and one fragment shader. Write vertex shaders using GLSL (OpenGL Shading Language).
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
#version 330 core
Declare version and core schema. In OpenGL 3.3 and higher, the GLSL version number matches the OpenGL version.in
Declare Input Vertex Attributevec3
Vector type, vec.x, vec.y, vec.z get different componentsgl_Position
Is the output of the Vertex Shader. We have to assign position data to it. it is avec4
type
Compile the shader
const char *vertexShaderSource = R"(
#version 330
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 bPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
)";
auto vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"
<< infoLog << std::endl;
return -1;
}
glCreateShader(GL_VERTEX_SHADER)
Create a vertex shader whose type is GL_VERTEX_SHADER to indicate its type.glShaderSource(vertexShader, 1,&vertexShaderSource, NULL);
Sets the source code of the shader object.glCompileShader(vertexShader);
Compile the shader.glGetShaderiv
Check if the compilation is successful
Fragment Shader
- What the fragment shader does is calculate the final color output of the pixel
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
- Fragment shaders only need to output one variable. The variable is defined using
out
the keyword . - Compiling a fragment shader is similar to compiling a vertex shader
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
shader program
- Shader Program Object (Shader Program Object) is the merged and finally linked version of multiple shaders.
- We must link (Link) multiple compiled shaders into a shader program object and activate it when rendering.
unsigned int 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::SHADER::PROGRAM::LINKING_FAILED\n
<< infoLog << std::endl;
return -1;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
glUseProgram(shaderProgram);
glCreateProgram
Create a shader program objectglAttachShader
Attaching vertex shaders and fragment shaders to program objects (modification of program object attributes)glLinkProgram(shaderProgram);
link them.glUseProgram(shaderProgram);
Bind the program object to the OpenGL Context and activate it.glDeleteShader
, Link remember to delete the shader objects in the future, we don't need them anymore.
link vertex attributes
- Earlier, we fed a bunch of data to the vertex shader via VBO. Allows us to specify any input in the form of vertex attributes.
- This is very flexible, but also means that we have to manually specify which part of the input data corresponds to which vertex attribute of the vertex shader.
glVertexAttribPointer
Tell OpenGL how to interpret the data in the VBOglEnableVertexAttribArray
Enables vertex attributes; vertex attributes are disabled by default.
Vertex Array Object (VAO)
- OpenGL's core mode requires us to use VAOs so it knows what to do with our vertex input. If we fail to bind the VAO, OpenGL will refuse to draw anything.
- A vertex array object stores the following:
- Calls to glEnableVertexAttribArray and glDisableVertexAttribArray.
- Vertex attribute configuration set by glVertexAttribPointer.
- Call the vertex buffer object associated with the vertex attribute through glVertexAttribPointer.
glGenVertexArrays
Create VAOs- Use VAO and VBO to draw the graphics process, as follows:
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
triangle!
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
- The glDrawArrays function, which uses the currently active shader, the previously defined vertex attribute configuration, and the vertex data of the VBO (indirectly bound through the VAO) to draw the primitive
element buffer object
- Drawing a rectangle requires 4 vertices, but glDrawArrays can only draw points, lines, and triangles. Can't draw a rectangle. Fortunately, you can draw two triangles for this purpose. The vertices are as follows:
float vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二个三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
- Specify the bottom right and top left twice! A rectangle has only 4 vertices instead of 6, which incurs 50% additional overhead. A better solution is to just store the distinct vertices, and set the order in which they are drawn. In this way, we only need to store 4 vertices to draw a rectangle. EBO does just that.
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glDrawElements
Draw using the index in the currently bound index buffer object.