mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-23 04:35:28 +08:00
convert 04/09 04/10 04/11
This commit is contained in:
503
04 Advanced OpenGL/09 Geometry Shader.md
Normal file
503
04 Advanced OpenGL/09 Geometry Shader.md
Normal file
@@ -0,0 +1,503 @@
|
||||
## 几何着色器(Geometry Shader)
|
||||
|
||||
本文作者JoeyDeVries,由Django翻译自http://learnopengl.com
|
||||
|
||||
在顶点和像素着色器张志坚有一个可选的着色器阶段,叫做几何着色器(geometry shader)。几何着色器以一个或多个表示为一个单独基本图形(primitive)的顶点作为输入,比如可以是一个点或者三角形。几何着色器在将这些顶点发送到下一个着色阶段之前,可以将这些顶点转变为它认为合适的内容。几何着色器有意思的地方在于它可以把(一个或多个)顶点转变为完全不同的基本图形(primitive),从而生成比原来多得多的顶点。
|
||||
|
||||
我们直接用一个例子深入了解一下:
|
||||
|
||||
```c++
|
||||
#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();
|
||||
}
|
||||
```
|
||||
|
||||
每个几何着色器开始位置我们需要声明输入的基本图形(primitive)类型,这个输入是我们从顶点着色器中接收到的。我们在in关键字前面声明一个layout标识符。这个输入layout修饰符可以从一个顶点着色器接收以下基本图形值:
|
||||
|
||||
|
||||
基本图形|描述
|
||||
---|---
|
||||
points |绘制GL_POINTS基本图形的时候(1)
|
||||
lines |当绘制GL_LINES或GL_LINE_STRIP(2)时
|
||||
lines_adjacency | GL_LINES_ADJACENCY或GL_LINE_STRIP_ADJACENCY(4)
|
||||
triangles |GL_TRIANGLES, GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN(3)
|
||||
triangles_adjacency |GL_TRIANGLES_ADJACENCYGL_TRIANGLE_STRIP_ADJACENCY(6)
|
||||
|
||||
这是我们能够给渲染函数的几乎所有的基本图形。如果我们选择以GL_TRIANGLES绘制顶点,我们要把输入修饰符设置为triangles。括号里的数字代表一个基本图形所能包含的最少的顶点数。
|
||||
|
||||
当我们需要指定一个几何着色器所输出的基本图形类型时,我们就在out关键字前面加一个layout修饰符。和输入layout标识符一样,output的layout标识符也可以接受以下基本图形值:
|
||||
|
||||
* points
|
||||
* line_strip
|
||||
* triangle_strip
|
||||
|
||||
使用这3个输出修饰符我们可以从输入的基本图形创建任何我们想要的形状。为了生成一个三角形,我们定义一个triangle_strip作为输出,然后输出3个顶点。
|
||||
|
||||
几何着色器同时希望我们设置一个它能输出的顶点数量的最大值(如果你超出了这个数值,OpenGL就会忽略剩下的顶点),我们可以在out关键字的layout标识符上做这件事。在这个特殊的情况中,我们将使用最大值为2个顶点,来输出一个line_strip。
|
||||
|
||||
这种情况,你会奇怪什么是线条:一个线条是把多个点链接起来表示出一个连续的线,它最少有两个点来组成。每个后一个点在前一个新渲染的点后面渲染,你可以看看下面的图,其中包含5个顶点:
|
||||
|
||||

