OpenGL Geometry Shader

目录

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

1. 什么是Geometry Shader

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

核心能力

  • 接收图元:接收顶点着色器输出的图元(点、线、三角形)
  • 变换顶点:对图元顶点进行变换处理
  • 生成新图元 :将原始图元转换为完全不同类型的图元
  • 输出更多顶点 :可能生成比输入更多的顶点,从而生成更多图元
cpp 复制代码
// C++中编译Geometry Shader
GLuint 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. 语法结构详解

完整模板

glsl 复制代码
#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常量 顶点数 典型用途
points GL_POINTS 1 点精灵、粒子
lines GL_LINES / GL_LINE_STRIP 2 线条、路径
lines_adjacency GL_LINES_ADJACENCY 4 网格线细分
triangles GL_TRIANGLES / GL_TRIANGLE_STRIP / GL_TRIANGLE_FAN 3 3D模型表面
triangles_adjacency GL_TRIANGLES_ADJACENCY 6 复杂网格处理

代码示例

glsl 复制代码
// 接收点
layout (points) in;

// 接收三角形
layout (triangles) in;

// 接收线段(带邻接信息)
layout (lines_adjacency) in;

重要特性:gl_in 接口块

glsl 复制代码
// 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 参数

glsl 复制代码
// 警告:必须设置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() - 发射顶点

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

EndPrimitive() - 结束图元

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

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

glsl 复制代码
// 从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)

glsl 复制代码
#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)

glsl 复制代码
#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)

glsl 复制代码
#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)

glsl 复制代码
#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)

glsl 复制代码
#version 330 core
out vec4 FragColor;

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

7.7 主程序分析 (normal_visualization.cpp)

cpp 复制代码
// 创建两个着色器程序
Shader shader("9.3.default.vs", "9.3.default.fs");
Shader normalShader("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 限制

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

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

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

glsl 复制代码
// ❌ 错误理解
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后锁定

glsl 复制代码
// ❌ 错误: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 法线计算顺序问题

glsl 复制代码
// 计算面法线时,叉乘顺序决定法线方向
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 矩阵变换注意事项

glsl 复制代码
// ❌ 错误:法线在模型空间中变换
vs_out.normal = model * aNormal;

// ✅ 正确:使用法线矩阵变换
mat3 normalMatrix = mat3(transpose(inverse(view * model)));
vs_out.normal = normalMatrix * aNormal;

原因

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

⚠️ 8.6 颜色广播特性

glsl 复制代码
out vec3 fColor;

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

⚠️ 8.7 Interface Block 建议

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

glsl 复制代码
// ✅ 推荐:使用结构体组织数据
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 粒子系统

glsl 复制代码
// 输入点精灵,输出四边形(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 爆炸效果

glsl 复制代码
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 树/草等植物生成

glsl 复制代码
// 从单点生成整株草
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 边框/轮廓线渲染

glsl 复制代码
// 从三角形生成线框
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教程和LearnOpenGL源码编写,日期:2026-05-14

相关推荐
郝学胜-神的一滴1 天前
中级OpenGL教程 005:为球体&平面注入法线灵魂
c++·unity·图形渲染·three.js·opengl·unreal
XX風2 天前
OpenGL 离屏多重采样抗锯齿 (Off-screen MSAA)
图形渲染
郝学胜-神的一滴4 天前
[简化版 GAMES 101] 计算机图形学 08:三角形光栅化上
c++·unity·游戏引擎·godot·图形渲染·opengl·unreal
RReality4 天前
【Unity Shader URP】视差贴图 实战教程
ui·平面·unity·游戏引擎·图形渲染·贴图
hele_two5 天前
VS Code + CMake 调用 SDL2 & SDL2_image 完整编译教程(Windows 平台)
c++·windows·vscode·图形渲染
hele_two5 天前
SDL2高效画实心圆的算法(一)
c++·算法·图形渲染
XX風5 天前
OpenGL Framebuffer及其附件使用详解
图形渲染
梵尔纳多5 天前
OpenGL 实例化
c++·图形渲染·opengl
hele_two5 天前
SDL2设置透明度
c++·图形渲染