【UE5】GBufferのCustomDataにアウトラインカラーとマスクを格納してみた – Unreal Engine 5.4

Unreal Engine

水ノ茉(こおり)の宣伝

叡智でえっちな同人作品を予定中…

Ci-en R18

同人作品の宣伝

  • 準 備 中 . . .

プラグインの宣伝

  • 準 備 中 . . .
  • 準 備 中 . . .

始まり

GBufferのCustomDataに任意の値を格納するエンジン改造をしていきます。

CustomDataとは?

読んで字の如くカスタムなデータです。

Unreal Engineには幾つかのシェーディングモデルがあり、それぞれで使用する値の種類が違います。

例えば以下のような感じ。

シェーディングモデル使用する値の種類
SubsurfaceSubsurface Color
Opacity
Clear CoatClear Coat
Clear Coat Roughness
Clear Coat Bottom Normal
ClothSubsurface Color
Cloth

これらシェーディングモデルごとに異なる種類のデータの格納先がCustomDataです。

エンコードの流れ

BasePassでGBufferに書き込むときの流れ。

// 格納したいデータたち
float3 OutlineColor = ...;
float OutlineMask = ...;
GBuffer.OutlineColor = OutlineColor;
GBuffer.OutlineMask = OutlineMask;

// 格納したい子を一時的にCustomDataに突っ込む
// シェーディングモデルごとに突っ込む内容が変わるよ
if (シェーディングモデルがToonLitの時)
{
    GBuffer.CustomData.rgb = GBuffer.OutlineColor;
    GBuffer.CustomData.a = GBuffer.OutlineMask;
}

// GBufferにCustomDataを書き込む
MRT[XX].r = GBuffer.CustomData.r;
MRT[XX].g = GBuffer.CustomData.g;
MRT[XX].b = GBuffer.CustomData.b;
MRT[XX].a = GBuffer.CustomData.a;

デコードの流れ

書き込んだGBufferを使う時の流れ。

// GBufferからCustomDataに戻す
GBuffer.CustomData.r = MRT[xx].r;
GBuffer.CustomData.g = MRT[xx].g;
GBuffer.CustomData.b = MRT[xx].b;
GBuffer.CustomData.a = MRT[xx].a;

// CustomDataから元の場所に戻す
if (シェーディングモデルがToonLitの時)
{
    GBuffer.OutlineColor = GBuffer.CustomData.rgb;
    GBuffer.OutlineMask = GBuffer.CustomData.a;
}

// 値の受け渡しの役目が終わったらゼロ埋め
GBuffer.CustomData = 0;

エンジン改造とは?

ゲームエンジンのコードを書き換える行為をエンジン改造と呼びます。

尚エンジン改造という呼称が正式かは知らんです。

これ以上の真面目な説明はググればヒットするので割愛します。

以下、実際の描画まわりの改造事例です。

前髪を透かしたり

毛先まで綺麗に表現できるポストプロセスなアウトラインを描いたり

キャラと背景を別々のシャドウマップに書き込んでセルフシャドウを抑制したり

髪影を落としたり

頂点IDノードを追加して、それを元に鼻に線を引いたり

独自のトゥーンパス群ですべて描いたり

Substrate (旧: Strata) や Ray Tracing, Path Tracing などは非対応

この子たちは現実的なライティングをする際にお世話になります。

トゥーン・セル・アニメ調を前提とした改造方針の場合は関わることが少ないので非対応とします。

データの流し方が違うので、両対応って結構面倒なんです。

リポジトリ

最新バージョンのみ保守しています。
過去バージョンが動かないと言われても基本的には知らんがなスタイルです。

実装

GBufferのエンコード、デコードは、実装があっちこっちに散らばっていて、時系列順に説明するのが難しいです。
ざーっと目を通してから読み直す方が理解がすんなり進むと思います。

Engine/Source/Runtime/RenderCore/Public/GBufferInfo.h