|
||||
|
||||
上面的着色器,我们只能输出一个线段,因为顶点的最大值设置的是2。
|
||||
|
||||
为生成更有意义的结果,我们需要某种方式从前一个着色阶段获得输出。GLSL为我们提供了一个内建变量,它叫做gl_in,它的内部看起来可能像这样:
|
||||
|
||||
```c++
|
||||
in gl_Vertex
|
||||
{
|
||||
vec4 gl_Position;
|
||||
float gl_PointSize;
|
||||
float gl_ClipDistance[];
|
||||
} gl_in[];
|
||||
|
||||
```
|
||||
|
||||
这里它被声明为一个interface block(前面的教程已经讨论过),它包含几个有意思的变量,其中最有意思的是gl_Position,它包含着和我们设置的顶点着色器的输出相似的向量。
|
||||
|
||||
要注意的是,它被声明为一个数组,因为大多数渲染基本图形(primitive)由一个以上顶点组成,几何着色器接收一个基本图形的所有顶点作为它的输入。
|
||||
|
||||
使用从前一个顶点着色阶段的顶点数据,我们就可以开始生成新的数据了,这是通过2个几何着色器函数EmitVertex和EndPrimitive来完成的。几何着色器需要你去生成/输出至少一个你定义为输出的基本图形。在我们的例子里我们打算至少生成一个线条(line strip)基本图形。
|
||||
|
||||
```c++
|
||||
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,所有为这个基本图形发射出去的顶点都将结合为一个特定的输出渲染基本图形。一个或多个EmitVertex函数调用后,重复调用EndPrimitive就能生成多个基本图形。这个特殊的例子里,发射了两个顶点,它们被从顶点原来的位置平移了一段距离,然后调用EndPrimitive将这两个顶点结合为一个单独的有两个顶点的线条。
|
||||
|
||||
现在你了解了几何着色器的工作方式,你就可能猜出这个几何着色器做了什么。这个几何着色器接收一个point基本图形作为它的输入,使用输入点作为它的中心,创建了一个水平线基本图形。如果我们渲染它,结果就会像这样:
|
||||
|
||||

|
||||
|
||||
并不是非常引人注目,但是考虑到它的输出是使用下面的渲染命令生成的就很有意思了:
|
||||
|
||||
```c++
|
||||
glDrawArrays(GL_POINTS, 0, 4);
|
||||
```
|
||||
|
||||
这是个相对简单的例子,它向你展示了我们如何使用几何着色器来动态地在运行时生成新的形状。本章的后面,我们会讨论一些可以使用几何着色器获得有趣的效果,但是现在我们将以创建一个简单的几何着色器开始。
|
||||
|
||||
### 使用几何着色器
|
||||
|
||||
为了展示几何着色器的使用,我们将渲染一个简单的场景,在场景中我们只绘制4个点,这4个点在标准化设备坐标的z平面上。这些点的坐标是:
|
||||
|
||||
```c++
|
||||
GLfloat points[] = {
|
||||
-0.5f, 0.5f, // Top-left
|
||||
0.5f, 0.5f, // Top-right
|
||||
0.5f, -0.5f, // Bottom-right
|
||||
-0.5f, -0.5f // Bottom-left
|
||||
};
|
||||
```
|
||||
|
||||
顶点着色器只在z平面绘制点,所以我们只需要一个基本顶点着色器:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (location = 0) in vec2 position;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(position.x, position.y, 0.0f, 1.0f);
|
||||
}
|
||||
```
|
||||
|
||||
我们会简单地为所有点输出绿色,我们直接在像素着色器里进行硬编码:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
out vec4 color;
|
||||
|
||||
void main()
|
||||
{
|
||||
color = vec4(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
}
|
||||
```
|
||||
|
||||
为点(point)的顶点(vertex)生成一个VAO和VBO,然后使用glDrawArrays进行绘制:
|
||||
|
||||
```c++
|
||||
shader.Use();
|
||||
glBindVertexArray(VAO);
|
||||
glDrawArrays(GL_POINTS, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
```
|
||||
|
||||
效果是黑色场景中有四个绿点(虽然很难看到):
|
||||
|
||||

|
||||
|
||||
但我们不是已经学到了所有内容了吗?对,现在我们将通过为场景添加一个几何着色器来为这个小场景增加点活力。
|
||||
|
||||
出于学习的目的我们将创建一个叫pass-through的几何着色器,它用一个point基本图形作为它的输入,并把它无修改地传(pass)到下一个着色器。
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (points) in;
|
||||
layout (points, max_vertices = 1) out;
|
||||
|
||||
void main() {
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
```
|
||||
|
||||
现在这个几何着色器应该很容易理解了。它简单地将它接收到的输入的无修改的顶点位置发射出去,然后生成一个point基本图形。
|
||||
|
||||
一个几何着色器需要像顶点和像素着色器一样被编译和链接,但是这次我们将使用GL_GEOMETRY_SHADER作为着色器的类型来创建这个着色器:
|
||||
|
||||
```c++
|
||||
geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
|
||||
glShaderSource(geometryShader, 1, &gShaderCode, NULL);
|
||||
glCompileShader(geometryShader);
|
||||
...
|
||||
glAttachShader(program, geometryShader);
|
||||
glLinkProgram(program);
|
||||
```
|
||||
|
||||
编译着色器的代码和顶点、像素着色器的基本一样。要记得检查编译和链接错误!
|
||||
|
||||
如果你现在编译和运行,就会看到和下面相似的结果:
|
||||
|
||||

|
||||
|
||||
它和没用几何着色器一样!我承认有点无聊,但是事实上,我们仍能绘制证明几何着色器工作了的点,所以现在是时候来做点更有意思的事了!
|
||||
|
||||
|
||||
|
||||
### 创建几个房子
|
||||
|
||||
绘制点和线没什么意思,所以我们将在每个点上使用几何着色器绘制一个房子。我们可以通过把几何着色器的输出设置为triangle_strip来达到这个目的,总共要绘制3个三角形:两个方形和一个屋顶。
|
||||
|
||||
在OpenGL中triangle strip(三角形带)绘制起来更高效,因为它所使用的顶点更少。第一个三角形绘制完以后,每个后续的顶点会生成一个毗连前一个三角形的新三角形:每3个毗连的顶点都能构成一个三角形。如果我们有6个顶点,它们以triangle strip的方式组合起来,那么我们会得到这些三角形:(1, 2, 3)、(2, 3, 4)、(3, 4, 5)、(4,5,6)因此总共可以表示出4个三角形。一个triangle strip至少要用3个顶点才行,它能生曾N-2个三角形;6个顶点我们就能创建6-2=4个三角形。下面的图片表达了这点:
|
||||
|
||||

|
||||
|
||||
使用一个triangle strip作为一个几何着色器的输出,我们可以轻松创建房子的形状,只要以正确的顺序来生成3个毗连的三角形。下面的图像显示,我们需要以何种顺序来绘制点,才能获得我们需要的三角形,图上的蓝点代表输入点:
|
||||
|
||||

|
||||
|
||||
上图的内容转变为几何着色器:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (points) in;
|
||||
layout (triangle_strip, max_vertices = 5) out;
|
||||
|
||||
void build_house(vec4 position)
|
||||
{
|
||||
gl_Position = position + vec4(-0.2f, -0.2f, 0.0f, 0.0f); // 1:bottom-left
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f); // 2:bottom-right
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4(-0.2f, 0.2f, 0.0f, 0.0f); // 3:top-left
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4( 0.2f, 0.2f, 0.0f, 0.0f); // 4:top-right
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4( 0.0f, 0.4f, 0.0f, 0.0f); // 5:top
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
build_house(gl_in[0].gl_Position);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这个几何着色器生成5个顶点,每个顶点是点(point)的位置加上一个偏移量,来组成一个大triangle strip。接着最后的基本图形被像素化,像素着色器处理整个triangle strip,结果是为我们绘制的每个点生成一个绿房子:
|
||||
|
||||

|
||||
|
||||
可以看到,每个房子实则是由3个三角形组成,都是仅仅使用空间中一点来绘制的。绿房子绿房子看起来还是不够漂亮,所以我们再给每个房子加一个不同的颜色。我们将在顶点着色器中为每个顶点增加一个额外的代表颜色信息的顶点属性。
|
||||
|
||||
下面是更新了的顶点数据:
|
||||
|
||||
```c++
|
||||
GLfloat points[] = {
|
||||
-0.5f, 0.5f, 1.0f, 0.0f, 0.0f, // Top-left
|
||||
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // Top-right
|
||||
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // Bottom-right
|
||||
-0.5f, -0.5f, 1.0f, 1.0f, 0.0f // Bottom-left
|
||||
};
|
||||
```
|
||||
|
||||
然后我们更新顶点着色器,使用一个interface block来像几何着色器发送颜色属性:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (location = 0) in vec2 position;
|
||||
layout (location = 1) in vec3 color;
|
||||
|
||||
out VS_OUT {
|
||||
vec3 color;
|
||||
} vs_out;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(position.x, position.y, 0.0f, 1.0f);
|
||||
vs_out.color = color;
|
||||
}
|
||||
```
|
||||
|
||||
接着我们还需要在几何着色器中声明同样的interface block(使用一个不同的接口名):
|
||||
|
||||
```c++
|
||||
in VS_OUT {
|
||||
vec3 color;
|
||||
} gs_in[];
|
||||
```
|
||||
|
||||
因为几何着色器把多个顶点作为它的输入,从顶点着色器来的输入数据总是被以数组的形式表示出来,即使现在我们只有一个顶点。
|
||||
|
||||
!!! Important
|
||||
|
||||
我们不是必须使用interface block来把数据发送到几何着色器中。我们还可以这么写:
|
||||
|
||||
in vec3 vColor[];
|
||||
|
||||
如果顶点着色器发送的颜色向量是out vec3 vColor那么interface block就会在比如几何着色器这样的着色器中更轻松地完成工作。事实上,几何着色器的输入可以非常大,把它们组成一个大的interface block数组会更有意义。
|
||||
|
||||
|
||||
然后我们还要为下一个像素着色阶段僧名一个输出颜色向量:
|
||||
|
||||
```c++
|
||||
out vec3 fColor;
|
||||
```
|
||||
|
||||
因为像素着色器只需要一个(已进行了插值的)颜色,传送多个颜色没有意义。fColor向量这样就不是一个数组,而是一个单一的向量。当发射一个顶点时,为了它的像素着色器运行,每个顶点都会储存最后在fColor中储存的值。对于这些房子来说,我们可以在第一个顶点被发射,对整个房子上色前,只使用来自顶点着色器的颜色填充fColor一次:
|
||||
|
||||
```c++
|
||||
fColor = gs_in[0].color; // gs_in[0] since there's only one input vertex
|
||||
gl_Position = position + vec4(-0.2f, -0.2f, 0.0f, 0.0f); // 1:bottom-left
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f); // 2:bottom-right
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4(-0.2f, 0.2f, 0.0f, 0.0f); // 3:top-left
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4( 0.2f, 0.2f, 0.0f, 0.0f); // 4:top-right
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4( 0.0f, 0.4f, 0.0f, 0.0f); // 5:top
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
```
|
||||
|
||||
所有发射出去的顶点都把最后储存在fColor中的值嵌入到他们的数据中,和我们在他们的属性中定义的顶点颜色相同。所有的分房子便都有了自己的颜色:
|
||||
|
||||

|
||||
|
||||
为了好玩儿,我们还可以假装这是在冬天,给最后一个顶点一个自己的白色,就像在屋顶上落了一些雪。
|
||||
|
||||
```c++
|
||||
fColor = gs_in[0].color;
|
||||
gl_Position = position + vec4(-0.2f, -0.2f, 0.0f, 0.0f); // 1:bottom-left
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f); // 2:bottom-right
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4(-0.2f, 0.2f, 0.0f, 0.0f); // 3:top-left
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4( 0.2f, 0.2f, 0.0f, 0.0f); // 4:top-right
|
||||
EmitVertex();
|
||||
gl_Position = position + vec4( 0.0f, 0.4f, 0.0f, 0.0f); // 5:top
|
||||
fColor = vec3(1.0f, 1.0f, 1.0f);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
```
|
||||
|
||||
结果就像这样:
|
||||
|
||||

|
||||
|
||||
你可以对比一下你的源码和着色器。
|
||||
|
||||
你可以看到,使用几何着色器,你可以使用最简单的基本图形就能获得漂亮的新玩意。因为这些形状是在你的GPU超快硬件上动态生成额,这要比使用顶点缓冲自己定义这些形状更为高效。几何缓冲在简单的经常被重复的形状比如体素(voxel)的世界和室外的草地上,是一种非常强大的优化工具。
|
||||
|
||||
#### 爆炸式物体
|
||||
|
||||
绘制房子的确很有趣,但我们不会经常这么用。这就是为什么现在我们将撬起物体缺口,形成爆炸式物体的原因!虽然这个我们也不会经常用到,但是它能向你展示一些几何着色器的强大之处。
|
||||
|
||||
当我们说对一个物体进行爆破的时候并不是说我们将要把之前的那堆顶点炸掉,但是我们打算把每个三角形沿着它们的法线向量移动一小段距离。效果是整个物体上的三角形看起来就像沿着它们的法线向量爆炸了一样。纳米服上的三角形的爆炸式效果看起来是这样的:
|
||||
|
||||

|
||||
|
||||
这样一个几何着色器效果的一大好处是,它可以用到任何物体上,无论它们多复杂。
|
||||
|
||||
因为我们打算沿着三角形的法线向量移动三角形的每个顶点,我们需要先计算它的法线向量。我们要做的是计算出一个向量,它垂直于三角形的表面,使用这三个我们已经的到的顶点就能做到。你可能记得变换教程中,我们可以使用叉乘获取一个垂直于两个其他向量的向量。如果我们有两个向量a和b,它们平行于三角形的表面,我们就可以对这两个向量进行叉乘得到法线向量了。下面的几何着色器函数做的正是这件事,它使用3个输入顶点坐标获取法线向量:
|
||||
|
||||
```c++
|
||||
vec3 GetNormal()
|
||||
{
|
||||
vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
|
||||
vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
|
||||
return normalize(cross(a, b));
|
||||
}
|
||||
```
|
||||
|
||||
这里我们使用减法获取了两个向量a和b,它们平行于三角形的表面。两个向量相减得到一个两个向量的差值,由于所有3个点都在三角形平面上,任何向量相减都会得到一个平行于平面的向量。一定要注意,如果我们调换了a和b的叉乘顺序,我们得到的法线向量就会使反的,顺序很重要!
|
||||
|
||||
知道了如何计算法线向量,我们就能创建一个explode函数,函数返回的是一个新向量,它把位置向量沿着法线向量方向平移:
|
||||
|
||||
```c++
|
||||
vec4 explode(vec4 position, vec3 normal)
|
||||
{
|
||||
float magnitude = 2.0f;
|
||||
vec3 direction = normal * ((sin(time) + 1.0f) / 2.0f) * magnitude;
|
||||
return position + vec4(direction, 0.0f);
|
||||
}
|
||||
```
|
||||
|
||||
函数本身并不复杂,sin(正弦)函数把一个time变量作为它的参数,它根据时间来返回一个-1.0到1.0之间的值。因为我们不想让物体坍缩,所以我们把sin返回的值做成0到1的范围。最后的值去乘以法线向量,direction向量被添加到位置向量上。
|
||||
|
||||
爆炸效果的完整的几何着色器是这样的,它使用我们的模型加载器,绘制出一个模型:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (triangles) in;
|
||||
layout (triangle_strip, max_vertices = 3) out;
|
||||
|
||||
in VS_OUT {
|
||||
vec2 texCoords;
|
||||
} gs_in[];
|
||||
|
||||
out vec2 TexCoords;
|
||||
|
||||
uniform float time;
|
||||
|
||||
vec4 explode(vec4 position, vec3 normal) { ... }
|
||||
|
||||
vec3 GetNormal() { ... }
|
||||
|
||||
void main() {
|
||||
vec3 normal = GetNormal();
|
||||
|
||||
gl_Position = explode(gl_in[0].gl_Position, normal);
|
||||
TexCoords = gs_in[0].texCoords;
|
||||
EmitVertex();
|
||||
gl_Position = explode(gl_in[1].gl_Position, normal);
|
||||
TexCoords = gs_in[1].texCoords;
|
||||
EmitVertex();
|
||||
gl_Position = explode(gl_in[2].gl_Position, normal);
|
||||
TexCoords = gs_in[2].texCoords;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
```
|
||||
|
||||
注意我们同样在发射一个顶点前输出了合适的纹理坐标。
|
||||
|
||||
也不要忘记在OpenGL代码中设置time变量:
|
||||
|
||||
```c++
|
||||
glUniform1f(glGetUniformLocation(shader.Program, "time"), glfwGetTime());
|
||||
```
|
||||
|
||||
最后的结果是一个随着时间持续不断地爆炸的3D模型(不断爆炸不断回到正常状态)。尽管没什么大用处,它却向你展示出很多几何着色器的高级用法。你可以用完整的源码和着色器对比一下你自己的。
|
||||
|
||||
#### 把法线向量显示出来
|
||||
|
||||
在这部分我们将使用几何着色器写一个例子,非常有用:显示一个法线向量。当编写光照着色器的时候,你最终会遇到奇怪的视频输出问题,你很难决定是什么导致了这个问题。通常导致光照错误的是,不正确的加载顶点数据,以及给它们指定了不合理的顶点属性,又或是在着色器中不合法的管理,导致产生了不正确的法线向量。我们所希望的是有某种方式可以检测出法线向量是否正确。把法线向量显示出来正是这样一种方法,恰好几何着色器能够完美地达成这个目的。
|
||||
|
||||
思路是这样的:我们先不用几何着色器,正常绘制场景,然后我们再次绘制一遍场景,但这次只显示我们通过几何着色器生成的法线向量。几何着色器把一个triangle基本图形作为输入类型,用它们生成3条和法线向量同向的线段,每个顶点一条。伪代码应该是这样的:
|
||||
|
||||
```c++
|
||||
shader.Use();
|
||||
DrawScene();
|
||||
normalDisplayShader.Use();
|
||||
DrawScene();
|
||||
```
|
||||
|
||||
这次我们会创建一个使用模型提供的顶点法线,而不是自己去生成。为了适应缩放和旋转我们会在把它变换到裁切空间坐标前,使用法线矩阵来法线(几何着色器用他的位置向量做为裁切空间坐标,所以我们还要把法线向量变换到同一个空间)。这些都能在顶点着色器中完成:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 position;
|
||||
layout (location = 1) in vec3 normal;
|
||||
|
||||
out VS_OUT {
|
||||
vec3 normal;
|
||||
} vs_out;
|
||||
|
||||
uniform mat4 projection;
|
||||
uniform mat4 view;
|
||||
uniform mat4 model;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = projection * view * model * vec4(position, 1.0f);
|
||||
mat3 normalMatrix = mat3(transpose(inverse(view * model)));
|
||||
vs_out.normal = normalize(vec3(projection * vec4(normalMatrix * normal, 1.0)));
|
||||
}
|
||||
```
|
||||
|
||||
经过变换的裁切空间法线向量接着通过一个interface block被传递到下个着色阶段。几何着色器接收每个顶点(带有位置和法线向量),从每个位置向量绘制出一个法线向量:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (triangles) in;
|
||||
layout (line_strip, max_vertices = 6) out;
|
||||
|
||||
in VS_OUT {
|
||||
vec3 normal;
|
||||
} gs_in[];
|
||||
|
||||
const float MAGNITUDE = 0.4f;
|
||||
|
||||
void GenerateLine(int index)
|
||||
{
|
||||
gl_Position = gl_in[index].gl_Position;
|
||||
EmitVertex();
|
||||
gl_Position = gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0f) * MAGNITUDE;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
GenerateLine(0); // First vertex normal
|
||||
GenerateLine(1); // Second vertex normal
|
||||
GenerateLine(2); // Third vertex normal
|
||||
}
|
||||
```
|
||||
|
||||
到现在为止,像这样的几何着色器的内容就不言自明了。需要注意的是我们我们把法线向量乘以一个MAGNITUDE向量来限制显示出的法线向量的大小(否则它们就太大了)。
|
||||
|
||||
由于把法线显示出来通常用于调试的目的,我们可以在像素着色器的帮助下把它们显示为单色的线(如果你愿意也可以更炫一点)。
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
out vec4 color;
|
||||
|
||||
void main()
|
||||
{
|
||||
color = vec4(1.0f, 1.0f, 0.0f, 1.0f);
|
||||
}
|
||||
```
|
||||
|
||||
现在先使用普通着色器来渲染你的模型,然后使用特制的法线可视着色器,你会看到这样的效果:
|
||||
|
||||

|
||||
|
||||
除了我们的纳米服现在看起来有点像一个带着隔热手套的全身多毛的家伙外,它给了我们一种非常有效的检查一个模型的法线向量是否有错误的方式。你可以想象下这样的几何着色器也经常能被用在给物体添加毛发上。
|
||||
|
||||
你可以从这里找到[源码](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_normals)和可显示法线的[着色器](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_normals_shaders)。
|
348
04 Advanced OpenGL/10 Instancing.md
Normal file
348
04 Advanced OpenGL/10 Instancing.md
Normal file
@@ -0,0 +1,348 @@
|
||||
## 实例化(Instancing)
|
||||
|
||||
本文作者JoeyDeVries,由Django翻译自http://learnopengl.com
|
||||
|
||||
比如说你有一个有许多模型的场景,而这些模型的顶点数据都一样,只是进行了不同的世界变换。想象一下,有一个场景中充满了草叶:每根草都是几个三角形组成的。你可能需要绘制很多的草叶,最终一次渲染循环中就肯能有成千上万个草需要绘制了。因为每个草叶只是由几个三角形组成,绘制一个几乎是即刻完成,但是数量巨大以后,执行起来就很慢了。
|
||||
|
||||
如果我们渲染这样多的物体的时候,也许代码会写成这样:
|
||||
|
||||
```c++
|
||||
for(GLuint i = 0; i < amount_of_models_to_draw; i++)
|
||||
{
|
||||
DoSomePreparations(); // bind VAO, bind Textures, set uniforms etc.
|
||||
glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
像这样绘制出你模型的其他实例,多次绘制之后,很快将达到一个瓶颈。和渲染真实的顶点相比,告诉GPU使用像glDrawArrays或glDrawElements这样的函数,去渲染你的顶点数据,会明显降低执行效率,这是因为OpenGL比在它可以绘制你的顶点数据之前必须做一些不必要的准备工作(比如告诉GPU从哪个缓冲读取数据,以及在哪里找到顶点属性,所有这些都会是CPU到GPU的总线变慢)。所以即使渲染顶点超快,给你的GPU下达这样的渲染命令却未必。
|
||||
|
||||
如果我们能够将数据一次发送给GPU,就会更方便,然后告诉OpenGL使用一个绘制函数,将这些数据绘制为多个物体。下面进入实例化。
|
||||
|
||||
实例化是一种只调用一次渲染函数却能绘制出很多物体的技术,它节省渲染一个物体时了从CPU到GPU的通信时间;而且只需做一次即可。要使用实例化渲染,我们必须将glDrawArrays和glDrawElements各自改为glDrawArraysInstanced和glDrawElementsInstanced。这些用于实例化的函数版本需要设置一个额外的参数,叫做实例化数量(instance count),它设置我们打算渲染实例的数量。这样我们就只需要把所有需要的数据发送给GPU一次就行了,然后告诉GPU它该如何使用一个函数来绘制所有这些实例。
|
||||
|
||||
就其本身而言,这个函数用处不大。渲染同一个物体一千次对我们来说没用,因为每个渲染出的物体不仅相同而且还在同一个位置;我们只能看到一个物体!出于这个原因GLSL在着色器中嵌入了另一个内建变量,叫做gl_InstanceID。
|
||||
|
||||
当调用任何一个实例化绘制函数的时候,gl_InstanceID在每个实例渲染时都会增加1,它的初值是0。如果我们渲染43个实例,那么在顶点着色器gl_InstanceID的值最后就是42。每个实例都拥有唯一的值意味着我们可以索引到一个位置数组,并将每个实例摆放在世界空间的不同的位置上。
|
||||
|
||||
我们调用一个渲染函数,在标准化设备坐标中绘制一百个2D四边形来看看实例化绘制的效果是怎样的。通过对一个储存着100个偏移量向量的索引,我们像每个实例四边形添加一个偏移量。最后,窗口被排列精美的四边形网格填满:
|
||||
|
||||

|
||||
|
||||
每个四边形是2个三角形所组成的,因此总共有6个顶点。每个顶点包含一个2D标准设备坐标位置向量和一个颜色向量。下面是例子中所使用的顶点数据,每个三角形为了适应屏幕都很小:
|
||||
|
||||
```c++
|
||||
GLfloat quadVertices[] = {
|
||||
// Positions // Colors
|
||||
-0.05f, 0.05f, 1.0f, 0.0f, 0.0f,
|
||||
0.05f, -0.05f, 0.0f, 1.0f, 0.0f,
|
||||
-0.05f, -0.05f, 0.0f, 0.0f, 1.0f,
|
||||
|
||||
-0.05f, 0.05f, 1.0f, 0.0f, 0.0f,
|
||||
0.05f, -0.05f, 0.0f, 1.0f, 0.0f,
|
||||
0.05f, 0.05f, 0.0f, 1.0f, 1.0f
|
||||
};
|
||||
```
|
||||
|
||||
像素着色器接收从顶点着色器发送来的颜色向量,设置为它的颜色输出,从而为四边形上色:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
in vec3 fColor;
|
||||
out vec4 color;
|
||||
|
||||
void main()
|
||||
{
|
||||
color = vec4(fColor, 1.0f);
|
||||
}
|
||||
```
|
||||
|
||||
到目前为止没有什么新内容,但顶点着色器开始变得有意思了:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (location = 0) in vec2 position;
|
||||
layout (location = 1) in vec3 color;
|
||||
|
||||
out vec3 fColor;
|
||||
|
||||
uniform vec2 offsets[100];
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 offset = offsets[gl_InstanceID];
|
||||
gl_Position = vec4(position + offset, 0.0f, 1.0f);
|
||||
fColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
这里我们定义了一个uniform数组,叫offset,它包含100个偏移量向量。在顶点着色器里,我们接收一个对应着当前实例的偏移量,这是通过使用gl_InstanceID来索引offsets得到的。如果我们使用实例化绘制100个四边形,使用这个顶点着色器,我们就能得到100位于不同位置的四边形。
|
||||
|
||||
我们一定要设置偏移位置,在游戏循环之前我们用一个嵌套for循环计算出它来:
|
||||
|
||||
```c++
|
||||
glm::vec2 translations[100];
|
||||
int index = 0;
|
||||
GLfloat offset = 0.1f;
|
||||
for(GLint y = -10; y < 10; y += 2)
|
||||
{
|
||||
for(GLint x = -10; x < 10; x += 2)
|
||||
{
|
||||
glm::vec2 translation;
|
||||
translation.x = (GLfloat)x / 10.0f + offset;
|
||||
translation.y = (GLfloat)y / 10.0f + offset;
|
||||
translations[index++] = translation;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里我们创建100个平移向量,它包含着10×10格子所有位置。除了生成translations数组外,我们还需要把数据发送到顶点着色器的uniform数组:
|
||||
|
||||
```c++
|
||||
shader.Use();
|
||||
for(GLuint i = 0; i < 100; i++)
|
||||
{
|
||||
stringstream ss;
|
||||
string index;
|
||||
ss << i;
|
||||
index = ss.str();
|
||||
GLint location = glGetUniformLocation(shader.Program, ("offsets[" + index + "]").c_str())
|
||||
glUniform2f(location, translations[i].x, translations[i].y);
|
||||
}
|
||||
```
|
||||
|
||||
这一小段代码中,我们将for循环计数器i变为string,接着就能动态创建一个为请求的uniform的location创建一个location字符串。将offsets数组中的每个条目,我们都设置为相应的平移向量。
|
||||
|
||||
现在所有的准备工作都结束了,我们可以开始渲染四边形了。用实例化渲染来绘制四边形,我们需要调用glDrawArraysInstanced或glDrawElementsInstanced,由于我们使用的不是索引绘制缓冲,所以我们用的是glDrawArrays对应的那个版本:
|
||||
|
||||
```c++
|
||||
glBindVertexArray(quadVAO);
|
||||
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
|
||||
glBindVertexArray(0);
|
||||
```
|
||||
|
||||
glDrawArraysInstanced的参数和glDrawArrays一样,除了最后一个参数设置了我们打算绘制实例的数量。我们想展示100个四边形,它们以10×10网格形式展现,所以这儿就是100.运行代码,你会得到100个相似的有色四边形。
|
||||
|
||||
#### 实例数组(instanced arrays)
|
||||
|
||||
在这种特定条件下,前面的实现很好,但是当我们有100个实例的时候(这很正常),最终我们将碰到uniform数据数量的上线。为避免这个问题另一个可替代方案是实例数组,它使用顶点属性来定义(这样就允许我们使用更多的数据了),当顶点着色器渲染一个新实例时它才会被更新。
|
||||
|
||||
使用顶点属性,每次运行顶点着色器都将让GLSL获取到下个顶点属性集合,它们属于当前顶点。当把顶点属性定义为实例数组时,顶点着色器只更新每个实例的顶点属性的内容而不是顶点的内容。这使我们在每个顶点数据上使用标准顶点属性,用实例数组来储存唯一的实例数据。
|
||||
|
||||
为了展示一个实例数组的例子,我们将采用前面的例子,把偏移uniform表示为一个实例数组。我们不得不增加另一个顶点属性,来更新顶点着色器。
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (location = 0) in vec2 position;
|
||||
layout (location = 1) in vec3 color;
|
||||
layout (location = 2) in vec2 offset;
|
||||
|
||||
out vec3 fColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(position + offset, 0.0f, 1.0f);
|
||||
fColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
我们不再使用gl_InstanceID,可以直接用offset属性,不用先在一个大uniform数组里进行索引。
|
||||
|
||||
因为一个实例数组实际上就是一个和位置和颜色一样的顶点属性,我们还需要把它的内容储存为一个顶点缓冲对象里,并把它配置为一个属性指针。我们首先将平移变换数组贮存到一个新的缓冲对象上:
|
||||
|
||||
```c++
|
||||
GLuint instanceVBO;
|
||||
glGenBuffers(1, &instanceVBO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
```
|
||||
|
||||
之后我们还需要设置它的顶点属性指针,并开启顶点属性:
|
||||
|
||||
```c++
|
||||
glEnableVertexAttribArray(2);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*)0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glVertexAttribDivisor(2, 1);
|
||||
```
|
||||
|
||||
代码中有意思的地方是,最后一行,我们调用了glVertexAttribDivisor。这个函数告诉OpenGL什么时候去更新顶点属性的内容到下个元素。它的第一个参数是提到的顶点属性,第二个参数是属性除数(attribute divisor)。默认属性除数是0,告诉OpenGL在顶点着色器的每次迭代更新顶点属性的内容。把这个属性设置为1,我们就是告诉OpenGL我们打算在开始渲染一个新的实例的时候更新顶点属性的内容。设置为2代表我们每2个实例更新内容,依此类推。把属性除数设置为1,我们可以高效地告诉OpenGL,location是2的顶点属性是一个实例数组(instanced array)。
|
||||
|
||||
如果我们现在再次使用glDrawArraysInstanced渲染四边形,我们会得到下面的输出:
|
||||
|
||||

|
||||
|
||||
和前面的一样,但这次是使用实例数组实现的,它使我们为绘制实例向顶点着色器传递更多的数据(内存允许我们存多少就能存多少)。
|
||||
|
||||
你还可以使用gl_InstanceID从右上向左下缩小每个四边形。
|
||||
|
||||
```c++
|
||||
void main()
|
||||
{
|
||||
vec2 pos = position * (gl_InstanceID / 100.0f);
|
||||
gl_Position = vec4(pos + offset, 0.0f, 1.0f);
|
||||
fColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
结果是第一个实例的四边形被绘制的非常小,随着绘制实例的增加,gl_InstanceID越来越接近100,这样更多的四边形会更接近它们原来的大小。这是一种很好的将gl_InstanceID与实例数组结合使用的法则:
|
||||
|
||||

|
||||
|
||||
如果你仍然不确定实例渲染如何工作,或者想看看上面的代码是如何组合起来的,你可以在这里找到应用的源码。
|
||||
|
||||
这些例子不是实例的好例子,不过挺有意思的。它们可以让你对实例的工作方式有一个概括的理解,但是当绘制拥有极大数量的相同物体的时候,它极其有用,现在我们还没有展示呢。出于这个原因,我们将咋接下来的部分进入太空来看看实例渲染的威力。
|
||||
|
||||
#### 小行星带
|
||||
|
||||
想象一下,在一个场景中有一个很大的行星,行星周围有一圈小行星带。这样一个小行星大可能包含成千上万的石块,对于大多数显卡来说几乎是难以完成的渲染任务。这个场景对于实例渲染来说却不再话下,由于所有小行星可以使用一个模型来表示。每个小行星使用一个变换矩阵就是一个经过少量变化的独一无二的小行星了。
|
||||
|
||||
为了展示实例渲染的影响,我们先不使用实例渲染,来渲染一个小行星围绕行星飞行的场景。这个场景的大天体可以从这里下载,此外要把小行星放在合适的位置上。小行星可以从这里下载。
|
||||
|
||||
为了得到我们理想中的效果,我们将为每个小行星生成一个变换矩阵,作为它们的模型矩阵。变换矩阵先将小行星平移到行星带上,我们还要添加一个随机位移值来作为偏移量,这样才能使行星带更自然。接着我们应用一个随机缩放,以及一个随机旋转向量。最后,变换矩阵就会将小行星变换到猩猩的周围,同时使它们更自然,每个行星都有别于其他的。
|
||||
|
||||
```c++
|
||||
GLuint amount = 1000;
|
||||
glm::mat4* modelMatrices;
|
||||
modelMatrices = new glm::mat4[amount];
|
||||
srand(glfwGetTime()); // initialize random seed
|
||||
GLfloat radius = 50.0;
|
||||
GLfloat offset = 2.5f;
|
||||
for(GLuint i = 0; i < amount; i++)
|
||||
{
|
||||
glm::mat4 model;
|
||||
// 1. Translation: displace along circle with 'radius' in range [-offset, offset]
|
||||
GLfloat angle = (GLfloat)i / (GLfloat)amount * 360.0f;
|
||||
GLfloat displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
|
||||
GLfloat x = sin(angle) * radius + displacement;
|
||||
displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
|
||||
GLfloat y = displacement * 0.4f; // y value has smaller displacement
|
||||
displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
|
||||
GLfloat z = cos(angle) * radius + displacement;
|
||||
model = glm::translate(model, glm::vec3(x, y, z));
|
||||
// 2. Scale: Scale between 0.05 and 0.25f
|
||||
GLfloat scale = (rand() % 20) / 100.0f + 0.05;
|
||||
model = glm::scale(model, glm::vec3(scale));
|
||||
// 3. Rotation: add random rotation around a (semi)randomly picked rotation axis vector
|
||||
GLfloat rotAngle = (rand() % 360);
|
||||
model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));
|
||||
// 4. Now add to list of matrices
|
||||
modelMatrices[i] = model;
|
||||
}
|
||||
```
|
||||
|
||||
这段代码看起来还是有点吓人,但我们基本上是沿着一个半径为radius的圆圈变换小行星的x和y的值,让每个小行星在-offset和offset之间随机生成一个位置。我们让y变化的更小,这让这个环带就会成为扁平的。接着我们缩放和旋转变换,把结果储存到一个modelMatrices矩阵里,它的大小是amount。这里我们生成1000个模型矩阵,每个小行星一个。
|
||||
|
||||
加载完天体和小行星模型后,编译着色器,渲染代码是这样的:
|
||||
|
||||
```c++
|
||||
// Draw Planet
|
||||
shader.Use();
|
||||
glm::mat4 model;
|
||||
model = glm::translate(model, glm::vec3(0.0f, -5.0f, 0.0f));
|
||||
model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
|
||||
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
|
||||
planet.Draw(shader);
|
||||
|
||||
// Draw Asteroid circle
|
||||
for(GLuint i = 0; i < amount; i++)
|
||||
{
|
||||
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(modelMatrices[i]));
|
||||
rock.Draw(shader);
|
||||
}
|
||||
```
|
||||
|
||||
我们先绘制天体模型,要把它平移和缩放一点以适应场景,接着,我们绘制amount数量的小行星,它们按照我们所计算的结果进行变换。在我们绘制每个小行星之前,我们还得先在着色器中设置相应的模型变换矩阵。
|
||||
|
||||
结果是一个太空样子的场景,我们可以看到有一个自然的小行星带:
|
||||
|
||||

|
||||
|
||||
这个场景包含1001次渲染函数调用,每帧渲染1000个小行星模型。你可以在这里找到场景的源码。
|
||||
|
||||
当我们开始增加数量的时候,很快就会注意到帧数的下降,而且下降的厉害。当我们设置为2000的时候,场景慢得已经难以移动了。
|
||||
|
||||
我们再次使用实例渲染来渲染同样的场景。我们先得对顶点着色器进行一点修改:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 position;
|
||||
layout (location = 2) in vec2 texCoords;
|
||||
layout (location = 3) in mat4 instanceMatrix;
|
||||
|
||||
out vec2 TexCoords;
|
||||
|
||||
uniform mat4 projection;
|
||||
uniform mat4 view;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = projection * view * instanceMatrix * vec4(position, 1.0f);
|
||||
TexCoords = texCoords;
|
||||
}
|
||||
```
|
||||
|
||||
我们不再使用模型uniform变量,取而代之的是把一个mat4的顶点属性,送一我们可以将变换矩阵储存为一个实例数组(instanced array)。然而,当我们声明一个数据类型为顶点属性的时候,它比一个vec4更大,是有些不同的。顶点属性被允许的最大数据量和vec4相等。因为一个mat4大致和4个vec4相等,我们为特定的矩阵必须保留4个顶点属性。因为我们将它的位置赋值为3个列的矩阵,顶点属性的位置就会是3、4、5和6。
|
||||
|
||||
然后我们必须为这4个顶点属性设置属性指针,并将其配置为实例数组:
|
||||
|
||||
```c++
|
||||
for(GLuint i = 0; i < rock.meshes.size(); i++)
|
||||
{
|
||||
GLuint VAO = rock.meshes[i].VAO;
|
||||
// Vertex Buffer Object
|
||||
GLuint buffer;
|
||||
glBindVertexArray(VAO);
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
|
||||
// Vertex Attributes
|
||||
GLsizei vec4Size = sizeof(glm::vec4);
|
||||
glEnableVertexAttribArray(3);
|
||||
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)0);
|
||||
glEnableVertexAttribArray(4);
|
||||
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)(vec4Size));
|
||||
glEnableVertexAttribArray(5);
|
||||
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)(2 * vec4Size));
|
||||
glEnableVertexAttribArray(6);
|
||||
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)(3 * vec4Size));
|
||||
|
||||
glVertexAttribDivisor(3, 1);
|
||||
glVertexAttribDivisor(4, 1);
|
||||
glVertexAttribDivisor(5, 1);
|
||||
glVertexAttribDivisor(6, 1);
|
||||
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
```
|
||||
|
||||
要注意的是我们将Mesh的VAO变量声明为一个public(公有)变量,而不是一个private(私有)变量,所以我们可以获取它的顶点数组对象。这不是最干净的方案,但这能较好的适应本教程。若没有这点hack,代码就干净了。我们声明了OpenGL该如何为每个矩阵的顶点属性的缓冲进行解释,每个顶点属性都是一个实例数组。
|
||||
|
||||
下一步我们再次获得网格的VAO,这次使用glDrawElementsInstanced进行绘制:
|
||||
|
||||
```c++
|
||||
// Draw meteorites
|
||||
instanceShader.Use();
|
||||
for(GLuint i = 0; i < rock.meshes.size(); i++)
|
||||
{
|
||||
glBindVertexArray(rock.meshes[i].VAO);
|
||||
glDrawElementsInstanced(
|
||||
GL_TRIANGLES, rock.meshes[i].vertices.size(), GL_UNSIGNED_INT, 0, amount
|
||||
);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
```
|
||||
|
||||
这里我们绘制和前面的例子里一样数量(amount)的小行星,只不过是使用的实例渲染。结果是相似的,但你会看在开始增加数量以后效果的不同。不实例渲染,我们可以流畅渲染1000到1500个小行星。而使用了实例渲染,我们可以设置为100000,每个模型由576个顶点,这几乎有5千7百万个顶点,而且帧率没有丝毫下降!
|
||||
|
||||

