LearnOpenGL------几何着色器、实例化
几何着色器
在顶点着色器和片元着色器之间还有一个可选的几何着色器,几何着色器输入的是一个图元(点或者三角形)的一组顶点,几何着色器可以在片元着色器之前对它们进行随意变化,可以变换为不同的图元,也可以生成更多的顶点
一、一个例子
这是一个几何着色器,它接受一个点图元作为输入,以这个点为中心,创建一条水平的线图元。
cpp
#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前面加一个布局修饰符(如下),如果我们要绘制三角形,就将修饰符修改为triangles,括号内的数字表示的是一个图元所包含的最小顶点数。
- points:绘制GL_POINTS图元(1)
- lines:绘制GL_LINES或GL_LINE_STRIP(2)
- line_adjacency:GL_LINE_ADJACENCY或GL_LINE_STRIP_ADJACENCY(4)
- triangles:GL_TRIANGLES、GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN(3)
- triangles_adjacency:GL_TRIANGLES_ADJACENCY或GL_TRIANGLE_STRIP_ADJACENCY(6)
还需要制定几何着色器的输出的图元类型------在out关键字前加布局修饰符,如果要绘制三角形,就将输出定义为triangle_strip,我们还需要设置最大顶点数
- points
- line_strip
- triangle_strip
为了获得前一着色器的输出变量,GLSL提供了一个接口块内建变量
cpp
in gl_Vertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
} gl_in[];
使用EmitVertex和EndPrimitive函数来生成新数据
- EmitVertex:gl_Position向量会被添加到图元中
- EndPrimitive:发射出的顶点都会合成为指定的输出渲染图元
二、建造几个房子
我们要绘制许多三角形,就需要把几何着色器的输出设置为triangle_strip(三角形带)。一个三角形带至少需要3个顶点,并会生成N-2个三角形(N为顶点数)
画房子我们需要三个三角形
首先添加Shader.h头文件中构造函数的重载函数,加入几何着色器
再修改points数据,加入颜色数据
cpp
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, // 右下
-0.5f, -0.5f, 1.0f, 1.0f, 0.0f // 左下
};
再修改顶点着色器,声明一个输出变量color
cpp
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
out VS_OUT {
vec3 color;
} vs_out;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
vs_out.color = aColor;
}
在几何着色器中声明输入gs_in[]输出fColor,并创建建房子函数
cpp
#version 330 core
layout(points) in;
layout(triangle_strip ,max_vertices = 5) out;
in VS_OUT {
vec3 color;
} gs_in[];
out vec3 fColor;
void bulid_house(vec4 position)
{
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.2, 0.2, 0.0, 0.0); // 3:左上
EmitVertex();
gl_Position = position + vec4( 0.2, 0.2, 0.0, 0.0); // 4:右上
EmitVertex();
gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0); // 5:顶部
EmitVertex();
EndPrimitive();
}
void main()
{
bulid_house(gl_in[0].gl_Position);
}
在片元着色器中声明输入变量fColor
cpp
#version 330 core
out vec4 FragColor;
in vec3 fColor;
void main()
{
FragColor = vec4(fColor, 1.0);
}

