こんにちは!! 株式会社スパーククリエイティブの小林です。

前回、セル・トゥーン表現用のシェーディングモデルToonLitを作成しました。今回は、ライティング処理を実装するための前準備として、GBufferレイアウトの変更とマテリアルプロパティ・入力ピンの実装をしていきます。

”前準備”とあるように、実装作業がメインなので、マテリアルを触ることがなく、映える挿絵がとても少ない回となっています。

記事の目次

    ※【2024年2月21日 一部記事内容修正】EULA(Unreal® Engineエンドユーザーライセンス契約)第4条a.項の内容に従い、コードの掲載方法を変更しました

    ディファードレンダリングのライティングについて(振り返り)

    Unreal Engine(以下、UE)でのディファードライティングについて、簡単におさらいをします。BasePassで法線やベースカラー、メタリックなどの材質情報・パラメータをGBufferに書き込みます。

    • ▲ベースカラー
    • ▲ワールド法線
    • ▲スペキュラ
    • ▲ラフネス

    LightsパスでGBufferを参照してライティング処理を行います。Lightsパスはライトごとにパスが生成・実行されるため、複数のライトを効率よく扱えます。

    今回は、この一連のながれのひとつであるGBufferの書き込み部分を実装していきます。

    ▲Lightsパスでディレクショナルライトのライティング処理
    ▲ポストエフェクトなどを適用した最終的な画

    GBufferレイアウトを考える

    セル・トゥーンのライティングを行うために必要なパラメータを、GBufferに格納する必要があります。

    いきなり格納するということは残念ながらできないため、まずはToonLitの「GBufferレイアウト」を考えるところから始めます。GBufferのどこに、なにを、格納・配置するかの意味を込めて「GBufferレイアウト」と呼んでいます。

    DefaultLitのGBufferレイアウトを見る

    いきなり独自のレイアウトを考えるのは難しいため、まずは汎用的に使用されるDefaultLitのGBufferレイアウトを見てみます。

    初期設定では、5枚のGBufferが使用されます。
    ※設定次第で接線が格納されるGBufferF、速度マップのVelocityは枚数から除いています。

    GBufferはアルファベット順に命名され、それぞれに固有のパラメータが格納されています。名称から推測が難しいパラメータと、UE固有のパラメータについての説明は以下のとおりです。

    Per Object GBuffer Dataは、プリミティブに Capsule Shadows や Contact Shadows が設定されているかのフラグ値が格納されています。設定されている場合は、影の落とし方を変える必要があるためです。詳細は公式ドキュメント(Capsule ShadowsContact Shadows)にお任せします。

    Custom Dataは、シェーディングモデルごとに固有のパラメータが格納される場所です。シェーディングモデルごとに異なるパラメータが格納され一意ではないため、名称が Custom(カスタム)となっています。細かく挙げると長くなるためざっくりとですが、Subsurface ColorやClear Coat、Opacityなど様々な値が格納されています。こちらも同様に詳細は公式ドキュメントShading Models)をご覧ください。

    Precomputed Shadow Factorsは、事前計算と書いてあるのでもしかしたらわかる方もいるかもしれませんが、これはライトベイクされた結果を格納しています。

    ToonLitのパラメータ数を洗い出す

    DefaultLitのGBufferレイアウトから察するに、レイアウトの空きはシェーディングモデルごとに固有のパラメータを格納できるCustomDataの4スロットのみとなります。
    さて、前回作成したセル・トゥーン表現に必要なパラメータ数はいくつでしょうか。軽く洗い出してみましょう。いったんは、パラメータの最適化はせず、片っ端からカウントします。

    DefaultLitでも使用している法線やベースカラーなどをカウントから除くと、追加で16スロット必要なことがわかりました。空きスロットの4に対して大幅に超過していますね。

    ToonLitのパラメータ数を削減する

    冒頭に”いったんは”と書いたとおり、何も考えず適当にパラメータを使用した結果が16スロットです。つまり、削減の余地があるということです。それではやっていきましょう。

    Face Forward VectorとFace Right Vector

    FaceShadowの種類によりますが、今回使用しているのは、高さ成分を除いた横と奥行きのライトベクトルだけを考慮した計算手法です。そのため、計算にはXYの2成分あれば十分です。
    ※UEは座標軸がUnityと異なる点に注意です。


    ▲赤矢印がX軸(奥行)、緑矢印がY軸(横)、青矢印がZ軸(縦)

    顔の右ベクトル(Face Right Vecotr)は前方ベクトル(Face Forward Vector)があれば外積から求められるため不要です。ノードで表すと以下のとおりです。
    ※正規化のタイミングを間違えているかもしれませんが、現実的ライティングをしているわけではないので良しとします。

    ▲削減前(6スロット)
    ▲削減後(2スロット)

    Face Shadow Enabled

    Face Shadowの有効性は Face Forward Vector の合計値が0.0か否かで判断することにして、スロットを削減します。

    スロット削減以前の根本的な話ですが、Faceと名の付くとおり、顔にのみ適用する表現です。こういう場合は、本当はToonFaceLitとして別のシェーディングモデルを作った方が手っ取り早いです。今回は大幅なレイアウト調整をする方法を紹介したかったので、ひとつのシェーディングモデルに詰め込んでいます。

    ▲Face Forward VectorがZeroVectorの場合はFace Shadowを無効とする

    Highlight EnabledとHighlight Mask

    ハイライトの有効性とマスクも Face Shadow Enabled と似たような感じで、 Highlight Exponent が0.0か否かで判断することでスロットを削減します。

    マスク値を用意している理由ですが、第3回から登場したタマモさんに獣耳が生えていることにあります。獣耳は形状・法線が複雑なため、自然なハイライト表現をするには調整にひと手間かかります。細かいパラメータ調整は今回の主軸とは異なるので、思い切ってマスクでハイライトを切ることにしました。

    • ▲白い箇所のみハイライト表現を適用
    • ▲マスク結果を基に、獣耳とアホ毛以外かつ眉より上にハイライト表現を適用

    パラメータの削減結果

    計算方法の見直しやひとつのパラメータに機能を統合することで、16スロットから9スロットにまで削減できました。

    GBufferレイアウトの空きの見直し

    パラメータ数を削減しても5スロットほど足りません。CustomDataが4スロットという時点で明らかに不足するだろうことは察せたかもしれませんが。

    GBufferに格納したいパラメータ数が5つ以上ある場合には、既存のGBufferスロットを置き換えるような対応が必要です。

    何を置き換えるかは、そのパラメータの使用状況に依存しますが、今回は以下のとおりです。置き換えることで7スロットほど追加で空きを作れました。

    ToonLitのGBufferレイアウト

    パラメータ数の削減と空きスロットを整理・確保し、レイアウト決めの準備が整いました。どのようなレイアウトにするかは実装者の好みに任せられます。

    ToonData0とToonData1という謎パラメータ名がありますが、これは、リムライトのパラメータを格納するか、事前計算陰という手法を紹介するかで悩んでおり、いったんは仮名で実装だけ進めるかたちを取った結果です。もしかしたら次回、名称の一括変更をするかもしれません。

    GBufferレイアウトとマテリアルプロパティ・入力ピンの実装

    ToonLitのGBufferレイアウトも決まりましたので、GBufferレイアウトとマテリアルプロパティ・入力ピンの実装に進みます。

    レイアウトについては先に説明したとおりで、マテリアルプロパティは、マテリアルの入力ピンのことを指します。エンジン側の実装名や公式ドキュメントがプロパティインプットピンで表記が揺れていますが、本連載では基本的にマテリアルプロパティ・入力ピンで進めていきます。

    追加するマテリアルプロパティ・入力ピンは、先ほどもGBufferパラメータとして登場した以下の7つです。エンジン改造のルールについては第4回を参照してください。

    ・ShadowColor
    ・HighlightExponent
    ・HighlightJitterIntensity
    ・ToonData0
    ・ToonData1
    ・FaceForwradVector
    ・FaceShadowTexture

    ▲右側のピンを接続する箇所の名称:マテリアルプロパティ・インプット・入力ピン

    実装内容の見かた

    実装は全てGitHubに上げていて、そのリンクをコード行数と併せて貼り付けています。

    ファイルごとにGitHubにアクセスして確認するのは手間だと思いますので、差分ファイルのみを纏めたReleasesを用意しています。

    これらのリポジトリにアクセスするには「エンジンの取得方法」の手順に含まれるEULAの同意・承認をしないと以下のような404エラーが発生します。EULAに同意・承認したGitHubアカウントからのアクセスをお願いします。

    Runtime/Engine/Public/SceneTypes.h

    必要なマテリアルプロパティを定義します。ShadowColorとHighlightExponentとHighlightJitterIntensityは、既存プロパティの表示名を変更することで流用可能なため、定義しません。

    GitHubで実装を見る(L194-L199)

    Runtime/Engine/Private/Materials/MaterialAttributeDefinitionMap.cpp

    入力ピンが接続されていない場合の初期値を指定します。

    ▲ピンが接続されていない場合の初期値設定

    初期値は後述するAdd関数の第5引数で指定されていますが、SubsurfaceColorとCustomData0、CustomData1は、複数のシェーディングモデルで使用されるプロパティのため、シェーディングモデルごとに初期値を設定した方が適切です。

    SubsurfaceColorとCustomData0は1.0、CustomData1は0.1が初期値として指定されており、ToonLitでは0.0を初期値としたいため、変更を加えています。

    GitHubで実装を見る(L36-L42) GitHubで実装を見る(L48-L60)

    追加したマテリアルプロパティのアトリビュートを設定します。

    GitHubで実装を見る(L294-L299)

    ToonLitを選択した際の入力ピンの表示名を変更していきます。

    ▲左からDefaultLit、ClearCoat、ToonLitを選択した際の表示名

    SubsurfaceColorピンの表示名をShadow Colorにします。

    GitHubで実装を見る(L423-L425)

    CustomData0ピンの表示名をHighlight Jitter Intensityにします。

    GitHubで実装を見る(L433-L435)

    CustomData1ピンの表示名をHighlight Exponentにします。

    GitHubで実装を見る(L440-L442)

    新しく追加した入力ピンはToonLitのみ接続が有効なため、CustomPinNames.Add関数によるシェーディングモデルごとの分岐を用意せずにキャッシュから取得した文字列を直にreturnします。

    GitHubで実装を見る(L474-L483)

    Runtime/Engine/Classes/Materials/Material.h

    入力ピンに格納される型に応じて用意します。
    これらはFMaterialInputのテンプレート構造体で、初期値を保持していたり、入力ピンとノードの接続情報を保持していたりします。

    GitHubで実装を見る(L396-L408)

    Runtime/Engine/Private/Materials/Material.cpp

    マテリアルプロパティの総数を変更してアサートを解消します。マテリアルプロパティは拡張を想定されているのか、所々でアサートが仕込まれており、変更するべき箇所が意外とわかりやすいです(シェーディングモデルにもアサート仕込んでほしい)。

    GitHubで実装を見る(L2747-L2753)

    OutDescriptionに情報をコピーしている部分です。

    GitHubで実装を見る(L5600-L5605)

    MaterialAttributeDefinitionMap.cppで実装した初期値、その関数を呼んでいる部分です。

    GitHubで実装を見る(L6212-L6217)

    入力ピンの有効性を指定します。ToonLitではAOとSpecular、Metallicは入力を受け付ける必要がないため、無効にします。

    GitHubで実装を見る(L6737-L6760) GitHubで実装を見る(L6767-L6773)

    SubsurfaceColorとCustomData0、CustomData1は有効にします。

    GitHubで実装を見る(L6782-L6788) GitHubで実装を見る(L6791-L6797) GitHubで実装を見る(L6800-L6806)

    追加で定義したプロパティは当然、有効にします。

    GitHubで実装を見る(L6828-L6835)

    Editor/MaterialEditor/Private/MaterialEditor.cpp

    マテリアルプロパティがスカラー型ならUMaterialExpressionScalarParameterを、ベクター型ならUMaterialExpressionVectorParameterを返します。

    GitHubで実装を見る(L4640-L4643) GitHubで実装を見る(L4653-L4656)

    Editor/UnrealEd/Private/MaterialGraph.cpp

    追加で定義したマテリアルプロパティを入力ピンとして表示する順番を指定します。指定とは書きましたが、シリアライズ対応が面倒になるため、末尾に追加していくことを推奨します。

    ▲Front Materialの下に表示されていく
    GitHubで実装を見る(L267-L272)

    Runtime/Engine/Classes/Materials/MaterialExpressionMakeMaterialAttributes.h

    MaterialAttributesノード用の入力ピンを定義します。

    ▲MakeMaterialAttributesノード
    GitHubで実装を見る(L74-L86)

    Runtime/Engine/Private/Materials/MaterialExpressions.cpp

    MaterialAttributesノードまわりの実装です。

    GitHubで実装を見る(L6624-L6629) GitHubで実装を見る(L6656-L6662) GitHubで実装を見る(L6690-L6695) GitHubで実装を見る(L6754-L6760)

    OutputIndexは、Outputs.Addされた順番に依存します。マジックナンバーになっているため、少しだけ取り扱いに注意してください。

    GitHubで実装を見る(L6792-L6797) GitHubで実装を見る(L6839-L6845) GitHubで実装を見る(L6883-L6888)

    Runtime/RenderCore/Private/ShaderMaterialDerivedHelpers.cpp

    CustomDataをGBufferに書き込む条件にToonLitを追加します。

    GitHubで実装を見る(L53-L59)

    Runtime/Engine/Private/Materials/HLSLMaterialTranslator.cpp

    SubsurfaceColorに接続されたノード(非接続の場合は初期値)を展開する条件にToonLitを追加します。

    GitHubで実装を見る(L954-L960)

    追加したマテリアルプロパティも同様の対応をします。

    GitHubで実装を見る(L1002-L1007)

    マテリアルプロパティ・入力ピンをHLSLコードとして挿入している箇所です。詳細はシェーダコード側で改めて触れます。

    GitHubで実装を見る(L2403-L2408)

    Runtime/Engine/Public/MaterialCachedData.h

    マテリアルプロパティを1つ以上追加する場合には、一部の変数型をuint32からuint64に拡張する必要があります。理由はビット演算絡みです。MP_SurfaceThicknessを見るとこの時点で31なので、追加した時点でオーバーします。

    “新しいマテリアルプロパティはここに書いてね”と親切に案内している割には、こういう隠れ対応を要求するのはいかがなものかと思いますが、安心してください、5.3からは標準でuint64対応が入っています。本音を言うともっと早く入れてほしかったですけどね。

    ▲MP_SurfaceThicknessの時点で31
    GitHubで実装を見る(L248-L254) GitHubで実装を見る(L259-L265)

    uint64対応、その2。

    GitHubで実装を見る(L356-L364)

    Runtime/Engine/Private/Materials/MaterialCachedData.cpp

    uint64対応、その3。

    GitHubで実装を見る(L39-L45)

    SetMatAttributeConditionally関数で該当するマテリアルプロパティ・入力ピンが接続されている場合に、ビットを立てています。この実装が原因で、uint64対応が必要になったというわけでした。

    GitHubで実装を見る(L512-L517)

    Runtime/Engine/Public/MaterialExpressionIO.h

    uint64対応、その4。

    GitHubで実装を見る(L305-L311) GitHubで実装を見る(L316-L322) GitHubで実装を見る(L326-L332) GitHubで実装を見る(L340-L346)

    Runtime/Engine/Classes/Materials/MaterialExpression.h

    uint64対応、その5。

    GitHubで実装を見る(L85-L93)

    シェーダファイルの拡張子について

    前回サラっとシェーダの編集をしていましたが、他では見ない拡張子が付与されていたと思います。これらはUEがシェーダファイルに割り当てている独自の拡張子です。

    ▲Shadersフォルダから抜粋

    拡張子はushusfの2種あります。各拡張子のフルネームと特徴は以下のとおりです。

    ush(Unreal Shader Headers)は、usfやushからインクルードすることが可能です。そのため、定義や関数の実装場所として多く使用されます。複数インクルードされることもあるため、ファイルの先頭には #pragma once を記述することが推奨されています。

    ▲コピーライト、説明、インクルードガードの順番がテンプレート

    usf(Unreal Shader Format)は、シェーダのエントリーポイントやエントリーポイントの直後に呼び出される関数の実装ファイルに使用されます。

    ▲ReflectionEnvironmentSkyLightingがエントリーポイント

    下記の BasePassPixelShader.usf にはエントリーポイントはありませんが、PixelShaderOutputCommon.usf という雑にいうとピクセルシェーダの親玉的存在から FPixelShaderInOut_MainPS 関数が呼ばれているため、実質エントリーポイント扱いとなり、usfでファイルが作成されています。

    ▲FPixelShaderInOut_MainPSは(実質)エントリーポイント

    ushとusfで役割が分かれているとおり、usfはushのようなインクルードする運用は BasePassPixelShader.usf のような一部例外はありますが基本的には非推奨です。

    コンパイルルールで弾かれないため、厳密な禁止は唱えられないのですが、大雑把に使うとエラー発生時のログから得られる情報が不正確になったり、Epicの気分次第では今後厳格化するとも限らないので、筆者的にはエントリーポイントはusf、それ以外はushという使い分けを推奨しています。

    ▲Shadersフォルダでusfをインクルードしているファイルをリストアップ

    Shaders/Private/MaterialTemplate.ush

    HLSLMaterialTranslator.cppの続きです。マテリアルプロパティ・入力ピンに接続されたノードないし初期値は、printfの要領でHLSLコードが挿入・展開されています。

    ▲PushParamした順番とおりに %s; にコードが展開される
    GitHubで実装を見る(L2961-L2981)

    Shaders/Private/BasePassCommon.ush

    CustomDataをGBufferに書き込む条件にToonLitを追加します。ShaderMaterialDerivedHelpers.cppで同様の実装をしましたね。

    本来であればC++側でバリエーションを作成しているため、シェーダコード側の重複対応は不要ですが、ShaderMaterialDerivedHelpersまわりの実装はUE5.0からの新機能です。

    流石にGBufferまわりの実装を互換性を残さずに一新することは躊躇われたようで、その結果として旧実装がこのようにシェーダ側に残されています。それゆえに重複対応という面倒な作業も発生しているので、早いところ新機能に移行してほしいところではあります。

    GitHubで実装を見る(L42-L48)

    Shaders/Private/DeferredShadingCommon.ush

    WRITES_CUSTOMDATA_TO_GBUFFERのバリエーションはGBufferエンコード時に作用するものです。エンコード時にも同様の条件を作るために、HasCustomGBufferData関数で特定のシェーディングモデル以外は、ゼロ値を強制するような挙動になっています。

    これが初見殺しです。少なくとも筆者はエンジン改造をし始めた頃、まんまと引っかかりました。しかもこれ、後述するGBufferHelpers.ushを見るとわかるのですが、GBufferには値が格納されているので、バッファから直に取り出す際には正常値、UEが用意しているデコード関数から取り出そうとするとゼロ値が出てくるという、非常に沼挙動です(怒)。

    GitHubで実装を見る(L320-L327)

    GBuffer構造体に追加で必要なGBufferパラメータを定義します。

    GitHubで実装を見る(L395-L410)

    Shaders/Private/BasePassPixelShader.usf

    CustomDataを一時変数であるSubsurfaceColorに格納しています。一時変数は、後述するSetGBufferForShadingModel関数内で、GBufferパラメータに再格納されます。

    GitHubで実装を見る(L879-L885) GitHubで実装を見る(L898-L904)

    GBufferをMRTに書き込む直前に、スロットの入れ替えを行なっています。

    GitHubで実装を見る(L1942-L1952)

    Shaders/Private/ShadingModelsMaterial.ush

    GBuffer構造体にパラメータを格納しています。GBuffer構造体に格納された値は、そのままMRTに書き込まれるため、8bitに収まるように調整しています。

    GitHubで実装を見る(L204-L223)

    Shaders/Private/GBufferHelpers.ush

    スロットの入れ替えをしているため、デコード処理も変更します。

    PrecomputedShadowFactorsにはFaceShadowのパラメータが格納されているため、上書き処理を遅延実行させます。

    また、先ほどのHasCustomGBufferData関数はここで呼ばれており、特定のシェーディングモデル以外の場合に、0.0で上書きしています。

    GitHubで実装を見る(L379-L388)

    ToonLitの場合にはスロットを入れ替え、それ以外の場合には初期値で埋めています。

    GitHubで実装を見る(L464-L495)

    GBufferレイアウトの変更とマテリアルプロパティの動作確認

    コードの変更が完了したら、ビルドないし[F5]キーで実行をします。

    ビルド完了後、Shading ModelにToonLitを選択したマテリアルを開いた際、入力ピンの有効性や名称が変わっていれば無事成功です。

    ▲Shading ModelはToonLitを選択
    ▲ToonLitを選択した際の入力ピンの有効性と表示名

    お疲れさまでした!!

    今回はライティング実装に必要な前準備の内容で、映え要素が圧倒的に少なく、人によっては好みが別れそうな作業内容でしたね。とはいえ、見た目的に派手なことをしている裏では、意外とこういう地味な作業をしていたりするのです。

    次回は、ライティング処理を実装していきます。

    小林新汰/Kobayashi Arata

    新卒でSPARK CREATIVEに入社後、Unreal Engineをはじめとした様々なグラフィック業務に従事している。

    ●SPARKグループ公式サイト
    spark-group.jp
    ●SPARK CREATIVE Techブログ
    tech.spark-creative.co.jp
    ●SPARK CREATIVE 採用ページ(新卒採用/キャリア採用)
    spark-group.jp/recruit/

    TEXT_小林新汰 / Kobayashi Arata(SPARK CREATIVE
    EDIT_小村仁美 / Hitomi Komura(CGWORLD)