アウトラインカラーとアウトラインマスクの定義を追加します。

GBufferSlotの頭文字を取ってGBSです。

githubで実装を見る(L44-L45)

Engine/Source/Runtime/Engine/Private/ShaderCompiler/ShaderGenerationUtil.cpp

先ほどのGBSの名称を取得する関数です。

名称は後述するシェーダー側のFGBufferData構造体で定義しているものと一致させる必要があります。
※尚5.4時点でもこの仕組みは整備されていないため、GBSまわりはぶっちゃけ実装スキップしてもいいです。

githubで実装を見る(L518-L521)

GBufferに格納する要素を指定しています。

CustomDataスロットを格納するパターンと、使用するスロットごとに格納する2パターンがあります。

―――まぁ、嘘なんですけどね。

SetSlotsForShadingModelType関数を使用している箇所がないため、この仕様は存在していません。

将来的にやりたいことの布石なんだろなぁ程度の認識でいいと思います。

たぶん、こんな感じね。

// 現行

// SetGBufferForShadingModel関数でCustomDataにマージ
GBuffer.CustomData.rgb = GBuffer.OutlineColor;
GBuffer.CustomData.a = GBuffer.OutlineMask;

// マージされたものを書き込み
MRT[XX] = GBuffer.CustomData.x;
MRT[XX] = GBuffer.CustomData.y;
MRT[XX] = GBuffer.CustomData.z;
MRT[XX] = GBuffer.CustomData.w;
// 予想

// SetGBufferForShadingModel関数でCustomDataにマージ
if (bMergeCustom)
{
    GBuffer.CustomData.rgb = GBuffer.OutlineColor;
    GBuffer.CustomData.a = GBuffer.OutlineMask;
}

// 実際は動的分岐じゃなくてバリエーションだろうけど、可視化の都合、動的分岐で書いてるよ
if (bMergeCustom)
{
    // マージされたものを書き込むバージョン
    MRT[XX].r = GBuffer.CustomData.x;
    MRT[XX].g = GBuffer.CustomData.y;
    MRT[XX].b = GBuffer.CustomData.z;
    MRT[XX].a = GBuffer.CustomData.w;
}
else
{
    // 使用するスロットを格納、スロットの割り当てはC++側で自動的に詰める
    MRT[XX].r = GBuffer.OutlineColor.r;
    MRT[XX].g = GBuffer.OutlineColor.g;
    MRT[XX].b = GBuffer.OutlineColor.b;
    MRT[XX].a = GBuffer.OutlineMask;
}

githubで実装を見る(L1750-L1758)

という訳で現行ではGBS_CustomDataにマージされた値を格納する仕様のみが採用される感じです。

githubで実装を見る(L1912)

Engine/Source/Runtime/RenderCore/Private/ShaderMaterialDerivedHelpers.cpp

シェーディングモデルのToonLitがCustomDataに書き込めるようにしています。

UE5からGBufferエンコード、デコードまわりのシェーダーコードがC++で作成されるようになりました。

完全な移行は出来ておらず、現状ではC++とシェーダーの両対応が必要という、保守管理がちょっと怠い感じです。

githubで実装を見る(L50-L54)

Engine/Shaders/Private/BasePassCommon.ush

「シェーディングモデルのToonLitがCustomDataに書き込めるようにしています。」のシェーダー側の対応です。

githubで実装を見る(L45-L49)

Engine/Shaders/Private/DeferredShadingCommon.ush

FGBufferData構造体にアウトラインカラーとアウトラインマスクを追加しています。

githubで実装を見る(L315)

CustomDataを持っている、書き込んでいるシェーディングモデルにToonLitを追加しています。

この対応を忘れるとGBufferには書き込めているけど、デコードする際に仲間外れにされて、ゼロ埋めされるという初見殺しポイントです。

githubで実装を見る(L392-L395)

アウトラインカラーとマスクは一時的にCustomDataに入っているので、それを元の場所に移動して、CustomDataはゼロにするという部分です。

