三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

OpenGL Geometry Shader

OpenGL Geometry Shader

目录

  1. 什么是Geometry Shader
  2. 在渲染管线中的位置
  3. 语法结构详解
  4. 输入布局限定符
  5. 输出布局限定符
  6. 内置变量与函数
  7. 实战代码分析
  8. 重要注意事项与陷阱
  9. 实际应用场景
  10. 性能考量

1. 什么是Geometry Shader

Geometry Shader(几何着色器)是OpenGL渲染管线中的一个可选着色器阶段,它位于顶点着色器(Vertex Shader)和片段着色器(Fragment Shader)之间。

核心能力

  • 接收图元:接收顶点着色器输出的图元(点、线、三角形)
  • 变换顶点:对图元顶点进行变换处理
  • 生成新图元:将原始图元转换为完全不同类型的图元
  • 输出更多顶点:可能生成比输入更多的顶点,从而生成更多图元
// C++中编译Geometry ShaderGLuint geometryShader=glCreateShader(GL_GEOMETRY_SHADER);glShaderSource(geometryShader,1,&gShaderCode,NULL);glCompileShader(geometryShader);glAttachShader(program,geometryShader);glLinkProgram(program);

2. 在渲染管线中的位置

┌─────────────┐ ┌─────────────────┐ ┌─────────────┐ ┌─────────────┐ │ Vertex │ -> │ Geometry │ -> │ Fragment │ -> │ Rasterizer │ │ Shader │ │ Shader │ │ Shader │ │ (Fixed) │ │ (可编程) │ │ (可编程) │ │ (可编程) │ │ │ └─────────────┘ └─────────────────┘ └─────────────┘ └─────────────┘ 输入 图元变换/生成 逐片段计算 输出合并

Geometry Shader的优势在于能够在GPU上动态生成几何形状,无需预先在顶点缓冲区中定义复杂几何体。


3. 语法结构详解

完整模板

#version 330 core // ========== 输入布局限定符 ========== layout (input_primitive) in; // ========== 输入变量声明 ========== in VS_OUT { vec3 color; vec2 texCoords; } gs_in[]; // 注意:始终是数组! // ========== 输出布局限定符 ========== layout (output_primitive, max_vertices = N) out; // ========== 输出变量声明 ========== out vec3 fColor; out vec2 TexCoords; // ========== 主函数 ========== void main() { // 处理每个顶点... EmitVertex(); EndPrimitive(); }

关键点说明

组成部分必须性说明
#version 330 core必需指定GLSL版本
输入布局限定符必需声明接收的图元类型
输出布局限定符必需声明输出的图元类型和最大顶点数
gl_in[]自动可用内置输入接口块
EmitVertex()必需输出当前顶点
EndPrimitive()必需完成图元输出

4. 输入布局限定符

Geometry Shader必须声明它要接收的图元类型

输入类型OpenGL常量顶点数典型用途
pointsGL_POINTS1点精灵、粒子
linesGL_LINES / GL_LINE_STRIP2线条、路径
lines_adjacencyGL_LINES_ADJACENCY4网格线细分
trianglesGL_TRIANGLES / GL_TRIANGLE_STRIP / GL_TRIANGLE_FAN33D模型表面
triangles_adjacencyGL_TRIANGLES_ADJACENCY6复杂网格处理

代码示例

// 接收点 layout (points) in; // 接收三角形 layout (triangles) in; // 接收线段(带邻接信息) layout (lines_adjacency) in;

重要特性:gl_in接口块

// gl_in是内置的输入接口块,自动可用 in gl_Vertex { vec4 gl_Position; // 顶点着色器输出的位置 float gl_PointSize; // 点精灵大小 float gl_ClipDistance[]; // 裁剪距离 } gl_in[];

注意gl_in[]始终是数组,即使输入是单个顶点(如points),因为Geometry Shader处理的是图元级别的数据。


5. 输出布局限定符

Geometry Shader必须声明输出的图元类型最大顶点数

输出类型说明典型用法
points输出为独立点点精灵、粒子系统
line_strip顶点依次连接成线法线可视化、边界线
triangle_strip顶点依次组成三角形几何体生成

