だらけ者だらけ

だらけ者だらけの遊び場

UE4のOcclusion Cullingで良く聞かれる質問2 Occlusion Cullingによりオブジェクトが1フレーム消失することがある

この記事は UE4 Advent Calender 2018 その2 の4日目の記事にあたります。

今回のアドカレではOcclusion Cullingについて説明していますが、長くなったため2つに分割しました。こちらはその後半になります。

  1. 前半: Occlusion Culling自身の処理負荷を減らしたい
  2. 後半: Occlusion Cullingによりオブジェクトが1フレーム消失することがある(本記事)

前半でもお伝えしましたが、Occlusion Cullingがどのような仕組みかご存じない方は、まずオフィシャルドキュメントVisibility and Occlusion Cullingを参考にしてみたください。以下の説明では上記ドキュメントを読んだという前提で話を進めてまいります。

Occlusion Cullingによりオブジェクトが1フレーム消えて
パカツク

タイトルの通りなのですが、Occlusion Cullingが原因でオブジェクトが1フレーム消えてパカツいてしまうと、毎月各所からお問い合わせを頂くのですが、そのいずれも以下の二つのどちらかの問題となっていました。

  1. カットシーンなどカメラ切替時にオブジェクトの表示が1フレーム遅れる
  2. 新しくカメラに入ってきたオブジェクトの表示が1フレーム遅れる

これらの根本の原因は、オクルージョンカリングが参照するデプスが1フレーム前のものだからなのですが、今回は原因の詳細は追わず、修正方法をお伝えしていこうと思います。

原因1. カメラ切替時にオブジェクトの表示が1フレーム遅れる

例えばシーケンサーでカメラのカットが切り替わる以下の様なシーンを考えます。壁のアップから広い場面にカットが切り替わった際、キューブが1フレーム遅れて出現するのがわかるかと思います。

f:id:tempkinder:20181202233751g:plain
カメラのカット切替時にパカつく

こちらを修正したのが下のものです。上と比べるとパカ付きがなくなっているのがわかるかと思います。(モーションブラーでわかりにくくなってしまっていてごめんなさい!!)

f:id:tempkinder:20181202233833g:plain
カメラのカット切替時にパカつかない

それでは、具体的な直し方について説明します。

対策1: カメラのカットを切り替える

シーケンサーでカットを分割せずに一つのカメラを1フレームでテレポートさせたりすると、この様にパカついてしまう可能性があります。

f:id:tempkinder:20181202233944p:plain
一つのカメラを1フレームでテレポートさせる例(パカつく)

UE4では、カットが切り替わる瞬間は自動的にOcclusion CullingをOffにします。なので、明示的にカメラのカットを切り替える事でカットをなくすことができます。

f:id:tempkinder:20181202234054p:plain
カットを別々につくりそれらをつなげている(パカつかない)

対策2: カメラが大きく移動したとき、
自動的にOcclusion CullingがOffになるようにする

どうしても同一のカメラでやりたい場合、または単純にカメラの移動量が多くてこの様なパカつきが起きてしまう場合、以下のパラメータが有益かもしれません。この2つの閾値よりもカメラが1フレームで移動した場合、自動的にその瞬間のフレームのOcclusion CullingをOffとしてくれます。

  • CameraRotationThreshold (Default 45.0)
  • CameraTranslationThreshold (Default 1000)

※この2つもCVar化されていないので、動的変更はできませんが、CVar化しても問題はないかと思います。試して頂ければ。

これでも、うまくいかない場合、r.AllowOcclusionQueriesのOn/Offを手動で切り替えるしか道はないかもしれません。

原因2. 画面端から入ってきたオブジェクトがパカつく

以下の様に画面端のオブジェクトがカメラに入ってくる際に、パカパカと1フレーム遅れて出現してしまうことがあります。

f:id:tempkinder:20181202235716g:plain
画面端からオブジェクトが視野に入る際にパカつく
これは本来Occlusion Cullingされないはずのオブジェクトが誤って遮蔽されたと判定されてCullingされてしまうために起こるのですが、こちらもUE4の設定を変更することで修正することができます。
f:id:tempkinder:20181203000157g:plain
画面端からオブジェクトが視野に入ってもパカつかない
こちらの修正方法を見ていきたいと思います。

