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

Go further with Metal 4 games

元の動画を見る

Highlight

MetalFX が Apple プラットフォームで初めてフレーム補間に対応しました。新しいデノイズ用 upscaler と ray tracing 向けの intersection function buffer を組み合わせることで、Cyberpunk 2077 クラスの path tracing を Apple Silicon 上で動かせるようになります。


主要内容

ゲームレンダリングの課題は明白です。Cyberpunk 2077 級の映像は 1 ピクセルあたりのコストが非常に高く、解像度とフレームレートを同時に引き上げると処理が追いつきません。これまで MetalFX には時間方向の upscaler しかなく、開発者は解像度を犠牲にするか、フレームレートを犠牲にするか、あるいは自前でフレーム補間を実装するかの三択を迫られていました。しかし自前でフレーム補間を組もうとすると、UI レンダリング、ペーシング制御、ダブルバッファキューなど、扱うべき要素が膨大になります。

WWDC25 のこの Session では、3 つの武器が提示されました。MetalFX Frame Interpolator は、すでにレンダリング済みの 2 フレームの間に新たな 1 フレームを生成し、Metal パイプラインにネイティブ統合されます。MetalFX Denoised Upscaler は、ray tracing のデノイズと upscaling を 1 ステップに統合し、シーンごとの手動チューニングを不要にします。Ray tracing 側には intersection function buffer が新設され、DirectX の shader binding table をそのまま移植できる構造になりました。これら 3 つは重ねて使うことができます。まず intersection function buffer で ray tracing を最適化し、次に Denoised Upscaler で ray budget を圧縮し、最後に Frame Interpolator でフレームレートを倍にする、という流れです。


詳細

Frame Interpolator の組み込みに必要なのは 5 枚のテクスチャだけです。すなわち、現在フレームのカラー、前フレームのカラー、深度、motion vector、出力です。すでに MetalFX upscaler を組み込んでいる場合、motion vector と深度はそのまま流用できます。temporalScalerMTLFXFrameInterpolatorDescriptor.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 時間方向 upscaler オブジェクトを Frame Interpolator に渡し、両者で中間結果を共有します。それぞれを独立に呼ぶより効率的です。
  • depthReversed = YES:reversed Z(遠方が 0 に近く、近方が 1 に近い)を使うことを宣言します。最近のエンジンはほぼすべて reversed Z を採用して深度精度を高めています。
  • motionVectorScaleX/Y:motion vector を正規化座標からピクセル座標へスケーリングします。ここを誤るとゴーストが発生します。
  • 5 枚の入力テクスチャを一度にバインドします。カラー、前フレームカラー、深度、motion、出力。1 枚でも欠けると動きません。

Denoised Upscaler への移行コストはわずかです。すでに組み込んだ MTLFXTemporalScalerDescriptor に補助テクスチャを 4 枚追加するだけで済みます(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];

ポイントは以下の通りです。

  • 上 4 行は通常の時間方向 upscaler でも必要なものです。カラーフォーマット、出力フォーマット、深度フォーマット、motion フォーマットを指定します。
  • 新たに追加するのはノイズの乗っていない 4 枚の補助テクスチャです。diffuse albedo、specular albedo、normals、roughness の 4 種類で、ゲームの G-buffer に通常含まれているものをそのまま流用できます。
  • newTemporalDenoisedScalerWithDevice: の行が分水嶺となります。このファクトリメソッドに切り替えるだけで、デノイズと upscaling を同時にこなすオブジェクトが手に入ります。
  • normals は world space で、かつ符号付きフォーマットのテクスチャに格納する必要があります。これを守らないとカメラを動かした瞬間に品質が崩れます。Session でも 22:00 で繰り返し強調されているポイントです。

Ray tracing 側には intersection function buffer が導入され、DirectX の shader binding table の移植コストが極限まで下がりました。下地となるのは instance と geometry の 2 段階の 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 ごとに 2 種類の ray type(たとえば主光線と影光線)が存在することを宣言し、buffer の stride を決定します。
  • set_base_id(1):現在追跡しているのが 2 種類目の ray type(base id = 1、影光線)であることを示します。主光線を追う場合は 0 を渡します。
  • DirectX とは異なり、Metal では buffer のアドレスと stride を shader 側で設定します。DirectX は host 側の dispatch 時に設定する仕組みです。SIMD group 内のすべてのスレッドは同じ値を設定する必要があり、異なる値を設定すると未定義動作となります。
  • DirectX のプロジェクトを移植する際、instance offset、ray type index、geometry multiplier の 3 つの概念は一対一に対応します。違いは、Metal では geometry offset も開発者が制御するのに対し、DirectX では自動生成される点です。

