用 Metal 光线追踪增强你的应用
Enhance your app with Metal ray tracing
2021年6月9日
一句话判断
Metal Ray Tracing 在 Apple Silicon 上终于达到了生产可用的状态——如果你的渲染管线还在用 cubemap 做假反射,现在是时候认真考虑实时光追了。
这场 Session 讲了什么
Session 介绍了 Metal Ray Tracing 在 Apple Silicon 上的完整技术栈,包括 acceleration structure(加速结构)的构建与更新、ray query(光线查询)API、intersection(求交)API,以及如何将光追集成到现有的光栅化渲染管线中。
Apple Silicon 的 GPU 从架构层面支持光线追踪指令,不需要软件模拟。MTLAccelerationStructure 封装了 BVH(Bounding Volume Hierarchy)的构建——你提供几何数据,Metal 在 GPU 上构建加速结构。构建完成后,通过 MTLIntersectionQueryTable 发起光线求交查询。Session 展示了三种光追使用模式:纯光追渲染(适合离线渲染或简单场景)、混合渲染(光栅化+光追,适合实时应用)、以及 ray query 模式(在任意 shader 中发射光线,最灵活)。性能方面,M1 的光追性能足以在中等复杂度的场景中实现实时反射和软阴影。
值得深挖的点
Acceleration Structure 的动态更新
场景中的物体移动时,加速结构需要更新。完整重建 BVH 的代价很高(通常需要几毫秒),Apple 提供了 refit 操作——只调整已有 BVH 节点的包围盒,不改变树结构。对于小幅移动的物体,refit 的性能开销只有完整重建的 1/10。但 refit 不适用于大幅移动或拓扑变化的场景,长期使用会导致 BVH 质量下降(射线求交效率降低)。Session 建议每 N 帧 refit 一次,每 M 帧完整重建一次(N=1-3, M=30-60)。
Ray Query 与 Intersection Function 的配合
ray_query 是 Metal Shading Language 中的内建对象,让你在 compute shader 或 fragment shader 中直接发射光线。配合 intersection_function,你可以自定义求交逻辑——比如处理 alpha-tested geometry(带透明度裁剪的几何体)或者 SDF(Signed Distance Field)几何。这种灵活性是光追真正强大的地方:你不再被限制在三角面片的世界里。
代码片段
// 构建加速结构
func buildAccelerationStructure(
device: MTLDevice,
commandBuffer: MTLCommandBuffer,
meshes: [Mesh]
) -> MTLAccelerationStructure {
let descriptor = MTLPrimitiveAccelerationStructureDescriptor()
var geometryDescriptors: [MTLAccelerationStructureGeometryDescriptor] = []
for mesh in meshes {
let geometry = MTLAccelerationStructureTriangleGeometryDescriptor()
geometry.vertexBuffer = mesh.vertexBuffer
geometry.vertexBufferOffset = 0
geometry.triangleCount = mesh.triangleCount
geometryDescriptors.append(geometry)
}
descriptor.geometryDescriptors = geometryDescriptors
// 使用 GPU 构建加速结构
let accelerationStructure = device.makeAccelerationStructure(
size: device.accelerationStructureSizes(
descriptor: descriptor
).accelerationStructureSize
)
let encoder = commandBuffer.makeAccelerationStructureCommandEncoder()!
encoder.build(accelerationStructure: accelerationStructure,
descriptor: descriptor)
encoder.endEncoding()
return accelerationStructure
}
// Metal Shading Language 中的 ray query 使用
// 在 compute shader 中发射光线并获取交点信息
kernel void rayTracingKernel(
uint2 tid [[thread_position_in_grid]],
constant SceneData& scene [[buffer(0)]],
acceleration_structure<instancing> as [[buffer(1)]],
texture2d<float, access::write> output [[texture(0)]]
) {
// 从相机发射一条射线
ray ray = scene.camera.generateRay(tid);
// 创建 ray query
ray_query q;
q.reset(ray, as, intersection_params().accept_any_hit());
// 遍历所有交点
while (q.next()) {
// 获取交点信息
float3 hitPosition = q.intersection_point();
float3 hitNormal = q.intersection_normal();
// 简单的漫反射着色
float3 color = computeDirectLighting(
hitPosition, hitNormal, scene.lights);
output.write(float4(color, 1.0), tid);
return;
}
// 未命中任何物体,返回背景色
output.write(float4(scene.backgroundColor, 1.0), tid);
}
// 动态更新加速结构(refit)
func updateAccelerationStructure(
commandBuffer: MTLCommandBuffer,
accelerationStructure: MTLAccelerationStructure,
descriptor: MTLPrimitiveAccelerationStructureDescriptor
) {
let encoder = commandBuffer.makeAccelerationStructureCommandEncoder()!
// refit 比完整重建快 10 倍,适合小幅移动的物体
encoder.refit(accelerationStructure: accelerationStructure,
descriptor: descriptor)
encoder.endEncoding()
}
最佳实践
- 光追和光栅化混合使用:用光栅化做主要可见性判定(primary visibility),光追只做反射、阴影和全局光照。这样性能开销可控,视觉提升明显。
- 加速结构的更新策略采用”refit 为主,重建为辅”的模式。物体的包围盒每帧 refit,每 30-60 帧完整重建一次以保持 BVH 质量。
- 光线的 origin 和 direction 使用 32 位浮点数(不要用 half),光追对精度敏感,低精度会导致自相交(self-intersection)伪影。
还有什么值得关注
- Metal Performance Shaders 新增了
MPSSVGFdenoiser,可以对光追产生的噪点进行时域和空域联合降噪,让低样本数的光追结果看起来更干净。 MTLIntersectionFunctionTable支持动态切换自定义求交函数,适合场景中有多种几何类型(三角面片、SDF、粒子)的情况。- Apple 建议光追的 render target 使用
RGBA16Float格式,避免 HDR 颜色在多次弹射中出现截断。