WWDC Quick Look 💓 By SwiftGGTeam
Go further with Metal 4 games

Go further with Metal 4 games

观看原视频

Highlight

MetalFX 在 Apple 平台首次加入帧插值,配合新的去噪上采样器和光线追踪 intersection function buffer,让 Cyberpunk 2077 级别的路径追踪在 Apple Silicon 上跑得动。


核心内容

游戏渲染的难题摆在台面上:Cyberpunk 2077 这种级别的画面,每个像素都很贵,分辨率和帧率同时拉高就跑不动。过去 MetalFX 只有时序上采样器一个工具,开发者要么牺牲分辨率,要么牺牲帧率,要么手写一套帧间插值的脏活——而手写帧插值要处理 UI 渲染、节拍控制、双缓冲队列,工程量极大。

WWDC25 这场 Session 给出了三件武器:MetalFX 帧插值器(Frame Interpolator)把已经渲染好的两帧之间凭空生成一帧,原生集成进 Metal 管线;MetalFX 去噪上采样器(Denoised Upscaler)把光追的降噪和上采样合成一步,省掉每个场景的手工调参;光追这边新增 intersection function buffer,让 DirectX 的 shader binding table 能直接平移过来。三个特性可以叠加:先用 intersection function buffer 优化光追、再用去噪上采样器把光线预算压下来、最后用帧插值器把帧率翻倍。


详细内容

帧插值器的接入只要五张纹理:当前帧颜色、上一帧颜色、深度、运动向量、输出。如果已经接了 MetalFX 上采样器,运动向量和深度可以直接复用。把 temporalScaler 传给 MTLFXFrameInterpolatorDescriptor.scaler 还能拿到组合性能加成(08:35)。

// Create and configure the interpolator descriptor
MTLFXFrameInterpolatorDescriptor* desc = [MTLFXFrameInterpolatorDescriptor new];
desc.scaler = temporalScaler;

// Create the effect and configure your effect
id<MTLFXFrameInterpolator> interpolator = [desc newFrameInterpolatorWithDevice:device];
interpolator.motionVectorScaleX = mvecScaleX;
interpolator.motionVectorScaleY = mvecScaleY;
interpolator.depthReversed = YES;

// Set input textures
interpolator.colorTexture = colorTexture;
interpolator.prevColorTexture = prevColorTexture;
interpolator.depthTexture = depthTexture;
interpolator.motionTexture = motionTexture;
interpolator.outputTexture = outputTexture;

关键点:

  • desc.scaler = temporalScaler:把已有的 MetalFX 时序上采样器对象交给插值器,让两者共享中间结果,比独立调用更省。
  • depthReversed = YES:声明使用反向 Z(远处接近 0、近处接近 1),现代引擎几乎都用反向 Z 提高深度精度。
  • motionVectorScaleX/Y:把运动向量从归一化坐标缩放到像素坐标,必须填对,否则插值会出鬼影。
  • 五张输入纹理一次性绑定:颜色、上一帧颜色、深度、运动、输出。少一张就跑不起来。

去噪上采样器(Denoised Upscaler)的迁移路径很短——在已经接好的 MTLFXTemporalScalerDescriptor 基础上加四张辅助纹理就行(13:02)。

MTLFXTemporalScalerDescriptor* desc = [MTLFXTemporalScalerDescriptor new];
desc.colorTextureFormat = MTLPixelFormatBGRA8Unorm_sRGB;
desc.outputTextureFormat = MTLPixelFormatBGRA8Unorm_sRGB;
desc.depthTextureFormat = DepthStencilFormat;
desc.motionTextureFormat = MotionVectorFormat;

desc.diffuseAlbedoTextureFormat = DiffuseAlbedoFormat;
desc.specularAlbedoTextureFormat = SpecularAlbedoFormat;
desc.normalTextureFormat = NormalVectorFormat;
desc.roughnessTextureFormat = RoughnessFormat;

desc.inputWidth = _mainViewWidth;
desc.inputHeight = _mainViewHeight;
desc.outputWidth = _screenWidth;
desc.outputHeight = _screenHeight;
temporalScaler = [desc newTemporalDenoisedScalerWithDevice:_device];

关键点:

  • 前四行是普通时序上采样器就要填的:颜色格式、输出格式、深度格式、运动格式。
  • 新增四张噪声自由的辅助纹理:漫反射反照率(diffuse albedo)、镜面反射反照率(specular albedo)、法线(normals)、粗糙度(roughness)。游戏的 G-buffer 里通常都有,直接复用。
  • newTemporalDenoisedScalerWithDevice: 这一行是分水岭:换成这个工厂方法,就拿到了同时做去噪和上采样的对象。
  • 法线必须用世界空间、且纹理类型要带符号位,否则相机方向一转质量就掉。这点 Session 在 22:00 反复强调。

光追这边引入了 intersection function buffer,把 DirectX 的 shader binding table 移植成本压到最低。地面是 instance 和 geometry 两级 offset 索引(16:04):

