几何着色器(Geometry Shader)是 GPU 渲染管线中一个可选的可编程阶段, 能够以单输入图元为单位,动态生成或丢弃任意数量的新图元。 本文将深入讲解它的原理,并以草地渲染、头发丝模拟和拓扑结构修改为例, 展示其在实际项目中的应用方式和编写技巧。

几何着色器是什么?
在传统固定管线中,顶点数据一旦经过顶点着色器处理,其图元的形状和数量便已固定。 几何着色器打破了这一限制------它接收完整的图元(点、线段或三角形) 作为输入,可以在 GPU 上实时地决定输出多少个、什么形状的图元,甚至完全丢弃某个图元。
🔢
输入图元类型
points · lines · lines_adjacency · triangles · triangles_adjacency
📤
输出图元类型
points · line_strip · triangle_strip(最多 256 个顶点)
⚡
核心能力
每次调用可产生 0 ~ N 个新图元,支持自定义几何形态
🎯
典型应用
草地、头发丝、爆炸粒子、阴影体、法线可视化、层渲染
GLSL 几何着色器结构
与顶点/片段着色器不同,几何着色器需要在头部声明输入/输出图元类型及最大顶点数:
cs
#version 450 core
// 声明输入图元:每次接收一个完整三角形
layout(triangles) in;
// 声明输出:三角条带,最多输出 3 个顶点(= 1 个三角形)
layout(triangle_strip, max_vertices = 3) out;
void main() {
for (int i = 0; i < 3; i++) {
// gl_in[] 包含输入图元的所有顶点数据
gl_Position = gl_in[i].gl_Position;
EmitVertex(); // 提交一个顶点到输出流
}
EndPrimitive(); // 结束当前图元
}
关键机制: EmitVertex() 将当前所有输出变量的值收集为一个顶点, EndPrimitive() 将之前 Emit 的顶点组合成一个输出图元。 多次调用这对函数即可产生多个图元。
02
动态草地生成
草地是几何着色器最经典的应用场景之一。其核心思路是: 将地面铺设为稀疏的点云(GL_POINTS), 每个点在几何着色器中被展开为一根草叶------通常用 3~5 个三角形模拟出弯曲的叶片轮廓。 这样只需要传输极少量顶点到 GPU,就能渲染出数以百万计的草叶。

草地着色器完整实现
以下是一个具备风力弯曲动画效果的草地几何着色器。 每根草由5个顶点 构成的三角条带渲染, 根部固定、顶部随 uTime 偏移模拟风吹效果。
cs
#version 450 core
layout(points) in;
layout(triangle_strip, max_vertices = 10) out;
uniform mat4 u_MVP; // 模型-视图-投影矩阵
uniform float u_Time; // 当前时间(秒)
uniform float u_WindStr; // 风力强度
uniform vec2 u_WindDir; // 风向(归一化 XZ 方向)
out vec3 v_Color; // 传给片段着色器的颜色
out float v_UV_Y; // 草叶高度 UV(0=根 1=顶)
// 伪随机:根据位置生成稳定的随机值
float rand(vec2 co) {
return fract(sin(dot(co, vec2(127.1, 311.7))) * 43758.5453);
}
void main() {
vec4 root = gl_in[0].gl_Position; // 草根世界坐标
float r = rand(root.xz); // 该草的随机种子
float height = 0.4 + r * 0.6; // 草高 0.4~1.0
float width = 0.02 + r * 0.03; // 草宽 0.02~0.05
float angle = r * 3.1415926; // 朝向(随机角度)
// 风吹偏移量(顶部最大,根部为 0)
float phase = r * 6.28318; // 相位差,使每根草不同步
float wind = sin(u_Time * 1.5 + phase) * u_WindStr;
// 草叶宽度方向向量(XZ平面)
vec3 right = vec3(cos(angle), 0.0, sin(angle)) * width;
// 生成5个控制点(3段草叶,宽度逐渐收窄)
for (int seg = 0; seg < 3; seg++) {
float t = float(seg) / 2.0; // 归一化高度 t ∈ [0,1]
float windBend = wind * t * t; // 二次曲线弯曲(底部硬)
vec3 tipOff = vec3(u_WindDir.x, 0.0, u_WindDir.y) * windBend;
vec3 pos = root.xyz + vec3(0, height * t, 0) + tipOff;
float w = (1.0 - t * 0.9); // 越高越窄
// 左顶点
v_UV_Y = t;
v_Color = mix(vec3(0.2,0.5,0.1), vec3(0.5,0.8,0.2), t);
gl_Position = u_MVP * vec4(pos - right * w, 1.0);
EmitVertex();
// 右顶点
gl_Position = u_MVP * vec4(pos + right * w, 1.0);
EmitVertex();
}
// 草尖(单点,最后一个三角形的顶点)
vec3 tip = root.xyz + vec3(0, height, 0)
+ vec3(u_WindDir.x,0,u_WindDir.y) * wind;
v_UV_Y = 1.0;
v_Color = vec3(0.7, 0.9, 0.3);
gl_Position = u_MVP * vec4(tip, 1.0);
EmitVertex();
EndPrimitive();
}
性能技巧: 草地通常结合 LOD(细节层次) 使用------近处用几何着色器生成完整叶片, 远处改用 billboard 贴图或直接剔除。 还可利用 Transform Feedback 将生成的顶点写回 VBO 以避免重复计算。
03
头发丝模拟渲染
头发渲染是实时图形中最具挑战性的课题之一。 几何着色器可以将**每条折线段(line strip)**扩展为具有厚度的扁平多边形带(billboard strip), 使头发丝在任意视角下都保持像素级的可见宽度。

