1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00

Rewrite 04-09

This commit is contained in:
Meow J
2017-09-10 00:36:13 -04:00
parent 70b282baa5
commit 64b2f91ea0
2 changed files with 160 additions and 157 deletions

View File

@@ -3,171 +3,166 @@
原文 | [Geometry Shader](http://learnopengl.com/#!Advanced-OpenGL/Geometry-Shader)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | [Geequlim](http://geequlim.com), [KenLee](https://hellokenlee.github.io/)
翻译 | Meow J
校对 | 暂未校对
在顶点和片段着色器之间有一个可选的着色器,叫做几何着色器(Geometry Shader)几何着色器以一个或多个表示为一个单独基本图形primitive的顶点作为输入比如可以是一个点或三角形。几何着色器在将这些顶点发送到下一着色阶段之前,可以将这些顶点转变为它认为合适的内容。几何着色器有意思的地方在于它可以把(一个或多个)顶点变为完全不同的基本图形primitive从而生成比原来多得多的顶点。
在顶点和片段着色器之间有一个可选的<def>几何着色器</def>(Geometry Shader)几何着色器的输入是一个图元(如点或三角形)的一组顶点。几何着色器可以在顶点发送到下一着色阶段之前对它们随意变换。然而,几何着色器最有趣的地方在于,它能够将(这一组)顶点变为完全不同的图元,并且还能生成比原来多的顶点。
我们直接用一个例子深入了解一下
废话不多说,我们直接先看一个几何着色器的例子
```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);
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);
gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
EmitVertex();
EndPrimitive();
}
```
每个几何着色器开始位置我们需要声明输入的基本图形(primitive)类型这个输入是我们从顶点着色器中接收到的。我们在in关键字前声明一个layout标识符。这个输入layout修饰符可以从一个顶点着色器接收以下基本图形值:
几何着色器的顶部,我们需要声明从顶点着色器输入的图元类型。这需要在<fun>in</fun>关键字前声明一个布局修饰符(Layout Qualifier)。这个输入布局修饰符可以从顶点着色器接收下列任何一个图元值:
- `points`:绘制<var>GL_POINTS</var>图元时1
- `lines`:绘制<var>GL_LINES</var>或<var>GL_LINE_STRIP</var>时2
- `lines_adjacency`<var>GL_LINES_ADJACENCY</var>或<var>GL_LINE_STRIP_ADJACENCY</var>4
- `triangles`<var>GL_TRIANGLES</var>、<var>GL_TRIANGLE_STRIP</var>或<var>GL_TRIANGLE_FAN</var>3
- `triangles_adjacency`<var>GL_TRIANGLES_ADJACENCY</var>或<var>GL_TRIANGLE_STRIP_ADJACENCY</var>6
基本图形|描述
---|---
points |绘制GL_POINTS基本图形的时候1
lines |当绘制GL_LINES或GL_LINE_STRIP2
lines_adjacency | GL_LINES_ADJACENCY或GL_LINE_STRIP_ADJACENCY4
triangles |GL_TRIANGLES, GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN3
triangles_adjacency |GL_TRIANGLES_ADJACENCY或GL_TRIANGLE_STRIP_ADJACENCY6
以上是能提供给<fun>glDrawArrays</fun>渲染函数的几乎所有图元了。如果我们想要将顶点绘制为<var>GL_TRIANGLES</var>,我们就要将输入修饰符设置为`triangles`。括号内的数字表示的是一个图元所包含的最小顶点数。
这是我们能够给渲染函数的几乎所有的基本图形。如果我们选择以GL_TRIANGLES绘制顶点我们要把输入修饰符设置为triangles。括号里的数字代表一个基本图形所能包含的最少的顶点数。
接下来,我们还需要指定几何着色器输出的图元类型,这需要在<fun>out</fun>关键字前面加一个布局修饰符。和输入布局修饰符一样,输出布局修饰符也可以接受几个图元值:
当我们需要指定一个几何着色器所输出的基本图形类型时我们就在out关键字前面加一个layout修饰符。和输入layout标识符一样输出的layout标识符也可以接受以下基本图形值
- `points`
- `line_strip`
- `triangle_strip`
* points
* line_strip
* triangle_strip
有了这3个输出修饰符我们就可以使用输入图元创建几乎任意的形状了。要生成一个三角形的话我们将输出定义为`triangle_strip`并输出3个顶点。
使用这3个输出修饰符我们可以从输入的基本图形创建任何我们想要的形状。为了生成一个三角形我们定义一个triangle_strip作为输出然后输出3个顶点
几何着色器同时希望我们设置一个它最大能够输出的顶点数量如果你超过了这个值OpenGL将不会绘制**多出的**顶点),这个也可以在<fun>out</fun>关键字的布局修饰符中设置。在这个例子中,我们将输出一个`line_strip`并将最大顶点数设置为2个
几何着色器同时希望我们设置一个它能输出的顶点数量的最大值如果你超出了这个数值OpenGL就会忽略剩下的顶点我们可以在out关键字的layout标识符上做这件事。在这个特殊的情况中我们将使用最大值为2个顶点来输出一个line_strip。
这种情况你会奇怪什么是线条一个线条是把多个点链接起来表示出一个连续的线它最少有两个点来组成。每个后一个点在前一个新渲染的点后面渲染你可以看看下面的图其中包含5个顶点
如果你不知道什么是线条(Line Strip)线条连接了一组点形成一条连续的线它最少要由两个点来组成。在渲染函数中每多加一个点就会在这个点与前一个点之间形成一条新的线。在下面这张图中我们有5个顶点
![](../img/04/09/geometry_shader_line_strip.png)
上面的着色器,我们只能输出一线段,因为顶点的最大值设置为2。
如果使用的是上面定义的着色器,那么这将只能输出一线段,因为最大顶点数等于2。
为生成更有意义的结果,我们需要某种方式前一着色阶段获得输出。GLSL为我们提供了一个内建变量,它叫做`gl_in`,它的内部看起来可能这样:
生成更有意义的结果,我们需要某种方式来获取前一着色阶段输出。GLSL提供给我们一个<def>内建</def>(Built-in)变量,在内部看起来可能)是这样
```c++
in gl_Vertex
{
vec4 gl_Position;
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
} gl_in[];
```
这里它被声明为一个接口块(interface block前面的教程已经讨论过),它包含几个有意思的变量,其中最有意思的是`gl_Position`,它包含着和我们设置的顶点着色器输出相似的向量。
这里它被声明为一个<def>接口块</def>Interface Block我们在[上一节](08 Advanced GLSL.md)已经讨论过,它包含几个有意思的变量,其中最有趣的一个是<var>gl_Position</var>,它是和顶点着色器输出非常相似的一个向量。
要注意的是,它被声明为一个数组,因为大多数渲染基本图形由一个以上顶点组成,几何着色器接收一个基本图形的所有顶点作为它的输入
要注意的是,它被声明为一个数组,因为大多数渲染图元包含多于1个的顶点而几何着色器的输入是一个图元的**所有**顶点
使用从前一个顶点着色阶段的顶点数据,我们就可以开始生成新的数据了,这是通过2个几何着色器函数`EmitVertex`和`EndPrimitive`来完成的。几何着色器需要你去生成/输出至少一个定义为输出的基本图形。在我们的例子里我们打算至少生成一个线条line strip基本图形
有了之前顶点着色阶段的顶点数据,我们就可以使用2个几何着色器函数<fun>EmitVertex</fun>和<fun>EndPrimitive</fun>,来生成新的数据了。几何着色器希望你能够生成输出至少一个定义为输出的图元。在我们的例子中,我们需要至少生成一个线条图元
```c++
void main() {
gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0);
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);
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`将这两个顶点合为一个单独的有两个顶点的线条。
每次我们调用<fun>EmitVertex</fun>时,<var>gl_Position</var>中的向量会被添加到图元中来。当<fun>EndPrimitive</fun>被调用时,所有发射出的(Emitted)顶点都会合成为指定的输出渲染图元。在一个或多个<fun>EmitVertex</fun>调用之后重复调用<fun>EndPrimitive</fun>能够生成多个图元。在这个例子中,我们发射了两个顶点,它们从原始顶点位置平移了一段距离,后调用了<fun>EndPrimitive</fun>将这两个顶点合为一个包含两个顶点的线条。
现在你了解了几何着色器的工作方式,你可能猜出这个几何着色器做了什么。这个几何着色器接收一个基本图形——点,作为它的输入,使用输入点作为它的中心,创建了一个水平线基本图形。如果我们渲染它,结果就会像这样:
现在你(大概)了解了几何着色器的工作方式,你可能已经猜出这个几何着色器是做什么的了。它接受一个点图元作为输入,以这个点为中心,创建一条水平的线图元。如果我们渲染它,看起来会是这样
![](../img/04/09/geometry_shader_lines.png)
并不是非常引人注目,但考虑到它的输出是使用下面的渲染命令生成的就很有意思
目前还并没有什么令人惊叹的效果,但考虑到这个输出是通过调用下面的渲染函数来生成的,它还是很有意思
```c++
glDrawArrays(GL_POINTS, 0, 4);
```
这是个相对简单的例子,它向你展示了我们如何使用几何着色器来动态地在运行时生成新的形状。本章的后面,我们会讨论一些可以使用几何着色器获得有趣的效果,但现在我们将以创建一个简单的几何着色器开始。
虽然这是一个比较简单的例子,它的确向你展示了如何能够使用几何着色器来动态地生成新的形状。在之后我们会利用几何着色器创建出更有意思的效果,但现在我们仍将从创建一个简单的几何着色器开始。
## 使用几何着色器
为了展示几何着色器的使用,我们将渲染一个简单的场景,在场景中我们只绘制4个点这4个点在标准化设备坐标的z平面上。这些点的坐标是
为了展示几何着色器的用,我们将渲染一个非常简单的场景,我们只会在标准化设备坐标的z平面上绘制四个点。这些点的坐标是:
```c++
GLfloat points[] = {
-0.5f, 0.5f, // 左上
0.5f, 0.5f, // 右上
0.5f, -0.5f, // 右下
-0.5f, -0.5f // 左下
float points[] = {
-0.5f, 0.5f, // 左上
0.5f, 0.5f, // 右上
0.5f, -0.5f, // 右下
-0.5f, -0.5f // 左下
};
```
顶点着色器只在z平面绘制点所以我们只需要一个基本顶点着色器:
顶点着色器只需要在z平面绘制点就可以了,所以我们将使用一个基本顶点着色器:
```c++
#version 330 core
layout (location = 0) in vec2 position;
layout (location = 0) in vec2 aPos;
void main()
{
gl_Position = vec4(position.x, position.y, 0.0f, 1.0f);
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
}
```
我们会简单地为所有点输出绿色,我们直接在片段着色器里进行硬编码
直接在片段着色器中硬编码,将所有的点都输出为绿色
```c++
#version 330 core
out vec4 color;
out vec4 FragColor;
void main()
{
color = vec4(0.0f, 1.0f, 0.0f, 1.0f);
FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
```
为点的顶点生成一个VAO和VBO然后使用`glDrawArrays`进行绘制:
为点的顶点数据生成一个VAO和一个VBO然后使用<fun>glDrawArrays</fun>进行绘制:
```c++
shader.Use();
shader.use();
glBindVertexArray(VAO);
glDrawArrays(GL_POINTS, 0, 4);
glBindVertexArray(0);
```
果是黑色场景中有四个绿点(虽然很难看到)
果是在黑暗的场景中有四个(很难看见的)绿点:
![](../img/04/09/geometry_shader_points.png)
是这些不都是我们之前学过的内容吗?是的,但现在我们会给这个场景加入一个几何着色器,给它增加点活力。
我们之前不是学过这些吗?是的,但现在我们将会添加一个几何着色器,为场景添加活力。
出于学习目的我们将创建一个叫pass-through几何着色器,它用一个point基本图形作为它的输入并把它无修改地传pass到下一个着色器
出于学习目的我们将创建一个<def>传递</def>(Pass-through)几何着色器,它会接收一个点图元,并直接将它**传递**(Pass)到下一个着色器
```c++
#version 330 core
layout (points) in;
layout (points, max_vertices = 1) out;
void main() {
gl_Position = gl_in[0].gl_Position;
void main() {
gl_Position = gl_in[0].gl_Position;
EmitVertex();
EndPrimitive();
}
```
现在这个几何着色器应该很容易理解了。它简单地将它接收到的输入的无修改的顶点位置发射出去,然后生成一个point基本图形
现在这个几何着色器应该很容易理解了,它只是将它接收到的顶点位置不作修改直接发射出去,生成一个点图元
一个几何着色器需要像顶点片段着色器一样编译和链接,但这次我们将使用`GL_GEOMETRY_SHADER`作为着色器类型来创建这个着色器
顶点片段着色器一样,几何着色器也需要编译和链接,但这次在创建着色器时我们将使用<var>GL_GEOMETRY_SHADER</var>作为着色器类型:
```c++
geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
@@ -178,28 +173,27 @@ glAttachShader(program, geometryShader);
glLinkProgram(program);
```
编译着色器的代码和顶点片段着色器的基本一样。要记得检查编译和链接错误!
着色器编译的代码和顶点片段着色器代码都是一样的。记得检查编译和链接错误!
如果你现在编译运行,会看到和下面似的结果:
如果你现在编译运行程序,会看到和下面似的结果:
![](../img/04/09/geometry_shader_points.png)
和没用几何着色器一样!我承认有点无聊,但是事实上,我们仍能绘制证明几何着色器工作了的点,所以现在是时候做点更有意思的事了!
和没使用几何着色器时是完全一样!我承认这是有点无聊,但既然我们仍然能够绘制这些点,所以几何着色器是正常工作的,现在是时候做点更有趣的东西了!
## 造几个房子
绘制点和线没什么意思,所以我们将在每个点上使用几何着色器绘制一个房子。我们可以通过把几何着色器的输出设置为`triangle_strip`来达到这个目的,总共要绘制3个三角形:两个用来组成方形和另表示一个屋顶。
绘制点和线并没有那么有趣,所以我们会使用一点创造力,利用几何着色器在每个点的位置上绘制一个房子。要实现这个,我们可以几何着色器的输出设置为<def>triangle_strip</def>,并绘制个三角形:其中两个组成一个正方形,另一个用作房顶。
OpenGL中三角形带(triangle strip)绘制起来更高效,因为它所使用顶点更少。第一个三角形绘制完后,每个后续顶点会生成一个毗连前一个三角形的新三角形每3个毗连的顶点都能构成一个三角形。如果我们有6个顶点,它们以三角形带的方式组合起来,那么我们会得到这些三角形:1, 2, 3)、(2, 3, 4)、(3, 4, 5)、(4,5,6因此总共可以表示出4个三角形。一个三角形带至少要3个顶点才行,它能生曾N-2个三角形6个顶点我们就能创建6-2=4个三角形。下面的图片表达了这点:
OpenGL中三角形带(Triangle Strip)绘制三角形更高效的方式,它使用顶点更少。第一个三角形绘制完后,每个后续顶点将会在上一个三角形边上生成另一个三角形每3个临近的顶点将会形成一个三角形。如果我们一共有6个构成三角形带的顶点,那么我们会得到这些三角形:(1, 2, 3)、(2, 3, 4)、(3, 4, 5)和(4, 5, 6),共形成4个三角形。一个三角形带至少要3个顶点,并会生成N-2个三角形。使用6个顶点我们创建6-2 = 4个三角形。下面这幅图展示了这点:
![](../img/04/09/geometry_shader_triangle_strip.png)
使用一个三角形带作为一个几何着色器的输出,我们可以轻松创建房子形状,只要以正确的顺序生成3个连的三角形。下面的图像显示,我们需要以何种顺序来绘制点,才能获得我们需要的三角形,图上的蓝点代表输入点:
通过使用三角形带作为几何着色器的输出,我们可以很容易创建出需要的房子形状,只要以正确的顺序生成3个连的三角形就行了。下面这幅图展示了顶点绘制的顺序,蓝点代表的是输入点:
![](../img/04/09/geometry_shader_house.png)
上图的内容转变为几何着色器:
变为几何着色器是这样的
```c++
#version 330 core
@@ -207,36 +201,35 @@ 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:左下
{
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1:左下
EmitVertex();
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0); // 2:右下
EmitVertex();
gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f);// 2:右下角
gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3:左上
EmitVertex();
gl_Position = position + vec4(-0.2f, 0.2f, 0.0f, 0.0f);// 3:左
gl_Position = position + vec4( 0.2, 0.2, 0.0, 0.0); // 4:右
EmitVertex();
gl_Position = position + vec4( 0.2f, 0.2f, 0.0f, 0.0f);// 4:右上
EmitVertex();
gl_Position = position + vec4( 0.0f, 0.4f, 0.0f, 0.0f);// 5:屋顶
gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0); // 5:顶部
EmitVertex();
EndPrimitive();
}
void main()
{
void main() {
build_house(gl_in[0].gl_Position);
}
```
这个几何着色器生成5个顶点每个顶点是点point的位置加上一个偏移量,来组成一个大三角形带。接着最后的基本图形被像素化,片段着色器处理整三角形带,结果是为我们绘制的每个点生成一个绿房子:
这个几何着色器生成5个顶点每个顶点都是原始点的位置加上一个偏移量,来组成一个大三角形带。最终的图元会被光栅化,然后片段着色器处理整三角形带,最终在每个绘制的点处生成一个绿房子:
![](../img/04/09/geometry_shader_houses.png)
可以看到,每个房子实是由3个三角形组成,都是仅仅使用空间中一点来绘制的。绿房子看起来还是不够漂亮,所以我们再给每个房子一个不同的颜色。我们将在顶点着色器中为每个顶点增加一个额外的代表颜色信息的顶点属性
可以看到,每个房子实际上是由3个三角形组成的——全部都是使用空间中一点来绘制的。这些绿房子看起来是有点无聊,所以我们再给每个房子分配一个不同的颜色。为了实现这个,我们需要在顶点着色器中添加一个额外的顶点属性,表示颜色信息,将它传递至几何着色器,并再次发送到片段着色器中
下面是更新的顶点数据:
下面是更新的顶点数据:
```c++
GLfloat points[] = {
float points[] = {
-0.5f, 0.5f, 1.0f, 0.0f, 0.0f, // 左上
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // 右下
@@ -244,12 +237,12 @@ GLfloat points[] = {
};
```
然后我们更新顶点着色器,使用一个接口块来项几何着色器发送颜色属性
然后我们更新顶点着色器,使用一个接口块将颜色属性发送到几何着色器中
```c++
#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec3 color;
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
out VS_OUT {
vec3 color;
@@ -257,12 +250,12 @@ out VS_OUT {
void main()
{
gl_Position = vec4(position.x, position.y, 0.0f, 1.0f);
vs_out.color = color;
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
vs_out.color = aColor;
}
```
我们还需要在几何着色器中声明同的接口块(使用一个不同的接口名)
下来我们还需要在几何着色器中声明同的接口块使用一个不同的接口名
```c++
in VS_OUT {
@@ -270,82 +263,82 @@ in VS_OUT {
} gs_in[];
```
因为几何着色器把多个顶点作为它的输入,从顶点着色器来输入数据总是以数组的形式表示出来,即使现在我们只有一个顶点。
因为几何着色器是作用于输入的一组顶点的,从顶点着色器来输入数据总是以数组的形式表示出来,即便我们现在只有一个顶点。
!!! Important
我们不是必须使用接口块来把数据发送到几何着色器中。我们可以这写:
我们不是必须用接口块来向几何着色器传递数据。如果顶点着色器发送的颜色向量是`out vec3 vColor`我们可以这写:
in vec3 vColor[];
in vec3 vColor[];
如果顶点着色器发送的颜色向量是out vec3 vColor那么接口块就会在比如几何着色器这样的着色器中更轻松地完成工作。事实上,几何着色器的输入可以非常大,它们组成一个大的接口块数组会更有意义
然而,接口块在几何着色器这样的着色器中会更容易处理一点。实际上,几何着色器的输入能够变得非常大,它们合并为一个大的接口块数组会更符合逻辑一点
然后我们还要为下一个像素着色阶段声明一个输出颜色向量:
接下来我们还需要为下个片段着色器阶段声明一个输出颜色向量:
```c++
out vec3 fColor;
```
因为片段着色器只需要一个(插值的)颜色,发送多个颜色并没有什么意义。所以,<var>fColor</var>向量就不是一个数组,而是一个单独的向量。当发射一个顶点的时候,每个顶点将会使用最后在<var>fColor</var>中储存的值,来用于片段着色器的运行。对我们的房子来说,我们只需要在第一个顶点发射之前,使用顶点着色器中的颜色填充<var>fColor</var>一次就可以了。
因为片段着色器只需要一个已进行了插值的颜色传送多个颜色没有意义。fColor向量这样就不是一个数组而是一个单一的向量。当发射一个顶点时为了它的片段着色器运行每个顶点都会储存最后在fColor中储存的值。对于这些房子来说我们可以在第一个顶点被发射对整个房子上色前只使用来自顶点着色器的颜色填充fColor一次
```c++
fColor = gs_in[0].color; //只有一个输出颜色所以直接设置为gs_in[0]
gl_Position = position + vec4(-0.2f, -0.2f, 0.0f, 0.0f); // 1:左下
fColor = gs_in[0].color; // gs_in[0] 因为只有一个输入顶点
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1:左下
EmitVertex();
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0); // 2:右下
EmitVertex();
gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f); // 2:右下
gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3:左上
EmitVertex();
gl_Position = position + vec4(-0.2f, 0.2f, 0.0f, 0.0f); // 3:左
gl_Position = position + vec4( 0.2, 0.2, 0.0, 0.0); // 4:右
EmitVertex();
gl_Position = position + vec4( 0.2f, 0.2f, 0.0f, 0.0f); // 4:右上
gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0); // 5:顶部
EmitVertex();
gl_Position = position + vec4( 0.0f, 0.4f, 0.0f, 0.0f); // 5:屋顶
EmitVertex();
EndPrimitive();
EndPrimitive();
```
所有发射出的顶点都最后储存在fColor中的值嵌入到他们的数据中,和我们在他们的属性中定义的顶点颜色相同。所有的房子便都有了自己的颜色:
所有发射出的顶点都将嵌有最后储存在<var>fColor</var>中的值,即顶点颜色属性值。所有的房子都会有它们自己的颜色
![](../img/04/09/geometry_shader_houses_colored.png)
为了好玩儿,我们可以假装这是冬天,最后一个顶点一个自己的白色,就像在屋顶上落了一些雪。
仅仅是为了有趣,我们可以假装这是冬天,最后一个顶点的颜色设置为白色,给屋顶落上一些雪。
```c++
fColor = gs_in[0].color;
gl_Position = position + vec4(-0.2f, -0.2f, 0.0f, 0.0f);
fColor = gs_in[0].color;
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1:左下
EmitVertex();
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0); // 2:右下
EmitVertex();
gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f);
gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3:左上
EmitVertex();
gl_Position = position + vec4(-0.2f, 0.2f, 0.0f, 0.0f);
gl_Position = position + vec4( 0.2, 0.2, 0.0, 0.0); // 4:右上
EmitVertex();
gl_Position = position + vec4( 0.2f, 0.2f, 0.0f, 0.0f);
gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0); // 5:顶部
fColor = vec3(1.0, 1.0, 1.0);
EmitVertex();
gl_Position = position + vec4( 0.0f, 0.4f, 0.0f, 0.0f);
fColor = vec3(1.0f, 1.0f, 1.0f);
EmitVertex();
EndPrimitive();
EndPrimitive();
```
结果就像这样:
最终结果看起来是这样
![](../img/04/09/geometry_shader_houses_snow.png)
你可以对比一下你的[源码](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_houses)和[着色器](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_houses_shaders)
你可以将你的代码与[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/9.1.geometry_shader_houses/geometry_shader_houses.cpp)的OpenGL代码进行比对
你可以看到,使用几何着色器,你可以使用最简单的基本图形就能获得漂亮的新玩意。因为这些形状是在你的GPU超快硬件动态生成的,这要比使用顶点缓冲自己定义这些形状更为高效。几何缓冲简单经常重复的形状比如体素voxel)的世界和室外草地上,是一种非常强大的优化工具
你可以看到,有了几何着色器,你甚至可以将最简单的图元变得十分有创意。因为这些形状是在GPU超快硬件动态生成的,这会比在顶点缓冲中手动定义图形要高效很多。因此,几何缓冲简单而且经常重复的形状来说是一个很好的优化工具,比如体素(Voxel)世界中的方块和室外草地的每一根草
# 爆破物体
尽管绘制房子的确很有趣,但我们不会经常这么做。因此我们接下来要更上一层楼,去爆破物体!虽然这个我们可能也不会经常用到,但是它能向你展示一些几何着色器的强大之处。
尽管绘制房子非常有趣,但我们不会经常这么做。这也是为什么我们接下来要继续深入,来爆破(Explode)物体!虽然这也是一个不怎么常用的东西,但是它能向你展示几何着色器的强大之处。
当我们说对一个物体进行爆破(Explode)的时候并不是说我们将要把之前的那堆顶点炸掉,但是我们打算把每个三角形沿着它们的法线向量移动一小段距离。视觉效果是整个物体上的三角形看起来就像沿着它们的法线向量爆炸了一样。纳米服上的三角形的爆炸式效果看起来是这样的:
当我们说**爆破**一个物体时,我们并不是指要将宝贵的顶点集给炸掉,我们是要将每个三角形沿着法向量的方向移动一小段时间。效果就是,整个物体看起来像是沿着每个三角形的法线向量**爆炸**一样。爆炸三角形的效果在纳米装模型上看起来是这样的:
![](../img/04/09/geometry_shader_explosion.png)
这样一个几何着色器效果的一好处是,它可以用到任何物体上,无论它们多复杂
这样几何着色器效果的一好处是,无论物体有多复杂,它都能够应用上去
因为我们打算沿着三角形的法线向量移动三角形的每个顶点,我们需要计算它的法线向量。我们要做的是计算出一个向量,它垂直于三角形表面,使用这三个我们已经的到的顶点就能做到。你可能记得在变换教程中,我们可以使用 **叉乘**获取一个垂直于两个其他向量的向量。如果我们有两个向量a和b它们平行于三角形表面,我们就可以对这两个向量进行叉乘得到法线向量了。下面几何着色器函数做的正是这件事,它使用3个输入顶点坐标获取法线向量:
因为我们想要沿着三角形的法向量移每个顶点,我们首先需要计算这个法向量。我们要做的是计算垂直于三角形表面的向量仅使用我们能够访问的3个顶点。你可能记得在[变换](../01 Getting started/07 Transformations.md)小节中,我们使用<def>叉乘</def>来获取垂直于其它两个向量的一个向量。如果我们能够获取两个平行于三角形表面的向量<var>a</var>和<var>b</var>,我们就能够对这两个向量进行叉乘来获取法向量了。下面这个几何着色器函数做的正是这个,来使用3个输入顶点坐标获取法向量:
```c++
vec3 GetNormal()
@@ -356,22 +349,22 @@ vec3 GetNormal()
}
```
这里我们使用减法获取了两个向量a和b它们平行于三角形表面两个向量相减得到一个两个向量的差值,由于所有3个点都三角平面上,任何向量相减都得到一个平行于平面的向量。一定要注意,如果我们换了a和b的叉乘顺序,我们得到的法线向量就会使反的,顺序很重要!
这里我们使用减法获取了两个平行于三角形表面的向量<var>a</var>和<var>b</var>。因为两个向量相减能够得到这两个向量之间的差值,并且三个点都位于三角平面上,对任意两个向量相减都能够得到一个平行于平面的向量。注意,如果我们换了<fun>cross</fun>函数中<var>a</var>和<var>b</var>的位置,我们得到一个指向相反方向的法向量——这里的顺序很重要!
知道了如何计算法线向量我们就能创建一个explode函数,函数返回的是一个新向量,它位置向量沿着法线向量方向平移
既然知道了如何计算法向量,我们就能创建一个<fun>explode</fun>函数了,它使用法向量和顶点位置向量作为参数。这个函数返回一个新向量,它位置向量沿着法线向量进行位移之后的结果
```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);
float magnitude = 2.0;
vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude;
return position + vec4(direction, 0.0);
}
```
函数本身并不复杂sin正弦函数把一个time变量作为它的参数,它根据时间返回一个-1.0到1.0之间的值。因为我们不想让物体坍缩,所以我们sin返回的值做成0到1的范围。最后的值去乘以法线向量direction向量被添加到位置向量上。
函数本身应该不是非常复杂。<fun>sin</fun>函数接收一个<var>time</var>参数,它根据时间返回一个-1.0到1.0之间的值。因为我们不想让物体**向内爆炸**(Implode)我们sin值变换到了[0, 1]的范围。最终的结果会乘以<var>normal</var>向量,并且最终的<var>direction</var>向量会被加到位置向量上。
爆炸效果的完整的几何着色器是这样的,它使用我们的模型加载器绘制一个模型:
使用我们的[模型加载器](../03 Model Loading/01 Assimp.md)绘制一个模型时,<def>爆破</def>(Explode)效果的完整片段着色器是这样的
```c++
#version 330 core
@@ -382,7 +375,7 @@ in VS_OUT {
vec2 texCoords;
} gs_in[];
out vec2 TexCoords;
out vec2 TexCoords;
uniform float time;
@@ -390,7 +383,7 @@ vec4 explode(vec4 position, vec3 normal) { ... }
vec3 GetNormal() { ... }
void main() {
void main() {
vec3 normal = GetNormal();
gl_Position = explode(gl_in[0].gl_Position, normal);
@@ -406,35 +399,35 @@ void main() {
}
```
注意我们同样在发射一个顶点前输出了合适的纹理坐标。
注意我们在发射顶点前输出了对应的纹理坐标。
也不要忘记在OpenGL代码中设置time变量:
而且别忘了在OpenGL代码中设置<var>time</var>变量:
```c++
glUniform1f(glGetUniformLocation(shader.Program, "time"), glfwGetTime());
shader.setFloat("time", glfwGetTime());
```
后的结果是一个随着时间持续不断地爆炸的3D模型不断爆炸不断回到正常状态。尽管没什么大用处它却向你展示出很多几何着色器高级用法。你可以用[完整的源码](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_explode)和[着色器](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_explode_shaders)对比一下你自己的
终的效果是3D模型看起来随着时间不断在爆破它的顶点在这之后又回到正常状态。虽然这并不是非常有用它的确向你展示几何着色器高级用法。你可以将你的代码和[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/9.2.geometry_shader_exploding/geometry_shader_exploding.cpp)完整的源码进行比较
# 显示法向量
# 法向量可视化
在这部分我们将使用几何着色器写一个非常有用的例子:绘制出任意一个物体的全部法向量。当编写光照着色器的时候,你可能会得到一些奇怪的渲染输出,但是你却又很难判断是什么导致了这个问题。通常导致光照错误的是,由不正确加载顶点数据或者错误地定义定点属性又或者在着色器中错误地处理等原因导致的法向量错误。我们希望有某种方式可以检测出我们使用法向量是正确。一种比较好的方法去检测我们的法向量是否有错误是直接把物体的法向量全部绘制出来,而恰恰好几何着色器能完美地达成这个目标
在这部分中,我们将使用几何着色器来实现一个真正有用的例子:显示任意物体的法向量。当编写光照着色器,你可能会最终会得到一些奇怪的视觉输出,但又很难确定导致问题的原因。光照错误很常见的原因就是法向量错误,这可能是由于不正确加载顶点数据错误地将它们定义为顶点属性或在着色器中不正确地管理所导致的。我们想要的是使用某种方式来检测提供的法向量是正确的。检测法向量是否正确的一个很好的方式就是对它们进行可视化,几何着色器正是实现这一目的非常有用的工具
思路是这样的:我们先不用几何着色器正常绘制场景。然后我们再次绘制一遍场景,但这次只显示我们通过几何着色器生成的法线向量。几何着色器一个三角形基本图形作为输入类型用它们生成3条沿法向量方向的法线——每个顶点一条。伪代码应该类似这个样子
思路是这样的:我们先不使用几何着色器正常绘制场景。然后再次绘制场景,但这次只显示通过几何着色器生成向量。几何着色器接收一个三角形图元,并沿着法向量生成三条线——每个顶点一个法向量。伪代码看起来会像是这样
```c++
shader.Use();
shader.use();
DrawScene();
normalDisplayShader.Use();
normalDisplayShader.use();
DrawScene();
```
这次我们会在几何着色器中使用模型提供的顶点法线,而不自己生成为了适应(由于 view 和 model 矩阵产生的)缩放和旋转,我们会在把法向量变换到裁空间坐标系(clip-space)前,先把它变成法向量矩阵(几何着色器接受在裁切空间坐标系下的位置向量,因此我们也需要把法向量换到同一空间坐标系下)。这些步骤都能在顶点着色器中完成:
这次在几何着色器中,我们会使用模型提供的顶点法线,而不自己生成为了适配(观察和模型矩阵的)缩放和旋转,我们在将法线变换到裁空间坐标之前,先使用法线矩阵变换一次(几何着色器接受的位置向量是剪裁空间坐标,所以我们应该将法向量换到相同的空间中)。这可以在顶点着色器中完成:
```c++
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out VS_OUT {
vec3 normal;
@@ -446,13 +439,13 @@ uniform mat4 model;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
gl_Position = projection * view * model * vec4(aPos, 1.0);
mat3 normalMatrix = mat3(transpose(inverse(view * model)));
vs_out.normal = normalize(vec3(projection * vec4(normalMatrix * normal, 1.0)));
vs_out.normal = normalize(vec3(projection * vec4(normalMatrix * aNormal, 1.0)));
}
```
经过变换的裁切坐标系空间法向量通过一个接口块被传递到下个着色阶段。几何着色器接收每个顶点的信息(包括位置向量和法向量),从每个定点的位置上沿法向量方向画一条法线
变换的裁空间法向量会以接口块的形式传递到下个着色阶段。接下来,几何着色器接收每个顶点(包括一个位置向量和一个法向量),并在每个位置向量处绘制一个法线向量
```c++
#version 330 core
@@ -463,43 +456,43 @@ in VS_OUT {
vec3 normal;
} gs_in[];
const float MAGNITUDE = 0.4f;
const float MAGNITUDE = 0.4;
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;
gl_Position = gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0) * MAGNITUDE;
EmitVertex();
EndPrimitive();
}
void main()
{
GenerateLine(0); // First vertex normal
GenerateLine(1); // Second vertex normal
GenerateLine(2); // Third vertex normal
GenerateLine(0); // 第一个顶点法线
GenerateLine(1); // 第二个顶点法线
GenerateLine(2); // 第三个顶点法线
}
```
到现在为止,像这样的几何着色器的内容就不言自明了。需要注意的是我们我们把法线向量乘以一个MAGNITUDE向量来限制显示出的法线向量大小(否则它们就大了)。
像这样的几何着色器应该很容易理解了。注意我们将法向量乘以一个<var>MAGNITUDE</var>向量来限制显示出的法向量大小(否则它们就有点大了)。
由于把法线显示出来通常用于调试目的,我们可以片段着色器的帮助下把它们显示为单色的线(如果你愿意也可以更炫一点)。
因为法线的可视化通常都是用于调试目的,我们可以使用片段着色器,将它们显示为单色的线(如果你愿意也可以是非常好看的线):
```c++
#version 330 core
out vec4 color;
out vec4 FragColor;
void main()
{
color = vec4(1.0f, 1.0f, 0.0f, 1.0f);
FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
```
现在先使用普通着色器渲染你的模型,然后使用特制的法线可视化着色器,你看到类似这样的效果:
现在,首先使用普通着色器渲染模型,使用特别的**法线可视化**着色器渲染,你看到这样的效果:
![](../img/04/09/geometry_shader_normals.png)
除了我们的纳米现在看起来有点像一个带着隔热手套的全身多毛的家伙外,我们拥有了一种非常有效的检查一个模型的法线向量是否有错误的方式。你可以想象到这样的几何着色器也能被用在给物体添加毛发上
尽管我们的纳米现在看起来像是一个体毛很多而且带着隔热手套的人,它能够很有效地帮助我们判断模型的法线是否正确。你可以想象到这样的几何着色器也经常用于给物体添加<def>毛发</def>(Fur)
你可以从这里找到[源码](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_normals)和可显示法线的[着色器](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_normals_shaders)
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/9.3.geometry_shader_normals/normal_visualization.cpp)找到源码