Unity Shader 几何着色器:动态生成图元与顶点拓扑修改

几何着色器(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 参与中间数据的处理。

相关推荐
不吃凉粉19 小时前
Unity与AS通信
unity·游戏引擎
两水先木示1 天前
【Unity】探索小地图迷雾散开Shader效果(基础版)
unity·游戏引擎
WarrenMondeville2 天前
AI复刻街头霸王到 Unity
unity·游戏引擎
玖玥拾2 天前
Lua 基础语法(六)xLua Lua 调用 C# 交互
开发语言·unity·c#·lua
xcLeigh2 天前
Unity基础:MonoBehaviour的生命周期——Awake、OnEnable、Start、Update执行顺序
unity·游戏引擎
qq_213157892 天前
其域lcc2丢失部分lod/远处显示空洞的解决方式
unity·lcc2
辻弋2012 天前
自动检测硬件生成驱动清单、实时监控FPS、120秒性能记录——30+项优化模块化开关,所有改动可精确复原
服务器·windows·游戏引擎·电脑·开源软件
点心的游戏开发世界3 天前
GDScript 入门笔记:目录
开发语言·笔记·游戏引擎·godot
XR技术研习社3 天前
关于 PICO 串流测试中报错 IndexOutOfRangeException: renderPassIndex 的两个排障方法
unity·ar·xr·vr
想做后端的前端3 天前
Unity · 性能优化:内存管理完全指南:从托管堆到跨桥开销
unity·性能优化·游戏引擎