《Mesh Shader 新一代几何管线:Task/Mesh 着色器》

传统渲染管线的顶点/几何着色器以三角形为最小单位,只能按 CPU 给定的顶点缓冲执行,无法在 GPU 内自由生成几何。Mesh Shader 用 Task/Mesh 两级着色器取代整个几何阶段,让 GPU 决定“画哪些、生成多少三角形”,天然支持网格化细分、GPU 剔除与程序化几何。本文讲解传统管线的局限、Task 与 Mesh 着色器工作流、网格化与剔除、Vulkan/DX12 实现差异与性能案例分析。

传统渲染管线把几何处理固定成"顶点着色器逐顶点、几何着色器逐三角形、光栅化器固定"。这套结构服务了二十多年,却在几何生成与 GPU 自主剔除上力不从心:GPU 只能加工 CPU 喂进来的顶点流,无法"自己决定画什么、自己生成三角形"。Mesh Shader 把整个几何阶段替换成 Task(任务) 与 Mesh(网格) 两级可编程阶段,让 GPU 从数据缓冲里抓取网格、做剔除、做 LOD 选择,甚至程序化生成几何。本文从传统局限讲起,覆盖工作流、网格化、剔除、Vulkan/DX12 实现与性能分析,可与 GPU Driven 渲染 互相补充。


一、传统顶点管线的局限

一句话:固定顶点管线的粒度是"顶点/三角形",GPU 无法在内部决定"加载哪份网格、生成几个三角形",所有几何决策都被 CPU 前置——这既是历史包袱,也是性能天花板。

1.1 三个历史痛点

  • 顶点着色器只见顶点:无法获得邻居信息,做不了细分、做不了网格级剔除;
  • 几何着色器效率低:一条线程处理一个三角形、输出受限(最多 1024 字节),放大量几何生成时吞吐暴跌;
  • 绘制粒度固定:vkCmdDrawIndexedIndirect 只能按"顶点缓冲 + 索引"执行,GPU 无法中途改画别的网格或改三角形数量。

1.2 几何生成只能靠"预烘焙"

想画"程序化草、动态细分曲面、GPU 端 LOD 生成的网格",传统管线只能:CPU 预生成顶点缓冲、或写进几何着色器(低效)。后果是 GPU 强大的并行几何能力被浪费。

1.3 从"数据流"到"数据驱动"

传统:CPU 提交顶点流 → 固定顶点/几何阶段 → 光栅化。
Mesh Shader:GPU 读取任意缓冲 → Task 选择与剔除 → Mesh 生成三角形 → 光栅化。几何的"内容"第一次由 GPU 着色器决定。


二、Mesh Shader 工作流:Task + Mesh 两级

一句话:Mesh Shader 把几何阶段拆成 Task(决定"做不做、做哪些")与 Mesh(决定"怎么做三角形")两级,前者按块决策、后者按网格输出三角形,职责分离让剔除与生成都能高效并行。

2.1 两级着色器

  • Task Shader(任务着色器,也称 Amplification Shader):以"任务组"为单位执行,每个任务组对应一个网格批次,可读取对象数据做剔除/LOD 决策,并指定它要发射的 Mesh 线程数(1~4096)。
  • Mesh Shader(网格着色器):一个线程组负责生成一个"网格对象"(一组三角形 + 顶点 + 图元数据),输出交给光栅化器。
Task Shader 阶段(每对象一个任务组):
  读取对象 → 视锥/遮挡剔除 → 决定 LOD
  若可见: 发射 meshThreadCount 条线程给 Mesh Shader
  若不可见: 发射 0 条线程(该对象完全不进入网格化)
Mesh Shader 阶段(每对象一个线程组):
  读顶点/包围盒 → 生成顶点输出 → 生成图元输出
  → 光栅化器

2.2 与旧管线的对比

特性传统顶点管线Mesh Shader 管线
几何决策者CPUGPU(Task 阶段)
最小处理单元顶点 / 三角形网格批次 / 对象
剔除位置CPU 或独立 PassTask Shader 内直接做
几何生成预烘焙/几何着色器Mesh Shader 程序化
三角形吞吐固定顶点流每个网格对象任意规模

2.3 术语:Amplification