MTLAccelerationStructureInstanceDescriptor *grassInstanceDesc, *treeInstanceDesc = . . .;
grassInstanceDesc.intersectionFunctionTableOffset = 0;
treeInstanceDesc.intersectionFunctionTableOffset  = 1;

shader 端则用 intersection_function_buffer 标签声明 intersector,再设置 ray type 数量和 base id(13:01):

metal::raytracing::intersector<intersection_function_buffer, instancing, triangle> trace;
trace.set_geometry_multiplier(2); // Number of ray types, defaults to 1
trace.set_base_id(1);             // Set ray type index, defaults to 0

关键点:

  • geometry_multiplier(2):声明每个 geometry 有两种 ray type(比如主光线和阴影光线),决定了 buffer 的步长。
  • set_base_id(1):当前正在追的是第二种 ray type(base id = 1,阴影光线);如果追主光线就填 0。
  • 和 DirectX 不同,Metal 把 buffer 地址和 stride 放在 shader 里设,DirectX 放在 host 端 dispatch 时设。SIMD group 内所有线程必须设同一个值,否则行为未定义。
  • 移植 DirectX 项目时,instance offset、ray type index、geometry multiplier 三个概念一一对应,只是 Metal 把 geometry offset 也交给开发者控制,DirectX 是自动生成。

帧插值的节拍(pacing)是最容易踩坑的地方。Session 给了一个 PresentThread 辅助类做参考实现(12:45),用 MTLSharedEvent + kqueueEVFILT_TIMER 做亚毫秒级节拍控制,确保插值帧和真实帧按等长间隔上屏。判断节拍是否对的简单方法:开 Metal HUD,看 Frame Interval 直方图——只有一两个 bucket 就是对的,超过两个就是节拍掉了。

另外两个新工具值得记一下:MTLFX_EXPOSURE_TOOL_ENABLED 环境变量开启后会在画面上叠加一张灰色棋盘格,曝光值传对了就是恒定中灰、传错了会闪烁(04:25);reactive mask 让开发者把粒子、烟花这种没有写进 motion/depth 的透明效果标出来,避免上采样把它们当成纹理细节给糊掉(06:46)。


核心启发

  • 做什么:把已有的 MetalFX 上采样器升级为帧插值器

    • 为什么值得做:渲染管线只需要在 tone-mapping 之后多插一步,运动向量和深度可以复用,工程量小但能直接把目标帧率翻倍。30 FPS 的输入就够用,比硬抠优化划算得多。
    • 怎么开始:先用 Metal HUD 看当前帧率和 frame interval 直方图确认基线;然后按 MTLFXFrameInterpolatorDescriptor 接最小可用版本,UI 先用 Composited UI 模式(最简单,让插值器自己 unblend);跑通后再考虑 Offscreen UI 或 Every-Frame UI 提升质量。
  • 做什么:把光追的降噪环节替换成 MetalFX 去噪上采样器

    • 为什么值得做:传统 denoiser 每个场景都要美术手工调参,去噪上采样器是机器学习模型,零调参就出片,还能把 ray budget 压下来——同样的画质用更少的光线。
    • 怎么开始:先确认 G-buffer 里有 normals(世界空间、带符号位)、diffuse albedo、specular albedo、roughness 这四张;接好之后再加 specular hit distance、denoiser strength mask、transparency overlay 三个可选输入精修质量;金属材质的 diffuse albedo 记得调暗,颜色应该在 specular albedo 里。
  • 做什么:从 DirectX 移植光追游戏时优先用 intersection function buffer

    • 为什么值得做:DirectX 的 shader binding table 和 Metal 4 的 intersection function buffer 概念几乎一一对应,instance offset、ray type、geometry multiplier 都能直接平移,shader 改动比之前的 visible function table 路线少得多。
    • 怎么开始:先在 instance descriptor 上设 intersectionFunctionTableOffset,再在 geometry descriptor 上设同样的字段;shader 里用 intersector<intersection_function_buffer, ...> 声明,把 set_geometry_multiplier 设成 ray type 数量、set_base_id 设当前 ray type 索引;最后把 buffer、size、stride 通过 intersection_function_buffer_arguments 传给 trace.intersect
  • 做什么:用 MTLFX_EXPOSURE_TOOL_ENABLED 环境变量校准上采样器的 exposure 输入

    • 为什么值得做:曝光值传错会导致 flickering 和 ghosting,但肉眼很难直接判断哪里错;棋盘格调试器是 Apple 给的”试纸”,开一下就知道有没有问题。
    • 怎么开始:在 Xcode 的 scheme 里加这个环境变量,跑游戏看画面上的灰色棋盘格。恒定中灰就是对的;过暗、过亮或运行时颜色变化都说明 exposure 和 tone mapper 不匹配,需要调整传给 upscaler 的值。

关联 Session

评论

GitHub Issues · utterances