Write in front
Then the previous section, when the development environment was set up, of course, we wanted to write 3D applications right away. But we also need some patience, because OpenGL is a low-level API, so we have a little more basic knowledge, before we begin to draw 3D graphics, we will be familiar with the concepts and processes of modern OpenGL by drawing a triangular program.
In this section you can learn how to compile, link, and use methods for caching objects Vao and VBO GLSL shader programs The basic flow of OpenGL drawings A brief overview of the drawing pipeline
Unlike using advanced drawing APIs, such as those in the Java Swing drawing, in MFC, you need to understand the underlying knowledge when drawing graphics using OpenGL. In modern OpenGL, the pipeline to complete the drawing is different from the old fixed pipeline, which allows the user to customize the shader themselves, which makes the drawing more flexible. The modern drawing line is shown in the following figure (from: OpenGL wiki Rendering_pipeline_overview):
This drawing pipeline is more complex, and you just need to focus on vertex shader vertex shader and fragment shader slice shader. The vertex shader is responsible for converting user-specified vertices to internal representations, and the slice shader determines the color of the resulting image. The vertex shader and the slice shader can communicate by passing variables. Using these two shaders, you can draw the basic graphics, the main process is:
(1) user specifies or loads vertex attribute data in the program
(2) transfer vertex attribute data to GPU, processing vertex data by vertex shader
(3) The color of the final graphic is responsible for the element shader
According to this step, we are familiar with the related concepts and operations. Vbo and Vao
The data that is specified or loaded in the OpenGL program is stored in the CPU, and to speed up graphics rendering, it is necessary to take advantage of the GPU, so that data needs to be sent to the GPU. In the GPU, VBO is vertex buffer object, which is responsible for the storage of the actual data, while Vao is the vertex array object, which records the details of how the data is stored and how it is used.
OpenGL is a state machine, and we need to switch between different states when drawing graphics. For example, the color set when the color buffer is cleared by Glclearcolor setting in the previous section, OpenGL remembers this state and resets the color buffer with this color when glclear is called. OpenGL will always use this status value until the different colors are set using Glclearcolor again.
The advantage of using VAO is that if more than one object needs to be drawn, then we set the vertex data, data parsing method and so on to draw the object once, then save it through Vao, the subsequent drawing operation no longer need to repeat this process, only need to set Vao to the current Vao, Then OpenGL uses these state information. When there are more objects in the scene, the advantage is obvious. The relationship between Vao and Vbo is shown in the following figure (picture from best practices for working with Vertex Data):
In the image above, the vertex attributes include positions, texture coordinates, normals, colors, and more, and the data for each attribute can be stored in different buffer. We can create multiple VBO and Vao in the program as needed.
The pseudo-code using VAO and VBO is as follows (from so):
Initialization:
for each batch
generate, store, and bind a VAO
bind all the buffers needed for a draw call
unbind the VAO
main loop/whenever you render: For each
batch
bind VAO
gldrawarrays (...); Gldrawelements (...); etc.
Unbind VAO
So how do you create Vbo and Vao? object creation and use in OpenGL is not the same as object creation in C + +, the following code describes how objects are created and used in C + + (from [Learning modern 3D Graphics programming]):
struct Object
{
int count;
float opacity;
char *name;
};
Creates an object.
Object NewObject;
Sets the state of the object
Newobject.count = 5;
newobject.opacity = 0.4f;
Newobject.name = "Some String";
Creating and using objects in OpenGL is similar to this:
Create object not allowed to use custom name
gluint objectName;
Glgenobject (1, &objectname);
Set object state
Glbindobject (gl_modify, objectName);
Globjectparameteri (Gl_modify, Gl_object_count, 5);
Globjectparameterf (Gl_modify, gl_object_opacity, 0.4f);
Note that OpenGL creates an object with the Gluint type as the object identifier, not a custom name, which does not cause the object to duplicate. In OpenGL, before each object is used, it is bound to the context object, the so-called target, such as the gl_modify target in the example above.
Step1: Create VBO we create this:
Gluint vboid;
Glgenbuffers (1, &vboid);
API void Glgenbuffers (Glsizei n, gluint * buffers);
Here n Specifies the number of buffer to produce, while buffers is the address of the identifier. One or more buffer can be generated at a time.
Step2: Transfer vertex data to VBO or pre-allocate space for VBO.
In this section we draw a triangle, for a triangle to specify vertices in 3D space, you must use three-dimensional coordinates. The vertex coordinates need to be processed by the vertex shader in order to be used in the production triangle, which involves coordinate transformation and so on, this section does not do in-depth discussion. After the coordinates are converted, the vertex coordinates eventually fall in the normalized device coordinate system (normalized device coordinate, NDC), where the coordinate range of NDC is [ -1,1], so here we simplify the processing and set the vertex coordinates all within this range, specified as:
Glfloat vertices[] = {
-0.5f, 0.0f, 0.0f,
0.5f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f
};
The transfer of data to the GPU needs to be implemented through function Glbufferdata.
API void Glbufferdata (Glenum target,
Glsizeiptr size,
Const GLVOID * data,
Glenum usage);
1. The target parameter in the function represents the destination of the binding, including targets such as Gl_array_buffer for vertex attributes (vertex properties), Gl_element_array_buffer for index drawing, and so on.
The 2.size parameter represents the amount of space that needs to be allocated, in bytes.
The 3.data parameter is used to specify the data source, and if data is not empty it will copy the buffer to initialize it, otherwise it simply allocates space for the predetermined size. After the space is pre-allocated, the buffer contents can be updated later through Glbuffersubdata.
The 4.usage parameter specifies the data usage pattern, for example Gl_static_draw is specified as static drawing, the data remains constant, Gl_dynamic_draw is specified as dynamic drawing, and the data is updated frequently.
We draw a static triangle here, vertex attribute vertex attribute This concept includes vertex position, texture coordinates, normal vector, color and other attribute data, so our vertex position data is suitable for binding to Gl_array_buffer target, At the same time the data is initialized at the time of the transfer, so this can be done:
Glbindbuffer (Gl_array_buffer, vboid);
Glbufferdata (Gl_array_buffer, sizeof (vertices), vertices, gl_static_draw);
STEP3: notifies OpenGL how to interpret this array of vertex attributes
After transferring the data to the GPU, we also need to tell OpenGL how to interpret the data, that is, to tell its data format, because the data is a byte block from the bottom. To tell OpenGL how to interpret the data, use the function Glvertexattribpointer.
API void Glvertexattribpointer (Gluint index,
Glint size,
Glenum type,
Glboolean normalized,
Glsizei Stride,
Const GLVOID * pointer);
1. The index for the vertex attribute is the index of the attribute in the vertex shader, and the index starts from 0.
2. Parameter size each attribute data consists of several components. For example, each of the above vertices has a property of 3 float, and the size is 3. The number of components must be one of the four values of 1,2,3,4.
3. The parameter type represents the data type of the attribute component, for example, the above vertex data is FLOAT and the gl_float is filled in.
4. The parameter normalized indicates whether to normalize, when the integer type is stored, if it is set to Gl_true, when it is accessed as a floating-point number, the signed integer is converted to [ -1,1], unsigned to [0,1]. Otherwise, it is converted directly to float type without normalization.
5. The parameter stride represents the interval between successive two vertex attributes, calculated as the size of the bytes. When the vertex properties are tightly arranged (tightly packed), you can fill in 0, and the value is calculated by OpenGL instead of us.
6. The parameter pointer represents the offset of the first component of the vertex attribute from the starting point of the data, calculated in bytes, in the buffer currently bound to the Gl_array_buffer buffer object.
The above function is very important, it is possible to get bored with multiple parameters in the first contact, and gradually become accustomed to it. Here is an illustration (from: Learn OpenGL) for an example of an array of attributes containing vertex positions above:
As we can see here, when the above function is called, the property index is 0 (corresponding to it later in the shader), the number of attributes is 3, the component data type is gl_float, normalized is set to Gl_false, and the parameter stride is 3*sizeof (gl_ FLOAT) = 12,
The offset of the pointer is 0, but is written as (glvoid*) 0 (cast), as follows:
Glvertexattribpointer (0, 3, gl_float,
3 * sizeof (Gl_float), (glvoid*) 0);
Glenablevertexattribarray (0);
For a detailed explanation of the stride and offset functions in the Glvertexattribpointer function, you can also refer to my other article on buffer object.
This allows us to create the VBO, transfer the data to the GPU, and tell OpenGL how to parse the data. Throughout the process, we called a number of functions that would be cumbersome if we needed to continue to invoke these functions at a later time, so Vao played a key role at this point. Vao can record the relevant information of VBO, in the future, only need to bind the corresponding Vao to find these states, easy to use OpenGL. Therefore, in the process of creating VBO, we use Vao to record. The method is to create and bind Vao before all VBO operations.
The final code for creating Vao and Vbo when you draw a triangle is as follows:
Specify vertex attribute data vertex position
glfloat vertices[] = {
-0.5f, 0.0f, 0.0f,
0.5f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f
};
Create a Cache object
gluint vaoid, vboid;
STEP1: Create and Bind Vao object
glgenvertexarrays (1, &vaoid);
Glbindvertexarray (vaoid);
STEP2: Create and bind Vbo object
glgenbuffers (1, &vboid);
Glbindbuffer (Gl_array_buffer, vboid);
STEP3: Allocating space to transmit data
glbufferdata (gl_array_buffer, sizeof (vertices), vertices, gl_static_draw);
STEP4: Specify the parsing mode and enable vertex properties
glvertexattribpointer (0, 3, Gl_float, Gl_false, 3 * sizeof (Gl_float), (glvoid*) 0); C19/>glenablevertexattribarray (0);
Unbind
glbindbuffer (gl_array_buffer, 0);
Glbindvertexarray (0);
At the end of the code, we temporarily unbind it to prevent subsequent operations from interfering with the current Vao and VBO.
Now it is only necessary to invoke the Vao drawing triangle in the program:
Glbindvertexarray (vaoid); Use Vao information
gluseprogram (SHADERPROGRAMID);//Use Shader
gldrawarrays (gl_triangles, 0, 3);
The shader is used here and is described later. The Gldrawarrays function uses VBO data to draw objects. It is used in the following ways:
API void Gldrawarrays (Glenum mode,
Glint First,
Glsizei count);
The 1.mode parameter represents the basic type of drawing, OpenGL prefabricated gl_points, Gl_line_strip and other basic types. A complex graph is made up of these basic types.
2.first represents the index of the first data in an array of enabled vertex attributes.
3.count indicates the number of vertices required for drawing.
When the above adjustment is used, we select Gl_triangles to represent the drawing triangle, using 3 vertices. Shader Program
Currently we use vertex shaders and chip shaders primarily. For shaders, a program written in the GLSL language (OpenGL shading Language), similar to a C language program, is used.
There are 3 steps to working with shaders: creating and compiling shader object creating shader program, linking multiple shader object to program enabling shader program when drawing scenes
The specific process is shown in the following figure: Created with Raphaël 2.1.0 Shader source file (shader file) Read source (read source) compiled source (compile source) chain-of-color object (Link shader objects) Shader program object
In the above process, a shader program object can contain multiple shader objects, such as a vertex shader (vertex shader), a geometry shader (geometry shader, subsequent introductions), and a slice shader (fragment shader). We can also put the shader source in the program code, of course, this practice is only as an example, not worth advocating.
Here we write a simple pass-through shader that prints the incoming vertex position in the vertex shader and outputs the specified color in the slice shader. The actual application of these two programs will determine the graphical final effect, here just to do a simple example.
The vertex shader code is:
#version //Specify GLSL version 3.3
layout (location = 0) in vec3 position;//vertex Property index
void Main ()
{
Gl_ Position = VEC4 (Position, 1.0); Output Vertex
}
Where Gl_position is a built-in variable that represents the vertex output position, the Gl_ prefix usually represents the built-in variable. The position is declared as a vec3 type, and vec3 represents a vector of 3 float types. Gl_positon is the VEC4 type, where the fourth component is 1.0 and is described later in this component.
The element shader code is:
#version
VEC4 color;//output element color
void Main ()
{
color = VEC4 (0.8, 0.8, 0.0, 1.0);
}
Specifies that the final color is yellow by color, and that the VEC4 type indicates that the color is RGB plus an alpha value that forms the final output color. About the alpha value is described later.
First create vertex and slice shader objects, and be aware of the error handling. Where the vertex shader code looks like this:
const glchar* Vertexshadersource = "#version 330\n" "Layout (location = 0) in VEC3 posi
Tion;\n "" void main () \ n "" {\ n gl_position = VEC4 (Position, 1.0); \ n} ";
STEP2 Create SHADER object//vertex shader Gluint Vertexshaderid = Glcreateshader (Gl_vertex_shader);
Glshadersource (Vertexshaderid, 1, &vertexshadersource, NULL);
Glcompileshader (Vertexshaderid);
Glint compilestatus = 0; Glgetshaderiv (Vertexshaderid, Gl_compile_status, &compilestatus);
Check the compilation status if (Compilestatus = = gl_false)//Get error report {Glint maxLength = 0;
Glgetshaderiv (Vertexshaderid, Gl_info_log_length, &maxlength);
Std::vector<glchar> ErrLog (maxLength);
Glgetshaderinfolog (Vertexshaderid, MaxLength, &maxlength, &errlog[0]);
Std::cout << "Error::shader vertex shader compile failed," << &errlog[0] << Std::endl; }
The slice shader also has similar processing, then creates and connects to form the shader program object, the code is as follows:
Gluint Shaderprogramid = Glcreateprogram ();//Create Program
Glattachshader (Shaderprogramid, Vertexshaderid);
Glattachshader (Shaderprogramid, Fragshaderid);
Gllinkprogram (shaderprogramid);
Glint Linkstatus;
GLGETPROGRAMIV (Shaderprogramid, Gl_link_status, &linkstatus);
if (Linkstatus = = Gl_false)
{
Glint maxLength = 0;
GLGETPROGRAMIV (Shaderprogramid, Gl_info_log_length, &maxlength);
Std::vector<glchar> ErrLog (maxLength);
Glgetprograminfolog (Shaderprogramid, MaxLength, &maxlength, &errlog[0]);
Std::cout << "Error::shader link failed," << &errlog[0] << Std::endl;
}
Note : After the shader object is linked to program, you can break the link, and if you don't need to link to another program, it's a good idea to release the resource:
After the link is completed detach
gldetachshader (Shaderprogramid, Vertexshaderid);
Gldetachshader (Shaderprogramid, Fragshaderid);
Free space
Gldeleteshader (Vertexshaderid) When you do not need to connect to another program;
Gldeleteshader (Fragshaderid);
Draw Triangles
Through the above using Vao and VBO to complete the data storage and parsing part of the work, through the shader to complete the graphics rendering, the two parts together, we can draw our triangle. The result of the operation is shown in the following figure:
The full code of the program can be downloaded from GitHub.
Note If the shader program fails, we get the graph as shown in the following figure:
On failure, check the shader code section. Refactoring Code
The shader code is divided into a shader class, which reads the shader source code from a file and creates a shader program that is more concise. The project structure using the shader class is:
Using shader classes to create shader code simplifies to:
Shader Shader ("Triangle.vertex", "Triangle.frag");
The shader program only reads the program source code, regardless of the file name and type.
The triangle drawing version and the shader class code implemented using the encapsulated class can be downloaded from GitHub. Add a vertex's color properties
The triangles drawn above, using the colors specified in the element shader, can be assigned to the shader as vertex position properties by specifying the vertex color properties vertex attribute. Modify the Vertex property array data to:
Glfloat vertices[] = {
//vertex coordinate vertex color
-0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f
};
We need to re-specify how OpenGL parses the data and need to update the shader.
STEP1: The data is first re-interpreted by Glvertexattribpointer, and the code becomes:
STEP4: Specify resolution and enable Vertex properties
//Vertex position Properties
glvertexattribpointer (0, 3, gl_float, Gl_false,
6 * sizeof (GL_ FLOAT), (glvoid*) 0);
Glenablevertexattribarray (0);
Vertex Color Properties
glvertexattribpointer (1, 3, Gl_float, Gl_false,
6 * sizeof (Gl_float), (glvoid*) (3 * sizeof (gl_float )));
Glenablevertexattribarray (1);
Note here that the parameters of the Glvertexattribpointer are set, the stride and pointer parameters are interpreted as shown in the following figure (from www.learnopengl.com):
That is, vertex position and color stride is 6 * sizeof (gl_float) = 24, vertex position data header offset is 0, and the color data first address offset is 3 * sizeof (gl_float) = 12.
STEP2 Update the shader, specify an index of 1 for the color attribute in the vertex shader, and the updated vertex shader is:
#version
(location = 0) in vec3 position;
Layout (location = 1) in VEC3 color;
Out VEC3 Vertcolor;
void Main ()
{
gl_position = VEC4 (Position, 1.0);
Vertcolor = color; Output vertex color
}
This is done by defining location = 1 To set the color attribute index to 1 and outputting it directly to the slice shader. The slice shader is:
#version
vec3 Vertcolor;
Out VEC4 color;
void Main ()
{
color = VEC4 (Vertcolor, 1.0);
}
The slice shader accepts the color vertexcolor of the vertex shader output and then directly acts as the final slice color. Note When passing a variable between a vertex shader and a slice shader, the variable must be of the same type and name, such as the Vertexcolor variable here.
The updated program works as shown in the following figure:
The full code can be downloaded from GitHub.
As shown in the figure above, we specify the three colors of red, green and blue in the vertex attribute, which actually generates more slices when the graph is produced, and the color of the slices is generated by color interpolation (interpolation). One method of color interpolation is linear interpolation, such as a line with one end of a point specified as red, the other end as green, and the color in the middle part of the point, which can be followed by the formula:
y=a+ (b−a) ∗t y = a + (b-a) *t
To create a color. When t=0, a value of a indicates that the color is red, the t=1 value is B is green, and when T is between 0 and 1, the final color is generated by blending red and green proportionally. In addition to the vertex color, the rest of the image is worth the color interpolation. The color interpolation theory also has a deep understanding behind it. Quick access to this section of the project using templates
Have a message to ask for the entire project, because GitHub upload binary on the VS project is not very suitable, here made a convenient template for use, can be downloaded from my github. How to use the template:
Step1: The template getting-started.zip copy value vs project template directory, as shown in the following figure:
STEP2: Create a new project using a template, as shown below:
Step3: Copy the libraries to the sibling directory of the new project.
Step4: Compile and run the new project. Learn more about shader compilation and linking Shader Compila