対策1: 画面に入ってきた瞬間のみオブジェクトのバウンスを一時的に大きくする

UE4は、画面視野の外から中に入ってきたオブジェクトに対して、その入った瞬間のみOcclusionのバウンスを拡大するオプションがあります。

r.ExpandNewlyOcclusionTestedBBoxesAmount (Default =0.0f)

このパラメータはDefaultで0のため無効なのですが、この値を上げることで瞬間的にオクルージョンカリング用のバウンスを大きくしてくれます。このバウンスはフラスタムカリングなどで使われるバウンスとは別で、あくまでオクルージョンカリング時にしか使わないバウンスなので他に影響を与えません。

試しに、ExpandNewlyOcclusionTestedBBoxesAmountを100(1m)に設定し、Occlusion Cullingで使われるバウンスを視覚化してみました。カメラから入ったオブジェクトのバウンスが大きくなっているのがわかるかと思います。(大きくなっているのがわかりやすい様に、拡大する時間を指定するr.FramesToExpandNewlyOcclusionTestedBBoxesも上げています)

f:id:tempkinder:20181203000438g:plain
オクルージョン用バウンスの視覚化
(エンジンに手を入れています。)

このパラメータを上げることにより、誤ってCullingされる可能性を軽減することができます。

おまけ: 常にOcclusion のバウンスを大きくする方法

上記設定は、オブジェクトがカメラに入ってきたタイミングのみBoundsを拡大するものですが、常に拡大させるオプションもあります。

  • ソースコード内部、OCCLUSION_SLOPの値を大きくする
  • r.ExpandAllOcclusionTestedBBoxesAmountの値を大きくする

もしも、前のオプションで効果がうまく出ない場合、最終手段としてこちらを設定するのもありかと思います。実際、処理負荷を注意深く確認して問題ないことがわかったので、このOCCLUSION_SLOPの値を500などに設定してリリースしたタイトルがあると伺っています。


以上です。Occlusion Cullingでパカついていそうな場合、上記を試して見ていだければ幸いです。
ありがとうございました!

UE4のOcclusion Cullingで良く聞かれる質問1: Occlusion Culling自身の処理負荷を減らしたい

この記事は UE4 Advent Calender 2018 その2 の4日目の記事にあたります。

Occlusion Cullingは、沢山のオブジェクトがシーンに存在するときなどに効いてくる、レンダリング関連の最適化手法の一つです。デフォルトでONなので皆さん気づかないうちに使っているのですが、どのような仕組みかご存じない方はまずオフィシャルドキュメントVisibility and Occlusion Cullingを参考にしてみてください。ちょうどUE4.21から、Epic GamesのTim Hobsonさんがドキュメントをアップデートし、Occlusion Cullingも含めたUE4のCullingシステムを非常に詳細に説明してくれてます。ナイスタイミング。以下の説明では上記ドキュメントを読んだという前提で話を進めてまいります。

便利なOcclusion Cullingなのですが、ほぼ毎月、類似の質問を各所からいただきます。なので今回はそれらをまとめてシェアしたいと思います。少々長いので記事は2つに分けさせてください。

  1. Occlusion Culling自身の処理負荷を減らしたい (本記事)
  2. Occlusion Cullingによりオブジェクトが1フレーム消失することがある

では早速、Occlusion Cullingの処理負荷の削減についてです。

Occlusion Cullingの処理負荷が気になったら。。。
まずその前に。。。

Occlusion Culling自身のコストを減らす前に、そもそもOcclusion Culllingの前段階で省けるオブジェクトは省いてしまうのが最強です。その省き方について「UNREAL FEST EAST 2018」における株式会社ユークス様のご講演で非常に詳しく説明されています。シーンにオブジェクトが多いゲームではマストな内容なので是非!!!ユークス様貴重な情報共有誠にありがとうございます!!

