四、高级OpenGL:“几何着色器”、“实例化”和“抗锯齿”
4.9 几何着色器
- 在顶点和片段着色器之间有一个可选的几何着色器(Geometry Shader),几何着色器的输入是一个图元(如点或三角形)的一组顶点
- 几何着色器可以在顶点发送到下一着色器阶段之前对它们随意变换
- 然而,几何着色器最有趣的地方在于,它能够将(这一组)顶点变换为完全不同的图元,并且还能生成比原来更多的顶点
- 废话不多说,我们直接先看一个几何着色器的例子:
#version 330 core layout (points) in; layout (line_strip, max_vertices = 2) out; void main() { gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex(); gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0); EmitVertex(); EndPrimitive(); }
- 在几何着色器的顶部,我们需要声明从顶点着色器输入的图元类型。这需要在in关键字前声明一个布局修饰符(Layout Qualifier)。
- 这个输入布局修饰符可以从顶点着色器接收下列任何一个图元值:
- 接下来,我们还需要指定几何着色器输出的图元类型,这需要在out关键字前面加一个布局修饰符。
- 和输入布局修饰符一样,输出布局修饰符也可以接受几个图元值:
points
line_strip
triangle_strip
- 有了这3个输出修饰符,我们就可以使用输入图元创建几乎任意的形状了。要生成一个三角形的话,我们将输出定义为
triangle_strip
,并输出3个顶点。 - 几何着色器同时希望我们设置一个它最大能够输出的顶点数量(如果你超过了这个值,OpenGL将不会绘制多出的顶点),这个也可以在out关键字的布局修饰符中设置。
- 在这个例子中,我们将输出一个line_strip,并将最大顶点数设置为2个。
- 如果使用的是上面定义的着色器,那么这将只能输出一条线段,因为最大顶点数等于2。
- 为了生成更有意义的结果,我们需要某种方式来获取前一着色器阶段的输出。GLSL提供给我们一个内建(Built-in)变量,在内部看起来(可能)是这样的:
in gl_Vertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; } gl_in[];
- 这里,它被声明为一个接口块(Interface Block,我们在上一节已经讨论过),它包含了几个很有意思的变量,其中最有趣的一个是gl_Position,它是和顶点着色器输出非常相似的一个向量。
- 要注意的是,它被声明为一个数组,因为大多数的渲染图元包含多于1个的顶点,而几何着色器的输入是一个图元的所有顶点。
- 有了之前顶点着色器阶段的顶点数据,我们就可以使用2个几何着色器函数,EmitVertex和EndPrimitive,来生成新的数据了。
void main() { gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex(); gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0); EmitVertex(); EndPrimitive(); }
- 每次我们调用EmitVertex时,gl_Position中的向量会被添加到图元中来。
- 当EndPrimitive被调用时,所有发射出的(Emitted)顶点都会合成为指定的输出渲染图元。
- 在一个或多个EmitVertex调用之后重复调用EndPrimitive能够生成多个图元。
- 在这个例子中,我们发射了两个顶点,它们从原始顶点位置平移了一段距离,之后调用了EndPrimitive,将这两个顶点合成为一个包含两个顶点的线条。
- 另外的进一步实践,详见代码:
- 造几个房子
- 爆破物体
- 法向量可视化
4.10 实例化
- 渲染几乎是瞬间完成的,但上千个渲染函数调用却会极大地影响性能。
- 如果我们需要渲染大量物体时,代码看起来会像这样:
for(unsigned int i = 0; i < amount_of_models_to_draw; i++) { DoSomePreparations(); // 绑定VAO,绑定纹理,设置uniform等 glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices); }
- 如果像这样绘制模型的大量实例(Instance),你很快就会因为绘制调用过多而达到性能瓶颈。
- 与绘制顶点本身相比,使用glDrawArrays或glDrawElements函数告诉GPU去绘制你的顶点数据会消耗更多的性能
- 如果我们能够将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体,就会更方便了。这就是实例化(Instancing)。
- 实例化这项技术能够让我们使用一个渲染调用来绘制多个物体,来节省每次绘制物体时CPU -> GPU的通信,它只需要一次即可。
- 如果想使用实例化渲染,我们只需要将
glDrawArrays
和glDrawElements
的渲染调用分别改为glDrawArraysInstanced
和glDrawElementsInstanced
就可以了。 - 这些渲染函数的实例化版本需要一个额外的参数,叫做实例数量(Instance Count),它能够设置我们需要渲染的实例个数。
- 这样我们只需要将必须的数据发送到GPU一次,然后使用一次函数调用告诉GPU它应该如何绘制这些实例。GPU将会直接渲染这些实例,而不用不断地与CPU进行通信。
- 这个函数本身并没有什么用。渲染同一个物体一千次对我们并没有什么用处,每个物体都是完全相同的,而且还在同一个位置。我们只能看见一个物体!出于这个原因,GLSL在顶点着色器中嵌入了另一个内建变量,
gl_InstanceID
。 - 在使用实例化渲染调用时,
gl_InstanceID
会从0开始,在每个实例被渲染时递增1。比如说,我们正在渲染第43个实例,那么顶点着色器中它的gl_InstanceID
将会是42。因为每个实例都有唯一的ID,我们可以建立一个数组,将ID与位置值对应起来,将每个实例放置在世界的不同位置。
一组例子:非实例化与实例化
- 具体看官网代码即可
glVertexAttribDivisor
- 这个函数告诉了OpenGL该什么时候更新顶点属性的内容至新一组数据。它的第一个参数是需要的顶点属性,第二个参数是属性除数(Attribute Divisor)。
- 默认情况下,属性除数是0,告诉OpenGL我们需要在顶点着色器的每次迭代时更新顶点属性。
- 将它设置为1时,我们告诉OpenGL我们希望在渲染一个新实例的时候更新顶点属性。
- 而设置为2时,我们希望每2个实例更新一次属性,以此类推。
-c 我们将属性除数设置为1,是在告诉OpenGL,处于位置值2的顶点属性是一个实例化数组。
另一组例子:小行星带
- 再次理解
glVertexAttribPointer
与顶点着色器location的关系! - 用四个vec4存储mat4
- 具体看官网代码即可
- 可以看到,在合适的环境下,实例化渲染能够大大增加显卡的渲染能力。正是出于这个原因,实例化渲染通常会用于渲染草、植被、粒子,以及上面这样的场景,基本上只要场景中有很多重复的形状,都能够使用实例化渲染来提高性能。
4.11 抗锯齿
- 关于Aliasing产生的原因,可参考GAMES101
- 超采样抗锯齿(Super Sample Anti-aliasing, SSAA)
- 使用比正常分辨率更高的分辨率(即超采样)来渲染场景,当图像输出在帧缓冲中更新时,分辨率会被下采样(Downsample)至正常的分辨率。这些额外的分辨率会被用来防止锯齿边缘的产生。
- 虽然它确实能够解决走样的问题,但是由于这样比平时要绘制更多的片段,它也会带来很大的性能开销。所以这项技术只拥有了短暂的辉煌。
多重采样
- 多重采样抗锯齿(Multisample Anti-aliasing, MSAA)
- 关于多重采样的原理,可参考GAMES101
OpenGL中的MSAA
- 大多数的窗口系统都应该提供了一个多重采样缓冲,用以代替默认的颜色缓冲。
- GLFW同样给了我们这个功能,我们所要做的只是提示(Hint) GLFW,我们希望使用一个包含N个样本的多重采样缓冲。
- 这可以在创建窗口之前调用glfwWindowHint来完成。
glfwWindowHint(GLFW_SAMPLES, 4);
- 现在再调用glfwCreateWindow创建渲染窗口时,每个屏幕坐标就会使用一个包含4个子采样点的颜色缓冲了。GLFW会自动创建一个每像素4个子采样点的深度和样本缓冲。这也意味着所有缓冲的大小都增长了4倍。
- 我们还需要调用glEnable并启用GL_MULTISAMPLE,来启用多重采样。在大多数OpenGL的驱动上,多重采样都是默认启用的,所以这个调用可能会有点多余,但显式地调用一下会更保险一点。
glEnable(GL_MULTISAMPLE);
离屏MSAA
- 具体操作过程见官网