Search Unity

高度なステレオレンダリングで AR と VR のパフォーマンスを最大限に引き出す方法

, 11月 21, 2017

Unity 2017.2 で、DX11 で実行可能な XR デバイス用のステレオインスタンシングのサポートをリリースしました。このリリースにより、開発者は HTC Vive、Oculus Rift、そして最新の Windows Mixed Reality の没入型ヘッドセットについて、さらなるパフォーマンス最適化を行えるようになります。この記事では、この素晴らしいレンダリングの進化とその活用方法についてより詳細に解説したいと思います。

XR レンダリング技術小史

XR レンダリングにおいて、ユニークでかつ他のレンダリングと明らかに異なる点の 1 つは、それぞれの目にあわせて 1 つずつ、合計 2 つのビューを生成する必要があることです。XR を見る人に立体視による 3D エフェクトを見せるために、これらの 2 つのビューが必要になるのです。しかし、どのように 2 つの視点からのビューをレンダリングするかに深く入っていく前に、XR 以前の単一視点のビューを生成する場合について見てみましょう。

従来のレンダリング環境では、シーンを 1 つのビューからレンダリングします。
ここで、オブジェクトの座標をレンダリングに適した空間に変換します。これを行うには、オブジェクトに一連の変換を適用します。この変換で、ローカルで定義された空間からそれらをスクリーン上に描画できる空間にオブジェクトの座標を変換するのです。

古典的な変換パイプラインは、変換はオブジェクト自身の、ローカルなオブジェクト空間の座標系から始まります。その後、オブジェクトの座標をワールド空間のものに変換するために、モデル行列またはワールド行列でオブジェクトを変換します。ワールド空間は、オブジェクトの初期の相対的な配置のための共通の空間です。次に、ビュー行列を使って、オブジェクトをワールド空間からビュー空間に変換します。この変換が終わると、オブジェクトは視点から見えた位置に応じて配置された状態になります。そしてビュー空間への変換が終わると、射影行列を使って 2D スクリーンに射影し、クリップ空間にオブジェクトを配置することができます。続いて透視射影の分割が行われ、NDC(正規化されたデバイス座標)空間となり、最終的にビューポート変換が適用され、スクリーン空間の座標への変換されます。スクリーン空間への変換まで終わると、レンダーターゲットのフラグメントを生成することができます。ここでは、レンダーターゲットを 1 つだけレンダリングします。

この一連の変換は「グラフィックス変換パイプライン」と呼ばれることもあり、レンダリングの古典的なテクニックです。

参考資料:

現在の XR レンダリングの他に、同時に複数の視点を提示するシナリオも存在していました。ローカルマルチプレイヤーで、分割したスクリーンにそれぞれレンダリングするケースをご覧になったことがある方もいるでしょう。また、ゲーム内のマップやセキュリティカメラからの画像を表示するための小さなビューが配置されている例をご存知の方もいらっしゃるかもしれません。こうしたビューはシーンデータを共有しているかもしれませんが、多くの場合は最終的なレンダリングターゲットの他のデータはほとんど共有しません。

少なくとも、各ビューはしばしば明確にユニークなビューと射影行列を持っています。最終的なレンダリングターゲットを合成するために、グラフィックス変換パイプラインの他のプロパティも操作する必要があります。レンダリングターゲットが 1 つしかない初期の頃は、ビューポートを指定して、レンダリングするスクリーンに重ねてサブ画面となる矩形領域を指定して対応していました。GPU とそれに対応する API が進化するにつれて、別々のレンダーターゲットにレンダリングし、後でそれらを手動で合成することができるようになりました。

XR 固有の内容

現代の XR デバイスは、立体視による 3D エフェクトを使ってデバイスの着用者に奥行きを感じさせるために 2 つのビューを生成することが要件とされます。1 つのビューが 1 つの目に対応したビューとなります。 2 つの目で同じシーンをほぼ同じ視点から見ている一方で、各ビューはそれぞれに固有のビュー射影行列のセットを持っています。

ここから先に進む前に、いくつかの用語を定義しておきましょう。レンダリングに関わるエンジニアはさまざまなエンジンやユースケースでさまざまな用語や定義を使う傾向があるため、ここで定義した用語が必ずしも業界で使われる標準用語ではないということにご注意ください。これらの用語は、あくまで本記事での解説の便宜を図るために使われます。

シーングラフ – シーングラフはシーンをレンダリングするために必要な情報を整理し、レンダラーによって利用されるデータ構造を指す用語です。シーングラフはシーン全体を参照することも、ビューに表示される部分のみを参照することも可能です。ある部分のみを参照するシーングラフは、カリングされたシーングラフと呼ばれます。

レンダーループ/パイプライン – レンダーループとは、レンダーされたフレームをどのように構成するかという論理的なアーキテクチャを指す用語です。高水準のレンダリングループは、例えば次のようになります。

カリング -> シャドウ -> 不透明部分の描画 -> 透明部分の描画 -> ポストプロセッシング -> 表示

私たちはディスプレイに表示する画像を生成するために毎フレームこれらのステージを実行することになります。また、レンダーパイプラインという用語は Unity でも使用されています。これはこれから私たちが公開する予定のレンダリング機能(例えばスクリプタブルレンダーパイプライン)に関連しています。レンダーパイプラインは、描画コマンドを処理する GPU パイプラインを指す「グラフィックスパイプライン」などの他の用語と混同されることがあります。

用語の定義はこのくらいにして、VR レンダリングに話題を戻しましょう。

マルチカメラ

各眼のビューをレンダリングするもっとも簡単な方法はレンダーループを 2 回実行することです。各側の目に対応して設定を行い、レンダリングループを実行するわけです。こうすれば表示デバイスに送信する 2 つの画像が得られます。それぞれの目に対応して 1 つずつ、合計 2 つの Unity カメラが使用され、ステレオ画像を生成する処理が行うという実装が基本になります。これは Unity の XR サポートで使った初期の方法であり、サードパーティのヘッドセットプラグインの一部ではいまだに使われている手法です。

この方法は確実に動作しますが、マルチカメラは力任せの方法であり、CPU と GPU に関する限りもっとも効率が悪いものです。CPU は、レンダリングループをまるごと 2 回実行する必要があり、GPU で各側の目に合わせて 2 度描画されたオブジェクトのキャッシュを活用できる可能性も低いでしょう。

マルチパス

マルチパスは、Unity が XR レンダーループを最適化する上で最初の試みた方法でした。根本的な考え方は、ビューに依存しないレンダーループの部分を抽出することでした。つまり、XR における目の視点に明示的に依存していない作業を、各側の目ごとに行う必要をなくそうとしたのです。

この最適化を行う上で真っ先に試す手法はシャドウレンダリングです。シャドウはカメラの視点に明示的に依存しません。実際、Unity は 2 つのステップでシャドウを実装します。カスケードシャドウマップを生成し、シャドウをスクリーン空間にマッピングします。マルチパスでは、1 組のカスケードシャドウマップを生成し、そこから 2 つのスクリーン空間シャドウマップを生成することができます。なぜなら、スクリーン空間シャドウマップは視点に依存しているからです。私たちのシャドウの生成方法は、シャドウマップの生成ループが比較的緊密に結合されているため、スクリーン空間のシャドウマップは局所性の恩恵を受けることができます。残りのレンダリングワークロードは上記と異なり、レンダリングループを完全に反復することが必要です(例えば、各側の目それぞれだけにある不透明部分を描画するパスは片目ごとに分離して残りのレンダーループのステージで処理されます)。

2 つの目に対応する画像のレンダリングで共有できるもう 1 つのステップは、シャドウレンダリングほど明白ではないかもしれません。それは 2 つの目に対応する画像に対して、カリングを 1 回だけ実行するようにすることです。初期の実装では、各側の目に対応して、オブジェクトのリストを合計 2 つ生成するために視錐台カリングを使用していました。しかし、私たちは、両目で共有可能なカリングのための視錐台の生成を可能にしました(Cass Everitt のこの投稿を参考にしてください)。この手法だと、各側の目に対応した視錐台カリングを行う場合に比較して片目ごとにレンダリングする量が少し増えるのですが、頂点シェーダーのコスト、クリッピング、ラスタライゼーションのコストを余計にかけるより、カリングを 1 回だけにするほうが利点が大きいと判断しました。

マルチパスの段階でマルチカメラと比較するとかなり処理を軽減することができたのですが、まだ改良の余地がありました。

シングルパス

シングルパスステレオレンダリングとは、レンダリングループ全体を、2 回でなく、1 回だけ実行(特定の部分についてのみ、2 回実行)することです。

両目に対応した描画を実行するには、インデックスと一緒にすべての定数データを確実にバインドする必要があります。

各側の目に対応した画像の描画自体はどのように行われるのでしょうか。マルチパスでは 2 つの目がそれぞれ独自のレンダーターゲットを持たせますが、連続したドローコールのレンダリングターゲットをそれぞれ有効、無効に切り替えるコストは非常に高くなるため、シングルパスではそのようなことはできません。似たような手段としてレンダーターゲット配列を使用することも考えられますが、ほとんどのプラットフォームでスライスのインデックスを頂点シェーダーからエクスポートする必要があります。これは、GPU に重い処理を要求することになり、また、既存のシェーダーに手を加える必要も出てきます。

私たちの取った解決策は、倍の幅を持つレンダーターゲットを使用し、ビューポートをドローコールの間で切り替えることでした。各側の目に対応した画像は倍の幅にしたレンダーターゲットのどちらか半分にレンダリングできます。ビューポートの切り替えにはコストがかかりますが、レンダーターゲットを切り替えるよりも少なく、頂点シェーダーを使用するよりも侵略的ではありません(レンダーターゲットの幅を倍にすることにも特有の課題があり、それは特にポストプロセスで顕著になります)。ビューポート配列を使用する選択肢もありますが、レンダーターゲット配列を使う場合と同じ問題があります。インデックスは頂点シェーダーからしかエクスポートできないからです。もう 1 つ、ダイナミッククリッピングを使用するテクニックがありますが、ここでは説明しません。

両目画像をレンダリングするために 2 つの連続した描画を行う解決策にたどり着いたので、それをサポートする仕組みの準備に移りましょう。マルチパスは、基本的には単視点のレンダリングと似ているため、ビュー行列と射影行列を基本とした仕組みを使用できます。ビュー行列と射影行列を描画対象の目に対応したものに置き換えるだけで済みました。しかし、シングルパスでは、定まったバッファーのバインディングを不必要に切り替えることは望ましくありません。代わりに、両目のビュー行列と射影行列をバインドし、さらに unity_StereoEyeIndex を使ってそれらのインデックスをバインドします。これは、描画の間でフリップすることができます。これにより、シェーダー側で、シェーダーパス内でレンダリングするために使うビュー行列と射影行列のセットを選択することができます。

補足ですが、ビューポートと unity_StereoEyeIndex のステートの変化を最小限に抑えるために、目のドローパターンを変更することができます。左、右、左、右などの描画ではなく、左、右、右、左、左などの順序にできます。この場合は、交互に左右を切り替える場合に比べてステートの更新回数を半減できます。

上記のテクニックを適用することで、正確にマルチパスの 2 倍の速度が出るわけではありません。これは、カリングとシャドウについてはマルチパスの段階ですでに最適化されており、また、各側の目に対する描画の実行とビューポートの切り替えに CPU や GPU のコストがかかるためです。

シングルパスステレオレンダリングに関する詳細は、対応する Unity マニュアルのページを参照してください。

ステレオインスタンシング(インスタンシングを利用したシングルパス)

ここまでで、レンダーターゲット配列を使用する可能性について述べました。レンダーターゲット配列は、ステレオレンダリングのための自然なソリューションです。各目に対応するテクスチャ間で形式とサイズを揃え、レンダーターゲットの配列で使用するようにします。しかし、配列スライスをエクスポートするために頂点シェーダーを使用することは大きな欠点です。本当に必要なのは、頂点シェーダーからレンダーターゲットの配列インデックスをエクスポートする機能です。これにより、より簡単なインテグレーションとパフォーマンスの向上が可能になります。

頂点シェーダーからレンダーターゲット配列のインデックスをエクスポートする機能は、実際には一部の GPU と API で提供されており、より一般的になりつつあります。 DX11 では、この機能は VPAndRTArrayIndexFromAnyShaderFeedingRasterizer の機能オプションとして公開されています。

描画するレンダーターゲット配列のスライスを決めることができるようになったので、あとはどのようにスライスを選択するかが問題です。ここでは、これまでに論じたシングルパスのレンダーターゲットを倍の幅にする仕組みを活用します。unity_StereoEyeIndex を使用して、シェーダー内の SV_RenderTargetArrayIndex セマンティクスを設定することができます。API 側では、レンダーターゲット配列の両方のスライスに同じビューポートを使用できるので、ビューポートを切り替える必要はありません。また、頂点シェーダーからインデックスを作成するように行列を設定しています。

