【UE5】リムライトを描いてみた プラグイン編 – Unreal Engine 5.6

Unreal Engine
目次
  1. 水ノ茉の宣伝
  2. 始まり
  3. リムライトとは
    1. こういう感じ
  4. 選択肢:マテリアル / ポストプロセスマテリアル / プラグイン / エンジン改造
  5. プラグインとは
  6. マテリアル版の再現実装
    1. Shaders/Private/Rimlight.usf
    2. Source/RimlightRenderPipeline/Private/RimlightController.cpp
    3. Source/RimlightRenderPipeline/Private/RimlightRenderPipelineModule.cpp
    4. Source/RimlightRenderPipeline/Private/RimlightSubsystem.cpp
    5. Source/RimlightRenderPipeline/Private/RimlightViewExtension.cpp
    6. Source/RimlightRenderPipeline/Public/RimlightController.h
    7. Source/RimlightRenderPipeline/Public/RimlightRenderPipelineModule.h
    8. Source/RimlightRenderPipeline/Public/RimlightSettingsDataAsset.h
    9. Source/RimlightRenderPipeline/Public/RimlightSubsystem.h
    10. Source/RimlightRenderPipeline/Public/RimlightViewExtension.h
    11. 使ってみる
  7. 太さ単位の再現実装
    1. Shaders/Private/Rimlight.usf
    2. Source/RimlightRenderPipeline/Private/RimlightViewExtension.cpp
    3. Source/RimlightRenderPipeline/Public/RimlightSettingsDataAsset.h
  8. 内側に向けてぼかすの再現実装
    1. Shaders/Private/Rimlight.usf
    2. Shaders/Private/RimlightBlur.usf
    3. Source/RimlightRenderPipeline/Private/RimlightViewExtension.cpp
    4. Source/RimlightRenderPipeline/Public/RimlightSettingsDataAsset.h
  9. 最適化をしてみる
    1. Shaders/Private/Rimlight.usf
    2. Shaders/Private/RimlightBlur.usf
    3. Source/RimlightRenderPipeline/Private/RimlightViewExtension.cpp
    4. 最適化の結果
    5. 勘違いしてほしくないこと
  10. おわり!!!
  11. 深刻な雑談不足
  12. えちえち Cloth Simulation Season 2

水ノ茉の宣伝

準備中...
ゲームを作る予定なの
水ノ茉こおり

始まり

描画エンジニアになりたい方の入門編にもちょうどよいコンテンツ。

そう、リムライト
(尚主観。異論は認めよう。)

それを幾つかの実装方法で描いていきます。

リムライトとは

読んで字の如く。

物体の縁(リム)に当たる光の表現。

こういう感じ

選択肢:マテリアル / ポストプロセスマテリアル / プラグイン / エンジン改造

特に目を引くのはーー

「………」

何気に、プラグインかもしれない。

(恒例行事)

プラグインとは

知らんです。プラグインという名前の方なのでしょう。

Just a moment...

マテリアル版の再現実装

ポストプロセスマテリアル√の再現実装をしていきます。

プラグイン版の大まかな説明は過去に触れたことあるので、重複しそうな箇所や筆者が興味のない箇所は飛ばします。

githubでコミットを見る

Shaders/Private/Rimlight.usf

リムライトのシェーダーです。

githubで実装を見る

Source/RimlightRenderPipeline/Private/RimlightController.cpp

アウトライン編では構造体を保有していましたが、今回はデータアセットのポインタを持つ形に変更しています。

利点としてはデータアセットからパラメタ変更が可能なので、レベルに配置されたアクターを探してパラメタをいじる必要がない点です。

欠点は設計をサボって全部所謂SharedPtrで持っているので、GC管理が伸びに伸びている点です。理想はActorを所有者として、残りはWeakにして都度寿命チェックするべきでしょうが、あまりにも面倒だったのでサボりました。

githubで実装を見る

Source/RimlightRenderPipeline/Private/RimlightRenderPipelineModule.cpp

シェーダーを使用する際のモジュールの一般的な書き方です。

githubで実装を見る

Source/RimlightRenderPipeline/Private/RimlightSubsystem.cpp

