【UE5】ポストプロセスマテリアルでシャドウマップを扱えるようにしてみた

始まり

ポストプロセスマテリアルでシャドウマップを使えるようにする改造を過去にしました。

左が影ポストエフェクトなし、右が影ポストエフェクトあり


改造を施した当時のバージョンは5.0.3でした。

知っている方もいるかもしれませんが、5.0.3は改造非推奨バージョンに含まれます。

Unreal Engine 4系から5系への大幅アップデートが初期段階で、高頻度で修正コミットされていたことが理由ですね。筆者さんは待ちきれずに改造をしちゃった訳ですが。(エンジンコードの差分を見るのも結構勉強になるからね。)


そんな不安定なバージョンも過去の話になった今日、レンダリングまわりは5.2 ~ 5.3の間で結構な変更がありました。その結果、当時の改造箇所をマージしても動作不良を起こしていました。(実際に動かしてはいないが、愚直にマージした程度では動くはずもないだろうなぁくらいの変更量はあった。)


最近、個人プロジェクトを5.2から5.3にアップデートしたということもあり、まぁタイミングが良かったので、ちょっくら5.3対応しちゃいます。(ポストエフェクトでざっくりシェーディングをしたい時に本改造が意外と有用なのよ。)

実装内容に興味がない方々向け

「実装に興味はないわ、動けばいいんだよ」という方々は、githubのリリース機能から UnrealEngine-[version]-PPI-ShadowMap.zip をダウンロードして、WinMergeで差分を取り込んで下さい。ディレクトリは合わせているので簡単に取り込めると思います。

動作対象(絶対に動くとは言っていない)

  • 対応
    • Unreal Engine 5.3
    • Shadow MapとVirtual Shadow Map(以降「VSM」と表記。)の両対応
      • VSMは動作確認甘いのでクラッシュする可能性も
    • ディレクショナルライトのみ対応
    • 複数ビューやScene Capture、Scene Capture Cubeの対応
      • 動作確認甘いのでクラッシュする可能性も
  • 非対応

エンジン改造の(暗黙的な)ルール

エンジン改造を施した箇所にはコメントを残します。

以下はあくまで筆者さんがよく使うフォーマットの説明です。このフォーマットに沿う必要はありませんが、絶対にコメントだけは残してください。書かないと引継ぎする際にキレると思います。(書いていてもエンジン改造はキレるレベル。毎回過去の自分に怒ってる。)

既存の実装に追加で書き込む場合にはディレクティブでオリジナルコードを残すようにしています。残しておくとエンジンのバージョンアップ時にWinMergeする負担がかなり軽減できます。

//// ReinCarnation @kobayashi-arata 2023/12/27 ////
#if 0
    RenderLights(GraphBuilder, SceneTextures, TranslucencyLightingVolumeTextures, LightingChannelsTexture, SortedLightSet);
#else
    RenderLights(GraphBuilder, SceneTextures, TranslucencyLightingVolumeTextures, LightingChannelsTexture, SortedLightSet, &ShadowProjectionResourceMap);
#endif
//// ReinCarnation @kobayashi-arata 2023/12/27 ////


完全に新規で実装、挿入する場合にはディレクティブは不要です。でもコメントは残しましょう。

//// ReinCarnation @kobayashi-arata 2023/12/27 ////
#include "ShadowRendering.h"
//// ReinCarnation @kobayashi-arata 2023/12/27 ////

実装

前回は複数ビューの対応をしていませんでしたが、今回はやっていきましょうか。勉強がてらに。

実装はgithubのURLを添付していますが、サイト負荷になってしまうのでReleasesからダウンロードしてローカルで閲覧してください。行数もコメントしてあるので特に不自由ないと思います。(そもそもエンジン改造という行為自体が不自由そのもの。)

Engine/Source/Runtime/Renderer/Private/ShadowRendering.h