max_vertices 参数

// 警告:必须设置max_vertices,否则编译失败或行为未定义 layout (line_strip, max_vertices = 2) out; // 常见设置 layout (points, max_vertices = 1) out; // 单点 layout (line_strip, max_vertices = 2) out; // 线段 layout (triangle_strip, max_vertices = 3) out; // 单三角形 layout (triangle_strip, max_vertices = 5) out; // 复杂形状

6. 内置变量与函数

EmitVertex() - 发射顶点

void EmitVertex();
  • 将当前设置的gl_Position添加到输出图元
  • 每次调用后,该顶点的属性被锁定
  • 可多次调用生成多个顶点

EndPrimitive() - 结束图元

void EndPrimitive();
  • 将所有已发射的顶点组合成指定的输出图元
  • 每次调用后,图元数据被提交到管线下一阶段
  • 在发射新图元前必须调用

使用示例:生成法线可视化线段

// 从normal_visualization.gs的实际代码 layout (triangles) in; layout (line_strip, max_vertices = 6) out; // 3条线 × 2个顶点 in VS_OUT { vec3 normal; } gs_in[]; const float MAGNITUDE = 0.2; // 法线显示长度 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); // 第三个顶点 }

7. 实战代码分析

7.1 示例项目结构

9.3.geometry_shader_normals/ ├── 9.3.default.vs # 默认顶点着色器 ├── 9.3.default.fs # 默认片段着色器 ├── 9.3.normal_visualization.vs # 法线可视化顶点着色器 ├── 9.3.normal_visualization.gs # 法线可视化几何着色器 ├── 9.3.normal_visualization.fs # 法线可视化片段着色器 └── normal_visualization.cpp # 主程序

7.2 默认顶点着色器 (default.vs)

#version 330 core layout (location = 0) in vec3 aPos; // 顶点位置 layout (location = 2) in vec2 aTexCoords; // 纹理坐标 out vec2 TexCoords; uniform mat4 projection; uniform mat4 view; uniform mat4 model; void main() { TexCoords = aTexCoords; // MVP变换 gl_Position = projection * view * model * vec4(aPos, 1.0); }

7.3 默认片段着色器 (default.fs)

#version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D texture_diffuse1; void main() { // 采样纹理 FragColor = texture(texture_diffuse1, TexCoords); }

7.4 法线可视化顶点着色器 (normal_visualization.vs)

#version 330 core layout (location = 0) in vec3 aPos; // 顶点位置 layout (location = 1) in vec3 aNormal; // 顶点法线 out VS_OUT { vec3 normal; // 输出法线到Geometry Shader } vs_out; uniform mat4 view; uniform mat4 model; void main() { // ========== 核心:法线矩阵变换 ========== // transpose(inverse(view * model)) 是正确变换法线的标准方法 // 原因:法线需要逆矩阵变换才能在非均匀缩放时保持垂直 mat3 normalMatrix = mat3(transpose(inverse(view * model))); // 将法线从模型空间转换到视图空间 // vec4(..., 0.0) 确保法线作为方向向量,不受位移影响 vs_out.normal = normalize(normalMatrix * aNormal); // 输出到视图空间(而非裁剪空间) // Geometry Shader中需要与projection矩阵结合 gl_Position = view * model * vec4(aPos, 1.0); }

7.5 法线可视化几何着色器 (normal_visualization.gs)

#version 330 core // ========== 输入:接收三角形图元 ========== layout (triangles) in; // ========== 输出:输出线段,每个三角形生成3条法线 ========== layout (line_strip, max_vertices = 6) out; // ========== 接收顶点着色器传来的数据 ========== in VS_OUT { vec3 normal; } gs_in[]; // ========== 几何着色器不需要额外的uniform ========== // projection矩阵通过C++传递 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); // 顶点0的法线 GenerateLine(1); // 顶点1的法线 GenerateLine(2); // 顶点2的法线 }

7.6 法线可视化片段着色器 (normal_visualization.fs)

#version 330 core out vec4 FragColor; void main() { // 输出黄色,用于区分法线与实际模型 FragColor = vec4(1.0, 1.0, 0.0, 1.0); }