DX12 里 Task Shader 官方名称是 Amplification Shader,因为它"放大"工作:一个任务组决定把工作放大成 1~4096 个 Mesh 线程。Vulkan 里叫 Task Shader,语义相同。两者最终都进入光栅化,顶点属性由 Mesh Shader 输出数组提供。


三、Task Shader:剔除与工作量决策

一句话:Task Shader 是"GPU 内的剔除调度器"——它在对象粒度做可见性与 LOD 判断,把不可见对象直接"砍成 0 线程",从源头省下整块几何开销。

3.1 任务组的输入

Task Shader 读取场景对象缓冲,每个任务组对应一个对象(或一批小对象)。用 Mesh Shader 的 DispatchMesh 启动:CPU 只需按对象数量 dispatch,任务组内部自行决定工作量。

// DX12:DispatchMesh 启动 Task+Mesh 管线
pCmdList->DispatchMesh(objectCount, 1, 1);

3.2 输出:发射到 Mesh Shader 的参数

Task Shader 输出一个 task payload 结构,包含 Mesh 线程组要用的数据(如对象指针、LOD 级别、变换),并决定发射多少 Mesh 线程:

// HLSL:Task Shader 示例
struct TaskPayload {
    uint objectId;
    uint lodLevel;
    float4x4 transform;
};

[shader("task")]
void TaskMain(uint tid : SV_GroupThreadID,
              uint gid : SV_GroupID) {
    Object o = objects[gid];
    if (!frustumTest(o.bounds, frustumPlanes)) return; // 剔除:发 0 条
    uint lod = selectLOD(o, viewDistance);

    TaskPayload p;
    p.objectId  = gid;
    p.lodLevel  = lod;
    p.transform = o.transform;
    SetMeshOutputCounts(THREADS_FOR_MESH, 1);  // 指定 Mesh 线程数
    DispatchMeshPayload(p);                    // 携带 payload 到 Mesh 阶段
}

3.3 颗粒度选择

Task 数量不必等于对象数:可以一个 Task 处理多个小对象(批量),或一个大对象拆成多个 Task(并行生成)。典型配置:每 Task 组 14 个对象,Mesh 线程组 32256 条。


四、Mesh Shader:网格化与图元输出

一句话:Mesh Shader 自己从顶点缓冲读取数据、自己构建三角形图元,输出顶点属性数组与图元数组——“画什么"完全由它决定。

4.1 输出结构

Mesh Shader 的输出是定长数组(由 SV_GroupIndex 在组内协作填充):

// HLSL:Mesh Shader 核心
struct VertexOut { float4 position; float3 normal; float2 uv; };
struct MeshOut {
    VertexOut vertices[MAX_VERTS];
    uint      indices[MAX_PRIMS * 3];
};

[shader("mesh")]
void MeshMain(uint tid : SV_GroupThreadID,
              uint gid : SV_GroupID,
              in TaskPayload payload) {
    MeshOut output;
    uint primId = tid / PRIMS_PER_THREAD;
    uint vertBase = payload.objectId * VERTS_PER_OBJ;
    // 每个线程生成若干三角形,写入输出数组
    for (uint k = 0; k < PRIMS_PER_THREAD; ++k) {
        uint prim = primId * PRIMS_PER_THREAD + k;
        output.indices[prim * 3 + 0] = baseVertex(prim) + 0;
        output.indices[prim * 3 + 1] = baseVertex(prim) + 1;
        output.indices[prim * 3 + 2] = baseVertex(prim) + 2;
    }
    // 顶点属性直接从顶点缓冲读取 + 变换
    output.vertices[tid].position = mul(projView,
        float4(vertexData[vertBase + tid], 1.0));
    SetMeshOutputCounts(MAX_VERTS, MAX_PRIMS);
}

4.2 顶点索引的灵活性

传统管线索引来自顶点缓冲,Mesh Shader 的图元可以任意指向输出数组或从输入缓冲读取——这是程序化几何(细分、曲面细分、LOD 生成)的关键自由度。每个线程生成 1~3 个三角形(PRIMS_PER_THREAD),线程组内协作填满输出。

4.3 语义与光栅化衔接

