1. 流程
在顶点和片段着色器之间有一个可选的几何着色器(Geometry Shader),几何着色器的输入是一个图元(如点或三角形)的一组顶点。几何着色器可以在顶点发送到下一着色器阶段之前对它们随意变换。然而,几何着色器最有趣的地方在于,它能够将(这一组)顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。
1.1 一个简单例子
在几何着色器的顶部,我们需要声明从顶点着色器输入的图元类型。这需要在in关键字前声明一个布局修饰符(Layout Qualifier)。这个输入布局修饰符可以从顶点着色器接收下列任何一个图元值:
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_ADJACENCY或GL_TRIANGLE_STRIP_ADJACENCY(6)
接下来,我们还需要指定几何着色器输出的图元类型,这需要在out关键字前面加一个布局修饰符。和输入布局修饰符一样,输出布局修饰符也可以接受几个图元值(还可以指定组成该图元的最大顶点数):
points
line_strip
triangle_strip
接下来给一个简单的几何着色器例子:
#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();
}
可以看到,是从顶点着色器获取顶点数据的输出,然后把它组装成最多两个顶点组成的线元输出
那么它main函数写的是什么呢?
gl_Position是类似于顶点着色器的顶点向量,可以作为几何着色器的输出,这我们了解,那gl_in是什么呢,它其实也是GLSL的一个内建变量(被定义为了接口块),里面的变量之前都提到过了:
in gl_Vertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
} gl_in[];
其实就是从顶点着色器里的得到的输出的顶点数据,我们可以对这些数据在几何着色器里进行处理,比如上面的例子,因为我们只对每个传入的顶点数据单独做处理,所以gl_in数组只有一个顶点,我们对传入的每个顶点进行左右位移,然后分别用EmitVertex()这个GLSL自带函数输出,最后用EndPrimitive()结束输出,那么,这两个经过位移处理的点就变成了组成线元的两个点,它将在后面的光栅化阶段插值形成线,这样,假设我们总共输入在屏幕的四个角的点的话,最后渲染出的画面是这样:
可以看到,四个点被重新装配成线元渲染到了屏幕上
1.2 几何着色器的创建
同样,如果要使用自定义的几何着色器,我们也要将它创建,编译并附加到着色器程序里(和其他着色器一样):
unsigned int geometryShader;
geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometryShader, 1, &gShaderCode, NULL);
glCompileShader(geometryShader);
...
glAttachShader(program, geometryShader);
glLinkProgram(program);
1.3 造个房子
现在我们利用输入的每个点来造个房子,怎么做呢?我们先直接给出例子:
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;
void build_house(vec4 position)
{
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() {
build_house(gl_in[0].gl_Position);
}
发现,我们将输入的点经过位移变成了5个新的顶点,而且输出定义为了layout (triangle_strip, max_vertices = 5) out
,指用最大5个顶点完成一系列三角形的装配,这是怎么做到的?我们可以画张图来看:
我们发现,如果按我们在几何着色器里定义的顶点顺序的话,它是这么装配三角形的:
123,234,345
一共三个三角形,这也就可以解释为什么输出会定义的装配图元的最大顶点数了,因为它会按顺序组装三角形,所以需要限制范围
我们还可以传入顶点颜色
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();
因为知道了每个顶点的位置,我们还可以利用顶点着色器传入每个点对应的颜色,这样就可以得到不同颜色的房子:
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.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:顶部
fColor = vec3(1.0, 1.0, 1.0);
EmitVertex();
EndPrimitive();
这样就好像有了冬天屋顶落雪的感觉
1.4 爆炸效果
既然能修改顶点的位置,那我们能不能使一个面整体进行位移呢?比如做出物体爆炸的效果:
当然可以,我们让面按照法向量的方向位移,而知道了一个三角形面的三个顶点传入的顺序,我们就可以得到它的两条边的向量,做叉乘来求法向量:
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));
}
然后我们按时间变化定义这个面的位移量:
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);
}
总代码:
#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();
}
注意,因为传入的顶点数据是triangles,所以gl_in数组的大小变为了3,我们就可以对三个顶点同时做处理了
1.5 法向量可视化
同样的,因为我们之前完成了利用点元画线的操作,我们自然可以做到法向量的可视化:
首先,在顶点着色器里处理顶点数据,都是我们之前做过的操作
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out VS_OUT {
vec3 normal;
} vs_out;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
mat3 normalMatrix = mat3(transpose(inverse(view * model)));
vs_out.normal = normalize(vec3(projection * vec4(normalMatrix * aNormal, 0.0)));
}
之后,我们利用顶点着色器传入的法向量,对每个顶点画出一条法线(因为一个面三个顶点,组成线元就需要6个点):
#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;
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.0) * MAGNITUDE;
EmitVertex();
EndPrimitive();
}
void main()
{
GenerateLine(0); // 第一个顶点法线
GenerateLine(1); // 第二个顶点法线
GenerateLine(2); // 第三个顶点法线
}
效果:
可见,这种效果还可以用来生成毛发
2. 补充
教程中的爆炸代码只包含了无光照的着色器,为了结合前几节有光照的效果,要对输入输出进行重新定义:
顶点着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 Texcoord;
out VS_OUT{
vec3 normal;
vec3 fragPos;
vec2 texCoords;
}vs_out;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
vs_out.normal = mat3(transpose(inverse(model))) * aNormal;
vs_out.fragPos = vec3(model * vec4(aPos, 1.0));
vs_out.texCoords = Texcoord;
}
几何着色器:
对于要输出到片段着色器的变量,我们必须在EmitVertex的时候将这些变量赋给输出接口块一并发射出去(因为几何着色器是对一个图元的顶点处理,所以要在main函数里面发射多个点,但是在片段着色器里面还是一个顶点一个顶点处理的,所以我们的输出接口块不是数组)
#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
in VS_OUT {
vec3 normal;
vec3 fragPos;
vec2 texCoords;
} gs_in[];
out GS_OUT{
vec3 Normal;
vec3 FragPos;
vec2 TexCoords;
} gs_out;
uniform float time;
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);
}
void main() {
gl_Position = explode(gl_in[0].gl_Position, gs_in[0].normal);
gs_out.TexCoords = gs_in[0].texCoords;
gs_out.Normal = gs_in[0].normal;
gs_out.FragPos = gs_in[0].fragPos;
EmitVertex();
gl_Position = explode(gl_in[1].gl_Position, gs_in[1].normal);
gs_out.TexCoords = gs_in[1].texCoords;
gs_out.Normal = gs_in[1].normal;
gs_out.FragPos = gs_in[1].fragPos;
EmitVertex();
gl_Position = explode(gl_in[2].gl_Position, gs_in[2].normal);
gs_out.TexCoords = gs_in[2].texCoords;
gs_out.Normal = gs_in[2].normal;
gs_out.FragPos = gs_in[2].fragPos;
EmitVertex();
EndPrimitive();
}
片段着色器:
#version 330 core
struct Material {
sampler2D texture_diffuse1;
sampler2D texture_specular1;
sampler2D texture_reflection1;
samplerCube texture1;
sampler2D texture_normal1;
sampler2D texture_height1;
float shininess;
};
struct PointLight {
vec3 position;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
#define NR_POINT_LIGHTS 4
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
struct SpotLight {
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
out vec4 FragColor;
in GS_OUT{
vec3 Normal;
vec3 FragPos;
vec2 TexCoords;
} fs_in;
uniform vec3 viewPos;
uniform Material material;
uniform PointLight pointLights[NR_POINT_LIGHTS];
uniform DirLight dirLight;
uniform SpotLight spotLight;
//uniform sampler2D emission;
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
vec3 CalcSpotLight(SpotLight light, vec3 norm, vec3 FragPos, vec3 viewDir);
void main()
{
// 属性
vec3 norm = normalize(fs_in.Normal);
vec3 viewDir = normalize(viewPos - fs_in.FragPos);
vec3 R = reflect(- viewDir, norm);
vec3 reflectMap = vec3(texture(material.texture_reflection1, fs_in.TexCoords));
vec3 reflection = vec3(texture(material.texture1, R).rgb) * reflectMap * 2;
// 第一阶段:定向光照
vec3 result = CalcDirLight(dirLight, norm, viewDir);
// 第二阶段:点光源
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += CalcPointLight(pointLights[i], norm, fs_in.FragPos, viewDir);
// 第三阶段:聚光
result += CalcSpotLight(spotLight, norm, fs_in.FragPos, viewDir) + reflection;
FragColor = vec4(result, 1.0);
}
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
vec3 lightDir = normalize(-light.direction);
// 漫反射着色
float diff = max(dot(normal, lightDir), 0.0);
// 镜面光着色
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// 合并结果
vec3 ambient = light.ambient * vec3(texture(material.texture_diffuse1, fs_in.TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.texture_diffuse1, fs_in.TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.texture_specular1, fs_in.TexCoords));
return (ambient + diffuse + specular);
}
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// 漫反射着色
float diff = max(dot(normal, lightDir), 0.0);
// 镜面光着色
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// 衰减
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
// 合并结果
vec3 ambient = light.ambient * vec3(texture(material.texture_diffuse1, fs_in.TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.texture_diffuse1, fs_in.TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.texture_specular1, fs_in.TexCoords));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular);
}
vec3 CalcSpotLight(SpotLight light, vec3 norm, vec3 FragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - FragPos);
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
vec3 result;
// 执行光照计算
// 环境光
vec3 ambient = light.ambient * vec3(texture(material.texture_diffuse1, fs_in.TexCoords));
// 漫反射
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * (diff * vec3(texture(material.texture_diffuse1, fs_in.TexCoords)));
// 镜面光
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = light.specular * (spec * vec3(texture(material.texture_specular1, fs_in.TexCoords)));
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
diffuse *= attenuation * intensity;
specular *= attenuation * intensity;
return ambient + diffuse + specular;
}
效果: