だらけ者だらけ

だらけ者だらけの遊び場

Stationary Light の影について

この記事はUnreal Engine 4 (UE4) 其の弐 Advent Calendar 2015の16日の記事です。昨日はじゅる (@xxJulexx) | Twitterさんの映像クリエータ視点な使い方とか。でした。そもそもコンポジットをオフラインですることを前提に、BasePassと一部のポストエフェクトをそのまま出力できる機能があれば、UE4の映像制作がもっと捗りそうですね。

今回はライティングの細かい話をしようと思います。なんとなく設定している人が多い、ステーショナリーライトの仕組みです。カタカナと英語が乱立しますが、気が向いたら直させてください。

UE4のライティングは様々な組み合わせに応じて内部実装が変わります

f:id:tempkinder:20151215020601p:plain
高クオリティなライティングを出しつつも一定のパフォーマンスを出したい場合などでは、作成したライティング環境が内部でどんな計算をしているかを把握する必要があります。
今回は、不透明(Maskedも含む)オブジェクトに対して、Stationary Lightの影がどんな動作をするかを簡単に説明していきたいと思います。
話はStaticなMeshかMovableなMeshかで二分されます。



Static Meshの影について

Stationary Lightはなんで5個以上重ねちゃいけない?

ステーショナリーライトの陰ですが、公式サイトではこのように述べられています。

ライトマス は Stationary light (固定ライト) に対し ディスタンス フィールド シャドウマップ を生成します。
...
ライトは、シャドウマップ テクスチャの異なるチャンネルへの割り当てが必要なため、 4 つまたはそれ以下のオーバーラップした Stationary light (固定ライト) のみが静的シャドウを表現することができます。
Unreal Engine | Stationary lights (固定ライト)

どれか一個のオブジェクトに5個以上のステーショナリーライトが重なると、そのライトの中の4個はステーショナリーライト用のシャドウマップでレンダリングできますが、残りのライトはシーン全体を動的にシャドウ計算することになります。5個以上ステーショナリーライトが重なると、UE4は自動的に、どのライトを動的シャドウ計算にするか決めます。下の図で×印がついているのが動的計算とみなされたライトです。残念ながらどのライトを仲間外れにするかを自分で指示できなそうです。
f:id:tempkinder:20151214010733p:plain

ここまではみなさんご存知かとお思います。ですがこれ、なんでなんでしょう??



ステーショナリーライトのシャドウマップを持つのはライトではなく、(スタティックな)メッシュです。

ステーショナリーライトのシャドウマップは、シーンに置かれた各スタティックメッシュが持ちます。スタティックライトがライトマップにベイクされるのと同様です。この消費データは、Statを見ると NumSM / TextureSMといった項目で確認できます。

f:id:tempkinder:20151214011958p:plain

NUM SMが5と書いてあるのは、重なっているライトもカウントされているからですが、TextureSMのサイズを見ると、4個のマップを持っているものとデータ量が変わらないのがわかるかと思います。実は、ステーショナリーライトは自身にライトのインデックス番号を1つ(0~3の値)持ち、そのインデックスに対応したシャドウマップに自身の影を書き込むのです。このインデックスはUE4が勝手に内部で決めます。
f:id:tempkinder:20151214020309p:plain
どのライトが仲間外れになるのか? 気になる方はULightComponent::ReassignStationaryLightChannels関数の中身を参考にすると良いと思います。


Stationary LightのShadow Mapは、G-Bufferを経由してライティング計算時に参照されます

f:id:tempkinder:20151214022049p:plain
上図のように、スタティックメッシュに焼かれたシャドウマップはG-Bufferに埋め込まれて、ライティング計算に送られます。ステーショナリーライトのための計算がG-Bufferの前後にあるのは非効率に見えるかもしれません。ですが、ランタイムでのデプス比較を用いたシャドウ計算がないため、高速かつ高精細な影がステーショナリーライトで実現できているのです。
これが、スタティックなメッシュがスタティックなメッシュによって受けるステーショナリーライトの影の計算です。つまり、動的オブジェクトが関係する場合、もちろん挙動は変わります。



Movable Meshの影について

Movable ObjectがStationary Lightに及ぼす影

次はMovableなメッシュがStationary Lightが及ぼす影についてです。上記でだらだら話していたシャドウマップのことはもう全く関係ありません。忘れてください。話を変えて、以下のようなポイントライト一個のシーンを考えます。周りに浮いているのはすべてMovableに設定されたオブジェクトです。(注: Directional LightはCascade Shadowが使わるので今回はポイントライトやスポットライトに限定します。後で説明します。)
f:id:tempkinder:20151215013519p:plain

このとき、Stationary Lightは、MovableなMesh毎にシャドウマップをつくります。実際にシャドウマップをキャプチャすると以下のようなテクスチャです。
f:id:tempkinder:20151215013811j:plain
左上に小さくオブジェクトが描画されているのがわかるでしょうか?巨大なシャドウマップ一枚をグリッド上に分割し、各オブジェクトが及ぼすシャドウマップを作っています。

