OpenGL learning record (2)

References:
https://learnopengl.com/
https://learnopengl-cn.github.io/

The previous article realized the creation and opening of the OpenGL window. This time, two triangles are drawn in the window.

First, the vertex data. Each vertex is represented by a three-dimensional space coordinate. If you want to draw two adjacent triangles to form a quadrilateral, normally you need to prepare 6 vertices (two pairs of vertices are coincident). The GPU draws in units of triangles. The complete image will contain a large number of triangles, which will lead to a lot of waste of resources. 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, and then we only need to specify the order of drawing. The role of EBO is to achieve this purpose, which will be written later. So to draw a quadrilateral composed of two triangles, you can first declare four vertices:

//顶点数据
float vertices[] = {
    
    
	-0.5f, -0.5f, 0.0f,  	 //0
	0.5f, -0.5f, 0.0f,		 //1
	0.0f, 0.5f, 0.0f,		 //2
	//0.5f, -0.5f, 0.0f,	 
	//0.0f, 0.5f, 0.0f,		 
	0.8f, 0.8f, 0.0f		 //3
};

Then the vertex index tells OpenGL which vertices the two triangles are composed of:

//顶点索引
unsigned int indices[] = {
    
    
	0, 1, 2,   //第一个三角形使用的顶点
	2, 1, 3    //第二个三角形使用的顶点
};

Modern OpenGL requires us to set up at least one vertex and one fragment shader if we want to implement rendering functions. These two shaders need to be written in the GLSL language. In order for OpenGL to use it, we must dynamically compile its source code at runtime, so use the C-style string hard-coded form to declare. Take the vertex shader as an example, #version 330 core means to use GLSL3.3 version, which matches the OpenGL version, and uses the core mode. layout(location = 0) sets the location value of the input variable. in indicates the input vertex attribute, vec3 indicates that the input vertex is represented by a three-dimensional vector, and aPos is the variable name. gl_Position is a predefined vertex shader output variable. It is a four-dimensional variable, so the first three dimensions are occupied by the x, y, and z components of aPos, and the last bit is given a fixed value of 1.0. In the fragment shader, out is used to declare the output variable. We want to output an RGBA four-dimensional color vector, and the output color value can be assigned in the main function.

//顶点着色器编码
const char* vertexShaderSource =
"#version 330 core										 \n		  "
"layout(location = 0) in vec3 aPos;						 \n		  "
"void main() {											 \n		  "
"	gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);     \n	      "
"}														 \n		  ";

//片元着色器编码
const char* fragmentShaderSource =
"#version 330 core										 \n		  "
"out vec4 FragColor;									 \n	      "
"void main() {											 \n		  "
"	FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);		     \n		  "
"}														 \n		  ";

Next, the hardcoded vertex and fragment shaders need to be compiled. First define an unsigned int type variable, use glCreateShader to create a shader, its parameters require specifying the type of shader to be created, GL_VERTEX_SHADER means to create a vertex Shader, and GL_FRAGMENT_SHADER means to create a fragment Shader. After creating the corresponding Shader, use the glShaderSource method to attach the shader source code to the shader object. Parameter 1 is the shader object to be compiled, parameter 2 is the number of source strings specified to be passed, and parameter 3 is the real vertex shader. Source code, parameter 4 is an array specifying the length of the string, and nullptr means that each string ends with null. Finally, use glCompileShader to pass in the shader object for compilation.

//编译顶点着色器
unsigned int vertexShader; //定义
vertexShader = glCreateShader(GL_VERTEX_SHADER); //创建着色器,类型为顶点Shader,返回给定义的vertexShader
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr); //把着色器源码附加到着色器对象上,参数1是要编译的着色器对象,参数2是指定传递的源码字符串数量,参数3是顶点着色器真正的源码,参数4是指定字符串长度的数组,为nullptr表示每个字符串都以null结尾
glCompileShader(vertexShader); //编译Shader

//编译片元着色器
unsigned int fragmentShader; //定义
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //创建着色器,类型为片元Shader,返回给定义的fragmentShader
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr); //把着色器源码附加到着色器对象上
glCompileShader(fragmentShader); //编译Shader

