Highlight
Metal Mesh Shaders 用可编程的 Object Shader 和 Mesh Shader 两个阶段替代传统顶点着色器,允许在 GPU 上直接生成几何体并直接送入光栅化管线,无需额外的计算通道或中间缓冲区,支持程序化毛发渲染和 GPU-driven meshlet 剔除等场景。
核心内容
传统渲染管线的顶点着色器阶段有一个根本限制:它只能逐顶点处理已经存在于设备内存中的几何数据。如果你想在 GPU 上生成程序化几何体(比如给模型添加毛发),传统做法需要两个步骤:先用计算内核生成几何数据写入内存,再用渲染通道读取这些数据绘制。这带来了额外的内存分配、同步屏障和代码复杂度。
(01:19)Mesh Shaders 用两个新的可编程阶段替代顶点着色器:Object Shader 和 Mesh Shader。Object Shader 接收输入数据,处理并输出 payload 给 Mesh Shader。Mesh Shader 用 payload 数据生成程序化几何体,直接送入光栅化器和片元着色器。整个过程中几何数据只存在于 draw call 内部,不需要分配设备内存。
(04:22)Object Shader 和 Mesh Shader 的启动方式类似于计算内核:以线程组网格(threadgroup grid)的形式分派。每个线程组内的线程可以相互通信。每个 Object 线程组可以动态生成一个 Mesh 网格,并编程控制该网格的大小。
Mesh draw call 和传统 draw call 使用相同类型的 render command encoder,可以在同一个渲染通道中混合使用。
详细内容
程序化毛发渲染
(05:20)以平面上的毛发渲染为例。输入几何体被分成 tiles,每个 tile 对应一个 Object 线程组。Object 线程组计算该 tile 需要生成的毛发数量和每根毛发的曲线控制点,作为 payload 输出。然后 Object 线程组启动一个 Mesh 网格,其中每个 Mesh 线程组生成一根毛发。
Object Shader(MSL):
[[object]]
void objectShader(object_data CurvePayload *payloadOutput [[payload]],
const device void *inputData [[buffer(0)]],
uint hairID [[thread_index_in_threadgroup]],
uint triangleID [[threadgroup_position_in_grid]],
mesh_grid_properties mgp)
{
if (hairID < kHairsPerBlock)
payloadOutput[hairID] = generateCurveData(inputData, hairID, triangleID);
if (hairID == 0)
mgp.set_threadgroups_per_grid(uint3(kHairPerBlockX, kHairPerBlockY, 1));
}
关键点:
[[object]]属性标记这是 Object Shaderobject_data地址空间和[[payload]]属性定义 payload 输出mesh_grid_properties参数用于编程设置 Mesh 网格大小- 只有线程索引为 0 的线程设置网格大小,避免竞争
初始化 Object 阶段:
let meshPipelineDescriptor = MTLMeshRenderPipelineDescriptor()
meshPipelineDescriptor.objectFunction = objectFunction
meshPipelineDescriptor.payloadMemoryLength = payloadLength
meshPipelineDescriptor.maxTotalThreadsPerObjectThreadgroup = hairsPerBlock
关键点:
- 使用
MTLMeshRenderPipelineDescriptor替代传统的MTLRenderPipelineDescriptor payloadMemoryLength指定 payload 大小,上限 16KB- 每个 Object 线程组最多可生成 1024 个 Mesh 线程组
定义 Metal Mesh 类型:
struct VertexData { float4 position [[position]]; };
struct PrimitiveData { float4 color; };
using triangle_mesh_t = metal::mesh<
VertexData, // 顶点类型
PrimitiveData, // 图元类型
10, // 最大顶点数
6, // 最大图元数
metal::topology::triangle // 拓扑类型
>;
[[mesh]]
void myMeshShader(triangle_mesh_t outputMesh, ...);
关键点:
metal::mesh是 Metal 内置类型,定义顶点数据类型、图元数据类型、最大顶点数、最大图元数、拓扑类型- 拓扑类型支持 point、line、triangle
- Mesh Shader 的顶点数上限 256,图元数上限 512
- 整个
metal::mesh大小不能超过 16KB
Mesh Shader(MSL):
[[mesh]] void myMeshShader(triangle_mesh_t outputMesh,
uint tid [[thread_index_in_threadgroup]])
{
if (tid < kVertexCount)
outputMesh.set_vertex(tid, calculateVertex(tid));
if (tid < kIndexCount)
outputMesh.set_index(tid, calculateIndex(tid));
if (tid < kPrimitiveCount)
outputMesh.set_primitive(tid, calculatePrimitive(tid));
if (tid == 0)
outputMesh.set_primitive_count(kPrimitiveCount);
}
关键点:
[[mesh]]属性标记这是 Mesh Shader- 线程组内各线程协作编码顶点、索引和图元数据
set_vertex、set_index、set_primitive分别设置顶点、索引和图元属性- 只有一个线程设置图元总数,避免竞争
初始化 Mesh 阶段:
meshPipelineDescriptor.meshFunction = meshFunction
meshPipelineDescriptor.maxTotalThreadsPerMeshThreadgroup = vertexCountPerHair
创建 Mesh 渲染管线:
let meshPipeline: MTLRenderPipelineState
do {
let (pipeline, reflection) = try device.makeRenderPipelineState(
descriptor: meshRenderPipelineDescriptor,
options: [])
meshPipeline = pipeline
} catch {
print("The device can't create a mesh pipeline state: \(error)")
return
}
编码 Mesh 绘制:
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)
encoder.setRenderPipelineState(meshPipeline)
// 绑定各阶段资源
encoder.setObjectBuffer(objectBuffer, offset: 0, index: 0)
encoder.setMeshTexture(meshTexture, index: 2)
encoder.setFragmentBuffer(fragmentBuffer, offset: 0, index: 0)
// 定义线程组尺寸
let objectGridDimensions = MTLSize(width: trianglesPerModel, height: 1, depth: 1)
let threadsPerObject = MTLSize(width: hairsPerBlock, height: 1, depth: 1)
let threadsPerMesh = MTLSize(width: threadsPerHair, height: 1, depth: 1)
// 编码 Mesh 绘制命令
encoder.drawMeshThreadgroups(objectGridDimensions,
threadsPerObjectThreadgroup: threadsPerObject,
threadsPerMeshThreadgroup: threadsPerMesh)
encoder.endEncoding()
关键点:
- 使用
drawMeshThreadgroups替代传统 draw call - 需要指定 Object 网格维度、Object 线程组大小、Mesh 线程组大小
- 资源绑定分为
setObjectBuffer、setMeshTexture、setFragmentBuffer三个阶段
GPU-driven Meshlet 剔除
(13:16)Mesh Shaders 的另一个重要应用是高效处理大量几何体的场景渲染。核心思路是把场景网格拆分成更小的单元 meshlets,然后在 GPU 上执行细粒度的剔除。
传统 GPU-driven 管线需要两个通道:一个计算通道做视锥剔除、LOD 选择和编码绘制命令到设备内存;一个渲染通道执行这些绘制命令。Mesh Shaders 可以把这两个通道合并成单个渲染通道。
(14:35)Object Shader 对每个模型执行视锥剔除,计算可见 meshlets 的 LOD,然后启动 Mesh 网格。Mesh Shader 接收 meshlet ID 列表作为 payload,编码对应的 metal::mesh 对象。不可见的 meshlets 不会启动 Mesh 线程组,直接在 Object 阶段就被剔除。
这种架构的优势:
- 没有中间缓冲区存储绘制命令
- 没有计算通道和渲染通道之间的同步屏障
- 只有可见的几何体才会进入光栅化和片元着色阶段
核心启发
为 3D 角色添加程序化毛发/草地
用 Mesh Shaders 在 GPU 上实时生成毛发或草地。角色移动时,Object Shader 根据距离和视角动态调整每片区域的毛发密度(LOD),近处密集、远处稀疏。Mesh Shader 把每根毛发作为独立 mesh 生成。不需要预计算毛发几何体,也不需要大容量的中间缓冲区。入口 API 是 MTLMeshRenderPipelineDescriptor 和 drawMeshThreadgroups。
实现 GPU-driven 场景渲染器
把大场景拆分成 meshlets,用 Object Shader 在 GPU 上并行执行视锥剔除、遮挡剔除和 LOD 选择。只让可见的 meshlets 进入 Mesh Shader 生成几何体。这可以显著减少 CPU 端的场景管理开销,同时降低 GPU 上不可见几何体的处理成本。
用程序化几何体做粒子特效
爆炸、魔法效果等需要大量动态几何体的场景,可以用 Mesh Shaders 在 GPU 上直接生成粒子网格。每个 Object 线程组管理一个粒子系统,根据时间参数计算每个粒子的位置和大小,Mesh Shader 生成对应的 billboard 或几何体。整个过程不需要 CPU 参与,也不需要把粒子数据写回内存。
关联 Session
- Discover Metal 3 — Metal 3 的新特性概览
- Scale compute workloads across Apple GPUs — 跨 Apple GPU 扩展计算工作负载
- Program Metal in C++ with metal-cpp — 用 C++ 编写 Metal 代码
评论
GitHub Issues · utterances