先と同様にパラメタを構造体ではなく、データセットのポインタで持っている点が差分程度です。

githubで実装を見る

Source/RimlightRenderPipeline/Private/RimlightViewExtension.cpp

前回とパス投下の方法を変更しました。

githubで実装を見る

SubscribeToPostProcessingPassで投下するパスを登録しています。ここではBeforeDOFに投下しています。優先度としてはプラグインの後にマテリアル版が投下される感じです。

githubで実装を見る(L58-L64)

PostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessMaterialInputs& Inputs)

この引数の並びはポストプロセスマテリアルでお馴染みのやつです。エンジン改造でアプローチする民からすれば本当にお馴染みです。

前回のPrePostProcessPass_RenderThreadの違いとして、ポストプロセスマテリアルの設計は新しいもしくは書き換えたSceneColorを返して、それを引数に新たなパスを生成・処理する流れです。そのため前回のようにSceneTexturesに格納されたSceneColorを書き換える必要が基本的にはありません。合理的な設計で割と好きな部類です。

githubで実装を見る(L66-L135)

FastVRAMを打ち消していますが、これ別にしなくてもいいです。このフラグはXBOXでしか動作しないはずなので実質無意味です。形骸的な対応というやつです。

githubで実装を見る(L96)

Source/RimlightRenderPipeline/Public/RimlightController.h

ただのヘッダーです。

githubで実装を見る

Source/RimlightRenderPipeline/Public/RimlightRenderPipelineModule.h

ただのヘッダーです。

githubで実装を見る

Source/RimlightRenderPipeline/Public/RimlightSettingsDataAsset.h

今回から導入したデータアセット方式なパラメタです。

githubで実装を見る

ソフトボディ開発で使用し始めてからこの形式がマイブームになりました。UFUNCTIONで簡単にボタンを追加できるし、シリアライズ対応もデフォルトだし、独自データを手軽に保持・管理できるので良きです。

Source/RimlightRenderPipeline/Public/RimlightSubsystem.h

ただのヘッダーです。

githubで実装を見る

Source/RimlightRenderPipeline/Public/RimlightViewExtension.h

ただのヘッダーです。

githubで実装を見る

使ってみる

まずはリムライトのパラメタを作成します。

こんな感じです。

次にパラメタを保持するBPアクターを作成です。

RimlightControllerのRimlight Settingsに先ほどのデータアセットをセットします。

あとはRimlightControllerをレベルに配置して、パラメタの有効性にチェックを入れたらリムが描画されます。パラメタを同等にしたらポストプロセスマテリアル版と同じ見た目を再現できることが確認できます。

太さ単位の再現実装

最小実装の再現は出来ました。

次は太さ単位の部分を再現します。

githubでコミットを見る

Shaders/Private/Rimlight.usf

ワールド座標を取得するフォーマットです。

githubで実装を見る(L55-L56)

ワールド単位系のバリエーションが有効な場合のみ動作するようにしています。

githubで実装を見る(L61-L66)

githubで実装を見る(L68)

ResolvedViewのセットアップはおそらく要らないです。シンプルに消し忘れしました。ワールド座標を取得する方法はいくつかのフォーマットがあって、そのうちのひとつを試した際に利用して、そのまま消し忘れてコミットという流れでしょう。稀によくある。

githubで実装を見る(L83-L85)

Source/RimlightRenderPipeline/Private/RimlightViewExtension.cpp

WORLD_SPACE_UNITバリエーションを使えるようにしています。

githubで実装を見る(L18-L19)

WORLD_SPACE_UNITバリエーションの有無を指定しています。

githubで実装を見る(L113-L114)

githubで実装を見る(L117)

Source/RimlightRenderPipeline/Public/RimlightSettingsDataAsset.h

ワールド単位系にするかのチェックボックスです。

githubで実装を見る(L51-L52)

内側に向けてぼかすの再現実装

再現実装の最後はぼかし表現です。

githubでコミットを見る

Shaders/Private/Rimlight.usf

面倒だから割愛します。

githubで実装を見る

Shaders/Private/RimlightBlur.usf

ぼかし兼コンポジットシェーダーです。