A few concepts need to be introduced next. Vertex Buffer Objects (VBO), which will store a large number of vertices in the GPU memory, so that we can send a large amount of data to the graphics card at one time, instead of sending each vertex once, so its existence is necessary . Specifically in the code, first declare the VBO object with unsigned int, and then use glGenBuffers to generate the buffer object. The first parameter is the number of buffer objects to be generated, and the second is to input the array used to store the name of the buffer object. Because Just create a VBO, so there is no need for an array. Next use glBindBuffer to bind the newly created buffer to the GL_ARRAY_BUFFER target. OpenGL has many buffer object types, and the buffer type of the vertex buffer object is GL_ARRAY_BUFFER. OpenGL allows us to bind multiple buffers at the same time, as long as they are different buffer types. After binding VBO to GL_ARRAY_BUFFER, the last step is to copy the defined data to the currently bound buffer. Use the glBufferData method, parameter 1: the target buffer type, that is, GL_ARRAY_BUFFER, parameter 2: specify the size of the transmitted data (in bytes), that is, the size of the vertex array vertices, parameter 3: the actual data we want to send, that is, the vertex array , Parameter 4: Specify how the graphics card manages the given data. GL_STATIC_DRAW means that the data will not or will not change. Optional: GL_DYNAMIC_DRAW means that the data will be changed a lot; GL_STREAM_DRAW means that the data will change every time it is drawn.

//创建VBO(顶点缓冲对象)
unsigned int VBO;
glGenBuffers(1, &VBO); //生成缓冲区对象。第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的数组,由于只需创建一个VBO,因此不需要用数组形式
glBindBuffer(GL_ARRAY_BUFFER, VBO); //把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上,GL_ARRAY_BUFFER是一种顶点缓冲对象的缓冲类型。OpenGL允许同时绑定多个缓冲,只要它们是不同的缓冲类型。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //把定义的数据复制到当前绑定缓冲的函数。参数1:目标缓冲类型,参数2:指定传输数据的大小(以字节为单位),参数3:我们希望发送的实际数据,参数4:指定显卡如何管理给定的数据,GL_STATIC_DRAW表示数据不会或几乎不会改变。

Vertex Array Object (VAO), which encapsulates all data related to the vertex processor, it only records references to the vertex buffer and index buffer, as well as the layout of various attributes of the vertex rather than the actual data . VAOs can be bound like VBOs, and any subsequent vertex attribute calls will be stored in this VAO. The advantage of this is that when configuring the vertex attribute pointer, you only need to execute those calls once, and then you only need to bind the corresponding VAO when drawing the object. This makes it very simple to switch between different vertex data and attribute configurations, just by binding different VAOs. All the state just set will be stored in the VAO. When coding in OpenGL core mode, we are required to use VAOs.
Similar to creating a VBO, first declare, then create, and then bind, but the functions used are different, which are the creation function glGenVertexArrays and the binding function glBindVertexArray related to the vertex array object.

//创建VAO(顶点数组对象)
unsigned int VAO;
glGenVertexArrays(1, &VAO); //生成一个顶点数组对象
glBindVertexArray(VAO); //绑定VAO

Element Buffer Object (EBO), also called Index Buffer Object (IBO). By using EBO, the purpose of saving the number of vertices mentioned at the beginning and drawing triangles by vertex index can be achieved. Its creation method is basically the same as VBO, except that the buffer type must be defined as GL_ELEMENT_ARRAY_BUFFER.

//创建EBO(元素缓冲对象/索引缓冲对象)
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

The next step is to create the shader program object. After the declaration, use the glCreateProgram method to create a program object, return an ID reference, and assign it to the declared shaderProgram. Then use glAttachShader to attach the previously compiled vertex and fragment shaders to the program object, and finally link to a shader program object through glLinkProgram.

//创建着色器程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram(); //创建程序对象
glAttachShader(shaderProgram, vertexShader); //把之前编译的顶点着色器附加到程序对象上
glAttachShader(shaderProgram, fragmentShader); //把之前编译的片元着色器附加到程序对象上
glLinkProgram(shaderProgram); //链接为一个着色器程序对象

