Your guide to Metal ray tracing
Graphics & Games 进阶 20m

Metal 光线追踪指南

Your guide to Metal ray tracing

2023年6月5日

在 Apple 官方观看视频

一句话判断

Metal 光线追踪今年新增了曲线基元(Curve Primitives)——专门用于渲染毛发和植被的几何类型,内存占用更小、加速结构构建更快,还能在相机靠近时保持曲线平滑。

这场 Session 讲了什么

GPU 软件工程师 Pawel Szczerbuk 详细介绍了 Metal 光线追踪的使用方法和今年新增的特性。

光线追踪的基本工作流是:定义场景几何体 -> 构建加速结构(Acceleration Structure)-> 在 GPU 函数中创建光线并查询交点 -> 基于交点信息着色。加速结构通过递归分区几何体来快速排除不相交的区域。

Metal 支持三种几何描述符:三角形(最常用的图元)、包围盒(自定义交叉函数)和今年新增的曲线(Curve Primitives)。曲线基元专为毛发、皮毛和植被等大量细小几何体设计。

Session 用迪士尼的 Moana 岛屿场景作为渲染示例,详细讲解了加速结构的创建流程、实例化(Instancing)机制、以及光线交叉查询的实现方式。

曲线基元支持四种基函数:Bezier、Catmull-Rom、B-Spline 和 Linear。每个曲线段由 2-4 个控制点定义,控制点半径沿曲线插值。默认以 3D 圆柱体截面渲染,远距离时可以切换为扁平模式提升性能。

值得深挖的点

曲线基元的内存优势:传统方式用三角形近似曲线需要大量几何体,内存占用大。曲线基元直接用数学定义曲线,相邻曲线段共享控制点(通过索引缓冲区),内存占用显著减少。

加速结构的分配策略:Metal 允许从堆(Heap)分配加速结构,而不是直接从设备分配。从堆分配可以减少资源管理开销。Metal 还提供了查询堆大小和对齐需求的方法。

曲线的远/近优化:近距离观察时使用 3D 圆柱体截面(round curves),保证视觉质量;远距离时切换为扁平模式(flat curves),减少计算量。这种自适应策略在毛发等密集场景中很有用。

实例化(Instancing)的复用能力:同一个加速结构可以被多次引用,每次使用不同的变换矩阵。这在场景中有大量重复物体时(如森林中的树木)能大幅减少内存和构建时间。

代码片段

// 曲线几何描述符设置
MTLCurveGeometryDescriptor *curveDesc = [MTLCurveGeometryDescriptor curveGeometryDescriptor];
curveDesc.controlPointBuffer = controlPointBuffer;      // 控制点缓冲
curveDesc.radiusBuffer = radiusBuffer;                   // 半径缓冲
curveDesc.controlPointIndexBuffer = indexBuffer;         // 控制点索引
curveDesc.controlPointCount = numControlPoints;
curveDesc.curveCount = numCurveSegments;
curveDesc.curveType = MTLCurveTypeRound;                 // 圆柱体截面
curveDesc.curveBasis = MTLCurveBasisBezier;              // Bezier 基函数
curveDesc.curveEndCaps = MTLCurveEndCapsSphere;          // 球形端盖
// 加速结构分配
// 1. 计算所需空间
let sizes = device.accelerationStructureSizes(
    descriptor: accelerationDescriptor
)

// 2. 从堆分配(推荐,减少开销)
let accelStruct = heap.makeAccelerationStructure(
    size: sizes.accelerationStructureSize
)

// 3. 分配临时缓冲区(构建时使用)
let scratchBuffer = device.makeBuffer(
    length: sizes.buildScratchBufferSize,
    options: .storageModePrivate
)

// 4. 在 GPU 上构建
let commandBuffer = commandQueue.makeCommandBuffer()!
let encoder = commandBuffer.makeAccelerationStructureCommandEncoder()!
encoder.build(accelerationStructure: accelStruct,
              descriptor: accelerationDescriptor,
              scratchBuffer: scratchBuffer,
              scratchBufferOffset: 0)
encoder.endEncoding()
commandBuffer.commit()
// GPU 着色器中的光线交叉查询
ray r;
r.origin = rayOrigin;
r.direction = rayDirection;
r.min_distance = 0.001;
r.max_distance = FLT_MAX;

intersector<triangle_data, curve_data> itr;
intersection_result<triangle_data, curve_data> result;

// 执行交叉查询
result = itr.intersect(r, accelerationStructure, 0);

if (result.type == intersection_type::triangle) {
    // 处理三角形交叉
    float2 bary = result.triangle_barycentric_coord;
} else if (result.type == intersection_type::curve) {
    // 处理曲线交叉
    float u = result.curve_parameter;
}

最佳实践

  • 毛发和植被场景使用曲线基元:比三角形近似更省内存、构建更快,还能保持曲线在放大时的平滑度。
  • 从堆分配加速结构:减少资源管理开销,特别是在需要频繁重建加速结构的动态场景中。
  • 远距离用扁平曲线模式:如果毛发只在远景中出现,flat curves 能显著提升性能。
  • 利用实例化减少重复:场景中大量重复的几何体应该使用实例化而不是复制多份。
  • 预计算加速结构大小:在构建之前先查询所需空间,避免运行时分配失败。

还有什么值得关注

  • 曲线基元支持球形端盖(sphere end caps),让曲线两端看起来更自然。
  • 加速结构支持引用(refitting),当几何体位置变化时可以更新而不是完全重建。
  • 光线交叉查询支持自定义交叉函数,可以用于体渲染等特殊效果。
  • Metal 的光线追踪 API 设计考虑了动态场景——加速结构可以在 GPU 上异步构建,不会阻塞渲染管线。
WWDC 2023