7.7 主程序分析 (normal_visualization.cpp)

// 创建两个着色器程序Shadershader("9.3.default.vs","9.3.default.fs");ShadernormalShader("9.3.normal_visualization.vs","9.3.normal_visualization.fs","9.3.normal_visualization.gs");// 包含Geometry Shader// 渲染循环while(!glfwWindowShouldClose(window)){// ... 输入处理 ...// 1. 先渲染普通模型shader.use();shader.setMat4("projection",projection);shader.setMat4("view",view);shader.setMat4("model",model);backpack.Draw(shader);// 绘制模型// 2. 再渲染法线可视化(叠加在上方)normalShader.use();normalShader.setMat4("projection",projection);normalShader.setMat4("view",view);normalShader.setMat4("model",model);backpack.Draw(normalShader);// 绘制法线// ... 交换缓冲区 ...}

8. 重要注意事项与陷阱

⚠️ 8.1 max_vertices 限制

// ❌ 错误:如果发射超过max_vertices,超出的顶点会被静默丢弃 layout (line_strip, max_vertices = 2) out; void main() { EmitVertex(); // OK EmitVertex(); // OK EmitVertex(); // 第3个顶点会被丢弃! EndPrimitive(); }

建议:始终设置足够的max_vertices值,并考虑最坏情况。

⚠️ 8.2 输入数据始终是数组

// ❌ 错误理解 layout (points) in; void main() { gl_Position = gl_in.gl_Position; // 编译错误! } // ✅ 正确理解:即使只有一个顶点,也是数组 layout (points) in; void main() { gl_Position = gl_in[0].gl_Position; // 必须使用数组索引 }

⚠️ 8.3 顶点属性在EmitVertex后锁定

// ❌ 错误:EmitVertex后再修改属性无效 gl_Position = vec4(0.0); EmitVertex(); gl_Position = vec4(1.0); // 这个修改不会影响已发射的顶点 // ✅ 正确:先设置所有属性,再发射 gl_Position = vec4(0.0); gl_Color = vec3(1.0, 0.0, 0.0); EmitVertex();

⚠️ 8.4 法线计算顺序问题

// 计算面法线时,叉乘顺序决定法线方向 vec3 GetNormal() { vec3 a = gl_in[0].gl_Position - gl_in[1].gl_Position; vec3 b = gl_in[2].gl_Position - gl_in[1].gl_Position; // 交换a和b会反转法线方向! return normalize(cross(a, b)); }

左手定则/右手定则:取决于顶点的环绕顺序(Winding Order)。

⚠️ 8.5 矩阵变换注意事项

// ❌ 错误:法线在模型空间中变换 vs_out.normal = model * aNormal; // ✅ 正确:使用法线矩阵变换 mat3 normalMatrix = mat3(transpose(inverse(view * model))); vs_out.normal = normalMatrix * aNormal;

原因

  • 普通顶点:直接乘以MVP矩阵即可
  • 法线:需要保证变换后仍与表面垂直
  • 非均匀缩放时,逆矩阵变换是必须的

⚠️ 8.6 颜色广播特性

