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

相关推荐
呆呆敲代码的小Y3 小时前
【Unity-AI开发篇】| 游戏中接入DeepSeek实现AI对话,完整详细步骤
人工智能·游戏·unity·ai·游戏引擎·u3d·deepseek
相信神话20211 天前
第四章:Godot 4.6 核心概念与开发环境搭建
游戏引擎·godot·2d游戏编程·godot4·2d游戏开发
代数狂人1 天前
在Godot中应用面向对象原则:C#脚本实践
c#·游戏引擎·godot
Sator11 天前
Unity关于射击游戏人物动画的设计经验
游戏·unity·游戏引擎
冰凌糕1 天前
Unity3D Shader 坐标空间详解
unity
风酥糖2 天前
Godot游戏练习01-第20节-增加亿点点细节
游戏·游戏引擎·godot
智算菩萨2 天前
【OpenGL】6 真实感光照渲染实战:Phong模型、材质系统与PBR基础
开发语言·python·游戏引擎·游戏程序·pygame·材质·opengl
心前阳光2 天前
Unity之ScrollRect简易实现
unity·游戏引擎
WarrenMondeville2 天前
9.Unity面向对象-对象池
unity