www.slideshare.net
それでもまだOcclusion Cullingのコストが高い場合、中身を調整する必要が出てくるかもしれません。まずはおさらいとしてOcclusion Cullingの利点と欠点について見ていきます。

Occlusion Cullingの利点

以下の左図の様なシーンを考えます。

f:id:tempkinder:20181202205018p:plainf:id:tempkinder:20181202205010p:plain
沢山のキューブがあり、中央のキューブ群は白いシリンダーで見えなくなっています。つまり、右図の様に、シリンダーに完璧に隠れているキューブたちは描画しても1Pixelも貢献しないので、描画から省いても問題ありません。この省いて良いオブジェクトを選び出すのがOcclusion Cullingです。

Occlusion Cullingの利点

見えないオブジェクトに対しての処理負荷がCPU/GPU共に軽減する

CPU
不要なドローコールが減る
GPU
不要な描画が減る

Occlusion Cullingはタダではない

しかしこんなOcclusion Cullingにもコストはあります。Occlusion Cullingに投入されるオブジェクトは「他のオブジェクトに遮蔽されているかどうか」のチェックが必要です。このチェックにももちろんコストがかかります。つまり、下図の画面に見えているキューブ達には、余計にそのチェックのコストがかかっているということになります。

f:id:tempkinder:20181202215019p:plain:w256

Occlusion Cullingの欠点

見えるオブジェクトに対しての処理負荷がCPU/GPU共に増加する

CPU
Occlusion Cullingへの投入や結果の確認処理が追加
GPU
実際のOcclusion Cullingのテストが追加

試しに、約6500オブジェクト全てがオクルージョンカリングに投入させるようにしてみました。

f:id:tempkinder:20181202220012p:plain:w480

OcclusionCullingのみの処理負荷を抽出してみると、某プラットフォームでは以下のような処理負荷になりました。

CPU6.6ms
GPU3.0ms
Occlusion Queries
(投入されたObject)
6561 Objects
(100%)
現実的ではないのがわかるかと思います。しかし、UE4のデフォルト動作ではこの様なことは起こりません。簡単なテクニックで大幅に処理負荷を削減しています。

前フレームで見えていたStaticオブジェクトは確率的にOcclusion Cullingに投入する

非常に大きな更新がない限り前フレームで見えていたオブジェクトは今のフレームでも見えているだろう。。。というコンセプトのもと、UE4はOcclusion Cullingの処理負荷を軽減するために”前フレームで見えていたStaticオブジェクト”は確率的にオクルージョンカリングに投入します。

SceneVisibility.cpp内部 FetchVisibilityForPrimitives_Range()関数にて。。。

bRunQuery = (FractionMultiplier * Rnd) < GEngine->MaxOcclusionPixelsFraction;

この投入の確率を制御するパラメータがMaxOcclusionPixelsFractionです。Engine.iniにて記載されておりDefaultでは0.1、つまり10%になっています。(※厳密には10%ではないのですが詳細はコードを参照してください。)
このデフォルト設定によりOcclusion Cullingのコストは前に上げた例で以下の様に変化しました。


MaxOcclusionPixelsFraction1.00.1
CPU6.6ms1.9ms
GPU3.0ms0.3ms
Occlusion Queries
(投入されたObject)
6561 Objects
(100%)
668 Objects
(10.18%)
CPUは確率判定などを全オブジェクトに対して行うので10%まで減ることはないですが、単純に判定するオブジェクト数が10分の1になったGPUは負荷も10分の1程度になりました。いずれにせよCPU/GPU共に劇的に負荷を減らしています。

最後に結論。。。いや注意点。

このブログの結論としましては

結論

もしもOcclusion CullingのCPU/GPU負荷が気になったら。。。MaxOcclusionPixelsFractionも気にしてみましょう!

ということなのですが、いくつか注意点がありますのでシェアします。

注意点: Occlusion Cullingはランダムで発生するので、実際にカリングされるタイミングもラグが発生します

f:id:tempkinder:20181202223213g:plain

