上一节我们使用了GLFW和GLAD。接下来我们来绘制三角形。在OpenGL中,最小绘制对象是三角面片。所有的对象都是由若干个三角面片组成的。绘制也比较简单,我们只需确定顶点以及其每个顶点的颜色就可以绘制了!
绘制过程如上,我们需要定义顶点的数据,然后交给六个着色器处理。其中顶点、集合和片段是我们可以人为自定义的阶段。着色器非常重要,他是运行在GPU的小程序。用于渲染图形。在OpenGL中,我们使用GLSL着色器语言。它类似于C语言,还是比较好上手的。
基本过程就是,定义顶点数据->顶点着色(绘制每个顶点的位置 )->图元装配(根据绘制类型装配图元,后面会解释)->几何着色器->光栅化(确定图形的像素)->片元着色(绘制每个像素的颜色 )->测试混合
所以,顶点着色器的对象是每个顶点,片元着色器的对象是每个像素! 这点很重要。也就是说,我们会对每个顶点执行一次顶点着色器的程序,会对每个像素执行片元着色器。一般来说,像素的数目大于顶点的数目。所以片元着色器一般计算量会大于顶点着色器。
创建顶点 首先创建定义,利用数组来定义。因为我们绘制的是三角形,所以我们需要三个顶点。 我们用float数组声明顶点信息,数组内的每个元素最终组成三个三维向量(vec3):
1 2 3 4 5 float vertices[] = { -0.5f , -0.5f , 0.0f , 0.0f , 0.5f , 0.0f , 0.5f , -0.5f , 0.0f };
这些顶点基本上确定了。
顶点着色器 有了顶点,就需要让GPU来绘制顶点。顶点着色器的工作就是这样,这是最简单的绘制顶点的顶点着色器代码:
1 2 3 4 5 6 7 8 9 #version 330 core layout (location = 0 ) in vec3 aPos;out vec3 color;void main() { gl_Position = vec4 (aPos, 1.0 f); color = vec3 (0.2 , 0.3 , 1.0 ); }
第一句声明了版本,为3.3core版本。第二句读取我们刚刚的顶点,in表示输入变量,表示这个变量是由前面的步骤得到的变量,vec3表示三维向量,aPos是我们的变量名。aPos这个名字不定,只要符合规范,什么名字都可以。layout和location我们会在vao中讲解。
out为输出变量,表示给之后阶段用的变量。因为后面涉及到上色,所以可以提前在顶点着色器中先定义颜色(这里定义了蓝色的颜色)。
主函数中,我们将向量赋值给了gl_Position,这是glsl的内建变量,它表示的是顶点的屏幕坐标。这里提一嘴,屏幕坐标采用的是NDC坐标,这个坐标的范围$x\in[-1, 1], y\in[-1, 1]$,并且相对于你的窗口大小(不是屏幕的绝对大小!)。
片元着色器 若干个操作以后,来到了上色阶段。片元着色器就是用来计算颜色的。
1 2 3 4 5 6 7 8 #version 330 core in vec3 color;out vec4 FragColor;void main() { FragColor = vec4 (color, 1.0 f); }
in变量接受了之前在顶点着色器中输出的变量color,out表示最终的颜色FragColor(这个名字也可以随便命名)。提一嘴,在不改变帧缓冲的前提下,片元着色器只有一个输出变量!
将着色器编译 着色器的代码主要是字符串的形式,如下代码编译了着色器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const char *vshSource = "#version 330 core\n" "layout (location = 0 ) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4 (aPos.x, aPos.y, aPos.z, 1.0 );\n" "}\0 "; const char *fshSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4 (1.0 f, 0.5 f, 0.2 f, 1.0 f);\n" "}\n\0 "; unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1 , &vshSource, nullptr); glCompileShader(vertexShader); unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1 , &fshSource, nullptr); glCompileShader(fragmentShader); unsigned int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram);
在之后绘制图形之前,需要先绑定着色器状态:
1 glUseProgram(shaderProgram)
顶点数组对象——VAO 这里是难以理解的一部分。究竟什么是VAO和VBO?
VAO是顶点数组对象,VBO是顶点缓冲对象。那么它们的存在意义是什么?
假设我需要绘制一个三角形和一个矩形,在绘制的时候,怎么确定我要绘制的图形是三角形还是矩形?
VAO就是处理这种情况,在这个时候,只需要先将状态切换到三角形的VAO然后绘制。这样就可以确定我绘制的图形就是三角形。如果需要绘制矩形,那就需要将状态切换到矩形的VAO,然后绘制矩形。
简单来说,VAO确定绘制的图形。 并且,切换到对应图形的VAO状态之后,就可以解释顶点数据了。那为什么要解释顶点数据呢?
我们刚才定义了三角形的顶点,由一个一维数组存储。但OpenGL可不认识里面的数据,也就是不理解这个数据的意义是什么。试想一下,如果我不告诉你刚才的数组中,每三个元素组成一个三角形的坐标,你还会知道这些数据的意义是啥吗?当然不知道啊。所以我们需要一个能够解释顶点数据的“媒介”,这个媒介就是VAO。VAO就是为了解释顶点数据的。
既然如此,我们就要先定义VAO:
1 2 3 4 unsigned int vao; glGenVertexArrays (1 , &vao); glBindVertexArray (vao);
首先声明vao变量,它是无符号整型,但这vao没有任何效果,只是个普通变量,需要生成才能成为真正的VAO。在生成后,接下来绑定状态,将OpenGL状态机切换到三角形的vao状态。因为之后我们要对三角形的顶点数据进行解释。
1 2 3 glVertexAttribPointer (0 , 3 , GL_FLOAT, GL_FALSE, 3 * sizeof (float ), (void *) nullptr );glEnableVertexAttribArray (0 );
第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)
定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0
。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0
。
第二个参数指定顶点属性的大小。顶点属性是一个vec3
,它由3个值组成,所以大小是3。
第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*
都是由浮点数值组成的)。
下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float
之后,我们把步长设置为3 * sizeof(float)
。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
最后一个参数的类型是void*
,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。
顶点缓冲对象—VBO 有了顶点数据,我们需要传输到GPU去计算。所以需要一个媒介——VBO,它是一种缓冲数据流,可以一次性传输多个数据,这样效率比较高。
一般这样定义一个VBO:
1 2 3 4 5 6 unsigned int vbo; glGenBuffers (1 , &vbo); glBindBuffer (GL_ARRAY_BUFFER, vbo); glBufferData (GL_ARRAY_BUFFER, sizeof (vertices), vertices,GL_STATIC_DRAW);
首先声明vbo变量,它是无符号整型,但这vbo没有任何效果,只是个普通变量,需要生成然后绑定才能成为真正的VBO。然后生成一个VBO变量,并将他绑定为数组缓冲数据流。
最后传输数据,给出顶点数组的大小,顶点数组,以及绘制这些顶点的类型(静态还是动态)。至此,数据已经由VBO传到了GPU。
绘制图形 我们在while循环持续绘制图形:
1 2 3 4 5 6 7 8 9 10 11 12 13 while (!glfwWindowShouldClose (window)) { glClearColor (0.2f , 0.3f , 0.3f , 1.0f ); glClear (GL_COLOR_BUFFER_BIT); glUseProgram (shaderProgram); glBindVertexArray (vao); glDrawArrays (GL_TRIANGLES, 0 , 3 ); glfwSwapBuffers (window); glfwPollEvents (); }
需要注意的是,由于OpenGL是状态机,所以要绘制什么样的图形,传输什么样的数据到着色器,都要先做出相应的绑定!
glDrawArrays函数就是根据顶点数据的数组来绘制的图形,参数和功能如下:
1 2 3 4 void glDrawArrays (GLenum mode,GLint first, GLsizei count) ;
收尾工作 最后需要删除着色器程序(内存回收工作),并且关闭glfw窗口:
1 2 3 4 glDeleteProgram (shaderProgram);glDeleteShader (vertexShader);glDeleteShader (fragmentShader);glfwTerminate ();
所有代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 #include <iostream> #include "glad/glad.h" #include "GLFW/glfw3.h" int main () { glfwInit (); glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 3 ); glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 3 ); glfwWindowHint (GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow *window = glfwCreateWindow (800 , 600 , "三角形" , nullptr , nullptr ); if (window == nullptr ) { std::cout << "Error: Fail to create window! \n" ; glfwTerminate (); return -1 ; } glfwMakeContextCurrent (window); if (!gladLoadGLLoader ((GLADloadproc) glfwGetProcAddress)) { std::cout << "Error: Fail to initialize GLAD! \n" ; return -1 ; } float vertices[] = { -0.5f , -0.5f , 0.0f , 0.0f , 0.5f , 0.0f , 0.5f , -0.5f , 0.0f }; unsigned int vbo; glGenBuffers (1 , &vbo); glBindBuffer (GL_ARRAY_BUFFER, vbo); glBufferData (GL_ARRAY_BUFFER, sizeof (vertices), vertices, GL_STATIC_DRAW); unsigned int vao; glGenVertexArrays (1 , &vao); glBindVertexArray (vao); glEnableVertexAttribArray (0 ); glVertexAttribPointer (0 , 3 , GL_FLOAT, GL_FALSE, 3 * sizeof (float ), (void *) nullptr ); const char *vshSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" "}\0" ; const char *fshSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}\n\0" ; unsigned int vertexShader = glCreateShader (GL_VERTEX_SHADER); glShaderSource (vertexShader, 1 , &vshSource, nullptr ); glCompileShader (vertexShader); unsigned int fragmentShader = glCreateShader (GL_FRAGMENT_SHADER); glShaderSource (fragmentShader, 1 , &fshSource, nullptr ); glCompileShader (fragmentShader); unsigned int shaderProgram = glCreateProgram (); glAttachShader (shaderProgram, vertexShader); glAttachShader (shaderProgram, fragmentShader); glLinkProgram (shaderProgram); while (!glfwWindowShouldClose (window)) { glClearColor (0.2f , 0.3f , 0.3f , 1.0f ); glClear (GL_COLOR_BUFFER_BIT); glUseProgram (shaderProgram); glBindVertexArray (vao); glDrawArrays (GL_TRIANGLES, 0 , 3 ); glfwSwapBuffers (window); glfwPollEvents (); } glDeleteProgram (shaderProgram); glDeleteShader (vertexShader); glDeleteShader (fragmentShader); glfwTerminate (); }