Vertex attributes also need to be linked later. Vertex shaders allow us to specify any input in the form of vertex attributes. While this makes it very flexible, it does mean that we have to manually specify which part of the input data corresponds to which vertex attribute in the vertex shader. Therefore, we must specify how OpenGL should interpret vertex data before rendering.
Vertex buffer data is of the following form:
insert image description here
it is characterized in that position data is stored as 32-bit (4-byte) floating point values, each position contains 3 such values, and there are no gaps (or other value). These several values ​​are arranged tightly in the array (Tightly Packed), and the first value in the data is at the beginning of the buffer. Other data, such as UV, can also be inserted between different vertices, but now for the purpose of simply drawing triangles, only vertex coordinate data is required. For telling OpenGL how to parse the vertex data, you need to use the following two lines of code. glVertexAttribPointer: Tells OpenGL how to parse the vertex data (applies to each vertex attribute). The first parameter is the vertex attribute to be configured. Since the layout (location = 0) was declared when the vertex shader was hard-coded before, fill in 0; the second parameter is to specify the size of the vertex attribute, since the vertex is of type vec3 , which consists of three values, so it is 3; the third parameter is the type of the specified data, vec* in GLSL is composed of floating point values, so it is GL_FLOAT; the fourth parameter is whether you want the data to be standardized, that is Normalize, GL_FALSE is used here; the fifth parameter is the interval between consecutive vertex attribute groups. Since no information such as uv is used, the interval is only 3 floats; the sixth parameter indicates that the position data starts in the buffer The offset of the position (Offset), since the position data is at the beginning of the array, it is 0 here. Finally, use the glEnableVertexAttribArray method to use the vertex attribute position value as a parameter to enable the vertex attribute. Since the layout (location = 0) is declared earlier, it is 0, which is consistent with the first parameter in the glVertexAttribPointer method.

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)。参数1:要配置的顶点属性(layout(location = 0),故0),参数2:指定顶点属性的大小(vec3,故3),参数3:指定数据的类型,参数4:是否希望数据被标准化,参数5:在连续的顶点属性组之间的间隔,参数6:表示位置数据在缓冲中起始位置的偏移量(Offset)。
glEnableVertexAttribArray(0); //以顶点属性位置值作为参数,启用顶点属性,由于前面声明了layout(location = 0),故为0

Finally, in the code for looping rendering and drawing, the following steps are performed each time: glUseProgram(shaderProgram), each shader call and rendering call will use this program object; glBindVertexArray(VAO), when planning to draw multiple objects, first of all Generate/configure all VAOs (and necessary VBOs and property pointers), and then store them for later use. When drawing an object, take out the corresponding VAO, bind it, and unbind the VAO after drawing the object; glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO), bind EBO; glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0), draw, the first parameter is the drawing model, here select the triangle, the second parameter is the number of vertices to be drawn, draw two The triangle is 6 vertices, the third parameter is the index type GL_UNSIGNED_INT, and the fourth parameter specifies the offset in EBO, which is set to 0.

glUseProgram(shaderProgram); //调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)
glBindVertexArray(VAO); //绘制物体的时候就拿出相应的VAO,绑定它
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //绑定EBO
//glDrawArrays(GL_TRIANGLES, 0, 6);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //绘制,参数1:绘制模式(三角形),参数2:绘制顶点数(两个三角形6个顶点),参数3:索引的类型,参数4:指定EBO中的偏移量。

In this way, two triangles can be drawn in the OpenGL window. The overall code is as follows:

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>

//顶点数据
float vertices[] = {
    
    
	-0.5f, -0.5f, 0.0f,  	 //0
	0.5f, -0.5f, 0.0f,		 //1
	0.0f, 0.5f, 0.0f,		 //2
	//0.5f, -0.5f, 0.0f,	 
	//0.0f, 0.5f, 0.0f,		 
	0.8f, 0.8f, 0.0f		 //3
};

//顶点索引
unsigned int indices[] = {
    
    
	0, 1, 2,   //第一个三角形使用的顶点
	2, 1, 3    //第二个三角形使用的顶点
};

//顶点着色器编码
const char* vertexShaderSource =
"#version 330 core										 \n		  "
"layout(location = 0) in vec3 aPos;						 \n		  "
"void main() {											 \n		  "
"	gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);     \n	      "
"}														 \n		  ";

//片元着色器编码
const char* fragmentShaderSource =
"#version 330 core										 \n		  "
"out vec4 FragColor;									 \n	      "
"void main() {											 \n		  "
"	FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);		     \n		  "
"}														 \n		  ";


//检查输入函数
void processInput(GLFWwindow* window)
{
    
    
	//按下ESC键时,将WindowShouldClose设为true,循环绘制将停止
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
	{
    
    
		glfwSetWindowShouldClose(window, true);
	}
}

//视口改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    
    
	glViewport(0, 0, width, height); //OpenGL渲染窗口的尺寸大小
}

