はじめに
こちら記事はUnreal Engine (UE) Advent Calendar 2023 の記事になります。
また、こちらはUE5.3.2のレンダリングの不具合をもとにした記事です。
結論を先に書いておくと、Githubなどから見ることの出来る最新のエンジンではここで紹介する問題は修正されています。
Epic Games Launcherからダウンロードできる最新の5.3.2ではまだ改善されていないので、調査の過程でどのようなことを行ったのか含め参考になれば幸いです。
環境
Lumenのライティングがチラチラする
早速本題に入ります。先ほど紹介したプロジェクトでの一場面です。こちらの動画をご覧ください。
金属材質の花びんの表面にチラチラとした明るいノイズが確認できます。中央向って左側あたりが分かりやすいかも。
カメラを動かしておらず、かつ見ているものが変わらないのにチラチラとしたノイズが見えるのは違和感を覚えます。
今回はこの原因と改善方法を探っていきます。
エディタでちらつきの原因を調べてみる
レンダリングの問題(今回で言えばチラチラノイズ)にぶつかった際にどんな問題なのかを調べる必要があります。
そこで今回はUE5のツールの一つであるGPUDumpという機能を使いレンダリングのデバックを行います。
GPUDumpを使ってみる
GPUDumpは1フレームのグラフィック処理の手軽なデバックが出来るツールです。サードパーティーの同じようなツールもありますが、今回はこちらを使います。
詳しい機能は上のドキュメントなどを見て頂ければと思いますが、ここではこちらを使用してチラチラノイズがどのような問題なのかを見つけていきます。
エディタでCTRL + Shift + /
を押すとGPUDumpが実行されます。(暫く待つ必要があります。)
実行されるとプロジェクトのルートディレクトリのSaved/GPUDumps
に1フレームのキャプチャ結果が格納されます。また、出力が完了すると自動でこのダンプの場所がエクスプローラーで開きます。
結果を見てみましょう。
OpenGPUDumpViewer.bat
をダブルクリックするとWebブラウザなどでキャプチャした結果が見れるかと思います。
こちらは上から下に向ってGPUの処理を確認することが出来ます。
それぞれのコマンドをポチポチをクリックしていくと、それぞれのステップでどのような描画が行われているかが確認できます。最初は調べるの時間がかかりますが気合です。
今回のチラチラとした問題は、LumenReflections->ReflectionResolve
という箇所で確認できます。どうやら、このあたりの処理が怪しそうです。
LumenReflectionsで計算されているものはスぺキュラ反射というものです。
本当にチラチラノイズの問題がLumenのスぺキュラ反射なのかをエディタで確認してみましょう。
エディタのViewportのShowリストからスぺキュラ反射をオフにした表示ができます。
確認してみるとモヤモヤとしたノイズがでません。(スぺキュラ反射すべてがオフになるので真っ黒になります。)
確かにGPUDumpで確認した通りモヤモヤの原因はスぺキュラ反射で間違いなさそうです。
ここまでの調査で「今回のLumenのチラチラノイズ」=「Lumenのスぺキュラ反射処理の問題」であることが分かりました。
(一応すべてのチラチラしているノイズが「Lumenのスぺキュラ反射」が原因ではないので太字にしています。)
近くの処理も見てみる
せっかく出力したGPUDumpがあるので、試しに近くの処理の結果も見てみましょう。もしかしたら原因が分かるかもしれません。
LumenReflections->GenerateRays
という処理を見てみます。何やらカラフルのバッファーが表示されました。実はこのバッファーはスぺキュラ反射を計算する方向を示しています。別の言い方をすれば、ピクセルの色がどの方向からの光をスぺキュラ反射の計算に使用するかを表しているということです。
拡大してみてみると、緑のピクセルが殆どを占めている中に幾つかポツポツと色の異なるピクセルが存在しています。先ほどピクセルの色はスぺキュラ反射の光を計算する方向を示していると言いました。つまり色が周囲と大きく異なるピクセルは、スぺキュラ反射を計算する際に周囲とは大きく違う方向からの光をスぺキュラ反射の光として計算しているということです。(この辺りはやや内容が難しいかもしれません。)
もしも、緑色ピクセルが表す方向から来る光が暗く、それ以外のピクセルが表している方向から来る光が明るい場合を考えてみましょう。
スぺキュラ反射のライティングの結果として殆どが暗い結果の中に、一部明るいピクセルがまじりそうです。
先ほどのLumenReflections->ReflectionResolve
の結果を見てみましょう。
考えた通り、殆どが暗い結果のピクセルの中に明るい結果が表れていて、チラチラノイズの原因になっていそうです。
ここまででLumenのスぺキュラ反射がチラチラする原因が何となく特定出来てきました。
では、ここからは実際の処理を行っているシェーダー内部を見てみましょう。
原因をシェーダーから調べてみる
先ほどのDumpGPUの処理からLumenReflections->GenerateRays
に問題があることが分かりました。
ここからは実際のGPUの処理が書かれているシェーダーを見たり触ってこの問題の原因を調べていきます。
エンジンがインストールされているディレクトリ内のEngine/Shaders/
の中を何かしらのコードエディタで検索してみます。
Engine/Shaders/
はEpic Games Launcherからエンジンを落とすだけで見ることが出来ます。Githubなどからエンジンのソースを入手する必要はありません。
調べるのは先ほどの、スぺキュラ反射の方向を生成するGenerateRays
処理が良さそうです。全検索などで調べてみると、LumenReflections.usf
内にReflectionGenerateRaysCS
という処理があります。この中身を見てみましょう。
詳細は割愛しますが、反射方向の計算はこちらで行われています。GGXSampleというところで方向をサンプルして、それをワールド空間での反射計算の方向に変換している処理になります。
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
にあります。
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"という論文をもとに実装されています。論文の中に実装例が紹介されているので転載します。
// 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
の結果が異なります。
エンジン実装のE
が論文実装のu
、Vh
がwi
に相当します。
比較してみると
//エンジン実装
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
このような違いです。エンジン側の実装を以下のように変更すると論文実装と同じ結果になります。
//エンジン実装(変更後)
float Z = lerp(1.0, -Vh.z, E.y); //変更前 : float Z = lerp(-Vh.z, 1.0, E.y);
ソースの変更を保存してエディタでCTRL + Shift + .
を行うとシェーダーのコンパイルが行われます。暫くコンパイルを待って描画の比較をしてみましょう。
描画の比較
違いが分かるでしょうか?分かりづらいかもしれませんが 金属の花びんの中央向って左の部分を見るとチラチラとしたノイズが無くなっていることが分かります。多少の改善にはなったのではないでしょうか。
また、GPUDumpで反射方向のバッファーも見てみるとピクセルの色のエラーが無くなっています。
こちらの修正は冒頭でも言ったとおりGithubなどから見ることのできる最新のエンジンソースでも同様の修正が行われています。恐らくUE5.4などの後のバージョンでは反映されていると思われます。
何か問題に遭遇した場合に最新のエンジンソースを見ると実は解決しているかもしれません。自身で調査するよりも速く解決できるかもしれないので、そちらを確認することも重要かと思います。
最後に
レンダリングの気になる箇所を見つけ、GPUDumpなどのデバック機能を使い問題点を見つけ、シェーダーを改善することでレンダリング品質を改善するという流れが紹介できたかなと思います。
実際のゲーム開発などでは、より詳しい調査が必要な場面や原因が分かりづらい問題が殆かと思います。この記事は最初の一歩の一つの例として参考になれば幸いです。