WWDC Quick Look 💓 By SwiftGGTeam
Transform your geometry with Metal mesh shaders

Transform your geometry with Metal mesh shaders

观看原视频

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 Shader
  • object_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_vertexset_indexset_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 线程组大小
  • 资源绑定分为 setObjectBuffersetMeshTexturesetFragmentBuffer 三个阶段

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 是 MTLMeshRenderPipelineDescriptordrawMeshThreadgroups

实现 GPU-driven 场景渲染器

把大场景拆分成 meshlets,用 Object Shader 在 GPU 上并行执行视锥剔除、遮挡剔除和 LOD 选择。只让可见的 meshlets 进入 Mesh Shader 生成几何体。这可以显著减少 CPU 端的场景管理开销,同时降低 GPU 上不可见几何体的处理成本。

用程序化几何体做粒子特效

爆炸、魔法效果等需要大量动态几何体的场景,可以用 Mesh Shaders 在 GPU 上直接生成粒子网格。每个 Object 线程组管理一个粒子系统,根据时间参数计算每个粒子的位置和大小,Mesh Shader 生成对应的 billboard 或几何体。整个过程不需要 CPU 参与,也不需要把粒子数据写回内存。

关联 Session

评论

GitHub Issues · utterances