Mesh Shader 输出自动进入光栅化器,顶点属性语义沿用传统管线(SV_POSITION 等),材质着色在像素阶段照常。顶点流可以被 Mesh Shader"凭空生成”,也可以逐顶点变换后传递,两者在同一管线内共存。


五、网格化与程序化几何

一句话:网格化(Tessellation)是 Mesh Shader 最亮的应用——在 GPU 内动态生成细分几何,比如曲面细分、GPU 端地形块 LOD、程序化植被。

5.1 替代硬件细分

传统细分的 Hull/Domain 阶段以 patch 为单位、输出受硬件限制。Mesh Shader 可完全接管细分:Task 阶段决定细分级别,Mesh 阶段按级别生成顶点与图元。带宽与灵活度都更好。

Mesh Shader 细分流程:
  Task: 按距离决定 tessLevel(近处细分多)
  Mesh: 按 tessLevel 生成网格顶点 → 位移贴图偏移 → 输出三角形

5.2 程序化几何示例

// HLSL:由噪声程序化生成草叶(简化)
float3 genGrassVertex(float2 base, float t, float height, uint phase) {
    float3 pos = float3(base.x, height * noise(base + phase), base.y);
    return pos;
}
[shader("mesh")]
void GrassMesh(uint tid : SV_GroupThreadID, in TaskPayload p) {
    // 每线程生成一片草叶的若干三角形
    // 顶点由函数动态生成,无需任何顶点缓冲
}

5.3 约束与最佳实践

  • 输出数组大小是编译期常量(MAX_VERTS / MAX_PRIMS 上限);
  • 组内线程数需能整除输出,避免部分线程空转;
  • 位移贴图采样尽量在 Mesh 阶段一次完成,避免像素阶段重复采样;
  • 任务组之间的负载差异要用"任务数量 × 每任务工作量"平衡。

六、Vulkan 实现:VK_EXT_mesh_shader

一句话:Vulkan 用扩展 VK_EXT_mesh_shader 引入 Mesh/Task 阶段,通过计算着色器式管线与特殊接口暴露,跨厂商一致性好。

6.1 启用与管线创建

// C++/Vulkan:启用 mesh shader 扩展
VkPhysicalDeviceMeshShaderFeaturesEXT meshFeatures{};
meshFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT;
meshFeatures.meshShader = VK_TRUE;
meshFeatures.taskShader  = VK_TRUE;
// 加入 pNext 链创建逻辑设备

// 管线创建:用 mesh shader 作为唯一着色器阶段
VkPipelineShaderStageCreateInfo stage{};
stage.stage = VK_SHADER_STAGE_MESH_BIT_EXT;   // 或 TASK_BIT_EXT
stage.module = meshModule;
vkCreateGraphicsPipelines(device, ..., &pipeline, ...);

6.2 提交绘制

vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
                        layout, 0, 1, &sceneSet, 0, nullptr);
vkCmdDrawMeshTasksEXT(cmd, objectCount, 1, 1);   // 启动 Task 组

与 vkCmdDraw 的区别:不传顶点/索引缓冲,只用 drawCount 指定 Task 组数;每个 Task 组自行决定发射多少 Mesh 线程。

6.3 GLSL 侧写法

// GLSL:Vulkan Mesh Shader 接口
#extension GL_EXT_mesh_shader : require
layout(local_size_x = 32) in;

struct VertexOut { vec4 pos; vec3 nrm; };
layout(out) out vec4 positions[64];
layout(out) out vec3 normals[64];

void main() {
    uint tid = gl_LocalInvocationIndex;
    positions[tid] = transformVertices(tid);
    // 生成图元索引数组
    SetMeshOutputsEXT(64u, 64u);
}

七、DX12 实现:Amplification + Mesh

一句话:DX12 把 Task 称 Amplification、Mesh 称 Mesh,通过 DispatchMesh 与 SetMeshOutputCounts 驱动,概念与 Vulkan 一一对应,但库接口与命名更偏微软风格。

7.1 两阶段与入口

// C++/DX12:管线状态与提交
D3D12_SHADER_BYTECODE ampBytecode = { ampPtr, ampSize };
D3D12_SHADER_BYTECODE meshBytecode = { meshPtr, meshSize };
psoDesc.AS = ampBytecode;    // Amplification (Task)
psoDesc.MS = meshBytecode;   // Mesh
// ...
pCmdList->DispatchMesh(objectCount, 1, 1);

