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

今回から3回にわたって、エンジン改造を用いたセルシェーディングを紹介していきます。

記事の目次

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

    エンジン改造とは

    Unreal EngineのEngineフォルダに含まれるソースコードやシェーダを書き換えて、既存機能の拡張や独自の実装、改修を施す行為をエンジン改造と呼びます。

    普段皆さんが使用しているであろう、Epic Games Launcherやリポジトリから取得したUnreal Engineは、純正や素の状態を表す意味でバニラ(Vanilla)と呼びます。

    メリット

    第1回で掲載した画像の通り、エンジン改造は極論、リソースと技術力が許す限り、あらゆる表現が実装可能になります。

    連載第1回で紹介した、セルシェーダの実装方法3種類

    実際のゲームにも組み込まれているような例を挙げると、キャラクターと背景を別々のShadow Depthsに書き込んでセルフシャドウを抑制したり、SPARK GEARのような内製のエフェクトシステムを組み込むこともできたりします。その他にもCEDECやUNREAL FESTなどで改造事例が紹介されています。

    デメリット

    デメリットとしては、別のゲームエンジンであるUnityに比べると、求める表現に対して割に合わないほど実装手順が煩雑ということが挙げられます。

    煩雑な上にブループリントやマテリアルほど情報がネットに溢れていませんし、関連情報が見つかったとしてもエンジンのバージョンによっては互換性が消失していることもあり、万人が気軽にできる作業ではない現状があります。

    他に、ソースコードのビルドやシェーダのコンパイルに要する時間が長いという問題もあります。そして、解決方法がビルドマシンを強化する以外ないというのも金銭的に辛い部分です。

    ▲エンジンのバージョンアップごとにビルド時間がどんどん伸びている現状
    ▲シェーダのコンパイル

    エンジン改造は最終手段

    「エンジン改造を用いて」という内容に反しますが、初手からエンジン改造でアプローチすることは、保守コストを考えると避けるべきです。まずは一番簡単なプロジェクト実装、次に難しいプラグイン実装や拡張、最後にエンジン改造と、段階を踏んで検討を進めていくのが順当と言えます。

    というのが理想ですが、レンダリング周りに関しては、初手からエンジン改造という選択を取らざるを得ないケースが多々あります。

    本連載のテーマであるトゥーン表現にも関連しますが、Unreal EngineはLumenやNanite、Substrateなど、近年のアップデート傾向から察せるかもしれませんが、リアルな表現が得意なゲームエンジンです。その反面、セル調表現に関しては標準のサポートや機能が弱かったり、相性が悪かったりします。

    相性が悪い部分は気合で乗り切れますが、描画するためのパスが存在しないなどの根本的なところは、エンジン改造以外ではどうにもなりません。

    他にも見た目にこだわろうとすると、既存機能の枠組みでやりくりするよりも、改造で機能を追加した方が手っ取り早かったりしちゃうのです。

    始める前に

    エンジン改造という概念の説明を終えたところで、いよいよ実装を進めていきます。
    以下、前提事項です。

    ・Unreal Engine 5.2.1
    ・ディファードレンダリング対応
    ・フォワードレンダリング非対応
    ・PC向け対応
    ・PS4、PS5、Xboxは動作するはずだが、未検証
    ・モバイル端末とSwitchは非対応
    ・Lumen、Substrate非対応
    ・StatやPixelInspectorなどのプロファイラ対応は割愛

    目標とするルックをDCCツールやUnlitで作成

    まずは、DCCツールやUnlitで目標とするルックを作成します。

    Unlitで作成した方が、実装をエンジン側のシェーダに移植する際に互換性が概ね保たれるため、作業が簡単です。

    DCCツール側で最強のルックが完成しているのであれば、エンジニアさん次第ではありますが、それらを上手く取り込むことも極論できますので、頑張って悩んでいただければと思います。

    今回は、道中でUnlitの特徴にいくつか触れたいので、Unlitで作成していきます。

    Shade(陰・シェード)

    法線とライトの向きの内積をsmoothstepで2値に近づけます。

    smoothstepを使用することで、明と陰の境界線に少しぼかしを入れた見た目にすることができます。完全な2値にしたい場合は、smoothstepノードをstepノードに置換することで対応可能です。

    Base Color(明色)とShadow Color(陰影色)

    ライトが当たる部分にはBase Colorで色を塗り、ライトが当たらない部分や影が落ちる部分にはShadow Colorで色を塗ります。単純なことですが、 UEでは陰影色を指定することができないため、地味に重要な要素です。

    Unlit以外のシェーディングモデルでは、陰影色はSky Light Color(一般的にはAmbient Color)とBase Color、Diffuse Colorの乗算結果で決定されます。

    • ▲陰影色はSky Light Colorの影響に依存(明るさ)
    • ▲陰影色はSky Light Colorの影響に依存(色彩)

    色を自動的に算出してくれる反面、アーティストさんがねらった色味を出せない、出しにくい欠点があります。今回はそんな欠点を、陰影色を指定してかつ、スカイライトの処理にも手を加えることで解消を図っていきます。

    Specular

    髪や衣類に適用するハイライト表現です。

    BRDFの計算結果をsmoothstepに通すことで、あえて現実性を削り、トゥーン表現っぽくしています。

    BRDFで使用されている関数はBRDF.ushにまとめられているため、興味のある方は覗いてみると楽しいかもしれません。

    Hair Highlight、Angel Ring(天使の輪)

    第2回で紹介した天使の輪表現です。

    球形の法線を擬似的に転写することでさらに綺麗な丸っこい表現ができますが、調整に手間がかかるため、今回は頂点法線を利用したざっくりな表現で進めます。

    Face Shadow

    顔の影の形状を任意に表現するための手法です。第2回では法線転写で影を調整していましたが、今回はFace Shadowという別の手法です。

    大雑把に解説すると、顔の前方と左右どちらかのベクトル、これらとライトベクトルでそれぞれの内積を計算します。これによって傾き具合が算出できます。

    後はFace Shadow Textureという影の閾値が書き込まれたテクスチャを参照することで、NoLと同じ具合に影を書き込めるというものです。

    ▲Face Shadow Texture

    ちなみに、内積計算で求めているのでShadowというよりShadeの方が名称的には適している気がしますが、一般的に出回っている機能名がFace Shadowなので、そちらに合わせています。

    Rimlight(リムライト)

    一般的には逆光時に輪郭線を明るくする手法ですが、今回は逆光時に限定せず、輪郭線を明るくしています。

    トゥーン表現は明暗の階調が狭いため、画がのっぺりしやすいです。リムライトで常時照らすことで、誤魔化し程度ですが、のっぺり感を補間しています。

    Outline(輪郭線)

    輪郭線は第2回で紹介した背面法でも、第3回で紹介したポストプロセスでも、どちらでも好みの手法を採用していただければと思います。

    Unlitで表現が難しいこと

    以上の要素を基に、見た目を整えます。

    UEのライティング仕様では2段目の全体的に明るい方が正しい見た目ですが、1枚目も明暗がはっきりしていて、これはこれで好みではあるので、バリエーションとして載せておきます。

    Unlitだけでもそれっぽい画をつくれるのは、流石UEといったところでしょう。リアルな表現に重きを置いているとはいえ、UEは優秀なゲームエンジンのひとつです。このくらいの表現であれば簡単につくれてしまいます。

    そんなUnlitにも苦手な表現やできないことがあります。以下にいくつか紹介しますので、そこからエンジン改造の優位性や必要性の理解に繋げていただけると嬉しいです。

    Unlitのライティングのタイミング

    Unlitとそれ以外のシェーディングモデルでは、ライティングのタイミングが異なります。

    Unlit以外は、Base Passというところでライティングに必要なパラメータをGBufferと呼ばれる専用のバッファに書き込んで、Lightsというところでライティングをしています。

    それに対してUnlitは、Base Passでライティングをしており、GBufferへの書き込みはシェーディングモデルIDやビットフラグのみ、その他の要素はゼロ値で塗りつぶしています。

    このことからUnlitは、ディファードレンダリングでありながら、フォワードのような描画処理で動作していることがわかります。そしてこれが原因で、色々とできないこと、難しいことが発生しています。この仕様が諸悪の根源なのです。

    影・シャドウが落ちない

    Unlitは自身の影を落とすことはできますが、Unlit自身は影の影響を受けません。画像では右のUnlitがアタッチされた球形の影は落ちていますが、後方にある柱の影の影響を受けていないことが分かります。

    影は、ライティング時にShadow Mapという影情報が書き込まれたテクスチャを参照することで落とすことができます。

    • ▲影情報が書き込まれたShadow Map
    • ▲Shadow Mapを利用して影を落とす

    Shadow Mapの作成には、メインカメラから見た深度であるScene Depthと、ライトから見た深度であるShadow Depthsの2つの深度情報が必要です。

    ▲メインカメラから見た深度、Scene Depth
    ▲ライトから見た深度、Shadow Depths

    PrePassなどの設定により生成タイミングが異なりますが、UEの初期設定では、Scene DepthはBase Passで、Shadow DepthsはBase Passの後で作成が完了します。そしてこれらの深度情報を基に、ライティング直前にShadow Mapを作成します。

    ▲Scene Depth、Shadow Depths、Shadow Mapの生成タイミングを可視化

    Unlitがライティングを行なっているBase Passでは、Shadow Depthsがまだつくられていないため、影を落とすことができないのです。

    ポイントライトやスポットライトが当たらない

    Unlitは、ディレクショナルライト以外のライトを当てられません。

    ディファードレンダリングのため、1ライトにつき1つのパスが作成され、ライティングされます。

    ▲ライティングタイミングの可視化

    ライティングにはライトパラメータとライトを当てる先の情報が必要です。例えば、影計算に使用される法線や質感を決めるメタリックやラフネスなどです。これら法線やメタリックなどはGBufferに格納されています。

    UnlitはBase Pass以降で何かをすることを想定されていないため、GBufferはシェーディングモデルID以外はゼロで埋められます。最低限、法線ぐらい格納してくれてもいいじゃないと思いますが、なんとそれすらゼロで埋められます。

    ▲左の球がLit、右の球がUnlit。Unlitの部分だけGBufferの書き込みが虚無 ※Selection Outline ColorはGBufferA(名称がちがうのは気にしない)

    ちなみに、Unlitに割り振られているシェーディングモデルIDがゼロでかつ、残りのビットフラグもUnlitでは基本的に立たないため、ゼロ以外を書き込むことが本当に稀です。

    このように、ライトを当てようにも当てる先の情報が空っぽなので、当てられないのです。

    反射表現が難しい

    Unlitはリアルタイムであろうが、ベイクであろうが反射表現ができません。

    • ▲Litは反射表現可能
    • ▲Unlitは反射表現不可

    理由はディレクショナルライト以外が当てられないのと同様に、反射を計算しようにも材質情報が空っぽだからです。

    リアルタイムではなく、ベイクならBase Passでも計算できそうですが、標準ではサポートしていないため、各自でノードを組む必要があります。

    ポストプロセスで複雑な表現ができない

    第3回で紹介した複雑に組まれたポストプロセスは、Unlitをベースにすると実現できません。

    またまたディレクショナルライト以外が当てられないのと同様に、ポストプロセスで複雑な表現をするために必要なGBuffer情報がすっからかんです。

    表現に必要な要素だけを自前のRender Targetに書き込んで参照すれば可能ではありますが、負荷を考えると選択肢から除外されます。大人しくUnlit以外を利用するか、エンジン改造をしてGBufferに書き込むなどの対応が必要です。

    早見表

    パッと確認できるようにまとめます。

    シェーディングモデルの追加

    前置きが長くなりましたが、本題のエンジン改造に突入します。先ほど作成したルックを落とし込むために、専用のシェーディングモデルを追加します。

    いまさらですが、シェーディングモデルはUnityでいうところのシェーダです。Unityは右クリックと左クリックポチポチで簡単に作成できるのに対して、UEは色々と手続きを踏む必要があります。

    追加するシェーディングモデル名はLitやUnlitに倣って、ToonLitとします。

    エンジン改造のルール

    エンジン改造をする際には、コメントを記載することが暗黙的なルールです。

    ただし、記述のフォーマットは定められていないため、所属プロジェクトや会社さんによって結構個性があります。以降のコード内のコメントも、個性のひとつとして消化していただければと思います。

    //// cgw @kobayashi-arata 2023/09/16 ////
    #if 0
        // 改造前のコード
        // 改造前のコードを全部残すことで、エンジンのバージョンアップ時、差分比較が簡単になります。
        auto Flags = 0;
    #else
        // 改造後のコード
        auto Flags = 1;
    #endif
    //// cgw @kobayashi-arata 2023/09/16 ////
    
    //// cgw @kobayashi-arata 2023/09/16 ////
        // 改造前のコードを編集することなく、新しいコードを挿入するだけの場合は、囲いません。
        auto NewCode = ...;
    //// cgw @kobayashi-arata 2023/09/16 ////
    

    エンジンの取得方法

    リポジトリの取得方法については、公式の案内が親切です。
    https://www.unrealengine.com/ja/ue-on-github

    任意のバージョンのブランチを選択した上で取得することを推奨します。ブランチの切り替えは、サイズが大きすぎて事故が起こりやすいため推奨しません。落とす場所もできるだけルートディレクトリ直下にした方がgitに怒られずに済みます。

    無事に取得できると大体2.0~3.0GBほどになります。少ないと思うかもしれませんが、まだ、第一形態です。

    次にSetup.batを叩いて、その後にGenerateProjectFiles.batを叩いて、さらにその後にビルドをすると無事、化け物になります。

    ▲画像は5.3だが大体同じ

    ビルド手順としては、UE5をスタートアッププロジェクトに設定し、Development Editorを選択した上で、ソリューションのリビルドをします。

    ソリューションのリビルドは初回のみ必須です。初回以降は基本的にはソリューションのビルドは不要です。当然ですが、UE5以外のプロジェクトの配下のファイルを編集して、その結果を反映させたい場合は別です。

    ビルドに要する時間は開発環境とエンジンのバージョンに依存しますが、初回に関してだけは最低でも1時間以上かかります。オススメは、お出かけする前にポチッとしておくことです。

    無事にビルドが完了したら、よく見かけるプロジェクトのテンプレートが出てきます。テンプレートは適当に選んで問題ありませんが、プロジェクトの配置場所には注意が必要です。基本的にはエンジンが置かれているディレクトリに配置します。外部にも置けますが、パスを通すのがかなり面倒になるため、止めた方が良いでしょう。

    テンプレートを基にプロジェクトを作成してからVisual Studioに戻ると、ファイル変更の検出が案内されるため、[再読み込み]を選択します。

    選択するとソリューションエクスプローラーにプロジェクトが追加されていると思いますので、そちらを[スタートアッププロジェクトに設定]することで、無事に自前のエンジンを基にプロジェクトを立ち上げることができるようになります。

    間違えて[無視]を選択した場合は、GenerateProjectFiles.batを叩いて任意にソリューション情報を更新させてあげれば問題ありません。

    実装内容の見かた

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

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

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

    Runtime/Engine/Classes/Engine/EngineTypes.h

    シェーディングモデルを追加します。頭文字にはMaterialShadingModelの略称のMSMを付けるのが慣習です。

    GitHubで実装を見る(L618-L620)

    Runtime/Engine/Private/Materials/MaterialShader.cpp

    MSM_ToonLitの名称を取得する関数です。

    GitHubで実装を見る(L103-L105)

    Runtime/Engine/Private/Materials/HLSLMaterialTranslator.cpp

    マテリアルのシェーディングモデルにToonLitが使用されている場合は、MATERIAL_SHADINGMODEL_TOON_LITという定数を立てます。

    GitHubで実装を見る(L1933-L1939)

    Runtime/Engine/Private/Materials/MaterialHLSLEmitter.cpp

    Translatorと同様の処理です。差分としては、Emitterの方が一応新バージョン扱いです。どちらが使用されるかは環境によってまちまちなので、現状では両対応するのが無難です。

    GitHubで実装を見る(L613-L619)

    Runtime/RenderCore/Public/ShaderMaterial.h

    ToonLitに対応したシェーダコードを生成する際の、バリエーションの判定変数として使用されます。

    GitHubで実装を見る(L106-L108)

    Runtime/Engine/Private/ShaderCompiler/ShaderGenerationUtil.cpp

    先ほどのTranslator/EmitterでSetDefineされた場合は、ShaderMaterial.hに定義したMATERIAL_SHADING_MODEL_LITにSetDefineされた値が格納されます。

    GitHubで実装を見る(L149-L151)

    FETCH_COMPILE_BOOLはマクロです。定義元を確認すると、OutEnvironmentからGetしていることがわかります。

    GitHubでFETCH_COMPILE_BOOLマクロの実装を見る(L60)

    バリエーションに応じてシェーディングモデルが使用するGBufferスロットを指定します。指定情報を基にGBufferのエンコード、デコードのシェーダコードが生成されます。

    GitHubで実装を見る(L1800-L1806)

    Shaders/Private/Definitions.usf

    TranslatorやEmitterは、未定義の場合の挙動は担保されません。

    UEでは、使用箇所ごとに#ifndefで確認せず、先頭のどこか適当なところに、存在しない場合の処理を記述しておくのが形式的です。

    GitHubで実装を見る(L123-L127)

    Shaders/Private/ShadingCommon.ush

    シェーディングモデルIDのナンバリングは、EngineTypes.hで定義したEMaterialShadingModelと一致させる必要があります。

    GitHubで実装を見る(L33-L40)

    主にデバッグ用途で使用されるシェーディングモデルIDの色を取得する関数です。TODOにも記載があるとおり、PS4ではswitch文の最適化が不適切なため、PS4用にif文とそれ以外で使用するswitch文の2つのケースで実装されています。

    ちなみにこちらのTODO、UE4.25時代からあります。PS4の本体発売も終了しているので改善されないまま終わりそうな雰囲気ですね。もしかしたらSIEは改善していてEpic Gamesが対応を忘れている可能性もありますが。

    GitHubで実装を見る(L72-L74) GitHubで実装を見る(L92-L94)

    Shaders/Private/ShadingModels.ush

    ToonLitのライティング処理が行われる関数です。ライティングの実装は次回以降のため、いったんは真っ黒を返すような初期値をセットしています。

    GitHubで実装を見る(L961-L967)

    シェーディングモデルごとに対応したライティング関数を呼び出しています。

    GitHubで実装を見る(L993-L996)

    Shaders/Private/ReflectionEnvironmentPixelShader.usf

    Unlitと同様に環境光や反射表現の計算をスキップするようにしています。環境光については、ライティング実装時に改めて説明する予定です。

    GitHubで実装を見る(L334-L340)

    動作確認

    ひととおり準備ができたら、リビルドを実行します。

    シェーディングモデルにToonLitが追加されていて、かつ選択ができれば改造成功です。

    選択してもライティングが未実装なので、真っ暗になっているでしょう。抜け道としてEmissive Colorは繋いだ色がScene Colorに直接書き込まれるので、Unlitのように使うことはできます。

    お疲れさまでした!!!

    次回は、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)