int main() 
{
    
    
	glfwInit(); //初始化GLFW
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //告诉GLFW要使用OpenGL的版本号
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //主版本号、次版本号都为3,即3.3版本
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //告诉GLFW使用核心模式(Core-profile)

	//打开 GLFW Window
	GLFWwindow* window = glfwCreateWindow(1600, 1200, "My OpenGL Game", nullptr, nullptr);
	if (window == nullptr) //若窗口创建失败,打印错误信息,终止GLFW并return -1
	{
    
    
		printf("Open window failed.");
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window); //创建OpenGL上下文
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //用户改变窗口大小的时候,视口调用回调函数

	//初始化GLEW
	glewExperimental = true;
	if (glewInit() != GLEW_OK) //若GLEW初始化失败,打印错误信息并终止GLFW窗口
	{
    
    
		printf("Init GLEW failed.");
		glfwTerminate();
		return -1;
	}

	//创建VAO(顶点数组对象)
	unsigned int VAO;
	glGenVertexArrays(1, &VAO); //生成一个顶点数组对象
	glBindVertexArray(VAO); //绑定VAO

	//创建VBO(顶点缓冲对象)
	unsigned int VBO;
	glGenBuffers(1, &VBO); //生成缓冲区对象。第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的数组,由于只需创建一个VBO,因此不需要用数组形式
	glBindBuffer(GL_ARRAY_BUFFER, VBO); //把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上,GL_ARRAY_BUFFER是一种顶点缓冲对象的缓冲类型。OpenGL允许同时绑定多个缓冲,只要它们是不同的缓冲类型。
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //把定义的数据复制到当前绑定缓冲的函数。参数1:目标缓冲类型,参数2:指定传输数据的大小(以字节为单位),参数3:我们希望发送的实际数据,参数4:指定显卡如何管理给定的数据,GL_STATIC_DRAW表示数据不会或几乎不会改变。

	//创建EBO(元素缓冲对象/索引缓冲对象)
	unsigned int EBO;
	glGenBuffers(1, &EBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

	//编译顶点着色器
	unsigned int vertexShader; //定义
	vertexShader = glCreateShader(GL_VERTEX_SHADER); //创建着色器,类型为顶点Shader,返回给定义的vertexShader
	glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr); //把着色器源码附加到着色器对象上,参数1是要编译的着色器对象,参数2是指定传递的源码字符串数量,参数3是顶点着色器真正的源码,参数4是指定字符串长度的数组,为nullptr表示每个字符串都以null结尾
	glCompileShader(vertexShader); //编译Shader

	//编译片元着色器
	unsigned int fragmentShader; //定义
	fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //创建着色器,类型为片元Shader,返回给定义的fragmentShader
	glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr); //把着色器源码附加到着色器对象上
	glCompileShader(fragmentShader); //编译Shader

	//创建着色器程序对象
	unsigned int shaderProgram;
	shaderProgram = glCreateProgram(); //创建程序对象
	glAttachShader(shaderProgram, vertexShader); //把之前编译的顶点着色器附加到程序对象上
	glAttachShader(shaderProgram, fragmentShader); //把之前编译的片元着色器附加到程序对象上
	glLinkProgram(shaderProgram); //链接为一个着色器程序对象

	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)。参数1:要配置的顶点属性(layout(location = 0),故0),参数2:指定顶点属性的大小(vec3,故3),参数3:指定数据的类型,参数4:是否希望数据被标准化,参数5:在连续的顶点属性组之间的间隔,参数6:表示位置数据在缓冲中起始位置的偏移量(Offset)。
	glEnableVertexAttribArray(0); //以顶点属性位置值作为参数,启用顶点属性,由于前面声明了layout(location = 0),故为0



	//让程序在手动关闭之前不断绘制图像
	while (!glfwWindowShouldClose(window))
	{
    
    
		//检测输入
		processInput(window);

		//渲染指令
		glClearColor(0.f, 0.5f, 0.5f, 1.0f); //设置清空屏幕所用的颜色
		glClear(GL_COLOR_BUFFER_BIT); //清空屏幕的颜色缓冲区


		glUseProgram(shaderProgram); //调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)
		glBindVertexArray(VAO); //绘制物体的时候就拿出相应的VAO,绑定它
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //绑定EBO
		//glDrawArrays(GL_TRIANGLES, 0, 6);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //绘制,参数1:绘制模式(三角形),参数2:绘制顶点数(两个三角形6个顶点),参数3:索引的类型,参数4:指定EBO中的偏移量。


		//检查并调用事件,交换缓冲区
		glfwSwapBuffers(window); //交换颜色缓冲区,前缓冲区保存最终输出的图像,后缓冲区负责绘制渲染指令,当渲染指令执行完毕后,交换前后缓冲区,使完整图像呈现出来,避免逐像素绘制图案时的割裂感
		glfwPollEvents(); //检查触发事件,如键盘输入、鼠标移动等
	}


	glfwTerminate(); //关闭GLFW并退出
	return 0;
}

Guess you like

Origin blog.csdn.net/weixin_47260762/article/details/128172100