だらけ者だらけ

だらけ者だらけの遊び場

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初心者による公式ドキュメントの大事そうな点まとめ+α」書きます!です。
ありがとうございました!