7.2 HLSL 侧对照

[shader("amplification")]
void ASMain(uint tid : SV_GroupThreadID, uint gid : SV_GroupID) {
    // ... 剔除 / LOD ...
    SetMeshOutputCounts(MESH_THREADS, 1);
}
[shader("mesh")]
void MSMain(uint tid : SV_GroupThreadID, in TaskPayload p) {
    // ... 生成顶点/图元 ...
    SetMeshOutputCounts(MAX_VERTS, MAX_PRIMS);
}

7.3 与 Vulkan 的关键差异

维度Vulkan (VK_EXT_mesh_shader)DX12 (Amplification/Mesh)
Task 阶段名Task ShaderAmplification Shader
启动vkCmdDrawMeshTasksEXTDispatchMesh
输出语义自定义输出变量 + SetMeshOutputsEXTSetMeshOutputCounts
扩展状态扩展(厂商一致性)核心(D3D12 内置)
资源绑定Descriptor SetRoot Signature / 根描述符

两者底层语义等价(两级并行、payload 传递、GPU 自主几何),工程上只需适配 API 壳。


八、性能案例分析

一句话:Mesh Shader 的收益来自"剔除前置 + 几何自主 + 减少 CPU 往返",典型案例能省掉数倍 CPU 提交时间与不可见几何开销,但性能提升有前置条件。

8.1 典型案例:大型开放世界

指标传统管线Mesh Shader 管线
每帧 Draw Call1~2 万1 次 DispatchMesh
CPU 提交时间8~15 ms<1 ms
GPU 剔除的物体无(CPU 预剔除)全部(Task 阶段)
三角形生成预烘焙 LODGPU 端 LOD + 程序化

8.2 收益前提

  • 物体数量要大:几万对象时 Task 剔除的收益才显著;
  • 几何要可变:程序化/细分/LOD 生成才体现自由度;
  • CPU 已是瓶颈:若 GPU 满载、CPU 闲,收益主要在功耗而非帧率。

8.3 可能的反效果

  • 每个对象都做剔除,Task 阶段本身有开销,物体很少时反而更慢;
  • Mesh Shader 输出数组过大(MAX_VERTS 高)导致寄存器占用与占用率下降;
  • 驱动兼容性差异(不同厂商 mesh shader 优化程度不同)。

8.4 工程建议

  • 先用 vkCmdDrawMeshTasksEXT 做"透传式"(读取现有顶点缓冲原样输出),验证管线正确性;
  • 再用 RenderDoc 对比"传统 vs Mesh Shader"的几何阶段耗时;
  • 逐步迁移:先加 Task 剔除,再加程序化几何,最后做 LOD 生成;
  • 保留传统管线作为 fallback(低端机/驱动不支持 mesh shader)。

总结

Mesh Shader 把几何管线从"CPU 喂、GPU 吃"升级为"GPU 自己找食吃":Task 阶段做对象粒度剔除与工作量决策,Mesh 阶段自主生成顶点与三角形,两级并行让几何处理第一次完全可编程、可裁剪、可生成。它天然契合 GPU Driven 渲染的批处理需求,也是程序化几何、动态 LOD 与新世代引擎的几何基础。

环节角色职责
Task / Amplification调度器剔除、LOD 决策、发射 Mesh 线程数
Mesh生成器读数据/程序化生成顶点与图元
光栅化固定功能对输出数组做常规光栅化
APIVulkan / DX12DrawMeshTasksEXT / DispatchMesh

实践路径:先在 GPU Driven 架构里用 Task 阶段做对象剔除,再用 Mesh 阶段替换几何着色器式生成,最后把 LOD 选级与程序化细分搬进来。每一步验证"CPU 提交时间"与"几何阶段吞吐"两个指标,确保 Mesh Shader 真正换来了性能而不是复杂度。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机图形学」更多文章

  1. 《glTF 资产格式与场景加载管线》
  2. 《实时全局光照:GI 探针、SSGI、体积光照与 Lumen 思路》
  3. 《GPU Driven 渲染与 LOD:从 CPU 瓶颈到 GPU 剔除》