|
||||
|
||||
上图渲染了十万小行星,半径为150.0f,偏移等于25.0f。你可以在这里找到这个演示实例渲染的源码。
|
||||
|
||||
!!! Important
|
||||
|
||||
有些机器渲染十万可能会有点吃力,所以尝试修改这个数量知道你能获得可以接受的帧率。
|
||||
|
||||
就像你所看到的,在合适的条件下,实例渲染对于你的显卡来说和普通渲染有很大不同。处于这个理由,实例渲染通常用来渲染草、草丛、粒子以及像这样的场景,基本上来讲只要场景中有很多重复物体,使用实例渲染都会获得好处。
|
202
04 Advanced OpenGL/11 Anti Aliasing.md
Normal file
202
04 Advanced OpenGL/11 Anti Aliasing.md
Normal file
@@ -0,0 +1,202 @@
|
||||
## 反走样(Anti Aliasing)
|
||||
|
||||
本文作者JoeyDeVries,由Django翻译自http://learnopengl.com
|
||||
|
||||
在你的渲染大冒险中,你可能会遇到模型边缘有锯齿的问题。锯齿边出现的原因是由顶点数据像素化之后成为fragment的方式所引起的。下面是一个简单的立方体,它体现了锯齿边的效果:
|
||||
|
||||