头发丝 Billboard 展开着色器
cs
#version 450 core
layout(lines) in; // 每次接收一个线段(2个顶点)
layout(triangle_strip, max_vertices = 4) out;
uniform mat4 u_MVP;
uniform mat4 u_MV; // 模型-视图矩阵(用于视空间计算)
uniform mat4 u_Proj;
uniform float u_HairWidth; // 发丝世界空间宽度
in vec3 v_WorldPos[]; // 来自顶点着色器的世界坐标
in float v_SegT[]; // 段归一化长度(0~1)
out vec2 f_TexCoord;
out float f_Alpha; // 发丝边缘透明度
void main() {
// 在视空间中计算线段方向
vec4 p0_vs = u_MV * vec4(v_WorldPos[0], 1.0);
vec4 p1_vs = u_MV * vec4(v_WorldPos[1], 1.0);
vec3 dir = normalize(p1_vs.xyz - p0_vs.xyz);
// 垂直于线段方向的 "右向量"(始终朝向屏幕,形成 billboard)
vec3 up = vec3(0.0, 0.0, -1.0); // 视空间-Z 轴(朝向摄像机)
vec3 right = normalize(cross(dir, up)) * u_HairWidth * 0.5;
// 输出 4 个顶点形成矩形条带(覆盖线段两端)
// 左下
f_TexCoord = vec2(0.0, v_SegT[0]); f_Alpha = 1.0;
gl_Position = u_Proj * (vec4(p0_vs.xyz - right, 1.0));
EmitVertex();
// 右下
f_TexCoord = vec2(1.0, v_SegT[0]); f_Alpha = 1.0;
gl_Position = u_Proj * (vec4(p0_vs.xyz + right, 1.0));
EmitVertex();
// 左上
f_TexCoord = vec2(0.0, v_SegT[1]); f_Alpha = 1.0;
gl_Position = u_Proj * (vec4(p1_vs.xyz - right, 1.0));
EmitVertex();
// 右上
f_TexCoord = vec2(1.0, v_SegT[1]); f_Alpha = 1.0;
gl_Position = u_Proj * (vec4(p1_vs.xyz + right, 1.0));
EmitVertex();
EndPrimitive();
}
修改顶点拓扑结构
几何着色器不仅能生成新图元,还能重新组织现有顶点的连接关系。 以下介绍三种最具代表性的拓扑修改用法:
4.1 法线可视化调试
将每个三角形的法线渲染为一条线段,是调试光照问题的利器。 几何着色器可以在三角形重心处发射法线箭头,而无需修改主渲染逻辑。