シャドウ関数内で作成したShadowMapテクスチャリソースのコピーしたものを格納するための構造体を定義しています。

FShadowProjectionPassResourcesMapTInlineAllocator<4>で確保しているため、4つまでのビューはメモリの動的確保が省略されます。半透明の実装を流用しただけですね。

構造体名 説明
FShadowProjectionPassResources シャドウマップリソースとビューのサイズ、VSM(Virtual Shadow Map)のパラメータ
FShadowProjectionPassResourcesMap FShadowProjectionPassResourcesをViews分保持している
FShadowProjectionViewResourcesMap ポストプロセス関数に渡す際にconst修飾子を付けたいやつ

githubで実装を見る(L48-L52)
githubで実装を見る(L1794-L1863)

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

ShadowMapのShadowMapをコピーしています(笑)。

Virtual Shadow MapのShadowMap、Shadow MapのShadowMap、間違っていないのですが、VSMが登場したことにより、区別をつけるとこういう書き方になるのですよね。違和感やばい。やっぱり笑うわ。

githubで実装を見る(L2020-L2027)
githubで実装を見る(L2031-L2034)
githubで実装を見る(L2082-L2084)
githubで実装を見る(L2089-L2091)
githubで実装を見る(L2097-L2109)


シャドウ関数にFShadowProjectionPassResourcesMap引数を追加しています。

githubで実装を見る(L2119-L2126)
githubで実装を見る(L2170-L2177)


ShadowMapとVSMをコールにFShadowProjectionPassResourcesMap引数を追加しています。

githubで実装を見る(L2334-L2341)
githubで実装を見る(L2353-L2361)


モバイルは非対応なためnullptrを渡しています。

githubで実装を見る(L2480-L2487)

Engine/Source/Runtime/Renderer/Private/SceneRendering.h

ShadowMapの関数にFShadowProjectionPassResourcesMapの引数を追加しています。

githubで実装を見る(L91-93)
githubで実装を見る(L2305-2312)
githubで実装を見る(L2321-2328)

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

フォワードレンダリングは本改造非対応なためnullptrを指定します。

デフォルト引数で指定する形でも問題ありません。

記事で触れておきたかったのと、バージョンアップ時に本関数の使用箇所が増えた際、エラーを出力させて追加された場所を把握しておきたかったという理由で指定していないだけなので。

githubで実装を見る(L1028-L1035)

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

シャドウ関数にFShadowProjectionPassResourcesMap引数を追加しています。

githubで実装を見る(L1224-L1231)
githubで実装を見る(L1932-L1938)

Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.h

FShadowProjectionPassResourcesMapは前方宣言します。

5.2あたりからレンダリングまわりのクラスや構造体は定義系ファイルに纏められてヘッダーでのインクルードを減らす動きが見られました。これだけソリューションがデカいと焼け石に水な気もしますが、EpicさんなりにUnityより圧倒的に劣っているビルド時間の改善を頑張っているのでしょう。

githubで実装を見る(L63-L65)


ライティングとシャドウ関数にFShadowProjectionPassResourcesMap引数を追加します。

githubで実装を見る(L828-L835)
githubで実装を見る(L922-L929)

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

FShadowProjectionPassResourcesMapを宣言しています。

ライト関数の引数に渡すことで、作成・コピーしたシャドウマップとパラメータを取得しています。

githubで実装を見る(L3864-L3866)
githubで実装を見る(L3910-L3916)


ポストプロセス関数で扱えるようにFPostProcessingInputs構造体にセットします。

githubで実装を見る(L4414-L4416)
githubで実装を見る(L4429-L4431)

Engine/Source/Runtime/Renderer/Private/Shadows/ShadowSceneRenderer.h

VSMの関数にFShadowProjectionPassResourcesMapの引数を追加しています。

githubで実装を見る(L19-21)
githubで実装を見る(L81-88)

Engine/Source/Runtime/Renderer/Private/Shadows/ShadowSceneRenderer.cpp