githubで実装を見る

Source/RimlightRenderPipeline/Private/RimlightViewExtension.cpp

ぼかしを適用しないシンプルなリムと切り替えられるようにバリエーションを追加しています。

githubで実装を見る(L19-L20)

ぼかし兼コンポジットシェーダーの定義です。

githubで実装を見る(L37-L60)

githubで実装を見る(L63)

リムの種類でパス構成が変わるので分岐作成と追加したバリエーション対応です。

githubで実装を見る(L139)

githubで実装を見る(L143)

リム → ぼかし → ぼかし兼コンポジットな順番のパス構成です。

githubで実装を見る(L168-L273)

Source/RimlightRenderPipeline/Public/RimlightSettingsDataAsset.h

SHADER_PERMUTATION_ENUM_CLASSを使うためにはMAXの定義が必須なので追加しています。

githubで実装を見る(L24)

ぼかしのパラメタを追加です。

githubで実装を見る(L68-L69)

githubで実装を見る(L76-L77)

最適化をしてみる

長々とプラグイン版を書きましたが、再現実装をするだけならプラグイン版に存在意義はないです。普通にポストプロセスマテリアル版の方がイテレーション早いですからね。

プラグイン版の意義は主に複雑なパス構成に耐えられる、もしくは最適化の2択ぐらいと思います。ということで簡単に出来る負荷軽減を導入してみます。

現行の処理負荷がこんな感じです。

前者がポストプロセスマテリアル、後者がプラグインです。

マテリアル版が30,496nsナノ秒に対して、プラグイン版は25,344nsナノ秒です。

既にプラグイン版に多少の優位があります。

マテリアル版は汎用的な実装が故に使わない処理まで展開しています。対してプラグイン版は必要最小限のコードしか展開していないため、そのあたりで差分が起きています。

プラグイン版が微妙に内訳と総数が合っていませんが、Drawで絞っているのでおそらくClearとかが消えちゃってるんだと思います。あんまりに気にしないでください。

ここからさらに早くするために、ステンシルテストとリムのコンポジット方法をブレンドモードな方法の2つを試してみます。

githubでコミットを見る

Shaders/Private/Rimlight.usf

Unlit以外の箇所は処理する必要がないのでdiscardします。discardするとステンシルの書き込みもスキップされます。というかステンシルの書き込みをスキップするためにdiscardをします。

githubで実装を見る(L57-L59)

githubで実装を見る(L61)

githubで実装を見る(L68-L70)

githubで実装を見る(L72)

Shaders/Private/RimlightBlur.usf

乗算ブレンドモードで既存のSceneColorに対して書き込みを行うのでフェッチを廃止しています。リムを描き込まないピクセルは1.0で乗算になるので現行色が維持され、それ以外はリムのカラーが反映されます。

githubで実装を見る(L77-L78)

Source/RimlightRenderPipeline/Private/RimlightViewExtension.cpp

ぼかし版は最適化の都合でSceneColorを新しく作る必要がないのでスキップしています。

githubで実装を見る(L115)

ステンシルテストで使用するデプス&ステンシルテクスチャを作成しています。

ステンシルのクリア値は敢えて255にしています。この後にリムを描き込んだピクセルのステンシル値を0で上書きするので、0以外の値が都合良いという理由からです。

githubで実装を見る(L171)

githubで実装を見る(L173-L178)

ステンシルの書き込みを有効にしています。デプスは一切使わないのでNoActionです。

githubで実装を見る(L210)

リムを描き込んだピクセルはステンシル値を0で上書きSO_Replaceします。Frontのみ評価で、Backは動作させていませんが、同様の書き方をしています。これは筆者の癖です。

githubで実装を見る(L221-L224)

後続パスはステンシルテストを有効化して、ステンシル値が0のピクセルのみ処理するようにして最適化しています。

githubで実装を見る(L243)

githubで実装を見る(L254-L257)

githubで実装を見る(L276)

githubで実装を見る(L287-L290)

ブレンドモードを乗算にしています。これでSceneColorのフェッチを省略できます。あくまでSceneColor * RimlightColorという単純な乗せ方だから出来る稀有な例であることには注意です。