オブジェクトの裏にいっても確率的に投入されるのでしばらくしないとOcclusion Cullingされません。その分の負荷は増えてしまいますが、全オブジェクト毎フレームチェックするよりは良い効果を出す方が遥かに多いかと思います。

注意点: Was Rencently Renderedのタイミングもずれます

f:id:tempkinder:20181202223515p:plain

Was Recently RenderedはGPUに投入された際に更新されます。つまり、Occlusion Cullingでのカリングが確率的に行われたときに、絵としては別オブジェクトに遮蔽されて見えなくなったフレームと、実際にWasRecentlyRenderedが返すフレームにずれが生じる場合があります。

注意点: この確率的投入はStatic/Stationaryなオブジェクトのみ

このOcclusion Culllingの最適化はbAllowApproximateOcclusionがtrueのオブジェクトのみで行われます。ですがこの値はデフォルトではMovableに対応していません。

PrimitiveSceneProxy.cpp内部

, bAllowApproximateOcclusion(InComponent->Mobility != EComponentMobility::Movable)

Movableなオブジェクトは動き回るので前後関係が頻繁に変わるからという理由からだと思うのですが、見えていても毎フレーム投入されるのでご注意を。。
もしもMovableでもこの仕様でOKな場合、bAllowApproximateOcclusionのチェック式を変更したり、独自で拡張してみて動作を確認して頂ければ。手元で少し試しましたがうまく動作していたと思います。

注意点: MaxOcclusionPixelsFractionの詳細。CVarじゃないこいつ。
  • Engne.iniにて記載可能です。エンジンのBaseEngine.ini内部にDefault値0.1と記載されています。
  • CVar化されていません!なので動的変更は不可です。 (ですが、行いたい場合は拡張して頂ければ。。実装を見ている限り問題ないはずです。)
  • 1.0とすることで、全オブジェクトが毎フレームOcclusion Cullingに投入されるようになります。
  • オブジェクトが専有する画面のピクセルの割合がこの値よりも大きいと、さらに低い確率でこのオブジェクトを投入しようとします。(つまり、画面を占める割合が大きいオブジェクトは直ぐにカリングされるではないだろうという算段です。。)


以上です。最後に注意点だらけで嫌な感じになっちゃったかもしれませんが、強力な最適化手法なのでシェアさせていただきました。次は、Rwiさんの「UE4初心者による公式ドキュメントの大事そうな点まとめ+α」書きます!です。
ありがとうございました!

Testビルドでも色々計測できるようにする

Developmentビルドで正確なCPU負荷を図るのは難しい

UE4では、日々の開発はDevelopmentビルドで行い、最終的にリリースするパッケージはShippingビルドで作成するのが基本的なフローとなります。しかし、Developmentビルドはデータの整合性の確認であったりデータを集計していたりと、実際にリリースされるゲームでは除外されるデバッグ用コードが沢山含まれています。結果、DevelopmentビルドのCPU処理負荷はShippingビルドに比べて著しく重たく、実際にリリースされるゲームのCPU処理負荷を計測するのには適していません。 ※Developmentビルドでは様々なUE4のstat機能が使えます。それらを駆使して処理内容を理解したり投入されているオブジェクト数などを知ることに適しております。


正確なCPU負荷のトラッキングにはTestビルドを

しかし、Shippingビルドは実際にユーザに触れられるビルドなので、stat unitはおろかコンソールウィンドウなども閉じられてしまいます。なので、なるべく製品に近い状態でコンソールウィンドウなど最低限の機能のみを許可したTestビルドというオプションがUE4にはあります。 (※Testビルドは、エンジンカスタムビルドのみの機能で、ランチャーから落とすUE4ではTestビルドはないようです。)


Build.hのdefineを設定して、自分好みのTestビルドを

しかし、Testビルドはデフォルトではログなどの機能がOffになっており、これだと「GCの負荷のログを...ロード時間を...」などといったときに重宝するUE4のLogコマンドが使えなくなってしまいます。ただこれはデフォルトでオフなだけで、ユーザがBuld.hという一つのファイル内のdefineをそれぞれ設定することでカスタマイズ可能です。今回は自分がいつもTestビルドで変更している設定を紹介します。

