[UE5]Lumenのチラチラの原因を調べてみる

## はじめに

こちら記事はUnreal Engine (UE) Advent Calendar 2023 の記事になります。
また、こちらはUE5.3.2のレンダリングの不具合をもとにした記事です。

https://qiita.com/advent-calendar/2023/ue

:::note info
結論を先に書いておくと、Githubなどから見ることの出来る最新のエンジンではここで紹介する問題は修正されています。
Epic Games Launcherからダウンロードできる最新の5.3.2ではまだ改善されていないので、調査の過程でどのようなことを行ったのか含め参考になれば幸いです。

:::

## 環境
- OS：Windows10
- GPU：RTX2070
- エンジンバージョン：UE 5.3.2
- プロジェクト：建築インテリアレンダリング（Epic Games Launcharのサンプルから無料で入手可能）
![HighresScreenshot00000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/386da171-9618-e8de-ef06-4fd031a37d57.png)
  - ライトをディレクショナルライトのみに変更し明るさ調整
  - Reflection Captureの削除
  - Post Processs VolumeでGIとReflectionの計算方法をLumenに変更
      - 特にReflectionに関しては同じ設定にしないと問題を再現できない可能性が高いです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/57c20558-b816-3ea5-fc2d-55f4e02e4c2f.png)


:::note info
また、プロジェクトではハードウェアレイトレを使用しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/c352efc1-4baa-fe84-a183-6c740bf4bf2d.png)

:::


## Lumenのライティングがチラチラする
早速本題に入ります。先ほど紹介したプロジェクトでの一場面です。こちらの動画をご覧ください。
**金属材質の花びんの表面にチラチラとした明るいノイズ**が確認できます。中央向って左側あたりが分かりやすいかも。
![Jonathan2023.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/b8c99e77-83fa-87ce-b2f7-36c1c60455e8.gif)

カメラを動かしておらず、かつ見ているものが変わらないのにチラチラとしたノイズが見えるのは違和感を覚えます。

今回はこの原因と改善方法を探っていきます。



## エディタでちらつきの原因を調べてみる
レンダリングの問題（今回で言えばチラチラノイズ）にぶつかった際にどんな問題なのかを調べる必要があります。
そこで今回はUE5のツールの一つである**GPUDump**という機能を使いレンダリングのデバックを行います。

#### GPUDumpを使ってみる
GPUDumpは1フレームのグラフィック処理の手軽なデバックが出来るツールです。サードパーティーの同じようなツールもありますが、今回はこちらを使います。

https://docs.unrealengine.com/5.0/ja/gpudump-viewer-tool-in-unreal-engine/

https://historia.co.jp/archives/26483/

詳しい機能は上のドキュメントなどを見て頂ければと思いますが、ここではこちらを使用してチラチラノイズがどのような問題なのかを見つけていきます。
エディタで`CTRL + Shift + /` を押すとGPUDumpが実行されます。（暫く待つ必要があります。）
実行されるとプロジェクトのルートディレクトリの`Saved/GPUDumps`に1フレームのキャプチャ結果が格納されます。また、出力が完了すると自動でこのダンプの場所がエクスプローラーで開きます。

結果を見てみましょう。
`OpenGPUDumpViewer.bat`をダブルクリックするとWebブラウザなどでキャプチャした結果が見れるかと思います。
![2023-12-09_15h07_26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/ba96c2a1-4a2d-3ff2-9251-cab28eb6e47e.png)

こちらは上から下に向ってGPUの処理を確認することが出来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/1b7e7ca8-950b-7aa0-3adf-44f5bbc6e24a.png)


それぞれのコマンドをポチポチをクリックしていくと、それぞれのステップでどのような描画が行われているかが確認できます。最初は調べるの時間がかかりますが気合です。
今回のチラチラとした問題は、`LumenReflections->ReflectionResolve`という箇所で確認できます。どうやら、このあたりの処理が怪しそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/2101aded-74ed-b4d3-0242-b38a1c62b4c6.png)

