OpenGL(四)
以下内容均来自处网站:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
本文的主要内容是绘制第一个OpenGL的三角形。
首先记住三个词:
顶点数组对象:Vertex Array Object,VAO 顶点缓冲对象:Vertex Buffer Object,VBO 索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO
OpenGL中的事物都是三维的。但是屏幕是二维的,导致OpenGL的大部分工作是将3维坐标转化为适应于屏幕的2维坐标。三维坐标转二维坐标的过程是由OpenGL的图形渲染管线管理的。
图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
图形渲染管线的几个阶段:输入的顶点数据 --> 顶点着色器 (vertex shader) --> 形状(图元)装配(shape assembly)--> 几何着色器(geometry shader)--> 光栅化(rasterization)--> 片段着色器(fragment shader) --> 测试与混合
其中顶点着色器、几何着色器、片段着色器是可以自定义的着色器,而自定义的过程需要使用OpenGL着色器语言(GLSL)。
下面是过程的一些介绍:
顶点数据:以数组形式传递三个三维坐标,用来表示一个三角形,这个数组叫做顶点数据;顶点数据是一系列顶点的集合,而一个顶点是一个坐标数据的集合。顶点数据用顶点属性表示,可以包含我们想要的任意数据,这里为了简单,只包含了坐标数据。
顶点着色器:把一个单独的顶点作为输入,主要的目的是把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。
图元装配阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状;本节例子中是一个三角形。注:数据进来后,OpenGL并不知道该把数据渲染成什么样子,需要我们主动告诉OpenGL是渲染成点、线还是三角形,这些是基本的图元。这是其中的几个绘制的参数:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。
几何着色器:几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。
光栅化:几何着色器的输出会被传入光栅化阶段,这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段。在片段着色器运行之前会执行裁切。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。注:OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据。
片段着色器:主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
混合和测试:这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
可以看到,图形渲染管线非常复杂,它包含很多可配置的部分。然而,对于大多数场合,我们只需要配置顶点和片段着色器就行了。几何着色器是可选的,通常使用它默认的着色器就行了。在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。
1、顶点输入与顶点缓冲对象
开始绘制之前,先准备一些顶点数据,顶点的坐标是三维的,但是三维坐标不是任意的,只有在标准化设备坐标(Normalized Device Coordinate,NDC)下才会显示在屏幕上,超出这个范围的坐标不会被显示。这里所谓的标准化设备坐标指的是x,y,z的坐标范围都在-1到1之间的坐标。
这里我们以标准化坐标的形式准备一个顶点数组。如下:
float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f };
这样的顶点数据会被发送到顶点着色器。会在GPU中创建内存来存储顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给我们的显卡。
我们通过顶点缓冲对象VBO来管理这个内存,它会在GPU内存(显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这个过程非常快。
VBO是目前OpenGL教程中出现的第一OpenGL个对象,就像OpenGL里的其他对象一样,这个缓冲有一个独一无二的ID,我们可以使用glGenBuffers函数和一个缓冲ID生成一个VBO对象。
函数原型:void glGenBuffers(GLsizei n, GLuint *buffers)
两个参数:n:Specifies the number of buffer object names to be generated. buffers:Specifies an array in which the generated buffer object names are stored. 第一个参数是要创建的缓冲区对象的数量,第二个参数是用于存储单个ID或多个ID的GLuint变量或数组的地址。
相关链接:https://www.songho.ca/opengl/gl_vbo.html
这里我们只需要生成一个顶点缓冲对象,所以如下写:
unsigned int VBO; glGenBuffers(1, &VBO);
然后绑定缓冲到相应的目标上,使用glBindBuffer函数。
函数原型:void glBindBuffer(GLenum target, GLuint buffer)
两个参数:target: Specifies the target to which the buffer object is bound. The symbolic constant must be GL_ARRAY_BUFFER, GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER,GL_PIXEL_UNPACK_BUFFER, GL_TEXTURE_BUFFER,GL_TRANSFORM_FEEDBACK_BUFFER, or GL_UNIFORM_BUFFER. buffer: Specifies the name of a buffer object.
target: 指定缓冲区对象绑定到的目标。必须是上面那些指定的符号。buffer: 指定的缓冲区对象的名字
这里使用GL_ARRAY_BUFFER,当然OpenGL允许绑定多个缓冲,只要是不同的缓冲类型。代码如下:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
然后使用glBufferData函数将之前定义的数据复制到缓冲的内存中:
函数原型:void glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);
四个参数:target :Specifies the target buffer object. The symbolic constant must be GL_ARRAY_BUFFER,
GL_COPY_READ_BUFFER,
GL_COPY_WRITE_BUFFER,
GL_ELEMENT_ARRAY_BUFFER,
GL_PIXEL_PACK_BUFFER,
GL_PIXEL_UNPACK_BUFFER,
GL_TEXTURE_BUFFER,
GL_TRANSFORM_FEEDBACK_BUFFER, or GL_UNIFORM_BUFFER. size:Specifies the size in bytes of the buffer object's new data store. data:Specifies a pointer to data that will be copied into the data store for initialization, or NULL if no data is to be copied. usage:Specifies the expected usage pattern of the data store. The symbolic constant must be GL_STREAM_DRAW,
GL_STREAM_READ,GL_STREAM_COPY, GL_STATIC_DRAW, GL_STATIC_READ, GL_STATIC_COPY, GL_DYNAMIC_DRAW, GL_DYNAMIC_READ, or GL_DYNAMIC_COPY.
第一个参数和上一个函数基本一样,size:指定缓冲区对象的新数据存储区的大小(以字节为单位)。简单地说就是数据大小。data:指定指向将复制到数据存储区以进行初始化的数据的指针,或如果没有要复制的数据的话是null。简单地说是数据的地址。usage:指定数据存储的预期使用模式。必须是指定的这些常量符号。指定了我们希望显卡如何管理给定的数据。代码如下:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
总结一下上面几步使用的函数:glGenBuffers --> glBindBuffer --> glBufferData (生成对象 ->绑定对象 -> 缓冲数据)
现在我们已经把顶点数据储存在显卡的内存中,用VBO这个顶点缓冲对象管理。下面我们会创建一个顶点着色器和片段着色器来真正处理这些数据。
2、顶点着色器
顶点着色器(vertex shader)是几个可编程着色器中的一个,如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器。
着色器用的语言是OpenGL着色器语言(GLSL,OpenGL Shading Language)。我们使用GLSL编写着色器,然后编译它,这样就能在程序中使用它了。
下面你会看到一个非常基础的GLSL顶点着色器的源代码:
#version 330 core layout (location = 0) in vec3 aPos; void main() { gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); }
GLSL语言的开始都起始于一个版本声明,同时明确表示使用的是核心模式。
layout (location = 0)设置了输入变量的位置值,in关键字说明输入数据,vec3声明输入的是三维向量,代表一个位置。gl_Position是预定义的变量,这是个4维的向量,第四个分量不是用来表示位置的,而是用在透视除法上,此处不再详述。
这已经是最简单的顶点着色器了,我们对输入的数据没有做任何处理,我们自己定义的数据是标准化的设备坐标,但是一般来说输入的数据不是标准化的设备坐标,是需要做些处理转换到OpenGL的可视区域的。
哦,对了,参考的教程里面没有说这个着色器放在了哪里,其实在源码中可以看到,它是放在了一个字符串中,是下面这个样子的。
const char *vertexShaderSource = "#version 330 core " "layout (location = 0) in vec3 aPos; " "void main() " "{ " " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); " "} ";
当然这个字符串是一个C语言风格的字符串,后面章节中会创建自己的着色器类,同时GLSL源码也会写在单独的文件中,用的时候通过文件来读取。
3、编译顶点着色器
为了能够让OpenGL使用我们写的着色器,我们必须在运行时动态编译它的源码。
同创建顶点缓冲对象一样,创建着色器对象的过程差不多。代码如下:
unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER);
函数原型:GLuint glCreateShader(GLenum shaderType);
使用glCreateShader函数来创造一个空的着色器对象,返回一个可以被引用的非零值,其实就是生成一个着色器的ID。参数是指定类型的着色器,必须是下面其中的一个:GL_VERTEX_SHADER, GL_GEOMETRY_SHADER or GL_FRAGMENT_SHADER(这三个分别是顶点着色器、几何着色器、片段着色器)
接下来把着色器的源码附加到着色器对象上,使用glShaderSource函数。
函数原型:
void glShaderSource(GLuint shader, GLsizei count, const GLchar **string, const GLint *length);
四个参数,无返回值,第一个参数是着色器的ID,也就是我们上面创建的vertexShader变量,第二个参数说明源码字符串的数目,第三个参数是真正的源码,第四个暂时不管,设为NULL。代码如下:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
这样的话源码已经附加到着色器对象上了,接下来编译源码,使用glComplieShader函数
函数原型:void glCompileShader(GLuint shader);
参数为着色器对象,无返回值。编译的状态也会被作为着色器状态的一部分来存储,两个状态,GL_TRUE和GL_FALSE。可以通过调用glGetShaderiv函数来来查询状态。
glCompileShader(vertexShader);
监测是否编译成功的代码可以如下:
int success; char infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if(!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED " << infoLog << std::endl; }
4、片段着色器
用于计算最后输出的颜色,创建方法和顶点着色器基本一样。
GLSL源码:
const char *fragmentShaderSource = "#version 330 core " "out vec4 FragColor; " "void main() " "{ " " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); " "} ";
创建和编译着色器的过程不再赘述,直接源码:
unsigned int fragmentShader; fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader);
总结一下创建和编译着色器用到的函数:glCreateShader -> glShaderSource -> glCompileShader
5、着色器程序
上面两个着色器编译后,需要把着色器链接到一起成为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
下面是创建程序对象的代码:
unsigned int shaderProgram; shaderProgram = glCreateProgram();
glCreateProgram创建并返回一个可以引用的着色器程序ID。
然后我们将上面编译的着色器附加到着色器程序上,使用glAttachShader函数。
函数原型:void glAttachShader(GLuint program, GLuint shader)
无返回值,第一个参数是创建的着色器程序,第二个参数是上面编译的着色器。代码如下:
glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader);
附加完成后,我们用glLinkProgram链接它们。
函数原型:void glLinkProgram(GLuint program)
无返回值,参数是已经附加了编译的着色器的着色器程序。
glLinkProgram(shaderProgram);
得到的结果就是一个程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:
glUseProgram(shaderProgram);
这个是要写到渲染循环里的,每一帧都要调用一次这个函数
最后,在链接之后,着色器对象就没用了,可以删除了:
glDeleteShader(vertexShader); glDeleteShader(fragmentShader);
6、链接顶点属性
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
我们的顶点缓冲数据会被解析为下面这样子:
位置数据被储存为32位(4字节)浮点值。
每个位置包含3个这样的值。
在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
数据中第一个值在缓冲开始的位置。
有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
函数原型:void glVertexAttribPointer( GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid * pointer)
六个参数,如下介绍:
第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用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。我们会在后面详细解释这个参数。
代码如下:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);
注:每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0
现在会链接到它的顶点数据。
现在我们已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,代码会像是这样:
// 0. 复制顶点数组到缓冲中供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 1. 设置顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 2. 当我们渲染一个物体时要使用着色器程序 glUseProgram(shaderProgram); // 3. 绘制物体 someOpenGLFunctionThatDrawsOurTriangle();
每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(这其实并不罕见)。绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态呢?
接下来是顶点数组对象。
7、顶点数组对象
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。
OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。
一个顶点数组对象会储存以下这些内容:
glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
通过glVertexAttribPointer设置的顶点属性配置。
通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
创建一个VAO和创建一个VBO很类似:
unsigned int VAO; glGenVertexArrays(1, &VAO);
要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看起来像这样:
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: .. // 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();
8、绘制图形
要想绘制我们想要的物体,OpenGL给我们提供了glDrawArrays函数,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。
glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3);
使用glDrawArrays进行渲染。
函数原型:void glDrawArrays(GLenum mode, GLint first, GLsizei count)
三个参数,第一个是渲染的类型,点线面之类的,只能从下面几个选项里挑选:GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_LINE_STRIP_ADJACENCY, GL_LINES_ADJACENCY, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_TRIANGLE_STRIP_ADJACENCY, GL_TRIANGLES_ADJACENCY , GL_PATCHES 。第二个参数是指定数组中第一个绘制的索引。第三个是指定要渲染的索引数,也就是要绘制几个顶点。
这里我们绘制三角形,从第一个索引开始,绘制三个顶点。编译成功会出现下面的结果:
所有代码:
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow *window); // settings const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; const char *vertexShaderSource = "#version 330 core " "layout (location = 0) in vec3 aPos; " "void main() " "{ " " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); " "} "; const char *fragmentShaderSource = "#version 330 core " "out vec4 FragColor; " "void main() " "{ " " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); " "} "; int main() { // glfw: 初始化和配置GLFW // ------------------------------ glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif // 创建glfw窗口 // -------------------- GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); // glad: 加载所有的OpenGL函数指针 // --------------------------------------- if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // build and compile我们的着色器程序 // ------------------------------------ // 顶点着色器 int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader); // 检查着色器编译错误 int success; char infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED " << infoLog << std::endl; } // 片段着色器 int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader); // 检查着色器编译错误 glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED " << infoLog << std::endl; } // 链接着色器 int 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 " << infoLog << std::endl; } glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // 设置顶点数据(和缓冲),配置顶点属性 // ------------------------------------------------------------------ float vertices[] = { -0.5f, -0.5f, 0.0f, // left 0.5f, -0.5f, 0.0f, // right 0.0f, 0.5f, 0.0f // top }; unsigned int VBO, VAO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); // 先绑定顶点数组对象(VAO),然后绑定和设置顶点缓冲,再配置顶点属性。 glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 注意这是允许的, 对glVertexAttributePointer的调用将VBO注册为顶点属性的绑定顶点缓冲区对象,为的是我们以后能够安全的解绑 glBindBuffer(GL_ARRAY_BUFFER, 0); // 您可以在以后解除绑定VAO,这样其他VAO调用不会意外地修改此VAO,但这种情况很少发生。 //修改其他vao无论如何都需要调用glBindVertexArray,因此我们通常不会在不直接需要的情况下解除vao(或vbo)的绑定。 glBindVertexArray(0); // 取消注释此调用以绘制线框多边形。 //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 渲染循环 // ----------- while (!glfwWindowShouldClose(window)) { // 处理输入 // ----- processInput(window); // 渲染 // ------ glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 绘制第一个三角形 glUseProgram(shaderProgram); glBindVertexArray(VAO); // 因为我们只有一个VAO,所以没有必要每次都绑定它,但我们会这样做,以使事情更有条理 glDrawArrays(GL_TRIANGLES, 0, 3); // glBindVertexArray(0); // 不必每次都解绑 // glfw: 交换缓冲区和处理IO事件(按键按下/释放、鼠标移动等) // ------------------------------------------------------------------------------- glfwSwapBuffers(window); glfwPollEvents(); } // 可选:一旦资源超出其用途,则取消分配所有资源 // ------------------------------------------------------------------------ glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteProgram(shaderProgram); // glfw: 终止,清除所有先前分配的GLFW资源。 // ------------------------------------------------------------------ glfwTerminate(); return 0; } // 处理所有输入:查询GLFW是否在此帧按下/释放相关键并做出相应反应 // --------------------------------------------------------------------------------------------------------- void processInput(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); } // glfw: 每当窗口大小改变(通过操作系统或用户调整大小),这个回调函数就会执行 // --------------------------------------------------------------------------------------------- void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // 确保视口与新的窗口尺寸匹配;注意宽度和高度将显著大于视网膜显示上指定的尺寸。 glViewport(0, 0, width, height); }