2 つのドローコールを発行し、各々の描画を行う前に定数バッファーの unity_StereoEyeIndex の値を切り替えるという既存のテクニックを引き続き使用することもできますが、より効率的なテクニックがあります。それは GPU インスタンシングを使用して、1 回のドローコールを発行し、GPU が両目をカバーするように描画を多重化することです。既存のインスタンス数を 2 倍にすることは容易です(インスタンスの使用がない場合はインスタンス数を 2 に設定するだけです)。頂点シェーダーでは、インスタンス ID をデコードしてどちらの目に向けてレンダリングするかを決定することができます。

このテクニックの最大の効果は、API 側で生成するドローコールの数を文字通り半減し、CPU 時間を節約できることです。さらに GPU も 2 つの個別のドローコールを処理する必要がないため、同じ量の作業が生成されている場合でも、ドローをより効率的に処理することができます。また、従来のシングルパスのように、描画の間にビューポートを変更する必要がないため、ステートの更新を最小限に抑えられます。

注:このテクニックは本記事の執筆時点で Windows 10 または HoloLens でデスクトップ VR 体験を実行しているユーザーだけが利用可能です。

シングルパスマルチビュー

マルチビューは、ドライバー自身が両目をカバーするように個々のドローコールの多重化を処理できる特定の OpenGL/OpenGL ES 実装で利用可能な拡張機能です。ドローコールを明示的にインスタンス化し、シェーダー内の目のインデックスにインスタンスをデコードする代わりに、ドライバーは描画を複製し、シェーダーで(gl_ViewIDを介して)配列インデックスを生成する必要があります。

ステレオインスタンシングとは異なる基本的な実装の詳細が 1 つあります。ラスタライズされるレンダーターゲットの配列スライスを明示的に選択する代わりに、ドライバー自身がレンダーターゲットを決定します。gl_ViewID は、ビュー依存のステートを計算するために使用されますが、レンダーターゲットを選択するためには使用されません。実際の使用においては、これは開発者にとって大きな問題ではありませんが、興味深い部分ではあります。

ここまで述べたようなマルチビュー拡張の使い方をすれば、シングルパスインスタンシングのために構築したのと同じインフラストラクチャを使用できます。開発者は両方のシングルパス技術をサポートするために同じ土台を使用することができます。

高レベルパフォーマンスの概要

Unite Austin 2017 で、XR グラフィックスチームが XR グラフィックスのインフラストラクチャについて紹介し、さまざまなステレオレンダリングモードのパフォーマンスへの影響について簡潔に説明しました(講演動画はこちら)。正確なパフォーマンス分析については、また別のブログ記事で今後説明されると思われます。ここでは、下のグラフを示すのみに留めます。

グラフから読み取れるとおり、シングルパスとシングルパスインスタンシングは、マルチパスよりも CPU 負荷の面で大きく改善しています。ただし、シングルパスとシングルパスインスタンシングの差はそれほど大きくありません。その理由は、シングルパスに切り替えることで CPU のオーバーヘッドの大部分がすでに削減されているからと考えられます。シングルパスインスタンシングはドローコールの数を削減しますが、そのコストはシーングラフの処理に比べてかなり低いです。また、最近のグラフィックスドライバーがほとんどマルチスレッド化されていることを考慮すると、ドローコールの発行は CPU スレッドのディスパッチでかなり高速にできると考えられます。

10 replies on “高度なステレオレンダリングで AR と VR のパフォーマンスを最大限に引き出す方法”

Hi, great article! Are these advantages only applicable to stereo view rendering? Or will there be some way to utilize these features for super-multi-view rendering (32 views, for example). Thanks!

That’s very valuable informations. Single-pass instancing gives me impressive boost in performances over single-pass (despite the graph), meaning my computer is CPU-bound.

Thanks for the post, but the images seem off a bit – graphics for multi-pass and single-pass seem to be the same and the performance comparison info-graphic at the end is confusing (visually it looks like cpu is cut in half between MP and SP but the numbers say it’s around a 25% boost)

I had tried it and verified these kind of performance gains! Keep up the good work guys. It’d be lovely to see you guys automatically integrating into Nvidia’s (and I hope soon to come, AMD’s) native API’s along with facebooks, apis for getting even further performance benefits, with less faffing about. Great stuff – thanks!

Comments are closed.