|
||||
|
||||
也许不是立即可见的,如果你更近的看看立方体的边,你就会发现锯齿了。如果我们放大就会看到下面的情境:
|
||||
|
||||

|
||||
|
||||
这当然不是我们在最终版本的应用里想要的效果。这个效果,很明显能看到边是由像素所构成的,这种现象叫做走样(aliasing)。有很多技术能够减少走样,产生更平滑的边缘,这些技术叫做反走样技术(anti-aliasing)。
|
||||
|
||||
首先,我们有一个叫做超级采样反走样技术(super sample anti-aliasing SSAA),它暂时使用一个更高的解析度(以超级采样方式)来渲染场景,当视频输出在帧缓冲中被更新是,解析度便降回原来的普通解析度。这个额外的解析度被用来防止锯齿边。虽然它确实为我们提供了一种解决走样问题的方案,但却由于必须绘制比平时更多的fragment而降低了性能。所以这个技术只流行了一时。
|
||||
|
||||
这个技术的基础上诞生了更为现代的技术,叫做多采样反走样(multisample anti-aliasing)或叫MSAA,虽然它借用了SSAA的理念,但却以更加高效的方式实现了它。这节教程我们会展开讨论这个MSAA技术,它是OpenGL内建的。
|
||||
|
||||
### 多采样
|
||||
|
||||
为了理解什么是多采样,以及它是如何解决走样的问题的,我们先要更深入了解一个OpenGL像素器的工作方式。
|
||||
|
||||
像素器是是你的最终的经处理的顶点和像素着色器之间的所有算法和处理的集合。像素器将属于一个基本图形的所有顶点转化为一系列fragment。顶点坐标理论上可以含有任何坐标,但fragment却不是这样,这是因为它们与你的窗口的解析度有关。几乎永远都不会有顶点坐标和fragment的一对一映射,所以像素器必须以某种方式决定每个特定顶点最终结束于哪个fragment/屏幕坐标上。
|
||||
|
||||

