SPARK CREATIVE Tech Blog

https://spark-group.jp/

【UE5】影のLODを最低値にするエンジン改造

はじめに

はじめまして、クライアントエンジニアの下野です。

去年入社の新卒ですが、バタバタしてたらブログ公開前にもう1年経っていました。時の流れは速いです。

今回はUE5の負荷最適化として、影のLODを指定値にする処理を行います。

前提

エンジンのバージョンは5.6です。バージョンによってはコードが変わったり増えたり減ったりするので完全にコードを一致させても動かない場合があります。(一応5.4では動確してます)

Naniteはメッシュの扱いが特殊なので対応面倒なため未対応です。

SkeletalMeshはボーンだったりアニメーションだったりの対応があまりにも時間かかりそうだったり、キャラの影とかどうせ下げない気がするので未対応です。

エンジン改造を行うので、弊社の別ブログを見てエンジン改造の知識を持ってから見るとより理解出来ると思います。

LODとは

公式のドキュメントが分かりやすいと思います。

dev.epicgames.com

画面上に大きく映っているようなオブジェクトは嫌でも目に付くためハイポリで描画します。

しかし、遠かったり小さく映っているようなオブジェクトは精密に描画しても見えなかったりするため、負荷軽減のためにローポリにして描画します。

これがLODです。

LODはLevel of Detail(詳細度)の略称らしいです。

UnityでもUEでも0が高品質で、値が高くなるほどローポリになります。

本題

影のLODを最低にします。

オブジェクト自体が低LODだとさすがに気になりますが、影だったら気にならない物が多いです。

LOD0(高品質)

LOD3(低品質)

コンクリ板の影だけLOD3

見た目が気にならなくても、メッシュの投下処理はほぼ同じで負荷も似た感じになるので気になります。

LOD0(高品質)

LOD0だと2000頂点くらい

LOD3(低品質)

LOD3だと400頂点で1/5される

そのため、影のLODを強制的に最低品質にしてしまえば負荷は軽くなります。

エンジンのコードを読めばそれっぽいのがありますが、プレシャドウ限定だったりエディター限定だったり、メッシュごとに指定出来なかったり融通が利かないのでそこら辺の対応をしていきます。

なぜエンジン改造か

思い付きとかではなく、理由はちゃんとあります。

その気になれば下の手順みたいにメッシュ2つ投下して、片方は影無し高LOD描画もう片方は影以外投下しない最低LOD描画みたいにすることでエンジン改造しなくとも実装できます。

StaticMeshを2つ用意します。

StaticMeshを2つ用意

片方のメッシュはCastShadowをオフにして影を落とさない設定にします。

片方をCastShadowオフ

もう片方のメッシュはLOD最低にして影のみ描画します。

LOD最低に
影のみを表示する設定

完成したものの見た目

影だけLODが低く、見た目は高品質

ですが、この場合以下がデメリットになってきます。

  • プリミティブが2つ必要になるため、無駄な処理が走ります。(コンポーネント側の処理が特に)
  • 「メッシュを追加して片方影を切って、もう片方は影以外表示しないようにして」という処理を全マップ全オブジェクト個別に行っていくので、対応漏れが発生しやすく管理が大変になります。

最適化が目的なのに無駄な処理が走るのはあまり良くないし、設定が面倒だと使われない可能性もあるので良くないです。(個人利用の範疇ならいいかもしれないけど、私は極力楽をしたいのでこれはNG)

しかし、上記の問題をエンジン改造では解決できます。

具体的には、以下のような感じです。

  • 影の投下パスでLODを最低値に強制するので1つのプリミティブで完結でき無駄な処理が走りません。
  • コンポーネントに追加したフラグをオンオフするだけなので管理が楽になります。

↓みたいな感じでフラグ1つでオンオフするのが理想です。

コンポーネント側にフラグ追加して切り替えれるように実装

前置きは以上にして実際にコードを組んでいきます。

実装

コードはGitHubのリンクを貼り付けています。(Epicの規則的に)

URLをクリックした際に下画像が出る場合は、UnrealEngineのEULAの同意をしないといけないため、

リポジトリ取得の手順を踏んでからお試しください。

コンポーネントのフラグ作成

最初にフラグを作成します。

SkeletalMeshは未対応なのでStaticMeshComponentのみに実装します。

Engine/Source/Runtime/Engine/Classes/Components/StaticMeshComponent.h

両方対応する場合はPrimitiveSceneComponentに置くのが後の処理的に楽です。

継承関係は以下のようになっているので、一見するとMeshComponentでも良いかと思われますが、後の処理的にPrimitiveSceneComponentの方が楽できます。

コンポーネントの継承関係図

フラグを作成したらコンストラクタで初期化してあげたりも忘れないようにしましょう。

