こんにちは!! 株式会社スパーククリエイティブの小林です。
前回、セル・トゥーン表現用のシェーディングモデルToonLitを作成しました。今回は、ライティング処理を実装するための前準備として、GBufferレイアウトの変更とマテリアルプロパティ・入力ピンの実装をしていきます。
”前準備”とあるように、実装作業がメインなので、マテリアルを触ることがなく、映える挿絵がとても少ない回となっています。
ディファードレンダリングのライティングについて(振り返り)
Unreal Engine(以下、UE)でのディファードライティングについて、簡単におさらいをします。BasePassで法線やベースカラー、メタリックなどの材質情報・パラメータをGBufferに書き込みます。
LightsパスでGBufferを参照してライティング処理を行います。Lightsパスはライトごとにパスが生成・実行されるため、複数のライトを効率よく扱えます。
今回は、この一連のながれのひとつであるGBufferの書き込み部分を実装していきます。
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 Shadows、Contact 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と異なる点に注意です。
顔の右ベクトル(Face Right Vecotr)は前方ベクトル(Face Forward Vector)があれば外積から求められるため不要です。ノードで表すと以下のとおりです。
※正規化のタイミングを間違えているかもしれませんが、現実的ライティングをしているわけではないので良しとします。
Face Shadow Enabled
Face Shadowの有効性は Face Forward Vector の合計値が0.0か否かで判断することにして、スロットを削減します。
スロット削減以前の根本的な話ですが、Faceと名の付くとおり、顔にのみ適用する表現です。こういう場合は、本当はToonFaceLitとして別のシェーディングモデルを作った方が手っ取り早いです。今回は大幅なレイアウト調整をする方法を紹介したかったので、ひとつのシェーディングモデルに詰め込んでいます。
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で実装を見る(L294-L299)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のテンプレート構造体で、初期値を保持していたり、入力ピンとノードの接続情報を保持していたりします。
Runtime/Engine/Private/Materials/Material.cpp
マテリアルプロパティの総数を変更してアサートを解消します。マテリアルプロパティは拡張を想定されているのか、所々でアサートが仕込まれており、変更するべき箇所が意外とわかりやすいです(シェーディングモデルにもアサート仕込んでほしい)。
GitHubで実装を見る(L2747-L2753)OutDescriptionに情報をコピーしている部分です。
GitHubで実装を見る(L5600-L5605)MaterialAttributeDefinitionMap.cppで実装した初期値、その関数を呼んでいる部分です。
GitHubで実装を見る(L6212-L6217)入力ピンの有効性を指定します。ToonLitではAOとSpecular、Metallicは入力を受け付ける必要がないため、無効にします。
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
追加で定義したマテリアルプロパティを入力ピンとして表示する順番を指定します。指定とは書きましたが、シリアライズ対応が面倒になるため、末尾に追加していくことを推奨します。
GitHubで実装を見る(L267-L272)Runtime/Engine/Classes/Materials/MaterialExpressionMakeMaterialAttributes.h
MaterialAttributesノード用の入力ピンを定義します。
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された順番に依存します。マジックナンバーになっているため、少しだけ取り扱いに注意してください。
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コードとして挿入している箇所です。詳細はシェーダコード側で改めて触れます。
Runtime/Engine/Public/MaterialCachedData.h
マテリアルプロパティを1つ以上追加する場合には、一部の変数型をuint32からuint64に拡張する必要があります。理由はビット演算絡みです。MP_SurfaceThicknessを見るとこの時点で31なので、追加した時点でオーバーします。
“新しいマテリアルプロパティはここに書いてね”と親切に案内している割には、こういう隠れ対応を要求するのはいかがなものかと思いますが、安心してください、5.3からは標準でuint64対応が入っています。本音を言うともっと早く入れてほしかったですけどね。
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がシェーダファイルに割り当てている独自の拡張子です。
拡張子はushとusfの2種あります。各拡張子のフルネームと特徴は以下のとおりです。
ush(Unreal Shader Headers)は、usfやushからインクルードすることが可能です。そのため、定義や関数の実装場所として多く使用されます。複数インクルードされることもあるため、ファイルの先頭には #pragma once を記述することが推奨されています。
usf(Unreal Shader Format)は、シェーダのエントリーポイントやエントリーポイントの直後に呼び出される関数の実装ファイルに使用されます。
下記の BasePassPixelShader.usf にはエントリーポイントはありませんが、PixelShaderOutputCommon.usf という雑にいうとピクセルシェーダの親玉的存在から FPixelShaderInOut_MainPS 関数が呼ばれているため、実質エントリーポイント扱いとなり、usfでファイルが作成されています。
ushとusfで役割が分かれているとおり、usfはushのようなインクルードする運用は BasePassPixelShader.usf のような一部例外はありますが基本的には非推奨です。
コンパイルルールで弾かれないため、厳密な禁止は唱えられないのですが、大雑把に使うとエラー発生時のログから得られる情報が不正確になったり、Epicの気分次第では今後厳格化するとも限らないので、筆者的にはエントリーポイントはusf、それ以外はushという使い分けを推奨しています。
Shaders/Private/MaterialTemplate.ush
HLSLMaterialTranslator.cppの続きです。マテリアルプロパティ・入力ピンに接続されたノードないし初期値は、printfの要領でHLSLコードが挿入・展開されています。
Shaders/Private/BasePassCommon.ush
CustomDataをGBufferに書き込む条件にToonLitを追加します。ShaderMaterialDerivedHelpers.cppで同様の実装をしましたね。
本来であればC++側でバリエーションを作成しているため、シェーダコード側の重複対応は不要ですが、ShaderMaterialDerivedHelpersまわりの実装はUE5.0からの新機能です。
流石にGBufferまわりの実装を互換性を残さずに一新することは躊躇われたようで、その結果として旧実装がこのようにシェーダ側に残されています。それゆえに重複対応という面倒な作業も発生しているので、早いところ新機能に移行してほしいところではあります。
Shaders/Private/DeferredShadingCommon.ush
WRITES_CUSTOMDATA_TO_GBUFFERのバリエーションはGBufferエンコード時に作用するものです。エンコード時にも同様の条件を作るために、HasCustomGBufferData関数で特定のシェーディングモデル以外は、ゼロ値を強制するような挙動になっています。
これが初見殺しです。少なくとも筆者はエンジン改造をし始めた頃、まんまと引っかかりました。しかもこれ、後述するGBufferHelpers.ushを見るとわかるのですが、GBufferには値が格納されているので、バッファから直に取り出す際には正常値、UEが用意しているデコード関数から取り出そうとするとゼロ値が出てくるという、非常に沼挙動です(怒)。
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で上書きしています。
ToonLitの場合にはスロットを入れ替え、それ以外の場合には初期値で埋めています。
GitHubで実装を見る(L464-L495)GBufferレイアウトの変更とマテリアルプロパティの動作確認
コードの変更が完了したら、ビルドないし[F5]キーで実行をします。
ビルド完了後、Shading Modelに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)