先ほど対応したHasCustomGBufferData関数の結果が偽だとCustomDataがゼロになる箇所も一緒に見れますね。

githubで実装を見る(L993-L1003)

Engine/Shaders/Private/ShadingModelsMaterial.ush

アウトラインカラーとマスクをCustomDataに格納している箇所です。

これを忘れるとGBufferに書き込みされないです。

githubで実装を見る(L205-L211)

Engine/Shaders/Private/BasePassPixelShader.usf

SetGBufferForShadingModel関数でCustomDataに突っ込む前にカラーとマスクを格納しています。

githubで実装を見る(L1021-L1027)

Engine/Shaders/Private/GBufferHelpers.ush

先ほど対応したDecodeGBufferData関数と同様です。

githubで実装を見る(L386-L396)

Engine/Shaders/Private/PostProcessGBufferHints.usf

マウスカーソル位置のGバッファを可視化するデバッグ機能にアウトラインカラーとマスクを対応します。

デバッグ機能は r.PostProcessing.GBufferPicking 1 で有効化されます。

本デバッグ機能はRHIがDirectX12の場合に動作します。Vulkanは知りません。

githubで実装を見る(L234-L238)

簡単な動作確認

GBufferに格納する部分だけ整備していて、使用する部分は未整備です。

動作確認はRenderDocで軽く見ちゃいます。

RenderDocの他には r.PostProcessing.GBufferPicking 1 で確認するのもアリです。

BasePassを見るとこんな感じで書き込まれている
アウトラインカラーとマスク

おわり!!!

ピンの追加と違って結構簡単なんよね。

あとはデバッグ用に可視化とポストプロセスマテリアルで使えるようにノードの対応ですね。

関連するエンジン改造

気分転換

元々はこれで1本書く予定でしたが、キャラのシルエットに近しいアニメーションを見つける作業をしていたら飽きちゃった。

ポストプロセスで桜の花びらを作成。

動画をインポートして構成を完全再現するのは面倒なので、Aviutlで構成とフレームをざっくり確認して配置。

トゥーン描画パス群

UnityでいうところのForward+が出来るように専用パス群を新設しました。

技術的な部分の構築がメインでシェーダーは適当です。

我ながらトゥーンレンダリングの欲望詰め込みセットが出来て満足です。

オトメき Booth ♪

発送通知♪

届いた♪

ひゃあああ。

かわい!

全貌を見たいけれど畳むのが面倒なので今はお預けです。

大人しくしまっておきますわ。

ごめん、やっぱり我慢できんわ。

開封!!!

ひゃあああああ。

かぁあぁあいぃぃぃ。

満足です。

120時間

2末から制作を始めて早3週間。

企画に20時間、トゥーンに特化した描画パス構築に40時間、クエスト機能に50時間、残りは雑務や検証…

実務だと工数見積もりが必須なのでプライベートでも練習がてら記録を付けるようにしています。

振り返りが出来るので結構おすすめです。

クエスト機能に異様な時間を要していますが、言い訳をすると本業が描画R&Dプログラマなので、インゲームを触る機会がないんですよね。厳密には機会はあるけど、インゲームに興味がないから我儘を言って担当から外してもらっています。個人制作は例外ですけどね。

ブループリントで組んだものって内部的にTConstArrayViewに変換されていたりするんですかね。コンテナのメモリまわりがちょっと気になる。あとstd::find_ifに似たノードがないのも不便。C++ならAlgoでラッピングされているから特に困らないんだけど。

やっぱりインゲームプログラマって大変というか、面倒そうな担当ですね。

描画は所詮、APIが非対応なことは出来ない、それ以外は処理負荷や割当リソースに応じて可能という少ない選択肢しか用意されていないので、他担当と比較するとおそらく簡単な方です。

やってみないと分からないことは多いですね。

個人の範疇ならインゲームもなんだかんだコード書けるし楽しいかも。

面倒だけど。