githubで実装を見る(L286)

最適化の結果

最適化前が25,344ns、最適化後が17,696nsです。

1.4倍ほど高速化しました。

ステンシルテストの効果は画面内にリムを乗せる対象であるUnlitなピクセルがどの程度存在するかに依存するので、位置関係によっては効果が薄いですが、基本的には効果が望めます。SceneColorの書き込みをブレンドモードに変えた部分は常駐で効く最適化ですね。

ポストプロセスマテリアル版と比較すると1.7倍ほど高速化しています。

これこそがプラグイン版、ひいてはエンジン改造の大きな利点です。

勘違いしてほしくないこと

『軽量なら全部プラグイン版にすればいいじゃん』とアホなこと言う方が生まれるかもしれないので釘を刺しておきます。

最適化を本格的に行うフェーズに突入するまではプラグイン版は導入しない方がいいです。無論、プラグイン版にしないとパス構成上、表現が難しい場合は例外ですが、最適化のためにプラグイン版を選ぶというのは愚策です。ポストプロセスマテリアル版と比べると作業イテレーションが大幅に低下するので下手なプログラマだとスケジュールの遅延を招きます。作業に慣れている筆者ですら、ポストプロセスマテリアル版の方が早いと感じるので大半は遅延すると思います。

早いうちから最適化をする行為はプログラマという立場上否定しませんが、フィックスしていない状態で最適化を考えたところで仕様変更に振り回されて無に帰すのが大体のオチです。

それに描画の最適化なんて結構簡単です。とりあえず形作ることを最優先にすればいいのです。負荷的に動かないとなれば、表現を削ったり、オミットしたり、取れる手段なんていくらでもあります。だから初手から最適化を考えるのはやめておきましょう。ほとんどの人間が心を病むことになるはずです。

最適化が大好きな変態さんはその限りではないと思いますが。

尚筆者は最適化を考えて別パスで完結させる別ルートを開拓する変態とは別種の変わり者です。

おわり!!!

ポストプロセスマテリアルからプラグインに書き換えるだけで1.7倍ほど高速化する例もあるんだよという軽い紹介でした。

深刻な雑談不足

技術を載せて、はい、終わり♪な記事なんて真面目な奴らに任せておけば良いのです。筆者は不真面目だから、あくまでも技術はおまけ程度に載せているのです。本編は雑談なのです。なのですが…あまりにも深刻な雑談ネタ不足です。エロゲほっぽり出してライブラリ開発して遊んでるし、エロい進捗はCi-enがあるし、この場所の意義が…筆者のためのアーカイブという唯一の目的しか残っていません。かといってそれだけだとアイデンティティが薄いんですよね。我ながら悩ましい拘りです。

解決策はないので悩みを吐いただけです。何事も溜め込むのは良くないですからね。負は吐き出すと結構スッキリします。暫く経つとまた頭を抱えますが。

えちえち Cloth Simulation Season 2

CoffeeLiveで初代クロスを爆誕させました。

その後のソフボ開発の足掛かりにもなったので大変愛着のある子です。

がしかし、早くも世代交代のお時間です。

理由は制約構築が面倒過ぎるのです。

ご存じでしょうが、一般的なクロスシミュは構成、せん断、曲げ、この3つの制約を元に計算します。この制約はかなり布っぽくて、えっちです。問題はこの制約を汎用的に構築する手法が微妙な立ち位置にいることです。

お洋服がすべてクアッドメッシュで作られている場合であれば割と簡単に制約構築が済みますが、部分的に異なる場合は一気にルールが複雑になります。そういう背景や最適化などからレンダリング用のメッシュとクロス用のメッシュは分けて作られることがあります。

はい、とっても面倒なのです。

ということでクロスシミュと呼ぶにはあまりにも単純すぎる制約構築と計算を召喚です。

処理をGPUにお引越しです。

続きをやろうと思いましたが、『早くゲーム制作に戻れバカ』と脳内会議が勃発したので中断です。ライブラリ開発は楽しいから永遠とやりたいんだけどねぇ。それだとゲームを作る担当が居ないんだよねぇ。ひとりN役の難しさです。