|
||||
|
||||
这里我们看到一个屏幕像素网格,每个像素中心包含一个采样点(sample point),它被用来决定一个像素是否被三角形所覆盖。红色的采样点如果被三角形覆盖,那么就会为这个被覆盖像(屏幕)素生成一个fragment。即使三角形覆盖了部分屏幕像素,但是采样点没被覆盖,这个像素仍然不会任何像素着色器影响到。
|
||||
|
||||
你可能已经明白走样的原因来自何处了。三角形渲染后的版本最后在你的屏幕上是这样的:
|
||||
|
||||

|
||||
|
||||
由于屏幕像素总量的限制,有些边上的像素能被渲染出来,而有些则不会。结果就是我们渲染出的基本图形的非光滑边缘产生了上图的锯齿边。
|
||||
|
||||
多采样所做的正是不再使用单一采样点来决定三角形的覆盖范围,而是采用多个采样点。我们不再使用每个像素中心的采样点,取而代之的是4个子样本(subsample),用它们来决定像素的覆盖率。这意味着颜色缓冲的大小也由于每个像素的子样本的增加而增加了。
|
||||
|
||||

|
||||
|
||||
左侧的图显示了我们普通决定一个三角形的覆盖范围的方式。这个像素并不会运行一个像素着色器(这就仍保持空白),因为它的采样点没有被三角形所覆盖。右边的图展示了多采样的版本,每个像素包含4个采样点。这里我们可以看到只有2个采样点被三角形覆盖。
|
||||
|
||||
!!! Important
|
||||
|
||||
采样点的数量是任意的,更多的采样点能带来更精确的覆盖率。
|
||||
|
||||
多采样开始变得有趣了。2个子样本被三角覆盖,下一步是决定这个像素的颜色。我们原来猜测,我们会为每个被覆盖的子样本运行像素着色器,然后对每个像素的子样本的颜色进行平均化。例子的那种情况,我们在插值的顶点数据的每个子样本上运行像素着色器,然后将这些采样点的最终颜色储存起来。幸好,它不是这么运作的,因为这等于说我们必须运行更多的像素着色器,会明显降低性能。
|
||||
|
||||
MSAA的真正工作方式是,每个像素只运行一次像素着色器,无论多少子样本被三角形所覆盖。像素着色器运行着插值到像素中心的顶点数据,最后颜色被储存近每个被覆盖的子样本中,每个像素的所有颜色接着将平均化,每个像素最终有了一个唯一颜色。在前面的图片中4个样本中只有2个被覆盖,像素的颜色将以三角形的颜色进行平均化,颜色同时也被储存到其他2个采样点,最后生成的是一种浅蓝色。
|
||||
|
||||
结果是,颜色缓冲中所有基本图形的边都生成了更加平滑的样式。让我们看看当再次决定前面的三角形覆盖范围时多样本看起来是这样的:
|
||||
|
||||