Testビルドでもログを出力する方法

エンジンからの情報のアウトプットが閉じられているので、まずはログ出力機能をONにします。(特にGCの負荷を細かく計測するためにも、LogGarbage の情報などは必須です。)
f:id:tempkinder:20180615222235p:plain


TestビルドでもNamedEventsを出力する方法(4.19から)

各CPU ProfilerなどでCPUの処理負荷を階層的に計測するためのNamedEventsを、TestビルドでもONにすることができます。
f:id:tempkinder:20180615221926p:plain

注意点としてこのdefineはUE4のstat機能をoffにしてしまうのでそのまま1にするとDevelopmentビルドでstat機能が使えなくなってしまいます。下の様にTestで囲んで1にしてあげてください。
f:id:tempkinder:20180615221938p:plain


TestビルドでもProfileGPUを行う方法

GPU負荷はDevelopmentビルドで見ても問題ないのですが、ProfileGPUでレンダリング内容を簡単にしることもできるので、CPUでもRender Threadの解析には便利だったりします。
f:id:tempkinder:20180615222143p:plain


などなど。その他にもBuild.hには沢山の設定がありますので、自身が計測したい内容に基づいてDefineをONにしてみてください。

UE4で.iniファイルの設定をコマンド引数で上書きする方法

UE4には様々な設定を.iniという拡張子のファイルに設定できます。それらはShipping以外ではランタイムで変更可能なのですが、起動時に読まれる設定などはランタイムで変えても勿論効果がありません。なので、起動時には.iniの値を色々変えて置きたいというのはよくある需要です。特に、ナイトリーでビルドした各種プラットフォーム用ビルドの動作確認しているとき、「あの設定変えたらどうなるだろう??」と思っても、.iniファイルを1行書き換えてパッケージしなおすのはやはり面倒くさすぎます。


そういったときのため、UE4では実行時の引数で.iniファイルを上書きできます。

-ini:Engine:[ConsoleVariables]:r.screenpercentage=2000000,[/Script/Engine.GarbageCollectionSettings]:gc.MaxObjectsNotConsideredByGC=1

こんな感じです。(設定値には悪意があります。)


言葉でいうと以下な感じです。

-ini:(.iniファイルのPrefixなしの名前. EngineとかGameとか):(変数のカテゴリ):(変数=設定値),....

スペースはどこにも入れられないので、.iniからコピペしてくると凡ミスするのでお気をつけ下さい。
自分は、ロードやGCなどの調査ではこれが本当に役立っていてよく使います。

CustomDepthを拡張して色も出力してみる。

この記事は UE4 Advent Calender 2015 その1 の16日目の記事にあたります。

前回の記事は@nano06126728さんの何か適当にだしてみますでした。


やったこと: カスタムデプス計算時にカラーもできるようにしました。

本来のマテリアルでは、PixelDepthOffsetが一番下に来ていると思いますが、そこにCustomColorという項目を追加し、好きな値を入れられるようにしました。

f:id:tempkinder:20151210225715p:plain

ここで出力したCustomColorはG-Bufferの拡張領域となりますので、ポストプロセスマテリアルのSceneTextureで参照することができます。

f:id:tempkinder:20151210225722p:plain


非対象者: 拡張したいエンジニアの方へ

具体的な拡張の話はしません。使い道と仕組みについて簡単に説明します。もしも具体的な拡張方法が気になる方は以下のコードに従ってください。 

if (エンジニアでしょうか?)
{
	return; //このブログを閉じて、PixelDepthOffsetの実装をまねてください
}


なぜ実装した?: ①日本で流行るノンフォト、CustomDepth/Stencilをマスクとして行っているものもある。

ご存知の通り、ノンフォトレンダリングが日本で流行っており、UE4でも様々な方が挑戦しております。エピックゲームズジャパンの下田さんも、UnrealFes2015でノンフォトの講演を行っています。
forums.unrealengine.com

ここで、らりほまさんの下記のブログを見てみると、CustomDepthによるエッジのマスクの記載があります。

