View in English

  • メニューを開く メニューを閉じる
  • Apple Developer
検索
検索を終了
  • Apple Developer
  • ニュース
  • 見つける
  • デザイン
  • 開発
  • 配信
  • サポート
  • アカウント
次の内容に検索結果を絞り込む

クイックリンク

5 クイックリンク

ビデオ

メニューを開く メニューを閉じる
  • コレクション
  • トピック
  • すべてのビデオ
  • 利用方法

WWDC25に戻る

  • 概要
  • トランスクリプト
  • コード
  • Metal 4ゲームの知識を深める

    Metal 4の最新機能について学びましょう。新しいレイトレーシング機能は、極めて複雑で視覚的にリッチなワークロードをAppleシリコンに組み込む際に役立ちます。MetalFXを使用して、レンダリングのアップスケール、フレームの補間、シーンのノイズ除去を実行し、ワークロードをスケーリングする方法につい説明します。 このセッションの内容を十分理解できるよう、最初に「Discover Metal 4」および「Explore Metal 4 games」をご覧になることをおすすめします。

    関連する章

    • 0:00 - イントロダクション
    • 2:13 - レンダリングのアップスケール
    • 7:17 - フレーム補間
    • 13:50 - Metal 4によるレイトレーシング
    • 19:25 - アップスケールと同時にノイズ除去
    • 26:08 - 次のステップ

    リソース

      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC25

    • イマーシブなアプリを作成するためのMetalレンダリングの新機能
    • ゲームをレベルアップさせる方法
    • Metal 4による機械学習とグラフィックスの統合
    • Metal 4の概要
    • Metal 4ゲームの詳細

    WWDC23

    • Metalレイトレーシングのガイド

    WWDC22

    • MetalFX Upscalingでパフォーマンスを向上させる
  • このビデオを検索

    こんにちは Matias Koskelaです 今日は Appleプラットフォームでの 高度なゲームやプロ向けアプリの 開発をさらに前進させるうえで 役立つ手法と ベストプラクティスを紹介します

    このビデオの前に Metal 4の概要を説明する 「Metal 4の概要」と 最新版のMetalの使い方を紹介する 「Metal 4ゲームの詳細」を ご覧になることをお勧めします この「Metal 4ゲームの知識を深める」は Metal 4ゲームシリーズの第2部です また Metal 4による機械学習と グラフィックスの統合について 説明しているビデオもあります

    ご覧の「サイバーパンク2077」などの ゲームでは レンダリング品質の向上により リアルさが高まっています これによりピクセルあたりの 処理負荷が高まり 高解像度 高フレームレートの 難易度が上がっています Metalを使えば iPhoneからMacまで 幅広いAppleプラットフォーム向けに 高品質なフレームのレンダリングができます ラスタ化やレイトレーシングなどの 手法を使用する場合 Metalには使いやすいAPIがあります

    MetalFX Upscalingを使用すれば 高解像度 高フレームレートでも ワークロードのスケーリングができます

    さらに上を目指すなら 新しいMetalFXフレームインターポレータを 利用できます

    サイバーパンク2077などの最新ゲームは リアルタイムのパストレーシングで 現実感を出しています このようなリアルタイムの レンダリングの機能強化は Metal 4の優れた新機能によって 実現可能になっています レイトレーシングの機能強化や新しい MetalFXのノイズ除去アップスケーラがあり ゲーム内で必要なレイの数を減らして スケーリングを容易にします

    MetalFXのアップスケーラは高解像度 高フレームレートの実現に役立ちます 新しいMetalFXのフレームインターポレータで よりスムーズなゲームプレイを実現できます 新しいMetal 4のレイトレーシング機能で パフォーマンスがさらに向上し MetalFXのノイズ除去アップスケーラと 組み合わせて使用できます

    アップスケーリングは広く使用されており 多くのシナリオで パフォーマンス向上に役立ちます MetalFXには 機械学習ベースの アップスケーラがあり 2022年からAppleプラットフォームの 一部となっていますが 毎年改善が重ねられています

    MetalFX Upscalingに 新しいツールと手法が追加されており それらを使用すると ゲームの品質と パフォーマンスの向上に役立ちます 最初のステップは時間アップスケーリングを 適切にゲームに適用することです 露出の入力パラメータの適切な使用も そのプロセスの一部です 動的解像度により パフォーマンスを さらに向上させることができます リアクティビティヒントを使用して 特定の シナリオの品質を向上させることもできます

    一般的なレンダリングパイプラインを考えます 最初にフレームのラスタ化または レイトレーシングが行われ

    その後 モーションブラーなどの 効果の後処理が実行されます 次に 露出とトーンマッピングが適用され UIがレンダリングされ 最終的に フレームが プレイヤーに表示されます

    MetalFX Upscalingを追加するには ジッタ処理済みのレンダリングの後 後処理の前が最適です アップスケーラの組み込みの詳細は 「MetalFX Upscalingで パフォーマンスを向上させる」を ご覧ください 今年は ゲームの パフォーマンス向上に役立つ ツールや機能がさらに追加されます

    高品質を実現するには アップスケーラに 適正な露出値を設定することが重要です

    大きく誤った値を渡すと ちらつきや ゴーストが生じる可能性があります

    レンダリングパイプラインでは アップスケーラの入力色と出力色は リニア色空間にあります アップスケーラは 露出と呼ばれる パラメータを取ります 露出に色入力を乗算することで トーンマッピングにおいて 使用される露出におおよそ適した 輝度が得られます

    これは プレイヤーに表示される際の フレームの目に見える特徴を アップスケーラが理解するのに役立ちます この値は アップスケーラにとっての ヒントに過ぎず これによって出力の輝度は変更されません MetalFXには アップスケーラに送信する 露出の入力値の調整に役立つ 新しいツールが含まれています

    これは露出デバッガと呼ばれます 有効にするには 環境変数 MTLFX_EXPOSURE_TOOL_ENABLEDを設定します これでアップスケーラによりフレーム上に 灰色のチェッカー盤がレンダリングされ 露出値の逆数が適用されます

    ディスプレイ上で パイプラインの 最終段階でこのパターンが どう見えるかを確認できます

    アップスケーラに渡す露出値が トーンマッパーと合っていない場合 チェッカー盤の表示が 暗すぎたり明るすぎたりします

    ゲームの実行中にチェッカー盤の 輝度が変わる場合も 不整合があることの表れです

    露出値が適正であれば 格子のパターンは 均一なミッドグレーになります

    ゲームの複雑さがシーンによって 大きく変化する場合があるため 多くのゲームで動的解像度レンダリングが 採用されています

    フレームが複雑になるほど アップスケーラの入力解像度が下がります さらに課題が生じた場合は ゲームにおいて動的に さらなる入力解像度の引き下げが行われます MetalFXの時間アップスケーラで 動的サイズの入力がサポートされ すべてのフレームで同じサイズの入力を 渡す必要がなくなりました スケーリングの品質を最大限に高めるには 必要でない限り 最大スケールを 2倍より大きな設定にしないようにします

    MetalFXの時間アップスケーラの もう1つの新機能は ピクセルのリアクティビティに関する ヒントをアップスケーラに提供する オプションの機能です

    ゲームで透過エフェクトまたは 花火のようなパーティクルを レンダリングする場合 モーションテクスチャや深度テクスチャには レンダリングされません

    スケーリング比が高く 入力解像度が低いと パーティクルが背景に溶け込んで見えたり

    ゴーストが見えたりすることがあります これは レンダリングにおいて パーティクルがテクスチャのディテールや 鏡面ハイライトのように 表示されることがあるためです

    デベロッパがパーティクルの処理を コントロールできるように アップスケーラがリアクティブマスクという 新しいオプションの入力を 受け付けるようになります このマスクでは エフェクトの 対象領域をマーキングできます

    使用するには シェーダでリアクティブマスクの値を設定します 例えば G-Bufferのマテリアルタイプに 基づく値を使用します ホストコードで エンコーディングの前に 時間アップスケーラオブジェクトに テクスチャをバインドします

    リアクティブマスクを使用するのは 入力解像度を高くする方法を 取れない場合のみにしてください また 別のアップスケーラ用に調整された リアクティブマスク使用しないでください MetalFXのアップスケーラ出力で適切に 表示される領域がマスクされる 可能性があるためです アップスケーラを使用すると 高品質で 優れたパフォーマンスが得られます しかし より高いリフレッシュレートが 必要な場合があります 今年 MetalFXに 全Appleプラットフォーム 対応のフレーム補間が導入されます

    MetalFXのフレーム補間をゲームに 組み込むのはとても簡単です まず 補間オブジェクトを設定し 補間されたフレームにUIをレンダリングし フレームを適切に表示し ペースを調整します

    フレーム補間は レンダリング済みの ピクセルを利用するのに役立ち スムーズなゲーム体験を実現できます

    これは同じレンダリングパイプラインですが 今回はUIレンダリングがありません

    トーンマッピングの後で フレームを補間します 解像度とフレームレートを高くする場合は 同じパイプラインでアップスケーリングと 補間処理の両方を行うことができます

    MetalFXのフレーム補間を使用する場合 モーションベクトルと深度の 2つのフレームがレンダリングされます アップスケーラを採用している場合 同じモーションベクトルと深度を 使用できます モーションテクスチャのオブジェクトは 色がありますが 右に移動しているためです これらの入力により MetalFXで これら2つのレンダリングされた フレームの間にフレームが生成されます

    補間の設定をして 総合的なパフォーマンスを向上させるには アップスケールオブジェクトを 補間記述子に渡します インターポレータを作成する際 モーションスケールと深度の規則を 定義します 次に 必要な5つのテクスチャすべてを インターポレータにバインドします

    補間されたフレームの取得を開始したら UIのレンダリングについて考えます

    通常のレンダリングパイプラインでは 各フレームの最後にUIがレンダリングされ その位置はフレーム補間が行われるのと ほぼ同じ位置です

    UIレンダリングで要素がフレームに アルファブレンドされます 各フレームでテキストの変化があっても モーションテクスチャや 深度テクスチャは変更されません

    フレーム補間を有効にして 見栄えの良いUIを実現するには いくつかの方法があります

    フレーム補間を使用して UIをレンダリングするために よく使われる手法は3つです 合成UIとオフスクリーンUI フレームごとのUIです

    合成UIでは インターポレータは 前のフレームN - 1に加え UIなしの現在のフレームNと UIありの同じフレームNを取得します 合成UIは 最も簡単に導入できます このモードでは フレームインターポレータで UIありと UIなしのテクスチャの差分がわかります このように 補間後のフレームでの UIの削除と適切な位置への配置を 試行することができます ただし すでにブレンドされたピクセルの ブレンド解除を完璧に行うことはできません そこで そのためには 他のいずれかの 方法を使用します

    例えば オフスクリーンUIでは UIが完全に独立したUIテクスチャに レンダリングされます

    インターポレータはそれを 補間後のフレームに追加します これをインターポレータの入力にすると 読み込みと保存の負担から解放されます インターポレータがUIを出力に 書き込むことができるためです

    最後に フレームごとのUIでは UI処理はデベロッパのコードに依存し 必要なコードの変更量が 最も大きくなる可能性があります ただし この場合 補間後のフレームの UIを更新することもでき プレイヤーにとって最も スムーズな体験が得られます

    これで 補間後のフレームでも 見栄えの良いUIを表示できます ここで 補間されたフレームと ネイティブにレンダリングされたフレームの 両方を正しい順序 正しい間隔で表示する方法を 考える必要があります

    通常 ゲームのレンダリングは RenderスレッドとGPU Presentスレッドで構成されます Renderスレッドで GPUと Presentationに必要な処理の 準備を行います フレームがレンダリングされる際 インターポレータが レンダリングされた フレームと前のフレームの間に タイムスタンプ付きのフレームを 生成できます これにより ゲームで 補間されたフレームを表示できます 表示間隔の経過後 新しくレンダリングされた フレームを表示できます

    この間隔の長さを一貫した形で 決定するのは難しい場合があります しかし ゲームのペーシングを適切に 行うためには必要なことです

    新しいMetal HUDは ペーシングがずれている タイミングの特定に役立つ優れたツールです 有効化の方法の詳細については「ゲームを レベルアップさせる方法」をご覧ください このツールの優れた新機能についても 説明しています

    フレーム間隔のグラフをご覧ください 横軸は時間で 縦軸はフレーム間隔の長さです

    グラフが不規則なパターンを示していて フレームの更新間隔が長いことを示す スパイクがランダムに見える場合 ペーシングがずれています

    もう1つ ペーシングが ずれていることがわかるのは フレーム間隔のヒストグラムバケットが 2つ以上ある場合です

    ペーシングが修正されると ターゲットディスプレイの リフレッシュレートに合っていれば 平坦な線が表示されます 下回っていれば 一定のパターンの繰り返しになり ヒストグラムバケットが最大2つ存在します

    その処理を正しく行う方法の例がこちらで 便利なpresentHelperクラスを使用します 描画のループでは すべてが 低解像度のテクスチャにレンダリングされ MetalFXアップスケーラで アップスケールされます UIのレンダリング開始をヘルパーに 指示した後 UIがレンダリングされます 最後に インターポレータの呼び出しが presentHelperクラスで処理されます 実装の詳細については サンプルコードを確認してください

    ペーシングに加えて デルタタイムとカメラのパラメータを 適切に設定することも重要です 全パラメータが適正でないとオクルージョン 領域にアーティファクトが生じかねません

    適正なパラメータを使用すると オクルージョン領域が正しく処理されます

    これは インターポレータで 実際の シミュレーションのモーションの長さに 合わせて モーションベクトルを 調整できるようになったためです

    すべての入力とペーシングが適切になると 補間されたフレームが適切に表示されます また 補間の入力は適正な高さの フレームレートにする必要があります 補間前に 少なくとも30フレーム/秒と なるようにします

    アップスケーラとフレームインターポレータは ほぼどのようなレンダリングスタイルの スケーリングにも汎用的に使える手法です これに対して レイトレーシングは通常 ハイエンドの レンダリングシナリオで使用されます Metal 4では新しいレイトレーシング機能が 多数追加されており アクセラレーション構造のビルドや 交差関数に関連するものがあります

    Metalのレイトレーシングを使った Apple プラットフォーム向けゲームが増えています

    このデモでは リアルなライティングで ドローンが床面に反射する様子が見えます レイトレーシングの手法や 複雑さはゲームごとに異なります

    そのため 交差関数の管理の柔軟性を高め アクセラレーション構造のビルドの オプションを増やす必要があります

    Metal 4では この2つを効率化するための 新機能が導入されています

    Metalのレイトレーシングの基本事項 アクセラレーション構造のビルドや 交差関数の詳細については 「Metalレイトレーシングのガイド」 をご覧ください

    1本の木の周りに草が生えている単純な シーンのレイトレーシングを考えます

    このようにシンプルなシーンでも 複数種類のマテリアルがあります アルファテストされた木の葉や 不透明な木の幹などです

    そのため さまざまなレイトレーシングの 交差関数が必要になります 主光線用とシャドウレイ用で別々です

    交差関数バッファは引数バッファであり シーンの交差関数へのハンドルが 格納されます

    例えば 主光線をトレーシングするために 草と木の葉で同様の機能が 必要な場合があります 交差関数バッファを使用することにより 同じ交差関数を指す複数のエントリを 簡単に持つことができます

    交差関数バッファのインデックスを 設定するために必要なことは 1つはインスタンスレベルでの状態の設定で この例のシーンには2つの インスタンスがあります もう1つはジオメトリレベルで この場合 草には1つだけ 木には2つのジオメトリがあります インターセクタでは 木の幹に当たる シャドウレイにはどの交差関数を 使用するかの情報が必要です

    インスタンスのアクセラレーション構造を 作成する際 各インスタンス記述子で intersectionFunctionTableOffsetを 指定します

    プリミティブアクセラレーション構造を ビルドする際も ジオメトリ記述子に intersectionFunctionTableOffsetを 設定します

    シェーダでインターセクタを設定する際には "intersection_function_buffer"を タグに追加します

    次に ジオメトリの乗数を インターセクタに設定します 乗数は 交差関数バッファ内の 光線の種類の数です

    この例では ジオメトリごとに 2種類の光線があります したがって ここでの正しい値は2です その2種類の光線のうち トレーシングする光線の種類に対応する ベースインデックスの指定が必要です この例で 主光線をトレーシングするための ベースインデックスは0になります

    シャドウをトレーシングする場合の ベースIDは1です

    木の幹のインスタンスとジオメトリの影響 ジオメトリの乗数およびシャドウレイの ベースIDを組み合わせると 目的の交差関数を指すポインタが得られます

    intersectメソッドに 交差関数バッファの引数を渡して コードを完成させます

    バッファ サイズおよびストライドを 指定することにより 従来の他のAPIの場合と比較して 柔軟性が高くなります DirectXから移植する場合は シェーダバインディングテーブルを Metalの交差関数バッファに 簡単に移植できます

    DirectXでは 光線を送出する記述子を 作成する際に 交差関数バッファのアドレスと ストライドをホストに設定します Metalではこれをシェーダで設定します SIMDグループのすべてのスレッドで 同じ値を設定する必要があり そうしないと 動作が未定義になります

    光線タイプのインデックスとジオメトリの乗数は DirectXとMetalで 同じように扱われます アプリではシェーダでこれらを設定できます DirectXとMetalでは インスタンスの アクセラレーション構造の作成時に インスタンスごとのインスタンス オフセットインデックスを設定します ただしジオメトリオフセットインデックスは DirectXでは自動的に生成されますが Metalでは ジオメトリオフセットを デベロッパが柔軟に設定できます

    レイトレーシングが用いられた ゲームのMetalへの移植が 交差関数バッファにより 大幅に改善されます デベロッパの準備が整えば Metal 4では アクセラレーション構造のビルド方法を 最適化することもできます

    Metalではすでに アクセラレーション構造のビルドに関して さまざまな制御ができました デフォルトの動作に加え リフィットのための最適化ができ 大規模なシーンを実現したり アクセラレーション構造を 迅速にビルドしたりできます 今年はさらに柔軟性が高まり 高速交差を選択して レイトレーシングに かかる時間を短縮できます

    また アクセラレーション構造の メモリ使用量を最小限に抑える 選択をすることもできます

    使用のフラグはアクセラレーション構造の ビルドごとに設定でき すべてのアクセラレーション構造で 同じにする必要はありません

    新しいアクセラレーション構造の フラグにより レイトレーシングを レンダリングパイプラインに含められ よりニーズに合わせやすくなります 確率的影響に対して使用する場合は デノイザが必要です そして MetalFXアップスケーラに ノイズ除去を含められるようになりました

    単純なハイブリッドのレイトレーシングから 複雑なパストレーシングまで リアルタイムレイトレーシングが常に 使用されることが増えています この画像の例では レイトレーシングによって すべてが より地について見え

    反射が大幅に改善しています レイトレーシングで品質とパフォーマンスの 最適なトレードオフを実現するには ノイズ除去を使用する光線を少なくします

    新しいMetalFX APIを使用すれば アップスケーリングとノイズ除去を 組み合わせるには いくつかの入力を 追加するだけで簡単です ただし ノイズ除去アップスケーラを より強化し 入力を追加し 詳細情報を正しく処理することにより さらに品質を向上させることができます

    アップスケーラとデノイザを 組み合わせる前に 従来はどのような手順で 行われていたかを確認しましょう

    通常のリアルタイムのインタラクティブな レイトレーシングレンダリングパイプラインでは 複数のエフェクトを別々にトレーシングし 別々にノイズ除去を行い 結果を1つのノイズのないジッタ処理済み テクスチャにまとめます それをMetalFXの時間アップスケーラで アップスケールし さらに後処理を行います

    従来のデノイザでは シーンごとに 個別のパラメータ調整が必要でした こちらは パラメータの調整を 行わない場合のノイズ除去の一例です これに対して MetalFXのノイズ除去アップスケーラでは パラメータを調整する必要はありません これは メインのレンダリングの後 後処理の直前に適用されます MetalFXの機械学習を活用した手法により 幅広いシナリオで 堅牢性とパフォーマンスに優れた 高品質のノイズ除去と アップスケーリングが得られます 組み込むのも簡単です ノイズ除去アップスケーラを組み込むうえで アップスケーラの組み込みは よい出発点となります ここでは アップスケーラへの 入力を確認します 色 モーション 深度です 新しい結合APIは アップスケーラAPIのスーパーセットです

    新しいAPIでは ノイズのない 補助バッファを追加する必要があります これが左に表示されています ほとんどが すでにアプリで 使用されている可能性があります それぞれについて 詳しく見ていきましょう

    新しい入力の1つ目は法線です 最適な結果を得るには これらが ワールド空間内にある必要があります

    次は ディフューズアルベドです これは マテリアルの拡散放射輝度の ベースカラーです

    次の粗さは 表面がどれだけ滑らかか または粗いかを表し リニア値になっています 最後の入力は鏡面アルベドです レンダリングの鏡面反射輝度の ノイズのない近似値になります フレネル成分を含んでいる必要があります コードで これらの新しい入力を 追加するのは簡単です

    一般的な時間アップスケーラを 作成するのに必要なコードは10行程度です ノイズ除去バージョンを有効にするには スケーラの種類を変更し 付加的なテクスチャの種類を 追加する必要があります

    同様に スケーラをエンコードする場合 これはアップスケーラ呼び出しになります ここで唯一の違いは追加の入力テクスチャを バインドする必要があることです

    デノイザの基本的な使い方を設定した後 さらに改善するには いくつかの オプションの入力を使用し 組み込みに関する一般的な 落とし穴を回避します

    品質向上に利用できるオプションの 入力テクスチャがいくつかあります

    1つ目は鏡面反射到達距離です これは ピクセルの最初の可視点から 次に跳ね返る位置までの 光線の長さです 次に ノイズ除去強度マスクです これを使用することで ノイズ除去が 不要な領域のマーキングができます 最後は 透明オーバーレイで アルファチャネルをもとに色をブレンドする ために使用します アップスケールのみでノイズ除去されません

    組み込みで最も一般的な問題は 入力のノイズの多さです これを修正するには 標準的なパストレーシングのサンプリングに 対する改善をすべて行う必要があります NEE(Next-Event-Estimation)や 重点サンプリング法などです また 光源の多い大規模なシーンでは 実際に対象領域に影響する光源の サンプリングを主に行うようにします

    レイトレーシングのサンプリング品質に 関連するもう1つの課題が 相関を持つ乱数です 相関が強すぎる乱数ジェネレータを 使用すべきではありません 空間相関と時間相関のいずれも アーティファクトにつながりかねません

    補助データに関連する落とし穴の1つが 金属物質のディフューズアルベドに 関するものです この例では チェスの駒が金属製であるため 鏡面アルベドで色が表現されています この場合 チェスの駒にディフューズ アルベドを使用すると暗くなります

    最後に 法線関連のよくある 落とし穴があります MetalFXのノイズ除去アップスケーラで ノイズ除去判定を適切に行うには 法線がワールド空間にあることが前提です テクスチャのデータ型は符号ビットを 持つものを使用する必要があります そうでないと カメラの向きによっては 最適な品質が得られない可能性あります

    これらの項目すべてに対応することで 適切にノイズ除去され アップスケールされたフレームが得られます

    1つのレンダラにこれらの機能を すべて搭載したら どうなるか見てみましょう

    同僚がまとめてくれたデモで 先ほどお話したレンダリング パイプラインを使ったものがあります このデモでは新しいMetal 4の レイトレーシング機能を使って レンダリングのレイトレーシング部分を 最適化しています MetalFXのノイズ除去アップスケーラで ノイズ除去とアップスケーリングを 同時に行います 露出とトーンマッピングの後 MetalFXフレームインターポレータで フレームを補間します

    このデモでは グローバルイルミネーション 反射 シャドウ アンビエントオクルージョンなどの 高度なレイトレーシングの 照明効果を使用して 2台のロボットがチェスをするシーンを 生き生きと描き出しています

    右上のビューは MetalFXの処理を行う前のレンダリングです 他のビューでは 他のMetalFXの入力が 使用されています

    MetalFXのノイズ除去アップスケーラと フレームインターポレータの 両方を採用しています デノイザを使用すると 最終的な見た目を すべて手動で調整する手間がなくなり レンダリングが大幅に楽になります

    すでにMetalFXアップスケーラを 組み込んだことがある場合は フレーム補間へとアップグレードする よいチャンスです MetalFXが初めての場合は まずアップスケーラから確認してください 次に レイトレーシングのエフェクトに 今日説明した交差関数バッファなどの ベストプラクティスが用いられて いることを確認します そして ノイズ除去アップスケーラで ゲームの光線量を減らします

    皆さんのゲームで新機能を実際に ご活用いただき Metal 4を使って 何を作られるのか楽しみにしています ご視聴ありがとうございました

    • 6:46 - Reactive Mask

      // Create reactive mask setup in shader
      out.reactivity = m_material_id == eRain ? (m_material_id == eSpark ? 1.0f : 0.0f) : 0.8f;
      
      // Set reactive mask before encoding upscaler on host
      temporalUpscaler.reactiveMask = reactiveMaskTexture;
    • 8:35 - MetalFX Frame Interpolator

      // 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;
    • 12:45 - Interpolator present helper class

      #include <thread>
      #include <mutex>
      #include <sys/event.h>
      #include <mach/mach_time.h>
      
      
      class PresentThread
      {
          int m_timerQueue;
          std::thread m_encodingThread, m_pacingThread;
          std::mutex m_mutex;
          std::condition_variable m_scheduleCV, m_threadCV, m_pacingCV;
          float m_minDuration;
          
          uint32_t m_width, m_height;
          MTLPixelFormat m_pixelFormat;
          
          const static uint32_t kNumBuffers = 3;
          uint32_t m_bufferIndex, m_inputIndex;
          bool m_renderingUI, m_presentsPending;
          
          CAMetalLayer *m_metalLayer;
          id<MTLCommandQueue> m_presentQueue;
      
          id<MTLEvent> m_event;
          id<MTLSharedEvent> m_paceEvent, m_paceEvent2;
          uint64_t m_eventValue;
          uint32_t m_paceCount;
          
          int32_t m_numQueued, m_framesInFlight;
          
          id<MTLTexture> m_backBuffers[kNumBuffers];
          id<MTLTexture> m_interpolationOutputs[kNumBuffers];
          id<MTLTexture> m_interpolationInputs[2];
          id<MTLRenderPipelineState> m_copyPipeline;
          
          std::function<void(id<MTLRenderCommandEncoder>)> m_uiCallback = nullptr;
          
          void PresentThreadFunction();
          void PacingThreadFunction();
          
          void CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label);
      
      public:
          
          PresentThread(float minDuration, CAMetalLayer *metalLayer);
          ~PresentThread()
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              m_numQueued = -1;
              m_threadCV.notify_one();
              m_encodingThread.join();
          }
          void StartFrame(id<MTLCommandBuffer> commandBuffer)
          {
              [commandBuffer encodeWaitForEvent:m_event value:m_eventValue++];
          }
      
          void StartUI(id<MTLCommandBuffer> commandBuffer)
          {
              assert(m_uiCallback == nullptr);
              if(!m_renderingUI)
              {
                  CopyTexture(commandBuffer, m_interpolationInputs[m_inputIndex], m_backBuffers[m_bufferIndex], @"Copy HUDLESS");
                  m_renderingUI = true;
              }
          }
          
          void Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue);
          
          id<MTLTexture> GetBackBuffer()
          {
              return m_backBuffers[m_bufferIndex];
          }
      
          void Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat);
          
          void DrainPendingPresents()
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              while(m_presentsPending)
                  m_scheduleCV.wait(lock);
          }
          
          bool UICallbackEnabled() const
          {
              return m_uiCallback != nullptr;
          }
          
          void SetUICallback(std::function<void(id<MTLRenderCommandEncoder>)> callback)
          {
              m_uiCallback = callback;
          }
          
      };
      
      PresentThread::PresentThread(float minDuration, CAMetalLayer *metalLayer)
          : m_encodingThread(&PresentThread::PresentThreadFunction, this)
          , m_pacingThread(&PresentThread::PacingThreadFunction, this)
          , m_minDuration(minDuration)
          , m_numQueued(0)
          , m_metalLayer(metalLayer)
          , m_inputIndex(0u)
          , m_bufferIndex(0u)
          , m_renderingUI(false)
          , m_presentsPending(false)
          , m_framesInFlight(0)
          , m_paceCount(0)
          , m_eventValue(0)
      {
          id<MTLDevice> device = metalLayer.device;
          m_presentQueue = [device newCommandQueue];
          m_presentQueue.label = @"presentQ";
          m_timerQueue = kqueue();
          
          metalLayer.maximumDrawableCount = 3;
          
          Resize(metalLayer.drawableSize.width, metalLayer.drawableSize.height, metalLayer.pixelFormat);
          
          m_event = [device newEvent];
          m_paceEvent = [device newSharedEvent];
      	m_paceEvent2 = [device newSharedEvent];
      }
      
      
      void PresentThread::Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue)
      {
          id<MTLCommandBuffer> commandBuffer = [queue commandBuffer];
          
          if(m_renderingUI)
          {
              frameInterpolator.colorTexture = m_interpolationInputs[m_inputIndex];
              frameInterpolator.prevColorTexture = m_interpolationInputs[m_inputIndex^1];
              frameInterpolator.uiTexture = m_backBuffers[m_bufferIndex];
          }
          else
          {
              frameInterpolator.colorTexture = m_backBuffers[m_bufferIndex];
              frameInterpolator.prevColorTexture = m_backBuffers[(m_bufferIndex + kNumBuffers - 1) % kNumBuffers];
              frameInterpolator.uiTexture = nullptr;
          }
          
          frameInterpolator.outputTexture = m_interpolationOutputs[m_bufferIndex];
      
          [frameInterpolator encodeToCommandBuffer:commandBuffer];
          [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) {
              std::unique_lock<std::mutex> lock(m_mutex);
              m_framesInFlight--;
              m_scheduleCV.notify_one();
              m_paceCount++;
              m_pacingCV.notify_one();
          }];
          [commandBuffer encodeSignalEvent:m_event value:m_eventValue++];
          [commandBuffer commit];
      
          std::unique_lock<std::mutex> lock(m_mutex);
          m_framesInFlight++;
          m_numQueued++;
          m_presentsPending = true;
          m_threadCV.notify_one();
          while((m_framesInFlight >= 2) || (m_numQueued >= 2))
              m_scheduleCV.wait(lock);
      
          m_bufferIndex = (m_bufferIndex + 1) % kNumBuffers;
          m_inputIndex = m_inputIndex^1u;
          m_renderingUI = false;
      }
      
      void PresentThread::CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label)
      {
          MTLRenderPassDescriptor *desc = [MTLRenderPassDescriptor new];
          desc.colorAttachments[0].texture = dest;
          desc.colorAttachments[0].loadAction = MTLLoadActionDontCare;
          desc.colorAttachments[0].storeAction = MTLStoreActionStore;
          id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:desc];
          [renderEncoder setFragmentTexture:src atIndex:0];
          [renderEncoder setRenderPipelineState:m_copyPipeline];
          [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
          if(m_uiCallback)
              m_uiCallback(renderEncoder);
          renderEncoder.label = label;
          [renderEncoder endEncoding];
      }
      
      
      void PresentThread::PacingThreadFunction()
      {
          NSThread *thread = [NSThread currentThread];
          [thread setName:@"PacingThread"];
          [thread setQualityOfService:NSQualityOfServiceUserInteractive];
          [thread setThreadPriority:1.f];
          
          mach_timebase_info_data_t info;
          mach_timebase_info(&info);
          
          // maximum delta (0.1ms) in machtime units
          const uint64_t maxDeltaInNanoSecs = 100000000;
          const uint64_t maxDelta = maxDeltaInNanoSecs * info.denom / info.numer;
          
          uint64_t time = mach_absolute_time();
          
          uint64_t paceEventValue = 0;
          
          for(;;)
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              while(m_paceCount == 0)
                  m_pacingCV.wait(lock);
              m_paceCount--;
              lock.unlock();
              
              // we get signal...
              const uint64_t prevTime = time;
              time = mach_absolute_time();
      		m_paceEvent.signaledValue = ++paceEventValue;
      
              const uint64_t delta = std::min(time - prevTime, maxDelta);
              const uint64_t timeStamp = time + ((delta*31)>>6);
              
              struct kevent64_s timerEvent, eventOut;
              struct timespec timeout;
              timeout.tv_nsec = maxDeltaInNanoSecs;
              timeout.tv_sec = 0;
              EV_SET64(&timerEvent,
                       0,
                       EVFILT_TIMER,
                       EV_ADD | EV_ONESHOT | EV_ENABLE,
                       NOTE_CRITICAL | NOTE_LEEWAY | NOTE_MACHTIME | NOTE_ABSOLUTE,
                       timeStamp,
                       0,
                       0,
                       0);
              
              kevent64(m_timerQueue, &timerEvent, 1, &eventOut, 1, 0, &timeout);
              
              // main screen turn on...
              m_paceEvent2.signaledValue = ++paceEventValue;
          }
      }
      
      
      void PresentThread::PresentThreadFunction()
      {
          NSThread *thread = [NSThread currentThread];
          [thread setName:@"PresentThread"];
          [thread setQualityOfService:NSQualityOfServiceUserInteractive];
          [thread setThreadPriority:1.f];
          
      
          uint64_t eventValue = 0;
          uint32_t bufferIndex = 0;
      
          uint64_t paceEventValue = 0;
      
          for(;;)
          {
              std::unique_lock<std::mutex> lock(m_mutex);
              
              if(m_numQueued == 0)
              {
                  m_presentsPending = false;
                  m_scheduleCV.notify_one();
              }
              
              while(m_numQueued == 0)
                  m_threadCV.wait(lock);
              
              if(m_numQueued < 0)
                  break;
              lock.unlock();
      
              @autoreleasepool
              {
                  id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable];
      
      			lock.lock();
      			m_numQueued--;
      			m_scheduleCV.notify_one();
      			lock.unlock();
      
                  id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer];
                  [commandBuffer encodeWaitForEvent:m_event value:++eventValue];
                  CopyTexture(commandBuffer, drawable.texture, m_interpolationOutputs[bufferIndex], @"Copy Interpolated");
                  [commandBuffer encodeSignalEvent:m_event value:++eventValue];
      			[commandBuffer encodeWaitForEvent:m_paceEvent value:++paceEventValue];
      
                  if(m_minDuration > 0.f)
                      [commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration];
                  else
                      [commandBuffer presentDrawable:drawable];
                  [commandBuffer commit];
              }
              
              @autoreleasepool
              {
                  id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer];
                  id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable];
                  CopyTexture(commandBuffer, drawable.texture, m_backBuffers[bufferIndex], @"Copy Rendered");
      			[commandBuffer encodeWaitForEvent:m_paceEvent2 value:++paceEventValue];
                  if(m_minDuration > 0.f)
                      [commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration];
                  else
                      [commandBuffer presentDrawable:drawable];
                  [commandBuffer commit];
              }
              
              bufferIndex = (bufferIndex + 1) % kNumBuffers;
          }
      }
      
      void PresentThread::Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat)
      {
          if((m_width != width) || (m_height != height) || (m_pixelFormat != pixelFormat))
          {
              id<MTLDevice> device = m_metalLayer.device;
      
              if(m_pixelFormat != pixelFormat)
              {
                  id<MTLLibrary> lib = [device newDefaultLibrary];
                  MTLRenderPipelineDescriptor *pipelineDesc = [MTLRenderPipelineDescriptor new];
                  pipelineDesc.vertexFunction = [lib newFunctionWithName:@"FSQ_VS_V4T2"];
                  pipelineDesc.fragmentFunction = [lib newFunctionWithName:@"FSQ_simpleCopy"];
                  pipelineDesc.colorAttachments[0].pixelFormat = pixelFormat;
                  m_copyPipeline = [device newRenderPipelineStateWithDescriptor:pipelineDesc error:nil];
                  m_pixelFormat = pixelFormat;
              }
              
              DrainPendingPresents();
              
              m_width = width;
      		m_height = height;
              
              MTLTextureDescriptor *texDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixelFormat width:width height:height mipmapped:NO];
      		texDesc.storageMode = MTLStorageModePrivate;
              for(uint32_t i = 0; i < kNumBuffers; i++)
              {
                  texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageShaderWrite|MTLTextureUsageRenderTarget;
                  m_backBuffers[i] = [device newTextureWithDescriptor:texDesc];
                  texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget;
                  m_interpolationOutputs[i] = [device newTextureWithDescriptor:texDesc];
              }
              texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget;
              m_interpolationInputs[0] = [device newTextureWithDescriptor:texDesc];
              m_interpolationInputs[1] = [device newTextureWithDescriptor:texDesc];
      
          }
      }
    • 13:00 - Set intersection function table offset

      // Set intersection function table offset on host-side geometry descriptors
      NSMutableArray<MTLAccelerationStructureGeometryDescriptor *> *geomDescs ...;
      for (auto g = 0; g < geomList.size(); ++g)
      {
          MTLAccelerationStructureGeometryDescriptor *descriptor = ...;
          descriptor.intersectionFunctionTableOffset = g;
          ...
          [geomDescs addObject:descriptor];
      }
    • 13:01 - Set up the intersector

      // Set up the intersector
      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
    • 13:02 - Ray trace intersection function buffers

      // Ray trace intersection function buffers
      
      // Set up intersection function buffer arguments
      intersection_function_buffer_arguments ifb_arguments;
      ifb_arguments.intersection_function_buffer = raytracingResources.ifbBuffer;
      ifb_arguments.intersection_function_buffer_size = raytracingResources.ifbBufferSize;
      ifb_arguments.intersection_function_stride = raytracingResources.ifbBufferStride;
      
      // Set up the ray and finish intersecting
      metal::raytracing::ray r = { origin, direction };
      auto result = trace.intersect(r, ads, ifb_arguments);
    • 13:02 - Change of temporal scaler setup to denoised temporal scaler setup

      // Change of temporal scaler setup to denoised temporal scaler setup
      
      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];
    • 13:04 - Change temporal scaler encode to denoiser temporal scaler encode

      // Change temporal scaler encode to denoiser temporal scaler encode
      
      temporalScaler.colorTexture = _mainView;
      temporalScaler.motionTexture = _motionTexture;
      
      temporalScaler.diffuseAlbedoTexture = _diffuseAlbedoTexture;
      temporalScaler.specularAlbedoTexture = _specularAlbedoTexture;
      temporalScaler.normalTexture = _normalTexture;
      temporalScaler.roughnessTexture = _roughnessTexture;
      
      temporalScaler.depthTexture = _depthTexture;
      temporalScaler.jitterOffsetX = _pixelJitter.x;
      temporalScaler.jitterOffsetY = -_pixelJitter.y;
      temporalScaler.outputTexture = _upscaledColorTarget;
      temporalScaler.motionVectorScaleX = (float)_motionTexture.width;
      temporalScaler.motionVectorScaleY = (float)_motionTexture.height;
      [temporalScaler encodeToCommandBuffer:commandBuffer];
    • 16:04 - Creating instance descriptors for instance acceleration structure

      // Creating instance descriptors for instance acceleration structure
      MTLAccelerationStructureInstanceDescriptor *grassInstanceDesc, *treeInstanceDesc = . . .;
      grassInstanceDesc.intersectionFunctionTableOffset = 0;
      treeInstanceDesc.intersectionFunctionTableOffset  = 1;
      
      // Create buffer for instance descriptors of as many trees/grass instances the scene holds
      id <MTLBuffer> instanceDescs = . . .;
      for (auto i = 0; i < scene.instances.size(); ++i)
      . . .