|
||||
|
||||
这里每个像素包含着4个子样本(不相关的已被隐藏)蓝色的子样本是被三角形覆盖了的,灰色的没有被覆盖。三角形内部区域中的所有像素都会运行一次像素着色器,它输出的颜色被储存到所有4个子样本中。三角形的边缘并不是所有的子样本都会被覆盖,所以像素着色器的结果仅储存在部分子样本中。根据被覆盖子样本的数量,最终的像素颜色由三角形颜色和其他子样本所储存的颜色所决定。
|
||||
|
||||
大致上来说,如果更多的采样点被覆盖,那么像素的颜色就会更接近于三角形。如果我们用早期使用的三角形的颜色填充像素,我们会获得这样的结果:
|
||||
|
||||

|
||||
|
||||
对于每个像素来说,被三角形覆盖的子样本越少,像素受到三角形的颜色的影响也越少。现在三角形的硬边被比实际颜色浅一些的颜色所包围,因此观察者从远处看上去就比较平滑了。
|
||||
|
||||
不仅颜色值被多采样影响,深度和模板测试也同样使用了多采样点。比如深度测试,顶点的深度值在运行深度测试前被插值到每个子样本中,对于模板测试,我们为每个子样本储存模板值,而不是每个像素。这意味着深度和模板缓冲的大小随着像素子样本的增加也增加了。
|
||||
|
||||
到目前为止我们所讨论的不过是多采样发走样工作的方式。像素器背后实际的逻辑要比我们讨论的复杂,但你现在可以理解多采样反走样背后的概念和逻辑了。
|
||||
|
||||
### OpenGL中的MSAA
|
||||
|
||||
如果我们打算在OpenGL中使用MSAA,那么我们必须使用一个可以每个像素可以储存一个以上的颜色值的颜色缓冲(因为多采样需要我们为每个采样点储存一个颜色)。我们这就需要一个新的缓冲类型,它可以储存要求数量的多重采样样本,它叫做多样本缓冲(multisample buffer)。
|
||||
|
||||
多数窗口系统可以为我们提供一个多样本缓冲,以代替默认的颜色缓冲。GLFW同样给了我们这个功能,我们所要作的就是提示GLFW,我们希望使用一个带有N个样本的多样本缓冲,而不是普通的颜色缓冲,这要在创建窗口前调用glfwWindowHint来完成:
|
||||
|
||||
```c++
|
||||
glfwWindowHint(GLFW_SAMPLES, 4);
|
||||
```
|
||||
|
||||
当我们现在调用glfwCreateWindow,用于渲染的窗口就被创建了,这次每个屏幕坐标使用一个包含4个子样本的颜色缓冲。这意味着所有缓冲的大小都增长4倍。
|
||||
|
||||
现在我们请求GLFW提供了多样本缓冲,我们还要调用glEnable来开启多采样,参数是GL_MULTISAMPLE。大多数OpenGL驱动,多采样默认是开启的,所以这个调用有点多余,但通常记得开启它是个好主意。这样所有OpenGL实现的多采样都开启了。
|
||||
|
||||
```c++
|
||||
glEnable(GL_MULTISAMPLE);
|
||||
```
|
||||
|
||||
当默认帧缓冲有了多采样缓冲附件的时候,我们所要做的全部就是调用glEnable开启多采样。因为实际的多采样算法在OpenGL驱动像素器里已经实现了,所以我们无需再做什么了。如果我们现在来渲染教程开头的那个绿色立方体,我们会看到边缘变得平滑了:
|
||||
|
||||

