传统渲染管线把几何处理固定成"顶点着色器逐顶点、几何着色器逐三角形、光栅化器固定"。这套结构服务了二十多年,却在几何生成与 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 管线 |
|---|---|---|
| 几何决策者 | CPU | GPU(Task 阶段) |
| 最小处理单元 | 顶点 / 三角形 | 网格批次 / 对象 |
| 剔除位置 | CPU 或独立 Pass | Task 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 Shader | Amplification Shader |
| 启动 | vkCmdDrawMeshTasksEXT | DispatchMesh |
| 输出语义 | 自定义输出变量 + SetMeshOutputsEXT | SetMeshOutputCounts |
| 扩展状态 | 扩展(厂商一致性) | 核心(D3D12 内置) |
| 资源绑定 | Descriptor Set | Root Signature / 根描述符 |
两者底层语义等价(两级并行、payload 传递、GPU 自主几何),工程上只需适配 API 壳。
八、性能案例分析
一句话:Mesh Shader 的收益来自"剔除前置 + 几何自主 + 减少 CPU 往返",典型案例能省掉数倍 CPU 提交时间与不可见几何开销,但性能提升有前置条件。
8.1 典型案例:大型开放世界
| 指标 | 传统管线 | Mesh Shader 管线 |
|---|---|---|
| 每帧 Draw Call | 1~2 万 | 1 次 DispatchMesh |
| CPU 提交时间 | 8~15 ms | <1 ms |
| GPU 剔除的物体 | 无(CPU 预剔除) | 全部(Task 阶段) |
| 三角形生成 | 预烘焙 LOD | GPU 端 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 | 生成器 | 读数据/程序化生成顶点与图元 |
| 光栅化 | 固定功能 | 对输出数组做常规光栅化 |
| API | Vulkan / DX12 | DrawMeshTasksEXT / DispatchMesh |
实践路径:先在 GPU Driven 架构里用 Task 阶段做对象剔除,再用 Mesh 阶段替换几何着色器式生成,最后把 LOD 选级与程序化细分搬进来。每一步验证"CPU 提交时间"与"几何阶段吞吐"两个指标,确保 Mesh Shader 真正换来了性能而不是复杂度。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。