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 参与中间数据的处理。

相关推荐
郝学胜-神的一滴15 小时前
[简化版 GAMES 101] 计算机图形学 08:三角形光栅化上
c++·unity·游戏引擎·godot·图形渲染·opengl·unreal
nnsix15 小时前
Unity ILRuntime 笔记
unity·游戏引擎
nnsix17 小时前
Unity API 兼容的 .NET Standard 2.1 和 .NET Framework 区别
unity·游戏引擎·.net
mxwin17 小时前
Unity Shader 制作半透明物体 使用多Pass提前写入深度的方式 避免穿模
unity·游戏引擎
nnsix19 小时前
Unity HybridCLR 笔记
笔记·unity·游戏引擎
nnsix20 小时前
Unity Addressables 笔记
unity·游戏引擎
RReality20 小时前
【Unity Shader URP】视差贴图 实战教程
ui·平面·unity·游戏引擎·图形渲染·贴图
小清兔1 天前
Addressable的设置打包流程
笔记·游戏·unity·c#
3D霸霸2 天前
Sourcetree 拉取新工程
数据仓库·unity
程序员正茂2 天前
Unity3d中RawImage显示视频画面偏白的解决方法
unity·视频·rawimage