out vec3 fColor; void main() { // 设置一次颜色,该图元所有顶点都会使用这个颜色 fColor = vec3(1.0, 1.0, 0.0); EmitVertex(); // 使用黄色 EmitVertex(); // 也是黄色 EndPrimitive(); }

⚠️ 8.7 Interface Block 建议

当需要传递大量数据时,使用结构体(Interface Block):

// ✅ 推荐:使用结构体组织数据 in VS_OUT { vec3 normal; vec2 texCoords; vec3 tangent; vec3 bitangent; } gs_in[]; // ❌ 不推荐:大量单独声明 in vec3 normal0; in vec2 texCoords0; in vec3 normal1; // ...混乱且难以维护

9. 实际应用场景

9.1 粒子系统

// 输入点精灵,输出四边形(Billboard) layout (points) in; layout (triangle_strip, max_vertices = 4) out; void main() { vec4 center = gl_in[0].gl_Position; float size = gl_in[0].gl_PointSize; // 生成Billboard的四个角 gl_Position = center + vec4(-size/2, -size/2, 0, 0); EmitVertex(); gl_Position = center + vec4( size/2, -size/2, 0, 0); EmitVertex(); gl_Position = center + vec4(-size/2, size/2, 0, 0); EmitVertex(); gl_Position = center + vec4( size/2, size/2, 0, 0); EmitVertex(); EndPrimitive(); }

9.2 爆炸效果

layout (triangles) in; layout (triangle_strip, max_vertices = 3) out; uniform float time; // 计算面法线 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 = sin(time) * 0.5 + 0.5; return position + vec4(normal * magnitude * 2.0, 0.0); } void main() { vec3 normal = GetNormal(); gl_Position = explode(gl_in[0].gl_Position, normal); EmitVertex(); gl_Position = explode(gl_in[1].gl_Position, normal); EmitVertex(); gl_Position = explode(gl_in[2].gl_Position, normal); EmitVertex(); EndPrimitive(); }

9.3 树/草等植物生成

// 从单点生成整株草 layout (points) in; layout (triangle_strip, max_vertices = 12) out; // 3个叶片 × 4顶点 void main() { vec4 basePos = gl_in[0].gl_Position; // 生成多个叶片 for (int i = 0; i < 3; i++) { float angle = float(i) * 2.0 * 3.14159 / 3.0; gl_Position = basePos; EmitVertex(); gl_Position = basePos + vec4(cos(angle)*0.1, 0.3, sin(angle)*0.1, 0); EmitVertex(); gl_Position = basePos + vec4(cos(angle)*0.05, 0.15, sin(angle)*0.05, 0); EmitVertex(); gl_Position = basePos + vec4(cos(angle)*0.15, 0.45, sin(angle)*0.15, 0); EmitVertex(); EndPrimitive(); } }

9.4 边框/轮廓线渲染

// 从三角形生成线框 layout (triangles) in; layout (line_strip, max_vertices = 4) out; void main() { // 输出三条边 gl_Position = gl_in[0].gl_Position; EmitVertex(); gl_Position = gl_in[1].gl_Position; EmitVertex(); gl_Position = gl_in[2].gl_Position; EmitVertex(); gl_Position = gl_in[0].gl_Position; EmitVertex(); // 闭合 EndPrimitive(); }

10. 性能考量

✅ Geometry Shader的优势

场景优势说明
程序化几何在GPU上即时生成,无需预定义顶点数据
实例化简单形状草、粒子、树等重复元素可高效生成
调试可视化法线、包围盒等调试信息易于添加
动态LOD可根据距离动态调整几何复杂度

⚠️ Geometry Shader的劣势

问题说明
带宽限制输入和输出数据都需要通过总线传输
GPU负载动态生成大量几何体可能成为瓶颈
兼容性OpenGL ES 3.2之前不支持,WebGL完全不支持
优化困难编译器优化有限,需要手动优化

💡 性能优化建议

  1. 减少EmitVertex调用:批量处理顶点
  2. 合理设置max_vertices:过大浪费寄存器空间
  3. 避免分支语句:在Geometry Shader中使用if语句会影响性能
  4. 使用early discard:尽早丢弃不需要的几何体
  5. 考虑Compute Shader替代:现代GPU上Compute Shader可能更高效

总结

Geometry Shader是OpenGL渲染管线中强大的可选着色器阶段,它允许开发者:

  1. 接收和处理图元:从顶点着色器接收点、线、三角形等图元
  2. 动态生成几何:根据需要生成新的顶点,扩展几何复杂度
  3. 变换图元类型:将一种图元类型转换为另一种
  4. 实现特效:法线可视化、爆炸效果、粒子系统等

学习路径建议

  1. 入门:从法线可视化开始,理解基本语法
  2. 进阶:尝试Billboard和粒子系统
  3. 精通:实现复杂程序化几何生成

扩展阅读

  • LearnOpenGL - Geometry Shader原文
  • OpenGL Wiki - Geometry Shader
  • GLSL 4.0 Specification

本文档基于LearnOpenGL教程和LearnOpenGL源码编写,日期:2026-05-14

← 返回列表