三、爆破物体
此处的爆破不是要把物体炸了,而是让每个三角形沿着法向量的方向移动一小段时间。我们想要沿着三角形的法向量位移每个顶点,我们首先需要计算这个法向量。我们所要做的是计算垂直于三角形表面的向量。我们获得两个平行于三角形的表面向量a和b时,就可以通过叉乘来获得法向量方向
cpp
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));
}
知道法向量后,就可以进行爆炸
cpp
vec4 explode(vec4 position, vec3 normal)
{
float magnitude = 2.0;
vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude;
return position + vec4(direction, 0.0);
}
几何着色器看起来像这样
cpp
#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();
}
四、法向量可视化
我们将使用几何着色器来实现一个真正有用的例子:显示任意物体的法向量。当编写光照着色器时,你可能会最终会得到一些奇怪的视觉输出,但又很难确定导致问题的原因。很常见的原因就是法向量错误。
思路是我们先不使用几何着色器来绘制场景,之后再次绘制场景,但这次只显示通过几何着色器生成法向量。几何着色器接收一个三角形图元,并沿着法向量生成三条线------每个顶点一个法向量
顶点着色器
cpp
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out VS_OUT {
vec3 normal;
} vs_out;
uniform mat4 view;
uniform mat4 model;
void main()
{
gl_Position = view * model * vec4(aPos, 1.0);
mat3 normalMatrix = mat3(transpose(inverse(view * model)));
vs_out.normal = normalize(vec3(vec4(normalMatrix * aNormal, 0.0)));
}
- 为什么不×projection矩阵
- 投影矩阵用于将观察空间中的点映射到裁剪空间,它涉及到透视除法,这会改变向量的方向和长度。由于法线是方向向量,它们不应该被透视变换影响,因此我们通常不将它们与投影矩阵相乘
mat3(transpose(inverse(view * model)))
的意义- normalMatrix 用于将法线从模型空间变换到观察空间。这是因为法线需要与顶点位置保持一致的空间变换,但法线的变换与位置向量的变换不同。
- 逆矩阵:为了适配观察和模型矩阵的缩放和旋转(由于法线不需要平移)
- 转置矩阵:对于非正交变换(即包含非统一缩放或非纯旋转的变换),仅仅使用逆矩阵是不够的。在这种情况下,我们需要使用逆矩阵的转置来确保法线向量经过变换后仍然是单位长度,并且方向正确。这是因为非正交变换会扭曲空间,使得法线不再垂直于表面。
几何着色器,会接收每一个顶点(包括一个位置向量和一个法向量),并在每个位置向量处绘制一个法线向量
cpp
#version 330 core
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;
in VS_OUT {
vec3 normal;
} gs_in[];
const float MAGNITUDE = 0.4;
uniform mat4 projection;
void GenerateLine(int index)
{
gl_Position = projection * gl_in[index].gl_Position;
EmitVertex();
gl_Position = projection * (gl_in[index].gl_Position +
vec4(gs_in[index].normal, 0.0) * MAGNITUDE);
EmitVertex();
EndPrimitive();
}
void main()
{
GenerateLine(0); // 第一个顶点法线
GenerateLine(1); // 第二个顶点法线
GenerateLine(2); // 第三个顶点法线
}
- magnitude:用来限制显示出的法向量大小
片元着色器
cpp
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
实例化
面对数量巨大但是相同的物体要绘制时,使用glDrawArrays和glDrawElements的性能消耗就会很大,因为OpenGL在绘制之前需要准备很多工作,比如告诉GPU从哪个缓冲读取数据,从哪找顶点属性等等(大量DrawCall)
实例化渲染就是一次性将数据都发给GPU,使用一个绘制函数让OpenGL利用这些数据来绘制。我们只需要将glDrawArrays和glDrawElements的渲染调用分别改为 glDrawArraysInstanced 和 glDrawElementsInstanced 就可以了。GLSL在顶点着色器中嵌入了另一个内建变量,gl_InstanceID ,以此来对不同的物体设置一些别的效果(位置不同等)
片元着色器会从顶点着色器接受颜色向量,并将其设置为它的颜色输出,来实现四边形颜色
cpp
#version 330 core
out vec4 FragColor;
in vec3 fColor;
void main()
{
FragColor = vec4(fColor, 1.0);
}
顶点着色器会设置一个uniform变量offsets数组,它包含100个偏移向量来设置矩形的位置,我们会使用gl_InstanceID来索引offsets数组
cpp
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
out vec3 fColor;
uniform vec2 offsets[100];
void main()
{
vec2 offset = offsets[gl_InstanceID];
gl_Position = vec4(aPos + offset, 0.0, 1.0);
fColor = aColor;
}
在主代码中,我们首先创建绑定VAO、VBO,然后创建了一个偏移数组translations
cpp
glm::vec2 translations[100];
int index = 0;
float offset = 0.1f;
for(int y = -10; y < 10; y += 2)
{
for(int x = -10; x < 10; x += 2)
{
glm::vec2 translation;
translation.x = (float)x / 10.0f + offset;
translation.y = (float)y / 10.0f + offset;
translations[index++] = translation;
}
}
在渲染循环中,我们要把这个数组赋值给shader(一个数据一个数据的传)
cpp
shader.use();
for(unsigned int i = 0; i < 100; i++)
{
stringstream ss;
string index;
ss << i;
index = ss.str();
shader.setVec2(("offsets[" + index + "]").c_str(), translations[i]);
}
然后调用glDrawArraysInstanced来绘制三角形
cpp
glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
一、实例化数组
但是我们在实际中,总会超过100个实例。另一个代替方案就是实例化数组。
实例化数组被定义为一个顶点属性,当我们将顶点属性定义为一个实例化数组时,顶点着色器就只需要对每个实例,而不是每个顶点,更新顶点属性的内容了。
我们将顶点着色器修改如下
cpp
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;
out vec3 fColor;
void main()
{
gl_Position = vec4(aPos + aOffset, 0.0, 1.0);
fColor = aColor;
}
在主代码中,我们为translations数组中的偏移量创建一个VBO,绑定好数据后还需要设置它的顶点属性指针。glVertexAttribDivisor函数用来指定什么时候更新指定的数据,第一个参数是顶点属性,第二个参数指什么时候更新数据: glVertexAttribDivisor(2, 1); 表示layout = 2的数据,在渲染一个新实例的时候更新顶点属性
cpp
unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vec2) * 100, &translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1);
这样就可以直接画而不需要传uniform了
二、小行星带
在岩石的顶点着色器中,设置了model的实例化数组。因为一个mat4本质上是4个vec4,我们需要为这个矩阵预留4个顶点属性。因为我们将它的位置值设置为3,矩阵每一列的顶点属性位置值就是3、4、5和6。
cpp
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in mat4 aInstanceMatrix;
out vec2 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
TexCoords = aTexCoords;
gl_Position = projection * view * aInstanceMatrix * vec4(aPos, 1.0f);
}
片元着色器是正常的加载纹理
cpp
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture_diffuse1;
void main()
{
FragColor = texture(texture_diffuse1, TexCoords);
}
在主程序中,我们设置modelMatrices数组来为每个岩石设置不同的随机属性(位置、大小、旋转方向)。我们将岩石放在半径为150的环上
- srand函数使用当前时间作为种子来初始化随机数生成器。这确保了每次运行程序时,生成的随机数序列都是不同的
- 位移随机:
- x:模型在X轴上的位置,由圆的半径和随机位移确定。
- y:模型在Y轴上的位置,位移较小,以保持星域的高度比X和Z轴的宽度小。
- z:模型在Z轴上的位置,同样由圆的半径和随机位移确定。
glm::translate函数将模型位移到计算出的位置。
- 缩放随机:生成一个介于0.05和0.25之间的随机缩放因子,并使用glm::scale函数对模型进行缩放。
- 旋转随机:生成一个0到360之间的随机旋转角度,并选择一个固定的旋转轴(这里是(0.4, 0.6, 0.8))。glm::rotate函数根据这个角度和轴旋转模型。
cpp
unsigned int amount = 100000;
glm::mat4* modelMatrices;
modelMatrices = new glm::mat4[amount];
srand(static_cast<unsigned int>(glfwGetTime())); // initialize random seed
float radius = 150.0;
float offset = 25.0f;
for (unsigned int i = 0; i < amount; i++)
{
glm::mat4 model = glm::mat4(1.0f);
// 1. translation: displace along circle with 'radius' in range [-offset, offset]
float angle = (float)i / (float)amount * 360.0f;
float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
float x = sin(angle) * radius + displacement;
displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
float y = displacement * 0.4f; // keep height of asteroid field smaller compared to width of x and z
displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
float z = cos(angle) * radius + displacement;
model = glm::translate(model, glm::vec3(x, y, z));
// 2. scale: Scale between 0.05 and 0.25f
float scale = static_cast<float>((rand() % 20) / 100.0 + 0.05);
model = glm::scale(model, glm::vec3(scale));
// 3. rotation: add random rotation around a (semi)randomly picked rotation axis vector
float rotAngle = static_cast<float>((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;
}
我们需要为mat4四个顶点属性设置属性指针,并设置为实例化数组
cpp
// 顶点缓冲对象
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
for(unsigned int i = 0; i < rock.meshes.size(); i++)
{
unsigned int VAO = rock.meshes[i].VAO;
glBindVertexArray(VAO);
// 顶点属性
GLsizei vec4Size = sizeof(glm::vec4);
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(vec4Size));
glEnableVertexAttribArray(5);
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
glEnableVertexAttribArray(6);
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));
glVertexAttribDivisor(3, 1);
glVertexAttribDivisor(4, 1);
glVertexAttribDivisor(5, 1);
glVertexAttribDivisor(6, 1);
glBindVertexArray(0);
}
然后使用网格的VAO,这一次使用glDrawElementsInstanced进行绘制:
cpp
rockShader.use();
for(unsigned int i = 0; i < rock.meshes.size(); i++)
{
glBindVertexArray(rock.meshes[i].VAO);
glDrawElementsInstanced(
GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount
);
}
