一、简介
本文介绍了 OpenGL 中 Geometry Shader 的基本用法。
包括对 Geometry Shader 的简介,并给出了如何使用 Geometry Shader 从线段生成多边形
、从三角形生成点
、从三角形生成四边形
和 显示模型顶点法线
这几种情况的代码示例。
按照本文代码实现完成后,可以得到如下效果(显示模型顶点法线
):
二、Geometry Shader
1. Geometry Shader 简介
Geometry Shader(几何着色器,GS) 是图形渲染管线中的一个可选阶段,它位于 顶点着色器(Vertex Shader, VS) 和 光栅化阶段(Rasterization) 之间。主要作用是接收顶点着色器的输出数据,并生成新图元数据。
1.1 输入与输出
1). 输入数据类型
GS可以处理完整的 图元(如点、线段、三角形),GS 中需要在开始处使用以下代码定义输入的图元类型:
layout(input_primitive) in;
例如:
layout(triangles) in;
就表明该GS的输入(也即 VS 的输出)为GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN
这几种类型的图元之一。
GS 可接受的输入图元 input_primitive
为以下几种[1]:
input_primitive | OpenGL primitives |
---|---|
points | GL_POINTS |
lines | GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP |
lines_adjacency | GL_LINES_ADJACENCY, GL_LINE_STRIP_ADJACENCY |
triangles | GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN |
triangles_adjacency | GL_TRIANGLES_ADJACENCY, GL_TRIANGLE_STRIP_ADJACENCY |
GS每次调用可以访问一个图元的所有顶点数据,GS 中内置一个输入结构体变量:
in gl_PerVertex
{
vec4 gl_Position; // 当前顶点的输出位置
float gl_PointSize; // 光栅化点的像素宽度/高度
float gl_ClipDistance[]; // 用户定义的顶点到 clip plane 的距离
} gl_in[];
可以在GS中使用gl_in[id].gl_Position;
类似的代码处理输入的图元的各个顶点。例如:
// Geometry Shader
...
vec4 vertex_pos = gl_in[0].gl_Position;
...
就是将输入图元第一个顶点的gl_Position
属性赋值给变量vertex_pos
。
2). 输出数据类型
GS 可以输出与输入类型相同或不同的几何数据,如:
- 输入线段,输出多边形;
- 输入三角形,输出点;
- 允许生成新的顶点或丢弃某些图元;
在2.示例
中会对这几种情况提供相应的代码示例。
与输入图元类似,GS 中也需要在开始处定义输出图元的类型,代码如下:
layout(output_primitive, max_vertices = vert_count) out;
其中,out_primitive
为输入图元类型,vert_count
为单次调用GS的输出最大顶点数,这两者都是必须定义的参数。GS 可接受的的 out_primitive
类型如下:
output_primitive | OpenGL Primitive |
---|---|
points | GL_POINTS |
line_strip | GL_LINESTRIP |
triangle_strip | GL_TRIANGLE_STRIP |
1.2 Geometry Shader 用途
- 粒子系统:通过扩展点图元生成粒子。
- 动态细分:在场景中根据需求生成更多几何细节。
- 几何变换:如对图元执行缩放、旋转等变换操作。
- 轮廓生成:生成图元的轮廓线或阴影体。
- 法线可视化:用线段从顶点发射法线。
2. 使用 Geometry Shader 示例
2.1 从线段生成多边形
-
Vertex Shader 代码
#version 330 core layout (location = 0) in vec3 aPos; void main() { gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); }
-
Geometry Shader 代码
#version 330 core layout (lines) in; layout (triangle_strip, max_vertices=3) out; void main(){ // 线段的 第一个 点 vec4 p0 = gl_in[0].gl_Position; // 线段的 第二个 点 vec4 p1 = gl_in[1].gl_Position; // 新增加的点 vec4 p2 = vec4(0, 0, 0, 1); // 添加一个顶点到输出图元 gl_Position = p0; EmitVertex(); // 添加一个顶点到输出图元 gl_Position = p1; EmitVertex(); // 添加一个顶点到输出图元 gl_Position = p2; EmitVertex(); // 结束添加新图元 EndPrimitive(); }
-
Fragment Shader 代码
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }
-
顶点数据及渲染代码
... float vertices[] = { 0.0f, 0.5f, 0.0, // 上 -0.5f, -0.5f, 0.0f, // 左下 0.5f, -0.5f, 0.0f // 右下 }; unsigned int indices[] = { 0, 1, 2 // first Triangle }; ... glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 使用线框模式,绘制时只绘制 轮廓 glDrawElements(GL_LINE_STRIP, 3, GL_UNSIGNED_INT, 0); ...
-
渲染结果
未使用 Geometry Shader 的渲染结果:
使用 Geometry Shader 的渲染结果:
2.2 从三角形生成点
-
Vertex Shader 代码
同 2.1。 -
Geometry Shader 代码
#version 330 core layout (triangles) in; layout (points, max_vertices=3) out; void main(){ // 三角形的 第一个 点 vec4 p0 = gl_in[0].gl_Position; // 三角形的 第二个 点 vec4 p1 = gl_in[1].gl_Position; // 三角形的 第三个 点 vec4 p2 = gl_in[2].gl_Position; // 添加一个顶点到输出图元 gl_Position = p0; EmitVertex(); // 添加一个顶点到输出图元 gl_Position = p1; EmitVertex(); // 添加一个顶点到输出图元 gl_Position = p2; EmitVertex(); // 结束添加新图元 EndPrimitive(); }
-
Fragment Shader 代码
同 2.1。 -
顶点数据及渲染代码
... float vertices[] = { 0.0f, 0.5f, 0.0, // 上 -0.5f, -0.5f, 0.0f, // 左下 0.5f, -0.5f, 0.0f // 右下 }; unsigned int indices[] = { 0, 1, 2 // first Triangle }; ... glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 使用线框模式,绘制时只绘制 轮廓 glPointSize(10.0f); // 设置渲染 POINT 的大小以便于观察 glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0); ...
-
渲染结果
未使用 Geometry Shader 的渲染结果:
使用 Geometry Shader 的渲染结果:
2.3 从三角形生成四边形
-
Vertex Shader 代码
同 2.1。 -
Geometry Shader 代码
#version 330 core layout (triangles) in; layout (triangle_strip, max_vertices=4) out; void main(){ // 三角形的 第一个 点 vec4 p0 = gl_in[0].gl_Position; // 三角形的 第二个 点 vec4 p1 = gl_in[1].gl_Position; // 三角形的 第三个 点 vec4 p2 = gl_in[2].gl_Position; // 根据p0, p1, p2计算四边形的第四个点位置 vec4 p3 = p1 + (p0-p1) + (p2-p1); // 需要特别注意 Emit 顶点的顺序为 1,0,2,3 // 因为一个四边形是由 三角形 1,0,2 和三角形 0,2,3 组成的 // 添加一个顶点到输出图元 gl_Position = p1; EmitVertex(); // 添加一个顶点到输出图元 gl_Position = p0; EmitVertex(); // 添加一个顶点到输出图元 gl_Position = p2; EmitVertex(); // 添加一个顶点到输出图元 gl_Position = p3; EmitVertex(); // 结束添加新图元 EndPrimitive(); }
-
Fragment Shader 代码
同 2.1。 -
顶点数据及渲染代码
... float vertices[] = { 0.0f, 0.5f, 0.0, // 上 -0.5f, -0.5f, 0.0f, // 左下 0.5f, -0.5f, 0.0f // 右下 }; unsigned int indices[] = { 0, 1, 2 // first Triangle }; ... glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 使用线框模式,绘制时只绘制 轮廓 glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0); ...
-
渲染结果
未使用 Geometry Shader 的渲染结果:
使用 Geometry Shader 的渲染结果:
2.4 显示模型顶点法线
-
渲染流程
渲染分为两趟:首先使用showModelShader
渲染模型。showModelShader
由顶点着色器showModel.vert
和片段着色器showModel.frag
编译链接得到;然后使用visualNormalShader
渲染法线。visualNormalShader
由顶点着色器visualNormal.vert
、几何着色器visualNormal.geom
和片段着色器visualNormal.frag
编译链接得到; -
showModelShader
代码
顶点着色器showModel.vert
:#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNor; layout (location = 2) in vec2 aTexCoord; uniform mat4 model; uniform mat4 view; uniform mat4 projection; out vec3 vertexPos; out vec3 vertexNor; out vec2 textureCoord; out vec3 vertexSpaceNor; void main() { textureCoord = aTexCoord; // 裁剪空间坐标系 (clip space) 中 点的位置 gl_Position = projection * view * model * vec4(aPos, 1.0f); // 世界坐标系 (world space) 中 点的位置 vertexPos = (model * vec4(aPos,1.0f)).xyz; // 世界坐标系 (world space) 中 点的法向 vertexNor = mat3(transpose(inverse(model))) * aNor; // 观察坐标系中 (view space) 中 点的法向 vertexSpaceNor = mat3(transpose(inverse(view * model))) * aNor; }
片段着色器
showModel.frag
:#version 330 core out vec4 FragColor; in vec3 vertexPos; in vec3 vertexNor; in vec2 textureCoord; uniform vec3 cameraPos; uniform vec3 lightPos; uniform vec3 k; uniform sampler2D texture1; void main() { vec3 lightColor = vec3(1.0f, 1.0f, 1.0f); // Ambient // Ia = ka * La float ambientStrenth = k[0]; vec3 ambient = ambientStrenth * lightColor; // Diffuse // Id = kd * max(0, normal dot light) * Ld float diffuseStrenth = k[1]; vec3 normalDir = normalize(vertexNor); vec3 lightDir = normalize(lightPos - vertexPos); vec3 diffuse = diffuseStrenth * max(dot(normalDir, lightDir), 0.0) * lightColor; // Specular (Phong) // Is = ks * (view dot reflect)^s * Ls float specularStrenth = k[2]; vec3 viewDir = normalize(cameraPos - vertexPos); vec3 reflectDir = reflect(-lightDir, normalDir); vec3 specular = specularStrenth * pow(max(dot(viewDir, reflectDir), 0.0f), 2) * lightColor; // Specular (Blinn-Phong) // Is = ks * (normal dot halfway)^s Ls // float specularStrenth = k[2]; // vec3 viewDir = normalize(cameraPos - vertexPos); // vec3 halfwayDir = normalize(lightDir + viewDir); // vec3 specular = specularStrenth * // pow(max(dot(normalDir, halfwayDir), 0.0f), 2) * // lightColor; // Obejct color vec3 objectColor = texture(texture1, textureCoord).xyz; // Color = Ambient + Diffuse + Specular // I = Ia + Id + Is FragColor = vec4((ambient + diffuse + specular) * objectColor, 1.0f); }
-
visualNormalShader
代码
顶点着色器visualNormal.vert
:#version 330 core layout(location = 0) in vec3 aPos; layout(location = 1) in vec3 aNor; layout(location = 2) in vec2 aTexCoord; uniform mat4 model; uniform mat4 view; out vec3 vertexSpaceNor; void main() { // 观察空间坐标系 ( space) 中 点的位置 gl_Position = view * model * vec4(aPos, 1.0f); // 观察坐标系中 (view space) 中 点的法向 vertexSpaceNor = mat3(transpose(inverse(view * model))) * aNor; }
几何着色器
visualNormal.geom
:#version 330 core layout(triangles) in; layout(line_strip, max_vertices = 6) out; in vec3 vertexSpaceNor[]; uniform mat4 projection; void main() { vec4 p0 = gl_in[0].gl_Position; vec4 p1 = gl_in[1].gl_Position; vec4 p2 = gl_in[2].gl_Position; vec3 n0 = vertexSpaceNor[0]; vec3 n1 = vertexSpaceNor[1]; vec3 n2 = vertexSpaceNor[2]; gl_Position = projection * p0; EmitVertex(); gl_Position = projection * (p0 + vec4(n0, 0.0) * 0.03); EmitVertex(); EndPrimitive(); gl_Position = projection * p1; EmitVertex(); gl_Position = projection * (p1 + vec4(n1, 0.0) * 0.03); EmitVertex(); EndPrimitive(); gl_Position = projection * p2; EmitVertex(); gl_Position = projection * (p2 + vec4(n2, 0.0) * 0.03); EmitVertex(); EndPrimitive(); }
片段着色器
visualNormal.frag
:#version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0, 1.0, 0.0, 1.0); }
-
渲染代码
main.cpp
:... /****** 5.开始绘制 ******/ float rotate = 0.0f; while (!glfwWindowShouldClose(window)) { rotate += 0.05f; // input // ----- processInput(window); // render // ------ glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 清除颜色缓冲区 并且 清楚深度缓冲区 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 5.1 使用 showModelShader 渲染模型 showModelShader.use(); // model 矩阵 glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f)); model = glm::rotate(model, glm::radians(0.0f), glm::vec3(1.0f, 0.0f, 0.0f)); model = glm::rotate(model, glm::radians(rotate), glm::vec3(0.0f, 1.0f, 0.0f)); model = glm::rotate(model, glm::radians(0.0f), glm::vec3(0.0f, 0.0f, 1.0f)); model = glm::scale(model, glm::vec3(0.5f, 0.5f, 0.5f)); // view 矩阵 glm::mat4 view = glm::mat4(1.0f); view = glm::lookAt(camera_pos, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f)); // projection 矩阵 glm::mat4 projection = glm::mat4(1.0f); projection = glm::perspective(glm::radians(60.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); showModelShader.setMat4("model", model); showModelShader.setMat4("view", view); showModelShader.setMat4("projection", projection); showModelShader.setVec3("k", k[0], k[1], k[2]); showModelShader.setVec3("cameraPos", camera_pos); showModelShader.setVec3("lightPos", light_pos); ourModel.Draw(showModelShader); // 5.2 使用 visualNormalShader 渲染法向 visualNormalShader.use(); visualNormalShader.setMat4("model", model); visualNormalShader.setMat4("view", view); visualNormalShader.setMat4("projection", projection); ourModel.Draw(visualNormalShader); glfwSwapBuffers(window); // 在gfw中启用双缓冲,确保绘制的平滑和无缝切换 glfwPollEvents(); // 用于处理所有挂起的事件,例如键盘输入、鼠标移动、窗口大小变化等事件 } ...
-
渲染结果
三、全部代码
1. 环境需要
- Linux,或者 windos下使用wsl2。
- 安装GLFW和GLAD。请参考[OpenGL] wsl2上安装使用cmake+OpenGL教程。
- 安装glm。glm是个可以只使用头文件的库,因此可以直接下载release的压缩文件,然后解压到
include
目录下。例如,假设下载的release版本的压缩文件为glm-1.0.1-light.zip
。将glm-1.0.1-light.zip
复制include
目录下,然后执行以下命令即可解压glm源代码:unzip glm-1.0.1-light.zip
- 需要使用
Assimp
库加载obj模型,在 ubuntu 下可以使用以下命令安装Assimp
:sudo apt-get update sudo apt-get install libassimp-dev
- 需要下载 stb_image.h 作为加载
.png
图像的库。将 stb_image.h 下载后放入include/
目录下。
2. 资源代码
用于显示模型顶点法线
的全部代码以及模型文件可以在[OpenGL]使用GeometryShader实现显示模型法向中下载。
3. 编译运行及结果
编译运行:
cd ./build
cmake ..
make
./OpenGL_GeometryShader_VisualNormal
四、参考
[1.]OpenGL wiki-Geometry Shader
[2.]LearnOpenGL-CN-高级OpenGL-几何着色器