cs
#version 450 core
layout(triangles) in;
layout(line_strip, max_vertices = 6) out; // 原三角形 + 法线线段
uniform mat4 u_MVP;
uniform float u_NormalLen; // 法线箭头长度
out vec4 f_Color;
void main() {
vec3 p0 = gl_in[0].gl_Position.xyz;
vec3 p1 = gl_in[1].gl_Position.xyz;
vec3 p2 = gl_in[2].gl_Position.xyz;
// 计算三角形面法线
vec3 edge1 = p1 - p0;
vec3 edge2 = p2 - p0;
vec3 normal = normalize(cross(edge1, edge2));
vec3 center = (p0 + p1 + p2) / 3.0; // 三角形重心
// 发射原三角形(蓝色)
f_Color = vec4(0.4, 0.6, 1.0, 1.0);
for (int i = 0; i < 3; i++) {
gl_Position = u_MVP * gl_in[i].gl_Position;
EmitVertex();
}
EndPrimitive();
// 发射法线线段(橙色)
f_Color = vec4(1.0, 0.45, 0.1, 1.0);
gl_Position = u_MVP * vec4(center, 1.0);
EmitVertex();
gl_Position = u_MVP * vec4(center + normal * u_NormalLen, 1.0);
EmitVertex();
EndPrimitive();
}
4.2 阴影体(Shadow Volume)生成
阴影体算法需要在轮廓边 (一边被光照、另一边背光的边)处挤出阴影多边形。 几何着色器可利用 triangles_adjacency 输入,直接访问相邻三角形信息, 在 GPU 上自动检测轮廓边并生成阴影体侧面。

4.3 Cube Map 六面渲染(Layered Rendering)
几何着色器支持 gl_Layer 内置变量------通过在一次 draw call 中将同一图元分发到立方体贴图的六个面, 可实现环境贴图的一趟渲染(One-Pass Cubemap Rendering), 效率远高于传统的六次 Draw Call 方案。
cs
#version 450 core
layout(triangles) in;
// 一个三角形复制6份,分发到 Cubemap 6个面
layout(triangle_strip, max_vertices = 18) out;
// 6个面的视图-投影矩阵(在 CPU 端预计算)
uniform mat4 u_CubeFaceVP[6];
void main() {
for (int face = 0; face < 6; ++face) {
gl_Layer = face; // 指定写入 Cubemap 第 face 层
for (int v = 0; v < 3; ++v) {
gl_Position = u_CubeFaceVP[face] * gl_in[v].gl_Position;
EmitVertex();
}
EndPrimitive();
}
}
性能考量与现代替代方案
几何着色器功能强大,但也有其固有性能缺陷, 尤其在需要大量图元扩展时表现不如预期。理解其局限性有助于选择合适的工具。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 几何着色器 | 无需 CPU 干预,管线内完成 | 串行执行,破坏 GPU 并行度;顶点放大倍数有限(≤256) | 少量扩展 |
| Compute Shader + Indirect Draw | 完全并行;可生成任意数量顶点 | 实现复杂,需多 Pass | 大规模草地 |
| 细分着色器 (Tessellation) | 硬件加速,内置 LOD 控制 | 只适合均匀细分,无法改变拓扑 | 地形、曲面 |
| Mesh Shader (DX12/Vulkan) | 完全替代 GS,性能更优 | 硬件要求高(RTX+/RDNA2+) | 现代引擎首选 |
⚠️ 性能警告: 几何着色器的「顶点放大」本质上是串行的 ------ GPU 无法在不知道前一图元产生多少顶点的情况下开始处理下一个。 对于每帧需要生成数百万草叶的场景, 强烈推荐改用 Compute Shader + DrawArraysIndirect 方案, 性能通常提升 3~5×。
何时选用几何着色器?
-
✓
调试与可视化法线、切线、包围盒、光栅化线框------即时可视,代码简洁。
-
✓
**扩展倍数小的场景(< 10×)**粒子 Billboard、细小爆炸碎片,每个输入图元只产生少量输出。
-
✓
Layered Rendering利用 gl_Layer 一次渲染 Cubemap / Shadow Map Array,减少 DrawCall。
-
✓
Transform Feedback 数据捕获配合 TF 将生成的图元写回缓冲区供后续 Pass 使用。
-
✗
**不适合:大规模图元放大(草地数十万)**此类场景请使用 Compute Shader + Indirect Draw 替代。
总结
几何着色器是 GPU 渲染管线中一个极具创造力的可编程阶段。 它允许开发者在 GPU 上以图元为单位进行动态几何生成、拓扑重构和层路由, 无需 CPU 参与中间数据的处理。