Frame Interpolator のペーシング(pacing)は、最も詰まりやすい箇所です。Session ではリファレンス実装として PresentThread というヘルパークラスが紹介されています(12:45)。MTLSharedEventkqueueEVFILT_TIMER を組み合わせてサブミリ秒精度のペーシング制御を行い、補間フレームと実フレームが等間隔で表示されるようにする実装です。ペーシングが正しいかを判定する簡単な方法は、Metal HUD を開いて Frame Interval ヒストグラムを見ることです。山が 1〜2 個に収まっていれば正常、3 個以上になっていればペーシングが崩れているサインです。

その他に、覚えておきたい新しいツールが 2 つあります。MTLFX_EXPOSURE_TOOL_ENABLED 環境変数を有効にすると画面にグレーのチェッカーボードが重畳表示され、露出値が正しければ一定の中間グレーで表示され、誤っていればチラつきます(04:25)。reactive mask は、motion や depth に書き込まれていないパーティクルや花火のような透明エフェクトを開発者が明示的にマークする仕組みで、upscaler がそれらをテクスチャの細部と誤認してぼかしてしまう問題を回避できます(06:46)。


重要ポイント

  • 何をするか:既存の MetalFX upscaler を Frame Interpolator にアップグレードする

    • なぜやる価値があるか:レンダリングパイプラインに対しては tone mapping のあとに 1 ステップ追加するだけで済み、motion vector と深度は流用できます。工数は小さく、それでいて目標フレームレートを倍にできます。30 FPS の入力でも十分機能するので、ハードな最適化を頑張るより費用対効果が高い選択肢です。
    • 始め方:まず Metal HUD で現在のフレームレートと frame interval ヒストグラムを確認してベースラインを把握します。次に MTLFXFrameInterpolatorDescriptor の最小構成で接続します。UI はまず Composited UI モード(最も簡単で、Frame Interpolator 側で自動的に unblend されます)から始め、動作確認後に Offscreen UI や Every-Frame UI に切り替えて品質を高めます。
  • 何をするか:ray tracing のデノイズ工程を MetalFX Denoised Upscaler に置き換える

    • なぜやる価値があるか:従来の denoiser はシーンごとにアーティストが手動でチューニングする必要がありましたが、Denoised Upscaler は機械学習モデルなので、ゼロチューニングで仕上がります。さらに ray budget も削減でき、より少ない光線で同等の画質が得られます。
    • 始め方:まず G-buffer に normals(world space で符号付きフォーマット)、diffuse albedo、specular albedo、roughness の 4 枚が揃っているかを確認します。接続後に、specular hit distance、denoiser strength mask、transparency overlay の 3 つの任意入力を追加して品質を追い込みます。金属マテリアルでは diffuse albedo を暗くし、色情報は specular albedo に持たせるのが原則です。
  • 何をするか:DirectX から ray tracing ゲームを移植する際は intersection function buffer を優先する

    • なぜやる価値があるか:DirectX の shader binding table と Metal 4 の intersection function buffer は概念がほぼ一対一に対応し、instance offset、ray type、geometry multiplier をそのまま移植できます。従来の visible function table 経由のルートと比べて shader の変更量が大幅に少なくて済みます。
    • 始め方:まず 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 環境変数で upscaler の exposure 入力を校正する

    • なぜやる価値があるか:露出値を誤ると flickering や ghosting が発生しますが、目視では原因の特定が困難です。チェッカーボードのデバッガは Apple が用意した「試験紙」のようなもので、有効化するだけで問題の有無が判別できます。
    • 始め方:Xcode の scheme にこの環境変数を追加してゲームを起動し、画面上のグレーのチェッカーボードを観察します。一定の中間グレーであれば OK です。暗すぎる、明るすぎる、あるいは実行中に色が変動する場合は、exposure と tone mapper の整合が取れていないので、upscaler に渡す値を調整します。

関連 Session

コメント

GitHub Issues · utterances