初期値はフラグ適用したいメッシュと適用したくないメッシュどっちが多いかで好きに決めて下さい。

Engine/Source/Runtime/Engine/Private/Components/StaticMeshComponent.cpp

ついでに、BPとかで動的に変更する可能性もあるのでGetterとかSetterとかも用意してあると便利です。

Engine/Source/Runtime/Engine/Private/Components/StaticMeshComponent.cpp

RenderThreadで使うProxyに値渡し

StaticMeshComponentはゲームスレッドで値変更したりに使いますが、LODの計算や描画はレンダースレッドで行います。そのため、RenderThreadで使うクラスに値を渡してあげる必要があります。

レンダースレッドではFStaticMeshSceneProxyを使用しますが、Proxyの初期化はProxyDescを経由する必要がため、先にDescの変数を作成します。

Engine/Source/Runtime/Engine/Public/StaticMeshSceneProxyDesc.h

StaticMeshComponentを使って初期化されるので、値を渡すのを忘れないようにしましょう。

コンストラクタでInitializeFromStaticMeshComponent関数を呼び出し、呼び出した関数内で値を初期化しているので若干注意が必要です。

Engine/Source/Runtime/Engine/Private/StaticMeshSceneProxy.cpp

次にFStaticMeshSceneProxyに変数とGetter関数を作成します。

変数
Engine/Source/Runtime/Engine/Public/StaticMeshSceneProxy.h
Engine/Source.Runtime/Engine/Private/StaticMeshSceneProxy.cpp

関数
Engine/Source/Runtime/Engine/Public/StaticMeshSceneProxy.h

関数はoverrideと書いてる通り、親クラスであるPrimitiveSceneProxyにもGetterを用意しておきます。

レンダースレッドでは基底クラスのPrimitiveSceneProxyで値を取ったり関数呼び出しを多用しますが、毎回StaticMeshSceneProxyにキャストして関数呼び出ししたりというのは負荷が気になるので、基底は常にFalseを返し、StaticMesh継承している場合のみ変数見て値を返せるようにします。

Engine/Source/Runtime/Engine/Public/PrimitiveSceneProxy.h

ここまでで一旦エラーが出ないことを確認するためにビルドをします。

成功するとStaticMeshComponentにフラグが追加されていると思います。


LODの強制

変数や関数の作成が終わったら、次にLODを強制的に最低品質にします。

ShadowのLOD計算関数内で先程追加したPrimitiveSceneProxyのGetterを参照して、フラグがTrueの場合に最低品質のLODを探してセットする感じです。

Engine/Source/Runtime/Renderer/Private/ShadowSetup.cpp

これでLOD指定の半分が終わりです。なぜ半分かというと、上記修正はスタティックメッシュの中でもキャッシュされて値が変化しないメッシュ用のコードだからです。

キャッシュされないメッシュは別の場所を修正する必要があります。

DynamicMeshはこのパスでLOD計算をせず、一回持ち帰ってProxy内でLOD計算をするため、影投下時のビューにフラグを入れてチェックします。

Engine/Source/Runtime/Renderer/Private/ShadowSetup.cpp

次にFStaticMeshSceneProxy::GetLODMask関数で動的なLOD計算が必要な物の処理があるため、ここに先ほど追加したビューのフラグが有効でかつ、Proxyの変数がTrueの場合にLODの配列の一番大きい値を返すようにします。

Engine/Source/Runtime/Engine/Private/StaticMeshSceneProxy.cpp

これでコードは完成です。

デバック

デバックします。

余談ですが、5.6になってコンテンツブラウザから直接fabを開けるようになってたのが便利だなと思いました。(前まではEpic開いてライブラリから入れてたので)

見た目

フラグ適用前
フラグ適用後

見た目が変わっています。気になる場合はコンポーネントのフラグをオフにすればいいので対応も楽です。(シーケンサーとかのムービー中はオフにするみたいな)

処理負荷

stat gpuでも良いですが、今回はUnrealInsightにします。

フラグ適用前(平均1.60ms)
フラグ適用後(平均1.30ms)

変わってるのが分かればいいや程度なので、エディター計測だったり適当に用意したマップだったりするので一概には言えませんが、今回は0.3msほど短縮出来ました。

終わり

いかがでしたか?

この程度のコード量でも結構負荷変わるので「絶対高品質な見た目が欲しい」とかじゃないなら雑に作って置いておいてもいいと思います。(フラグ設定しなければ動きませんし)

快適に遊べるゲームを作りたいならやっぱり最適化は必須になるので、このような見た目が気にならない所には犠牲になってもらうしかないのかなとも思います。

今回は強制的に最低品質にしましたが、bool変数をintで用意して、指定の品質に下げる等の対応でも十分な効果は得られると思います。

©2025 SPARKCREATIVE Inc.