D言語くん以外のオブジェクトにもクリース線と色の境界線が描画されてしまっているので、マスキングを行います。CustomDepth をマスクとして利用します。
UE4 にて特定の Actor に対して輪郭線・クリース線・色の境界線を描画する、あるいはD言語くんを真の姿にする方法 | rarilog

下記の画像のように陰や背景に不要にエッジがかかってしまうのを防ぐために、マスクが必要という訳です。らりほまさん、画像の転載許可ありがとうございます!!

http://rarihoma.xvs.jp/data/2015/02/22/1/14.png


なぜ実装した?: ②Customdept/Stencilhでは情報が足りない。

便利なCustomDepthですが、そのオブジェクトが出力したデプス値を保存する機能なのでので、ユーザが自由に変更することはできません。

f:id:tempkinder:20151210235145j:plain

そのため、UE4.9から、CustomStencilによって好きな値を1つ出力できるようになりました。

f:id:tempkinder:20151210235350j:plain

しかし、このステンシルも問題があります。このステンシル値は、メッシュのレンダリング項目でのみ設定できます。

f:id:tempkinder:20151210235550j:plain

つまり、メッシュ単位で任意の一つの値しか出力できません。1つのメッシュの中でも、キャラクタの目や眉毛にはエッジをかけてほしくないとか。。。鼻筋のエッジラインは非常に細くしたいとか。。。エッジをより詳細に調整したいとなると、ステンシルやデプス値では限界があります。そんなこんなで、デプスもステンシルも出力するならば、カラーも一緒に出力するようにすればいいじゃん。そして、その値を好きにマテリアルで調整できれば、マスクだけじゃなくて特殊な使い方をしたい場合など、幅広く貢献できるかなと思って作ってみました。

使用例

ごめんなさい。具体的にノンフォトレンダリングに適用したかったんですが、アセットが間に合いませんでした。。基本的には、好きなものを出力して好きに使えばよいだけです。一例としては、カスタムデプスやステンシルのマスキングでは制御できないようなマスクを作ることでしょうか。例えば下記のように、そのままのエッジ抽出アルゴリズムだと、目、口、鼻に過剰なエッジが出てしまいます。(実はこのエッジアルゴリズムだと眉毛もほんのり赤くなる。)

f:id:tempkinder:20151213213826p:plain

CustomColorでマスキングして省くことができます。

f:id:tempkinder:20151213213839p:plain


後はマテリアルIDなど、エッジの色や太さの制御など、やりたいことをご自由にというものです。このようにエッジを細かく制御して実際の製品で実現させたのが、俺屍2です。CEDECで具体的な実装について説明していますので、ご参考にしてください。ノンフォトレンダリングは、制御の難しさから、検証レベルまではいけるもののプロダクションレベルまで持ってくのが非常に難しい技術かと思います。これを携帯器PSVitaで実現させた素晴らしい成功例かと思います。
cedec.cesa.or.jp
www.inside-games.jp



注意点: CustomDepthには負荷があります。オブジェクトをもう一度投入してレンダリングします。

f:id:tempkinder:20151210231613j:plain

ProfileGPUを見るとG-Bufferを作成する"BasePass"というものとは別の場所にCustomDepthという項目があります。CustomDepthがOnのオブジェクトは、もう一度GPUに投入されデプスが計算されるため追加の負荷がかかります。(本当は、もう一度投入するなんて暴挙に出るのならばカラーも出してしまえと思って拡張をしたのが発端です。)

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

f:id:tempkinder:20140112120104j:plain

明日は、ntaro@年末に向けて準備中 (@tarotarokun) | Twitterさんの「標準のUE4だとWindowsのマルチタッチが使えないための対応方法を書きます」です。インプットデバイス関連には疎いので、勉強させていただきます!

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を使った通信周りの書き方を解説するよ」です。
通信周りの情報がどんどん充実してきてて非常にありがたいです!楽しみです!

パーティクルにLight追加したらGPU負荷が跳ね上がった話

発端: パーティクル置いたら"Tiled" Deferredが走るようになった。