LumenReflectionsで計算されているものは[スぺキュラ反射](https://cgworld.jp/terms/%E3%82%B9%E3%83%9A%E3%82%AD%E3%83%A5%E3%83%A9.html)というものです。
本当にチラチラノイズの問題がLumenのスぺキュラ反射なのかをエディタで確認してみましょう。
エディタのViewportのShowリストからスぺキュラ反射をオフにした表示ができます。
確認してみるとモヤモヤとしたノイズがでません。(スぺキュラ反射すべてがオフになるので真っ黒になります。)
確かにGPUDumpで確認した通りモヤモヤの原因はスぺキュラ反射で間違いなさそうです。
![Editor_SpecularOff.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/5fb77ae5-1e97-84b3-3cba-43e61bd675f6.gif)

ここまでの調査で「**今回**のLumenのチラチラノイズ」＝「Lumenのスぺキュラ反射処理の問題」であることが分かりました。
（一応すべてのチラチラしているノイズが「Lumenのスぺキュラ反射」が原因ではないので**太字**にしています。）

#### 近くの処理も見てみる
せっかく出力したGPUDumpがあるので、試しに近くの処理の結果も見てみましょう。もしかしたら原因が分かるかもしれません。
`LumenReflections->GenerateRays`という処理を見てみます。何やらカラフルのバッファーが表示されました。実はこのバッファーは**スぺキュラ反射を計算する方向**を示しています。別の言い方をすれば、ピクセルの色がどの方向からの光をスぺキュラ反射の計算に使用するかを表しているということです。

![2023-12-09_15h36_46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/275e6774-6f15-764a-d18d-9949b93bda33.png)

拡大してみてみると、緑のピクセルが殆どを占めている中に幾つかポツポツと色の異なるピクセルが存在しています。先ほどピクセルの色はスぺキュラ反射の光を計算する方向を示していると言いました。つまり**色が周囲と大きく異なるピクセルは、スぺキュラ反射を計算する際に周囲とは大きく違う方向からの光をスぺキュラ反射の光として計算している**ということです。（この辺りはやや内容が難しいかもしれません。）
![2023-12-09_15h37_46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/df2c90d0-3287-3e1f-ada0-de86ce742b2c.png)

もしも、**緑色ピクセルが表す方向から来る光が暗く**、**それ以外のピクセルが表している方向から来る光が明るい**場合を考えてみましょう。
スぺキュラ反射のライティングの結果として殆どが暗い結果の中に、一部明るいピクセルがまじりそうです。

先ほどの`LumenReflections->ReflectionResolve`の結果を見てみましょう。
考えた通り、殆どが暗い結果のピクセルの中に明るい結果が表れていて、チラチラノイズの原因になっていそうです。
![2023-12-09_16h05_49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/940d363b-5e94-5537-f7c1-79024145a15c.png)



ここまででLumenのスぺキュラ反射がチラチラする原因が何となく特定出来てきました。
では、ここからは実際の処理を行っている**シェーダー内部**を見てみましょう。


## 原因をシェーダーから調べてみる
先ほどのDumpGPUの処理から`LumenReflections->GenerateRays`に問題があることが分かりました。
ここからは実際のGPUの処理が書かれているシェーダーを見たり触ってこの問題の原因を調べていきます。

エンジンがインストールされているディレクトリ内の`Engine/Shaders/`の中を何かしらのコードエディタで検索してみます。

:::note warn
`Engine/Shaders/`はEpic Games Launcherからエンジンを落とすだけで見ることが出来ます。Githubなどからエンジンのソースを入手する必要はありません。
:::



調べるのは先ほどの、スぺキュラ反射の方向を生成する`GenerateRays`処理が良さそうです。全検索などで調べてみると、`LumenReflections.usf`内に`ReflectionGenerateRaysCS`という処理があります。この中身を見てみましょう。


詳細は割愛しますが、反射方向の計算はこちらで行われています。GGXSampleというところで方向をサンプルして、それをワールド空間での反射計算の方向に変換している処理になります。
```hlsl:LumenReflections.usf
void ReflectionGenerateRaysCS(uint GroupId : SV_GroupID,uint GroupThreadId : SV_GroupThreadID)
{    
    ...
    float4 GGXSample = ImportanceSampleVisibleGGX(E, Alpha, TangentV);
    float3 WorldH = mul(GGXSample.xyz, TangentBasis);
    RayDirection = reflect(CameraVector, WorldH);
    ...
}
```

この中の`ImportanceSampleVisibleGGX`が物体のマテリアルの粗さをもとに方向を計算している関数になります。
``ImportanceSampleVisibleGGX``の中身を見てみましょう。こちらの関数の定義は`MonteCarlo.ush`にあります。

```hlsl:MonteCarlo.ush
float4 ImportanceSampleVisibleGGX(float2 E, float2 Alpha, float3 V)
{
	// stretch
	float3 Vh = normalize(float3(Alpha * V.xy, V.z));
 
	// "Sampling Visible GGX Normals with Spherical Caps"
	// Jonathan Dupuy & Anis Benyoub - High Performance Graphics 2023
	float Phi = (2 * PI) * E.x;
	float Z = lerp(-Vh.z, 1.0, E.y);
	float SinTheta = sqrt(saturate(1 - Z * Z));
	float X = SinTheta * cos(Phi);
	float Y = SinTheta * sin(Phi);
	float3 H = float3(X, Y, Z) + Vh;

	// unstretch
	H = normalize(float3(Alpha * H.xy, max(0.0, H.z)));

	return float4(H, VisibleGGXPDF_aniso(V, H, Alpha));
}
```

実装の詳細説明は割愛しますがこちらは、[Jonathan Dupuy & Anis Benyoub 2023, "Sampling Visible GGX Normals with Spherical Caps"](https://arxiv.org/pdf/2306.05044.pdf)という論文をもとに実装されています。論文の中に実装例が紹介されているので転載します。
```hlsl
// Sampling the visible hemisphere as half vectors (our method)
vec3 SampleVndf_Hemisphere(vec2 u, vec3 wi)
{
     // sample a spherical cap in (-wi.z, 1]
     float phi = 2.0f * M_PI * u.x;
     float z = fma((1.0f - u.y), (1.0f + wi.z), -wi.z);
     float sinTheta = sqrt(clamp(1.0f - z * z, 0.0f, 1.0f));
     float x = sinTheta * cos(phi);
     float y = sinTheta * sin(phi);
     vec3 c = vec3(x, y, z);
     // compute halfway direction;
     vec3 h = c + wi;
     // return without normalization (as this is done later)
     return h;
}
```

変数の表記が違いますが、実装の中身を比較するとエンジン実装と論文実装とで```Z```の結果が異なります。

:::note info
エンジン実装の```E```が論文実装の```u```、```Vh```が``wi``に相当します。
:::


比較してみると

```hlsl
//エンジン実装
Z = lerp(-Vh.z, 1.0, E.y) = E.y - Vh.z - Vh.z*E.y

//論文実装
z = fma((1.0f - u.y), (1.0f + wi.z), -wi.z) = 1.0 - u.y - wi.z*u.y
```
このような違いです。エンジン側の実装を以下のように変更すると論文実装と同じ結果になります。

```hlsl
//エンジン実装（変更後）
float Z = lerp(1.0, -Vh.z, E.y); //変更前 : float Z = lerp(-Vh.z, 1.0, E.y);
```




ソースの変更を保存してエディタで`CTRL + Shift + .`を行うとシェーダーのコンパイルが行われます。暫くコンパイルを待って描画の比較をしてみましょう。


## 描画の比較
変更前
![Jonathan2023.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/f21401f0-7b20-b767-741f-4451e5915efc.gif)

変更後
![Heitz2018.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/0f8fd95d-e33e-4048-7717-5b672816ff7e.gif)


違いが分かるでしょうか？分かりづらいかもしれませんが 金属の花びんの中央向って左の部分を見るとチラチラとしたノイズが無くなっていることが分かります。多少の改善にはなったのではないでしょうか。

また、GPUDumpで反射方向のバッファーも見てみるとピクセルの色のエラーが無くなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193601/7210589d-04d3-f684-0073-83f5b96a0da3.png)

:::note info
こちらの修正は冒頭でも言ったとおりGithubなどから見ることのできる最新のエンジンソースでも同様の修正が行われています。恐らくUE5.4などの後のバージョンでは反映されていると思われます。

何か問題に遭遇した場合に最新のエンジンソースを見ると実は解決しているかもしれません。自身で調査するよりも速く解決できるかもしれないので、そちらを確認することも重要かと思います。
:::


## 最後に
レンダリングの気になる箇所を見つけ、GPUDumpなどのデバック機能を使い問題点を見つけ、シェーダーを改善することでレンダリング品質を改善するという流れが紹介できたかなと思います。

実際のゲーム開発などでは、より詳しい調査が必要な場面や原因が分かりづらい問題が殆かと思います。この記事は最初の一歩の一つの例として参考になれば幸いです。