2021 年 8 月の全世界リリース以来、バトルロワイヤルゲーム『Naraka: Bladepoint』は大ヒットし続けています。特に、古代中国のファンタジーから着想を得たゲーム内の素晴らしいアートワークに世界中のプレイヤーが魅了されています。
NetEase 社の独立子会社として 24 Entertainment 社はポートフォリオを拡大し続けており、会社として初のデスクトップ向けゲーム『Naraka: Bladepoint』が瞬く間に世界中から注目を集めたことで、同社は大きな足跡を残しました。発売後 1 週間以内に Steam のチャートでトップ 10 に入ったり、『Rainbow Six:Siege』や『Splitgate』などの人気タイトルよりも多くのプレイヤーを獲得したりといった出来事が起こりました。
このゲームの中で起きるアクションアドベンチャー型のバトルロワイヤルは、カラフルな山頂、緑豊かな森、広大な古代都市など、あらゆる場所で繰り広げられ、そこにあるものすべてが印象的なディテールをもって描かれています。60 人が同時参加するマルチプレイヤーをサポートするために必要なパフォーマンスとフレームレートを維持しながら、これらの美しい環境を描き出すために、24 Entertainment 社はNVIDIA や Unity と密に連携を取りました。
24 Entertainment 社は、NVIDIA とのパートナーシップにより、Deep Learning Super Sampling(DLSS)という、高フレームレート・高解像度でリアルタイムに世界を動かすことができる最新のレンダリング技術への早期アクセス権を得ました。人工知能を活用した DLSS を使えば、グラフィックスのパフォーマンスとアートの全体的な品質の両方をどちらも妥協することなく向上させることができます。
その強力なパフォーマンスを維持するために、『Naraka: Bladepoint』では、低解像度でレンダリングし、ピクセルシェーディングの計算などを必要としないようにしています。DLSS は、ニューラルネットワークを用いて高解像度の画像を生成することで、ゲームユーザーが感じる芸術的なディテールを維持しています。高品質な結果が得られるだけでなく、人工知能を活用して不足している情報を補うことで、ゲームのレンダリング速度が約 2 倍になりました。これは、このレベルの対戦型マルチプレイヤーゲームには不可欠です。
DLSS により、24 Entertainment 社はフレームレートを落とすことなく、高解像度の描画結果と鮮明なディテールを実現しました。その結果、ネイティブ 4K と DLSS 4K の見分けがほぼつかないほどになりました。
AI は前のフレームを考慮するように訓練されているので、アンチエイリアシングなどの要素に寄与します。また、一貫したモデルを使用することで、ゲームごとにニューラルネットワークを再学習する必要がありません。
『Naraka: Bladepoint』の開発は数年前に始まり、Unity のスクリプタブルレンダーパイプライン(SRP)をベースに構築されたカスタムレンダリングパイプラインを実装しました。これは C# で記述されたさまざまなレンダリングアーキテクチャをプラグインすることができるものです。
24 Entertainment 社は、NVIDIA のエキスパート集団である Developer Relations サポート、および Unity の Core Support チームと協力して、Unity で初めて DLSS を活用した作品を作り上げました。
DLSS がリアルタイムプラットフォームでどのように機能するかをより深く理解するために、『Naraka: Bladepoint』のグラフィックチームが実装の詳細の一部や、ヒント、開発時に直面した課題を紹介しています。
DLSS を導入するための最初のステップは、低解像度の入力画像をアップサンプリングすることです。
最終的なフレームの品質への影響を軽減するために、24 Entertainment 社では、ブルーム、トーンマッピング、特殊効果のためのライティングなどのポストプロセッシングを適用する前にこのプロセスを実施します。そして、サンプリング後の高解像度画像にすべてのポストプロセッシングエフェクトを適用しました。
パイプライン内でのアップサンプリング処理は以下のように呼び出されました。
ステップ 1:画質モードを選択した後、チームは NVIDIA の関数getOptimalsettings を使って、入力サイズと推奨されるシャープネスを取得しました。画質モードによって、スケーリングが異なります。
ステップ 2:NVIDIA CreateFeature インターフェースを使って各カメラの DLSS 機能を用意し、それを使って選択された品質モード、レンダリング出力のターゲットサイズ、および追加のシャープネスパラメーターを設定しました。追加のシャープネスをかけることで、DLSS でより精細な出力を実現できるようにしています。
ステップ 3:ポストプロセッシングの前に、DLSS 推論の実行関数を呼び出しました。これには以下の入力が必要となります。
commandBuffer.ApplyDLSS(m_DLSSArguments);
DLSS への入力に互換性を持たせるため、パイプラインに若干の修正を加えました。
何と言っても、低解像度の入力ではオブジェクトを大規模にレンダリングすることが求められます。そのため、正しくパスを進めるためには、ビューポートを gbuffer で設定する必要があります。レンダリングターゲットは、正しくスケーリングされたサイズで作成する必要があります。
pixelRect = new Rect(0.0f, 0.0f, Mathf.CeilToInt(renderingData.cameraData.pixelWidth * viewportScale), Mathf.CeilToInt(renderingData.cameraData.pixelHeight * viewportScale)); commandBuffer.SetViewport(pixelRect); // RenderScale 対応
レンダリングが完了すると、DLSS の引数には、入力画像のサイズ、出力画像のサイズ、入力カラーレンダリングターゲット、デプスターゲットなどの低解像度画像関連のパラメーターが入力されます。
int scaledWidth = UpSamplingTools.Instance.GetRTScaleInt(cameraData.pixelWidth); int scaledHeight = UpSamplingTools.Instance.GetRTScaleInt(cameraData.pixelHeight);
// 引数の設定 m_DLSSArguments.SrcRect.width = scaledWidth; m_DLSSArguments.SrcRect.height = scaledHeight;
m_DLSSArguments.DestRect.width = cameraData.pixelWidth; m_DLSSArguments.DestRect.height = cameraData.pixelHeight;
m_DLSSArguments.InputColor = sourceHandle.rt; m_DLSSArguments.InputDepth = depthHandle.rt;
次に、ジッターオフセットの入力について説明します。ジッターオフセットは、フレーム間でのサンプル蓄積数を増やすことに関連しています。
プリミティブのラスタライズは、三角形で覆われた領域に基づいてピクセルがシェーディングされるだけであれば、かなり離散的なものになります。その結果、ギザギザとした不自然な外観になり、エッジも滑らかにはなりません。
しかし、解像度を上げれば、より精細で自然な仕上がりになります。しかし、不十分な離散的なサンプルしかない場合、連続的な信号を再構成することがいかに難しいかということも注目すべき点です。これはエイリアシングの原因にもなります。
出力結果が 4K でいいのであれば、単純に解像度を上げればエイリアシングは軽減されます。8K 解像度が必要な場合、レンダリングは 4 倍遅くなり、8K テクスチャのバンド幅(メモリ使用量)の問題も発生しやすくなります。
エイリアシングを解決するもう 1 つの方法は、GPU ハードウェアでサポートされているマルチサンプルアンチエイリアシング(通称 MSAA)です。MSAA は、画素の中央のサンプルのみをチェックするのではなく、各サブピクセルの位置にある複数のサンプルもチェックします。三角形のフラグメントの色は、プリミティブで覆われたピクセル内のサンプル数に応じて、エッジがより滑らかになるように調整できます。
テンポラルアンチエイリアシング(TAA)も、複数のフレームにわたってサンプルを蓄積する手法です。この方法では、フレームごとに異なるジッターを加えてサンプリング位置を変えています。そして、モーションベクトルを利用して、フレーム間の色の計算結果をブレンドします。
現在のフレームの各ピクセルの過去の色が分かれば、その情報をその先に行う描画に使うことができます。
ジッターとは、一般的には、アンダーサンプリングの問題を一度に解決しようとするのではなく、フレームをまたいでサンプルを蓄積できるように、ピクセル内のサンプリング位置をわずかに調整することを意味します。
そこで 24 Entertainment 社が採用したのが DLSS で、全体のレンダリング解像度を下げて今まで述べた問題を解決しただけでなく、望み通りの滑らかなエッジを持つ高品質な結果を得ることができました。
ここでは、推奨されるサンプルパターンは、ハルトン列で構成されています。ハルトン列はランダムに見えますが、より均等に空間をカバーする不一致の少ない数列です。
実際には、ジッターオフセットの適用は、かなり直感的に行うことができます。効率的に作業を進めるためには、以下の手順に従うようにします。
ステップ 1:特定のカメラのハルトン列から、さまざまな設定に応じてサンプルを生成します。出力ジッターは -0.5 ~ 0.5 の間であることが望ましいです。
Vector2 temporalJitter = m_HalotonSampler.Get(m_TemporalJitterIndex, samplesCount);
ステップ 2:ジッターを Vector4 に格納します。次に、ジッターを 2 倍し、スケールされた解像度で割ることで、ジッターをピクセル単位のスクリーン空間の値に変換します。これらを zw 要素に格納します。
この 2 つは、後で投影行列の修正に使用され、レンダリング結果全体に影響を与えます。
m_TemporalJitter = new Vector4(temporalJitter.x, temporalJitter.y, temporalJitter.x * 2.0f / UpSamplingTools.GetRTScalePixels(cameraData.pixelWidth), temporalJitter.y * 2.0f / UpSamplingTools.GetRTScalePixels(cameraData.pixelHeight) );
ステップ 3:ビュー投影行列をグローバルプロパティ UNITY_MATRIX_VP に設定します。こうすれば、頂点シェーダーが同じ関数を呼び出して画面上のワールド空間での位置を変換しているため、シェーダーが何の変更もなくスムーズに動作するようになります。
var projectionMatrix = cameraData.camera.nonJitteredProjectionMatrix; projectionMatrix.m02 += m_TemporalJitter.z; projectionMatrix.m12 += m_TemporalJitter.w;
projectionMatrix = GL.GetGPUProjectionMatrix(projectionMatrix, true); var jitteredVP = projectionMatrix * cameraData.viewMatrix;
ジッターオフセットの入力が解決されたので、モーションベクトルツールを使ってモーションベクトルを生成することができます。
カメラと動いている物体の位置関係が変わると、画面の見え方も変わることがあります。そのため、複数のフレームに渡ってサンプルを蓄積することが目的の出力の場合、前のスクリーン空間での位置を特定する必要があります。
図のようにカメラの位置が q から p に変わると、ワールド空間の同じ点がまったく違うスクリーン上の点に投影されてしまうことがあります。これらを差し引いた結果がモーションベクトルで、画面上の現フレームに対する前フレームの相対的な位置を示します。
モーションベクトルを算出するための手順は以下の通りです。
ステップ 1:パイプラインの深度のみのパスで、動いている物体のモーションベクトルを計算する。深度のみのパスは、動いている物体の深度を導き出し、深度テストに役立てるために使用されます。
ステップ 2:カメラの動きに合わせて空のピクセルを埋めます。カメラの前フレームのビュー射影行列を使ってワールド空間での位置を変換し、前のスクリーン空間での位置を得ます。
ステップ 3:DLSS の引数にモーションベクトルに関連するプロパティを詰める。
モーションベクトルのスケーリングを使うと、目的に応じて様々な忠実度のモーションベクトルを作成することができます。この例では、24 Entertainment 社は、モーションベクトルをスクリーン空間で生成したため、幅と高さのスケールをマイナスに設定していますが、DLSS ではピクセル単位で設定する必要があります。
ここで負の符号が使われているのは、実装されているパイプラインでは、減算時に現在のフレームと前のフレームの位置が入れ替わるためです。
m_DLSSArguments.MotionVectorScale = new Vector2(-scaledWidth, -scaledHeight); m_DLSSArguments.InputMotionVectors = motionHandle.rt;
24 Entertainment 社のレンダリングパイプラインは、ディファードフレームワークとフォワードフレームワークを組み合わせたものであることに注意しましょう。メモリを節約するために、すべてのレンダリングターゲットはスケーリングなしで割り当てられます。
DLSS マネージャーは RTHandle システムを使って、カメラの作成時にスケールされたレンダリングターゲットを一度だけ割り当てることで、カメラループごとに割り当てが行われないようにしています。
時間的な効果を得るためには、深度のみのパスにモーションベクトルを生成します(動いている物体のみモーションベクトルを計算する必要があります)。次に、フルスクリーンパスを使って、カメラのモーションベクトルを生成します。
DLSS マネージャーは、RTHandle システムをサポートし、ポストプロセッシングの最初の段階でスケールされたレンダーターゲットを割り当てます。
ここで、DLSS 評価ステップでは、ジッターを除いて、DLSS 引数に必要なすべての情報を得ることができます。
時間的なマルチサンプリングを可能にするために、ジッターを加えます。
『Naraka:Bladepoint』に使われた 24 Entertainment 社のパイプラインにはハルトンパターンの位相指数や行列のジッターの有無など、カメラの情報をキャッシュするシステムがありました。このチームは、モーションベクトルパスを除くすべてのラスタライズ工程で、ビュー投影とジッター行列を活用しました。モーションベクトルパスでは、ノンジッター行列を使用しました。
ミップマップとは、あらかじめ計算された一連の画像のことで、それぞれの画像はその前のレベルの半分の解像度となっています。これを使ってテクスチャを効率的にダウンフィルタリングし、スクリーンのピクセルに寄与する元のテクスチャのすべてのテクセルをサンプリングします。
このレンダリング例では、カメラから遠いオブジェクトは、ミップマップで低解像度のテクスチャをサンプリングすることで、よりきれいな結果を得ることができます。
テクスチャをサンプリングする際には、必ずミップマップバイアスを追加してください。DLSS はチェッカーボードレンダリングなど他の類似アルゴリズムと同様にアップサンプリング方式であるため、低解像度のビューポートレンダリングではテクスチャの解像度を上げる必要があります。そのため、高解像度で再構成してもテクスチャがぼやけることはありません。
このようにして、ミップマップバイアスを算出することができます。
MipLevelBias = log2(RenderResolution.x / DisplayResolution.x)
出力がマイナスの場合は、レベルにバイアスをかけて解像度の高い画像に戻すことができます。ミップマップバイアスを設定した場合のみ、低解像度のテクスチャをサンプリングしても DLSS の画質は変わりません。
DLSS 機能はカメラ用にキャッシュしておくべきです。
カメラの変更に伴うサイズや画質モードの変化を考慮して、まったく新しい機能を作らなければなりません。場合によっては、同じサイズ、同じ品質モードの複数のカメラが同時にレンダリングされることもあります。各機能は前のフレームの情報を利用するため、他のカメラでは利用できません。
『Naraka: Bladepoint』では、24 Entertainment 社のチームが、各カメラのハッシュ記述子をキーにして、機能を辞書にキャッシュし、すべての機能が 1 つのカメラでしか使用されないようにしました。
commandBuffer.ApplyDLSS(m_DLSSArguments, cameraDescriptor);
最後に、DLSS と他のアンチエイリアシング手法を併用しないことに注意してください。複数の手法を混ぜて使うと、予測せぬアーティファクトが発生します。
NVIDIA との緊密な技術提携により、ここまでご紹介してきた DLSS をはじめとする機能を Unity と HDRP に統合することができました。今後はこれをさらに推し進めていきたいと思います。
この統合は、Unity 2021.2 から利用可能で、他の Unity コア機能と同様にメンテナンスされています。ここでは、NVIDIA の技術がどのようにして Unity に統合されたのか、その詳細をご紹介します。
コアに DLSS を組み込む
コアの深い部分では、DLSS モジュールのために適切な C# スクリプティング API レイヤーを作成しました。このレイヤーは、プラットフォームの互換性、エンジン内#defineのサポート(プラットフォームからDLSS固有のコードを除外することができる)、適切なドキュメント も扱います。SRP で DLSS を補完するための公式 API です。ここからスタートして、任意のスクリプタブルレンダーパイプラインに統合します。
HDRP におけるグラフィックスの統合
DLSS は、上述のスクリプティング API を使用する HDRP の Dynamic Resolution System (DRS)機能として実装しました。
『Naraka: Bladepoint』で採用した TAA のジッタリングアルゴリズムと同じものをベースとして、同じようにポストプロセッシング前の解像度アップサンプリングを行い、ポストプロセッシングはフル解像度でレンダリングする実装になっています。
改善点の 1 つは、Unity の DLSS の実装は Unity の DRS をベースに構築されているため、リアルタイムで素早く解像度を切り替えることができることです。このシステムは、Unity の RTHandle システムと完全に統合されており、DLSS のようなハードウェアの動的解像度システム(テクスチャエイリアスベース)と、ソフトウェアの動的解像度システム(ビューポートベース)をサポートしています。先に述べたミップマップバイアス機能は Unity で開発されたものですが、他のすべての DRS フィルターや DRS 技術でも利用可能です。
パーティクルのゴーストを抑えるために、特別なパスを追加して、動的解像度システムが HDRP のすべての機能に対応できるようにしました。
また、DLSS のバージョニングやフレーム内の状態を完全に把握できる包括的なデバッグパネルを追加しました。
最後に、VR とリアルタイムレイトレーシングに加えて、DX11、DX12、Vulkan への対応を追加しました。
ここでは、皆さんのプロジェクトで DLSS を有効にする方法を説明します。
2. プロジェクトに必要なカメラで DLSS を有効にします。
詳細については、DLSS スクリプタブルサポートおよび HDRP での DLSS サポートに関するドキュメンテーションをご覧ください。