何もないシーン。 f:id:tempkinder:20141226170124j:plain

プロファイルしてみる。ライティングは1.0msぐらい。

(Startar Contetnsの)P_Sparksをおいてみた。

プロファイルしてみる。ライティングは4.03ms!!!!

TiledDeferredという処理が走るようになっている。

どうして? パーティクルにライトモジュールがあるからです。

ということが起きました。実は、パーティクルのライトが一個でもあると、裏でこの"Tiled Deferred"が走ります。

この処理、非常に大雑把に言うと、ライトが影響を与える画像の範囲を前もって調べ、不要なピクセルへの描画処理を省く手法です。 ライトがシーンに沢山ある場合は非常に高速になるのですが。。。ライトが高々数個の場合は逆で、この”前もって調べる”という部分のコストが一定量かかってしまい、それらが処理負荷を増大させてしまいます。 仕組みについて詳しく説明しません。案の定いつもどおり要するに率直におおかたの予想通り平たくところはばからずに正直に言ってしまえば、もんしょさんがこちらについて詳しく書いておりますので、そちらを参考にしてください。

DirectXの話 第125回 - もんしょの巣穴

余談ですが、UE4の基本設定では、普通のポイントライトを80個以上置くとTiledDeferredが走ります。(もんしょさん、ありがとうございます。) 逆を言うと、それぐらいライトの数が増えないとTiledDeferredによる高速化の恩恵を得られないということです。なのに、パーティクルのライトは一個でもあると、このタイル化処理が走ってしまうのです。

では、パーティクルライトを使う&Tiled Deferredを走らせない という方法は取れないか?少し調べてました。.iniの設定等ではできなそうです。

ありました!.iniファイルに

r.TiledDeferredShading=0

と記載するか、下図の様に、Blueprintで直接コンソールコマンドを打ち込むことで、普通のディファードレンダリングにすることができます。(一括での変更のみ。このパーティクルライトだけTiledで。。。とかはできません。)

パーティクルライトは非常に綺麗なので使いたい人は多いと思います。 このパーティクルのライティングについてはtoaru_designerさんの以下のスライドが非常にわかりやすく説明していますので、こちらを参考にしてください! ただし、パーティクルライトの数が少ない場合は、上記のコマンドでTiledDeferredを走らせない方が良いでしょう。

まとめ

問題
パーティクルのライトモジュールを置くとTiledDeferredというものが走ってGPUコストが上がる。ライトが沢山ある場合は高速な機能だが、ライトが少ない場合は逆に負荷を高めてしまうのでOffにしたほうが良い。

TiledDeferredをOffにする方法
・”r.TiledDeferredShading=0”をDefaultEngine.iniに追加
・もしくはexecute console commandで”r.TiledDeferredShading 0”と打ち込む
備考
これはSM5が動くPC版等での設定です。試していませんが、モバイル版ではこのTiledDeferredの処理は走りません。試してませんが。試してませんけど。誰か試して。


原因: なんでTiledDeferredが走るの?

本件の問題となるコードも載せておきます。(UE4.6で検証) FDeferredShadingSceneRenderer::RenderLights()(RenderLights.cpp)内の以下コードです。

if (NumSortedLightsTiledDeferred > 0 || SimpleLights.InstanceData.Num() > 0) 
{
    // Update the range that needs to be processed by standard deferred to exclude the lights done with tiled
    StandardDeferredStart = NumSortedLightsTiledDeferred;
    RenderTiledDeferredLighting(RHICmdList, SortedLights, NumSortedLightsTiledDeferred, SimpleLights);
}               }

SimpleLightsが一個でもシーンにあると自動的にTiledDeferredが走ります。 このSimpleLightsがどういう過程で生成されるのか詳しく見ていませんが、パーティクルのLightモジュールは、SimpleLightsとして生成されるようです。

この部分を読むとモバイルなどではタイルベースの処理は走らないようになっている様ですが、試してはおりません。

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

f:id:tempkinder:20140112115953j:plain

なんか見つけたらちょいちょい書いてきます。