Developer Footer

  • ビデオ
  • WWDC25
  • Metal 4ゲームの知識を深める
  • メニューを開く メニューを閉じる
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    メニューを開く メニューを閉じる
    • アクセシビリティ
    • アクセサリ
    • App Extension
    • App Store
    • オーディオとビデオ(英語)
    • 拡張現実
    • デザイン
    • 配信
    • 教育
    • フォント(英語)
    • ゲーム
    • ヘルスケアとフィットネス
    • アプリ内課金
    • ローカリゼーション
    • マップと位置情報
    • 機械学習とAI
    • オープンソース(英語)
    • セキュリティ
    • SafariとWeb(英語)
    メニューを開く メニューを閉じる
    • 英語ドキュメント(完全版)
    • 日本語ドキュメント(一部トピック)
    • チュートリアル
    • ダウンロード(英語)
    • フォーラム(英語)
    • ビデオ
    Open Menu Close Menu
    • サポートドキュメント
    • お問い合わせ
    • バグ報告
    • システム状況(英語)
    メニューを開く メニューを閉じる
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles(英語)
    • フィードバックアシスタント
    メニューを開く メニューを閉じる
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(英語)
    • News Partner Program(英語)
    • Video Partner Program(英語)
    • セキュリティ報奨金プログラム(英語)
    • Security Research Device Program(英語)
    Open Menu Close Menu
    • Appleに相談
    • Apple Developer Center
    • App Store Awards(英語)
    • Apple Design Awards
    • Apple Developer Academy(英語)
    • WWDC
    Apple Developerアプリを入手する
    Copyright © 2025 Apple Inc. All rights reserved.
    利用規約 プライバシーポリシー 契約とガイドライン