|
||||
|
||||
这个箱子看起来平滑多了,在场景中绘制任何物体都可以利用这个技术。可以从这里找到这个简单的例子。
|
||||
|
||||
#### 离屏MSAA
|
||||
|
||||
因为GLFW负责创建多采样缓冲,开启MSAA非常简单。如果我们打算使用我们自己的帧缓冲,来进行离屏渲染,那么我们就必须自己生成多采样缓冲了;现在我们需要自己负责创建多采样缓冲。
|
||||
|
||||
有两种方式可以创建多采样缓冲,并使其成为帧缓冲的附件:纹理附件和渲染缓冲附件,和帧缓冲教程里讨论过的普通的附件很相似。
|
||||
|
||||
#### 多采样纹理附件
|
||||
|
||||
为了创建一个支持储存多采样点的纹理,我们使用glTexImage2DMultisample来替代glTexImage2D,它的纹理目标是GL_TEXTURE_2D_MULTISAMPLE:
|
||||
|
||||
```c++
|
||||
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
|
||||
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
|
||||
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
|
||||
```
|
||||
|
||||
第二个参数现在设置了我们打算让纹理拥有的样本数。如果最后一个参数等于GL_TRUE,图像上的每一个纹理像素(texel)将会使用相同的样本位置,以及同样的子样本数量。
|
||||
|
||||
为将多采样纹理附加到帧缓冲上,我们使用glFramebufferTexture2D,不过这次纹理类型是GL_TEXTURE_2D_MULTISAMPLE:
|
||||
|
||||
```c++
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);
|
||||
```
|
||||
|
||||
当前绑定的帧缓冲现在有了一个纹理图像形式的多采样颜色缓冲。
|
||||
|
||||
#### 多采样渲染缓冲对象(Multisampled renderbuffer objects)
|
||||
|
||||
和纹理一样,创建一个多采样渲染缓冲对象不难。而且还很简单,因为我们所要做的全部就是当我们指定渲染缓冲的内存的时候将glRenderbuffeStorage改为glRenderbufferStorageMuiltisample:
|
||||
|
||||
```c++
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);
|
||||
```
|
||||
|
||||
有一样东西在这里有变化,就是缓冲目标后面那个额外的参数,我们将其设置为样本数量,当前的例子中应该是4.
|
||||
|
||||
#### 渲染到多采样帧缓冲
|
||||
|
||||
渲染到多采样帧缓冲对象是自动的。当我们绘制任何东西时,帧缓冲对象就绑定了,像素器会对负责所有多采样操作。我们接着得到了一个多采样颜色缓冲,以及深度和模板缓冲。因为多采样缓冲有点特别,我们不能为其他操作直接使用它们的缓冲图像,比如在着色器中进行采样。
|
||||
|
||||
一个多采样图像包含了比普通图像更多的信息,所以我们需要做的是压缩或还原图像。还原一个多采样帧缓冲,通常用glBlitFramebuffer来完成,它从一个帧缓冲中复制一个区域粘贴另一个里面,同时也将任何多采样缓冲还原。
|
||||
|
||||
glBlitFramebuffer把一个4屏幕坐标源区域传递到一个也是4空间坐标的目标区域。你可能还记得帧缓冲教程中,如果我们绑定到GL_FRAMEBUFFER,我们实际上就同时绑定到了读和写的帧缓冲目标。我们还可以通过GL_READ_FRAMEBUFFER和GL_DRAW_FRAMEBUFFER绑定到各自的目标上。glBlitFramebuffer函数从这两个目标读取,并决定哪一个是源哪一个是目标帧缓冲。接着我们就可以通过把图像位块传送(Blitting)到默认帧缓冲里,将多采样帧缓冲输出传递到实际的屏幕了:
|
||||
|
||||
```c++
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
```
|
||||
|
||||
如果我们渲染应用,我们将得到和没用帧缓冲一样的结果:一个绿色立方体,它使用MSAA显示出来,但边缘锯齿明显少了:
|
||||
|
||||