その後、全てのMesh毎に、Shadowの影響をレンダリングをします。実際に一つのオブジェクトに対してライティングされた結果を見ると、以下のような黄色い部分が、1つのあるオブジェクトが関与する影部分の描画です。(描画範囲はShadow Frustumのステンシルテストで指定しています。)
f:id:tempkinder:20151215014324j:plain

つまり、Movable Meshに於けるStationary Lightの影のコストは以下になります。

Movable MeshにおけるStationary Lightの影のコスト =
ライトの影響下にあるMovable Meshの数 * 一つ一つのシャドウ生成及び描画コスト



Stationary LightのShadowは、Movable Mesh毎に計算される

(Directional LightはCascade Shadow MapやDF Shadowによりこの制限を受けません!Directional Lightの挙動はUE4 Docを参照してください。)

さて、先ほどのように、Stationary LightのShadowは、MovableなMeshに対して一個ずつShadow Mapが作成され、レンダリングも一個ずつ行われると言いました。その状況で例えば、ライトの影響範囲内に沢山Movable Meshをおいてみます。
f:id:tempkinder:20151215014837p:plain

これだけ置いた後のShadowMapは以下のように、ShadowMapで描画されるオブジェクトが増えているのがわかります。
f:id:tempkinder:20151215014934j:plain

そして、そのあとこの大量のオブジェクト1つ1つに対して影の影響を計算します。
f:id:tempkinder:20151215015420j:plain

実際にProfileGPUで見てみると、ShadowMap作成フェイズのShadowDepthsFromOpaqueProjectedで大量のメッシュ毎のシャドウマップが作成されているのがわかります。上図の赤いシャドウマップを作成しているフェイズですね。
f:id:tempkinder:20151215015524p:plain

そのあと、実際の影の影響を計算するフェイズのShadowProjectionOnOpaqueを見てみると、各メッシュに対して、実際のシャドウを計算しているのがわかります。
f:id:tempkinder:20151215015834p:plain

これがStationary Lightの影響範囲内にMovableなMeshを大量に置くと重たい理由です。

Stationary Lightは、Movable Mesh毎に別々に影の計算をする。別々に影の計算をするため、Movable Meshが増えれば増えるほどコストの増大が激しい。この一方、LightをMovableにすると、ShadowはMesh単位ではなく、ライト一個に対してまとめて計算されます。
なので、Stationary Lightの処理負荷はMovableなMeshの数に依存してしまうので、このように動的なオブジェクトに対する影響が多いライトはMovableにしたほうが影のコストが下がることがあるのです。

確認のため、試しに、ライトをMovableにしてみます。
f:id:tempkinder:20151215133423p:plain

そうしてプロファイリングすると、影描画がWholeSceneという一つにまとめられて、合計の描画コストも下がっているのがわかります。シーン全体で一つのシャドウマップを作成し一回でレンダリングするMovableのライトの方が、MovableなMesh毎に影を描画するStationary Lightの影よりも処理負荷が軽くなる良い例かと思います。
f:id:tempkinder:20151215134845j:plain



Directiona Lightの影について

f:id:tempkinder:20151215132442p:plain
。説明の通り、ライトの影響下にあるMovableなMeshはそのまま影生成のコストにつながります。今までの説明はPoint Lightで行いましたが、Directional Lightは基本的にシーン全体に影響を及ぼすのでこれでは問題です。そのためもあり、Directional Lightは基本的に、影描画をCascade Shadow Mapで行うようにしています。ですので、上記のシーンをプロファイルするとカスケードシャドウによってシーンが分割(Split)されて影描画されているのがわかります。
f:id:tempkinder:20151215132721j:plain

ですので、普段使いしている際Directional LightでMovableなメッシュによるコストだけが気になることは少ないと思います。一応Cascade Shadowの外のオブジェクトをポイントライトと同じ手法によって影描画するオプションがあります。Cascade Shadow Mapの設定のInset Shadows For Movable Objectsです。
f:id:tempkinder:20151215132839j:plain

こちらを設定し、Dynamic Shadow Distance Stationary Lightの距離を小さくして全てのオブジェクトをカスケードの外に出してみます。すると下記のように、オブジェクト毎に影描画用のフラスタムが作成されることが見て取れるかと思います。
f:id:tempkinder:20151215133221p:plain

プロファイルしても各オブジェクトのシャドウコストが発生しているのがわかります。
f:id:tempkinder:20151215133354j:plain



読んでいただきありがとうございます。

f:id:tempkinder:20140112120016j:plain

まとめると、Stationary Lightが作る影は。。。

  • StaticなMeshの場合、そのシャドウマップは(ライトマップと同様に)事前に計算され、各Meshが持ちます。レンダリング時はその値を読み込むだけで影の値がわかるので処理負荷が軽いです。
  • MovableなMeshの場合、そのシャドウはMesh毎に動的に計算されます。沢山のMovableなMeshが影響を受ける場合、Movable Lightにしたほうが処理が軽いことがあります。

明日は、ntaro@年末に向けて準備中 (@tarotarokun) | Twitterさんの「C++を使ったSocketを使った通信周りの書き方を解説するよ」です。
通信周りの情報がどんどん充実してきてて非常にありがたいです!楽しみです!