5.0.3では未対応だったVSMの対応です。

注意点としてVSMは適用される最適化によってはシャドウマップ ScreenShadowMaskTexture が生成されません。その場合はコピー元が存在しないため、クラッシュするかもしれません。筆者さんがVSMで遊ぶことが少ないため、割と雑な対応です。

githubで実装を見る(L459-L466)
githubで実装を見る(L592-L617)

Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessing.h

ポストプロセス関数の引数であるFPostProcessingInputsFShadowProjectionViewResourcesMapを追加します。

インクルードサイズを減らしたい場合は、ポインタに変えてもいいかもしれません。

TranslucentRendering.hがバリバリにインクルードされていたので、筆者も気にせずShadowRendering.hをインクルードしています。中途半端にValidate関数で使用しているのが悪いのでしょうね。そのうち改修入りそうな予感。

githubで実装を見る(L9-L11)
githubで実装を見る(L48-L51)
githubで実装を見る(L57-L59)

Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessing.cpp

ポストプロセス関数からポストプロセスマテリアル関数を呼ぶ際には、必要なリソース・パラメータをFPostProcessMaterialInputsに格納しなおしているんですよね。こういう所、意外とまめな実装していますよね。ぶっちゃけFPostProcessingInputsを渡しちゃってもいい気するのに。Epicさんの気分次第なのかな。

githubで実装を見る(L312-L314)
githubで実装を見る(L523-L524)

Engine/Source/Runtime/Renderer/Public/PostProcess/PostProcessMaterialInputs.h

前方宣言で使用するためポインタをくっつけています。

githubで実装を見る(L9-11)
githubで実装を見る(L130-L132)

Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessMaterial.h

ポストプロセスマテリアルのシェーダーパラメータに、VSMのパラメータのDistanceFadeMADとシャドウマップテクスチャとサンプラのLightAttenuationTexture, Samplerを追加しています。

Unreal Engineはアライメントを自動で整えてくれるため16の倍数を意識せずに定義できるのが嬉しいですね。

githubで実装を見る(L35-L39)

Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessMaterial.cpp

ポストプロセスマテリアルパラメータに作成・コピーしたシャドウマップリソースをセットしています。

作成・コピーしたシャドウマップリソースが存在しない場合には、ダミーのWhiteテクスチャをセットすることで影なしになるようにしています。全影にしたい場合はBlackにするだけです。(そんなケースないと思うけど。)

githubで実装を見る(L559-L561)
githubで実装を見る(L565-L567)
githubで実装を見る(L629-L642)

Engine/Shaders/Private/Common.ush

DistanceFadeMADをシェーダー側にも定義します。

githubで実装を見る(L796-L798)

Engine/Content/Functions/MF_LightAttenuationFromShadow.uasset

ポストエフェクトで使用される想定のマテリアル関数です。適当にエンジンコンテンツに配置していますが、場所はどこでもいいです。好きなところに配置してくださいな。

カスタムノードのコードは掲載するとEULA違反になるのでココでは見せられません。ローカルでご確認ください。


動作確認

MF_LightAttenuationFromShadowを使用したポストプロセスマテリアルを作成してUnlitに影を落として、SceneCaptureのキャプチャ結果をPlaneメッシュにアタッチしたマテリアルに投影してみます。

ポストプロセスマテリアル
ポストエフェクトなし(お洋服はDefaultLit)
ポストエフェクトあり


メインビューとSceneCaptureの両方でポストエフェクトが適用されていることを確認できました。
動作不良な箇所があったらコメントくださいな。気分次第では修正します。

可愛くてとっても好き。

VSMはUnlitマスクしている

VSMだとUnlitに影が落ちないのですが、どうやらVSM側でマスクをしているみたいです。

筆者は負荷的にVSM使っていない民なので、対応するほどの興味は沸きませんでした。

マスクされてるねぇ