|
||||
|
||||
你可以[在这里找到源代码](http://learnopengl.com/code_viewer.php?code=advanced/anti_aliasing_framebuffers)。
|
||||
|
||||
但是如果我们打算使用一个多采样帧缓冲的纹理结果来做这件事,就像后处理一样会怎样?我们不能在像素着色器中直接使用多采样纹理。我们可以做的事情是把多缓冲位块传送(Blit)到另一个带有非多采样纹理附件的FBO中。之后我们使用这个普通的颜色附件纹理进行后处理,通过多采样来对一个图像渲染进行后处理效率很高。这意味着我们必须生成一个新的FBO,它仅作为一个将多采样缓冲还原为一个我们可以在像素着色器中使用的普通2D纹理中介。伪代码是这样的:
|
||||
|
||||
```c++
|
||||
GLuint msFBO = CreateFBOWithMultiSampledAttachments();
|
||||
// Then create another FBO with a normal texture color attachment
|
||||
...
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0);
|
||||
...
|
||||
while(!glfwWindowShouldClose(window))
|
||||
{
|
||||
...
|
||||
|
||||
glBindFramebuffer(msFBO);
|
||||
ClearFrameBuffer();
|
||||
DrawScene();
|
||||
// Now resolve multisampled buffer(s) into intermediate FBO
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFBO);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
// Now scene is stored as 2D texture image, so use that image for post-processing
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
ClearFramebuffer();
|
||||
glBindTexture(GL_TEXTURE_2D, screenTexture);
|
||||
DrawPostProcessingQuad();
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
如果我们实现帧缓冲教程中讲的后处理代码,我们就能创造出没有锯齿边的所有效果很酷的后处理特效。使用模糊kernel过滤器,看起来会像这样:
|
||||
|
||||

|
||||
|
||||
你可以[在这里找到所有MSAA版本的后处理源码](http://learnopengl.com/code_viewer.php?code=advanced/anti_aliasing_post_processing)。
|
||||
|
||||
!!! Important
|
||||
|
||||
因为屏幕纹理重新变回了只有一个采样点的普通纹理,有些后处理过滤器,比如边检测(edge-detection)将会再次导致锯齿边问题。为了修正此问题,之后你应该对纹理进行模糊处理,或者创建你自己的反走样算法。
|
||||
|
||||
当我们希望将多采样和离屏渲染结合起来时,我们需要自己负责一些细节。所有细节都是值得付出这些额外努力的,因为多采样可以明显提升场景视频输出的质量。要注意,开启多采样会明显降低性能,样本越多越明显。本文写作时,MSAA4样本很常用。
|
||||
|
||||
#### 自定义反走样算法
|
||||
|
||||
可以直接把一个多采样纹理图像传递到着色器中,以取代必须先还原的方式。GLSL给我们一个选项来为每个子样本进行纹理图像采样,所以我们可以创建自己的反走样算法,在比较大的图形应用中,通常这么做。
|
||||
|
||||
为获取每个子样本的颜色值,你必须将纹理uniform采样器定义为sampler2DMS,而不是使用sampler2D:
|
||||
|
||||
```c++
|
||||
uniform sampler2DMS screenTextureMS;
|
||||
```
|
||||
|
||||
使用texelFetch函数,就可以获取每个样本的颜色值了:
|
||||
|
||||
```c++
|
||||
vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3); // 4th subsample
|
||||
```
|
||||
|
||||
我们不会深究自定义反走样技术的创建细节,但是会给你自己去实现它提供一些提示。
|
Reference in New Issue
Block a user