学习资料来源:LearnOpenGL CN
OpenGL简介
OpenGL是什么
一般它被认为是一个API(Application Programming Interface, 应用程序编程接口),包含了一系列可以操作图形、图像的函数。然而,OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范(Specification)。
OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由OpenGL库的开发者自行决定(译注:这里开发者是指编写OpenGL库的人)。因为OpenGL规范并没有规定实现的细节,具体的OpenGL库允许使用不同的实现,只要其功能和结果与规范相匹配(亦即,作为用户不会感受到功能上的差异)。
实际的OpenGL库的开发者通常是显卡的生产商。你购买的显卡所支持的OpenGL版本都为这个系列的显卡专门开发的。当你使用Apple系统的时候,OpenGL库是由Apple自身维护的。在Linux下,有显卡生产商提供的OpenGL库,也有一些爱好者改编的版本。这也意味着任何时候OpenGL库表现的行为与规范规定的不一致时,基本都是库的开发者留下的bug。
立即渲染模式与核心模式
早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便。OpenGL的大多数功能都被库隐藏起来,开发者很少有控制OpenGL如何进行计算的自由。而开发者迫切希望能有更多的灵活性。随着时间推移,规范越来越灵活,开发者对绘图细节有了更多的掌控。立即渲染模式确实容易使用和理解,但是效率太低。因此从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。
当使用OpenGL的核心模式时,OpenGL迫使我们使用现代的函数。当我们试图使用一个已废弃的函数时,OpenGL会抛出一个错误并终止绘图。现代函数的优势是更高的灵活性和效率,然而也更难于学习。立即渲染模式从OpenGL实际运作中抽象掉了很多细节,因此它在易于学习的同时,也很难让人去把握OpenGL具体是如何运作的。现代函数要求使用者真正理解OpenGL和图形编程,它有一些难度,然而提供了更多的灵活性,更高的效率,更重要的是可以更深入的理解图形编程。
OpenGL实现:状态机
OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。
假设当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL状态,从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。
当使用OpenGL的时候,我们会遇到一些状态设置函数(State-changing Function),这类函数将会改变上下文。以及状态使用函数(State-using Function),这类函数会根据当前OpenGL的状态执行一些操作。只要你记住OpenGL本质上是个大状态机,就能更容易理解它的大部分特性。
使用GLFW创建窗口
在官网下载源码,在本地使用CMake编译、生成即可使用GLFW库。
使用GLAD获取OpenGL函数地址
为OpenGL只是一个标准/规范,具体的实现是由驱动开发商针对特定显卡实现的。由于OpenGL驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询。有些库能简化此过程,其中GLAD是目前最新,也是最流行的库。
打开GLAD的在线服务,将语言(Language)设置为C/C++,在API选项中,选择3.3以上的OpenGL(gl)版本(我们的教程中将使用3.3版本,但更新的版本也能正常工作)。之后将模式(Profile)设置为Core,并且保证生成加载器(Generate a loader)的选项是选中的。现在可以先(暂时)忽略拓展(Extensions)中的内容。都选择完之后,点击生成(Generate)按钮来生成库文件。
GLAD提供了一个zip压缩文件,包含两个头文件目录,和一个glad.c文件。将两个头文件目录(glad和KHR)复制到你的Include文件夹中(或者增加一个额外的项目指向这些目录),并添加glad.c文件到你的工程中。
经过前面的这些步骤之后,你就应该可以将以下的指令加到你的文件顶部了:
1 |
创建窗口
1 |
|
glViewPort函数:前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)。
OpenGL幕后使用glViewport中定义的位置和宽高进行2D坐标的转换,将OpenGL中的位置坐标转换为你的屏幕坐标。例如,OpenGL中的坐标(-0.5, 0.5)有可能(最终)被映射为屏幕中的坐标(200,450)。注意,处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)。
当用户改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),比如上述代码中的framebuffer_size_callback。
注册这个函数,就是告诉GLFW我们希望每当窗口调整大小的时候调用这个函数:
1 | glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); |
当用户改变窗口大小时,将会执行framebuffer_size_callback这个函数。
一些常用回调函数
1 | //窗口大小调整,width和height为调整后的窗口大小 |
更多详细用法见摄像机一节。
渲染循环
我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入。因此,我们需要在程序中添加一个while循环,我们可以把它称之为渲染循环(Render Loop),它能在我们让GLFW退出前一直保持运行。
1 | while(!glfwWindowShouldClose(window)) { |
glfwWindowShouldClose
函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话该函数返回true然后渲染循环便结束了,之后为我们就可以关闭应用程序了。glfwPollEvents
函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态,并调用对应的回调函数(可以通过回调方法手动设置)。glfwSwapBuffers
函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。- 当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。我们可以在
main
函数的最后调用glfwTerminate
函数来完成。 glClear
函数将会把指定缓冲中的颜色全部改为glClearColor
中设定的颜色(RGBA)。对于OpenGL状态机来说,glClearColor
函数是一个状态设置函数,而glClear
函数则是一个状态使用的函数,它使用了当前的状态来获取应该清除为的颜色。
双缓冲
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
至此,窗口创建完成。
渲染管线(Pipeline)
简介
渲染管线由CPU和GPU共同完成,计算机需要从一系列的顶点数据、纹理等信息出发,把这些信息最终转换成一张人眼可以看到的图像。
《Real-Time Rendering》一书中将整个渲染管线分成三个阶段:
- 应用阶段(Application Stage)
- 几何阶段(Geometry Stage)
- 光栅化阶段(Rasterizer Stage)
目前各类软件、商业引擎(Unity,虚幻)中使用的渲染管线均可划分成上述三个大致的阶段。
OpenGL使用的渲染管线具体分为如下六个阶段
着色器(Shader)
图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
有些着色器允许开发者自己配置,可以更细致地让我们控制图形渲染管线中的特定部分,而且因为它们运行在GPU上,所以它们可以给我们节约宝贵的CPU时间。OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的
上图渲染管线中蓝色的三个部分可以由开发者进行自由编辑。
顶点着色器(Vertex Shader)
OpenGL没有提供默认的顶点着色器,必须由开发者完成编写。
- 输入:顶点坐标
- 输出:标准化设备坐标(Normalized Device Coordinates, NDC)
几何着色器(Geometry Shader)
OpenGL提供了默认的几何着色器,因此该着色器对于开发者来说是可选的。
- 输入:之前着色器处理过的输出
- 输出:几何处理后的顶点坐标
片段着色器(Fragment Shader)
OpenGL没有提供默认的片段着色器,必须由开发者完成编写。
- 输入:之前着色器处理过的输出
- 输出:片段颜色
OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据。
随后,片段数据将会进行Alpha测试(Alpha Test),混合(Blending),深度测试(Depth Test),模板测试(Stencil Test)等过程,最后成为显示在屏幕上的像素颜色。
标准化设备坐标(NDC)
一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。
与通常的屏幕坐标不同,y轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。最终你希望所有(变换过的)坐标都在这个坐标空间中,否则它们就不可见了。
你的标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport
函数提供的数据,进行视口变换(Viewport Transform)完成的。所得的屏幕空间坐标又会被变换为片段输入到片段着色器中。
2D渲染代码实现
定义需要渲染的顶点数据:
1 | //三角形三个角的坐标(x,y,z) |
目前是在平面上渲染,则不用考虑z坐标,将z坐标设为0。暂且不考虑在顶点着色器中进行坐标变换,因此直接使用NDC。
顶点缓冲对象(VBO)
定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。
我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
1 | //定义一个VBO |
- OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。
glBufferData
是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。第三个参数是我们希望发送的实际数据。glVertexAttribPointer
函数告诉OpenGL该如何解析顶点数据。
顶点数据可以以我们想要的方式放在数组中传递给缓冲,因此我们需要告诉OpenGL如何解释顶点数据。这是我们的数据解释方式:
该函数的参数列表如下
- 第一个参数指定我们要配置的顶点属性的编号,与下一句中
glEnableVertexAttribArray
的参数相匹配,表示我们配置的是编号为0的顶点属性。 - 第二个参数指定顶点属性的大小。顶点属性是一个
vec3
,它由3个值组成,所以大小是3。 - 第三个参数指定数据的类型,这里是
GL_FLOAT
(GLSL中vec*
都是由浮点数值组成的)。 - 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为
GL_TRUE
,所有数据都会被映射到0(对于有符号型signed
数据是-1)到1之间。我们把它设置为GL_FALSE
。 - 第五个参数叫做步长(Stride),简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节,因此设置为
3 * sizeof(float)
。 - 最后一个参数的类型是
void*
,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。
配置完之后使用glEnableVertexAttribArray
启用0号顶点属性。
顶点数组对象(VAO)
对于不同的顶点数据,如果在使用时每次都要重复性做上述那么多配置操作,将会非常麻烦,管理也不方便。因此,可以使用顶点数组对象(Vertex Array Object, VAO)进行管理。
一个VAO会储存如下信息:
glEnableVertexAttribArray
和glDisableVertexAttribArray
的调用。- 通过
glVertexAttribPointer
设置的顶点属性配置。 - 通过
glVertexAttribPointer
调用与顶点属性关联的顶点缓冲对象。
OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。
要想使用VAO,要做的只是使用glBindVertexArray
绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。
1 | //创建VAO |
顶点着色器源码
1 |
|
第一行表示使用3.3版本的核模式。
接下来,使用in
声明所有需要使用的输入顶点属性,现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。vec3
为数据类型,表示长度为3的float
向量。aPos为变量名。layout (location = 0)
设定了顶点属性编号,就是在配置VBO时使用glEnableVertexAttribArray
所设置的顶点属性编号,我们当初设置的是0号顶点属性。
在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x
、vec.y
、vec.z
和vec.w
来获取。
main
函数是着色器程序执行的主体。
gl_Position
置的值会成为该顶点着色器的输出。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把vec3
的数据作为vec4
构造器的参数,同时把w分量设置为1.0f
。由于我们的顶点数据已经是标准化设备坐标(NDC)了,我们不需要在顶点着色器中做任何处理。
片段着色器源码
1 |
|
第一行与顶点着色器相同,版本和核模式。
接下来,用out
声明输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色。我们在这里输出一个不透明的橘黄色(alpha值为1.0,1.0代表完全不透明)。
编译着色器
假设着色器源代码需要放在一个C风格字符串中,我们必须在运行时动态编译它的源代码。
1 | //字符串vertexShaderSource是源码 |
glShaderSource
函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数设置为NULL
。- 对于几何着色器和片段着色器,编译的方法完全相同,只需要将
glCreateShader
的参数改为GL_GEOMETRY_SHADER
和GL_FRAGMENT_SHADER
。
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
1 | unsigned int shaderProgram; |
这部分代码实现的独立性较强,为了以后的使用方便,也方便各类着色器源代码的管理方便,可以自己写一个头文件,里面实现从指定文件读入着色器源码、完成编译、检测错误的的过程,返回着色器程序对象即可。
渲染之前,只需使用如下代码启用当着色器程序对象。
1 | glUseProgram(shaderProgram); |
渲染循环源码
1 | while (!glfwWindowShouldClose(window)) { |
glDrawArrays
函数用于绘制最终结果。第一个参数,我们希望绘制的是一个三角形,这里传递GL_TRIANGLES
给它。第二个参数指定了顶点数组的起始索引(从顶点数据的第0个开始绘制),我们这里填0。最后一个参数指定我们打算绘制多少个顶点,这里是3。
结果如下:
索引缓冲对象(EBO)
假设我们不再绘制一个三角形而是绘制一个矩形。我们可以绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)。有两个顶点是重复的,一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)就是用来做这个事的。
EBO也是一个缓冲,他的定义和使用方式与VBO类似。类型为GL_ELEMENT_ARRAY_BUFFER
。
1 | float vertices[] = { |
在绘制时,我们不使用之前的glDrawArrays
,而是改为glDrawElements
。
1 | //先绑定EBO |
- 第一个参数指定了我们绘制的模式,这个和
glDrawArrays
的一样。第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。第三个参数是索引的类型,这里是GL_UNSIGNED_INT
。最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),我们会在这里填写0。
顶点数组对象同样可以保存索引缓冲对象的绑定状态。VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象。绑定VAO的同时也会自动绑定EBO。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。
使用EBO的代码如下:
1 | glBindVertexArray(VAO); |
运行程序会获得下面这样的图片的结果。左侧图片看应该起来很熟悉,而右侧的则是使用线框模式(Wireframe Mode)绘制的。线框矩形可以显示出矩形的确是由两个三角形组成的。
要想用线框模式(Wireframe Mode)绘制你的三角形,你可以通过glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
函数配置OpenGL如何绘制图元。第一个参数表示我们打算将其应用到所有的三角形的正面和背面,第二个参数告诉我们用线来绘制。之后的绘制调用会一直以线框模式绘制三角形,直到我们用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
将其设置回默认模式。
GLSL
着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。
一个典型的着色器有下面的结构:
1 |
|
当我们特别谈论到顶点着色器的时候,每个输入变量也叫顶点属性(Vertex Attribute)。我们能声明的顶点属性是有上限的,它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS
来获取具体的上限:
1 | int nrAttributes; |
数据类型
数据类型 | 含义 |
---|---|
int | 整型 |
float | 浮点数 |
double | 双精度浮点 |
bool | 布尔 |
uint | 无符号整型 |
vecn | 包含n 个float分量的默认向量,例如vec4 。n 可以取2,3,4 |
bvecn | 包含n 个bool分量的向量 |
ivecn | 包含n 个int分量的向量 |
uvecn | 包含n 个uint分量的向量 |
dvecn | 包含n 个double分量的向量 |
matn | n xn 的浮点数矩阵 |
sampler2D | 2D纹理 |
samplerCube | 盒纹理 |
同样,可以在GLSL中使用数组和结构体(与C语言类似),但是数组仅支持一维。
一个向量的分量可以通过vec.x
这种方式获取,这里x
是指这个向量的第一个分量。你可以分别使用.x
、.y
、.z
和.w
来获取它们的第1、2、3、4个分量。GLSL也允许你对颜色使用rgba
,或是对纹理坐标使用stpq
访问相同的分量。
向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(Swizzling)。同样也可以灵活使用构造函数。GLSL允许这样的语法:
1 | vec2 someVec; |
输入输出
虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。
顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location
这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。顶点着色器需要为它的输入提供一个额外的layout
标识,这样我们才能把它链接到顶点数据。这部分在之前2D渲染代码实现部分的例子中有用到。
另一个例外是片段着色器,它需要一个vec4
颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。
顶点着色器:
1 |
|
片段着色器:
1 |
|
你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片段着色器中声明了一个类似的vertexColor。由于它们名字相同且类型相同,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。由于我们在顶点着色器中将颜色设置为深红色,最终的片段也是深红色的。
Uniform
Uniform
是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform
是全局的(Global)。全局意味着uniform
变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform
值设置成什么,uniform
会一直保存它们的数据,直到它们被重置或更新。
我们可以在一个着色器中添加uniform
关键字至类型和变量名前来声明一个GLSL的uniform
。从此处开始我们就可以在着色器中使用新声明的uniform
了。我们来看看这次是否能通过uniform
设置三角形的颜色:
1 |
|
- 如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!
然后,在程序中给这个变量赋值:
1 | float timeValue = glfwGetTime(); |
glUniform4f
函数的后缀4f表示接受的数据类型。部分后缀举例如下:
后缀 | 数据类型 |
---|---|
f | 函数需要一个float作为它的值 |
i | 函数需要一个int作为它的值 |
ui | 函数需要一个uint作为它的值 |
3f | 函数需要3个float作为它的值 |
fv | 函数需要一个float向量/数组作为它的值 |
如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform
,否则三角形就不会改变颜色。我们计算greenValue
,然后每个渲染迭代都更新这个uniform
即可。
更多顶点属性
程序中:
1 | float vertices[] = { |
顶点着色器:
1 |
|
片段着色器:
1 |
|
虽然我们只指定了三个顶点的颜色,OpenGL将会自动以插值的形式计算出其他位置的颜色。效果如下:
自己的着色器类
上述着色器的相关操作非常繁琐,管理也很麻烦,我们自己最好完成一个类来对一些常用操作进行封装。比如:
1 | class Shader { |