SPARKCREATIVE Tech Blog

https://www.spark-creative.jp/

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

こんにちは!!!クライアントエンジニアの小林です。

今回はトゥーン・セル表現を題材にエンジン改造に触れていきます。
UnrealEngineでトゥーン表現をする場合、大体はUnlitシェーダかポストエフェクト、エンジン改造をすると思います。
なんで面倒なエンジン改造をすることが多いのかというのは、以下の表を見ると分かると思います。

Unlitシェーダ ポストエフェクト エンジン改造
影(Shadow)を落とせるか × △ (疑似的)
ディレクショナルライト以外を反映できるか × △ (疑似的)
特殊な演出を組み込みたい

Unlitシェーダもポストエフェクトもやろうと思えばできないことはないのですが、
エンジン改造に比べると面倒だったり制約が多くなる印象です。

そんな中でエンジン改造は極論、リソースの許す限りあらゆる表現を実装できます。
2022年の直近でいうとCEDECでのブループロトコルや、アンリアルフェスでのアイドルマスター、鬼滅などなど。
なんかもうリリースタイトルは大体改造してますね。
2022/09/17:ブルプロさんはいつリリースするんだろう。

ということでエンジン改造の推奨バージョン5.0.3がリリースされてから早数か月。
そろそろ手を付けてもいいだろうということでトゥーン表現向けのエンジン改造をやっていきます。
現状は3本構成で、シェーディングモデルの追加、マテリアルインプットの追加、Gバッファの拡張です。

作業環境

・windows10
visual studio 2019
visual studio code
・UnrealEngine 5.0.3 - release

改造内容

ToonLitというトゥーン表現専用のシェーディングモデルを追加します。

ライティングはディフューズカラーのみ反映します。
スペキュラやリムライトを考慮してもいいのですが、エンジン改造がメインのため適当にします。
また、Lightmass、Strata、PixelInspectorなどが非対応です。

改造ルール

UnrealEngineのエンジンコードに手を加える場合は、コメントを付けることが推奨されています。
推奨されているのですが記述フォーマットがおそらく決まっていません。
これに関しては正直決めてほしかったのですが、私の場合はこんな感じに書いています。

//// Add ShadingModel 2022/09/17 ////
    // 日付の前が改造内容
    float A = 0.0f;
//// Add ShadingModel 2022/09/17 ////

//// Add ShadingModel 2022/09/17 ////
#if 0
    float B = 0.0f;
#else
    // #if 0 ~ #else がUnrealEngineさんのオリジナルコード
    // #else ~ #endifが改造コード
    // オリジナルコードを残しておきたい場合にこの記述をしてます
    float B = 1.0f;
#endif
//// Add ShadingModel 2022/09/17 ////

\Engine\Source

\Runtime\Engine\Classes\Engine\EngineTypes.h

ToonLitをシェーディングモデル列挙体に追加しています。
UE5.0からMSM_StrataというPBRに特化したシェーディングモデルが追加されました。
特別な理由が無ければデフォルトのシェーディングモデルの下に追加していきます。

    MSM_SingleLayerWater        UMETA(DisplayName="SingleLayerWater"),
    MSM_ThinTranslucent         UMETA(DisplayName="Thin Translucent"),
    MSM_Strata                  UMETA(DisplayName="Strata", Hidden),
//// Add ShadingModel:ToonLit 2022/09/17 ////
    MSM_ToonLit                 UMETA(DisplayName="Toon Lit"),
//// Add ShadingModel:ToonLit 2022/09/17 ////
    /** Number of unique shading models. */
    MSM_NUM                     UMETA(Hidden),
    /** Shading model will be determined by the Material Expression Graph,

\Runtime\Engine\Classes\Materials\MaterialExpressionShadingModel.h

シェーディングモデルをFrom Material Expressionに選択した際に使用するShadingModelノードの対応をしています。

    virtual void GetCaption(TArray<FString>& OutCaptions) const override;
#endif
public:
//// Add ShadingModel:ToonLit 2022/09/17 ////
    UPROPERTY(EditAnywhere, Category=ShadingModel, meta=(ValidEnumValues="MSM_DefaultLit, MSM_Subsurface, MSM_PreintegratedSkin, MSM_ClearCoat, MSM_SubsurfaceProfile, MSM_TwoSidedFoliage, MSM_Hair, MSM_Cloth, MSM_Eye, MSM_ToonLit"))
//// Add ShadingModel:ToonLit 2022/09/17 ////
    TEnumAsByte<enum EMaterialShadingModel> ShadingModel = MSM_DefaultLit;
    //~ End UMaterialExpression Interface
};

\Runtime\Engine\Private\Materials\HLSLMaterialTranslator.cpp

そのマテリアル・シェーダーがToonLitか否かのフラグを立てています。

            bMaterialRequestsDualSourceBlending = true;
        }
    //// Add ShadingModel:ToonLit 2022/09/17 ////
        if (ShadingModels.HasShadingModel(MSM_ToonLit))
        {
            OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_TOON_LIT"), TEXT("1"));
            NumSetMaterials++;
        }
    //// Add ShadingModel:ToonLit 2022/09/17 ////

        if (ShadingModels.HasShadingModel(MSM_SingleLayerWater) && FDataDrivenShaderPlatformInfo::GetRequiresDisableForwardLocalLights(Platform))
        {

\Engine\Source\Runtime\Engine\Private\Materials\MaterialHLSLEmitter.cpp

            bMaterialRequestsDualSourceBlending = true;
    }
    //// Add ShadingModel:ToonLit 2022/09/17 ////
        if (ShadingModels.HasShadingModel(MSM_ToonLit))
        {
            OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_TOON_LIT"), TEXT("1"));
            NumSetMaterials++;
        }
    //// Add ShadingModel:ToonLit 2022/09/17 ////

        if (ShadingModels.HasShadingModel(MSM_SingleLayerWater) && FDataDrivenShaderPlatformInfo::GetRequiresDisableForwardLocalLights(InPlatform))
        {

\Runtime\Engine\Private\Materials\MaterialShader.cpp

ログなどで出力されるシェーディングモデル名と負荷計測です。

        case MSM_Eye:                ShadingModelName = TEXT("MSM_Eye"); break;
        case MSM_SingleLayerWater:   ShadingModelName = TEXT("MSM_SingleLayerWater"); break;
        case MSM_ThinTranslucent:    ShadingModelName = TEXT("MSM_ThinTranslucent"); break;
    //// Add ShadingModel:ToonLit 2022/09/17 ////
        case MSM_ToonLit:            ShadingModelName = TEXT("MSM_ToonLit"); break;
    //// Add ShadingModel:ToonLit 2022/09/17 ////
        default:                     ShadingModelName = TEXT("Unknown"); break;
    }
    return ShadingModelName;
    {
        INC_DWORD_STAT_BY(STAT_ShaderCompiling_NumUnlitMaterialShaders, 1);
    }
//// Add ShadingModel:ToonLit 2022/09/17 ////
    else if (ShadingModels.HasAnyShadingModel({ MSM_DefaultLit, MSM_Subsurface, MSM_PreintegratedSkin, MSM_ClearCoat, MSM_Cloth, MSM_SubsurfaceProfile, MSM_TwoSidedFoliage, MSM_SingleLayerWater, MSM_ThinTranslucent, MSM_ToonLit }))
//// Add ShadingModel:ToonLit 2022/09/17 ////
    {
        INC_DWORD_STAT_BY(STAT_ShaderCompiling_NumLitMaterialShaders, 1);
    }

\Runtime\Engine\Private\ShaderCompiler\ShaderGenerationUtil.cpp

どのGバッファを使うかなどを指定しています。

    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_EYE);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_SINGLELAYERWATER);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT);
//// Add ShadingModel:ToonLit 2022/09/17 ////
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_TOON_LIT);
//// Add ShadingModel:ToonLit 2022/09/17 ////

    FETCH_COMPILE_BOOL(MATERIAL_FULLY_ROUGH);
    FETCH_COMPILE_BOOL(MATERIAL_FULLY_ROUGH);

    {
    }

//// Add ShadingModel:ToonLit 2022/09/17 ////
    if (Mat.MATERIAL_SHADINGMODEL_TOON_LIT)
    {
        SetStandardGBufferSlots(Slots, bWriteEmissive, bHasTangent, bHasVelocity, bHasStaticLighting, bIsStrataMaterial);
    }
//// Add ShadingModel:ToonLit 2022/09/17 ////
}

void FShaderCompileUtilities::ApplyDerivedDefines(FShaderCompilerEnvironment& OutEnvironment, FShaderCompilerEnvironment * SharedEnvironment, const EShaderPlatform Platform)

\Engine\Source\Runtime\RenderCore\Public\ShaderMaterial.h

    uint8 MATERIAL_SHADINGMODEL_EYE : 1;
    uint8 MATERIAL_SHADINGMODEL_SINGLELAYERWATER : 1;
    uint8 MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT : 1;
//// Add ShadingModel:ToonLit 2022/09/17 ////
    uint8 MATERIAL_SHADINGMODEL_TOON_LIT : 1;
//// Add ShadingModel:ToonLit 2022/09/17 ////

    uint8 TRANSLUCENCY_LIGHTING_VOLUMETRIC_NONDIRECTIONAL : 1;
    uint8 TRANSLUCENCY_LIGHTING_VOLUMETRIC_DIRECTIONAL : 1;

\Engine\Shaders

\Private\DeferredLightingCommon.ush

ToonLitに落ちる影(Shadow)を2値化しています。
厳密にはBRANCH if( Shadow.SurfaceShadow + Shadow.TransmissionShadow > 0 )の前に2値化すると不都合が生じるのですが、シェーディングモデルの追加が目的なので一旦思考放棄します。

        LightAccumulator.EstimatedCost += 0.3f;        // add the cost of getting the shadow terms

    //// Add ShadingModel:ToonLit 2022/09/17 ////
        BRANCH if (GBuffer.ShadingModelID == SHADINGMODELID_TOON_LIT)
        {
            Shadow.SurfaceShadow = Shadow.SurfaceShadow > 0.5f ? 1.0f : 0.0f;
        }
    //// Add ShadingModel:ToonLit 2022/09/17 ////

        BRANCH
        if( Shadow.SurfaceShadow + Shadow.TransmissionShadow > 0 )
        {

\Private\Definitions.usf

ソース側で対応したToonLitか否かのフラグ受け渡し場所です。
#ifndef ~ #endifで囲われているので、ソース側から特に指示が無ければToonLitではないよ。というフラグが立ちます。

#define	MATERIAL_SHADINGMODEL_UNLIT                     0
#endif

//// Add ShadingModel:ToonLit 2022/09/17 ////
#ifndef MATERIAL_SHADINGMODEL_TOON_LIT
#define MATERIAL_SHADINGMODEL_TOON_LIT                  0
#endif
//// Add ShadingModel:ToonLit 2022/09/17 ////

#ifndef MATERIAL_SINGLE_SHADINGMODEL
#define	MATERIAL_SINGLE_SHADINGMODEL                    0

\Private\ShadingCommon.ush

#define SHADINGMODELID_SINGLELAYERWATER     10
#define SHADINGMODELID_THIN_TRANSLUCENT     11
#define SHADINGMODELID_STRATA               12      // Temporary while we convert everything to Strata
//// Add ShadingModel:ToonLit 2022/09/17 ////
#define SHADINGMODELID_TOON_LIT             13
#define SHADINGMODELID_NUM                  14
//// Add ShadingModel:ToonLit 2022/09/17 ////
#define SHADINGMODELID_MASK                 0xF     // 4 bits reserved for ShadingModelID			
    else if (ShadingModelID == SHADINGMODELID_SINGLELAYERWATER) return float3(0.5f, 0.5f, 1.0f);
    else if (ShadingModelID == SHADINGMODELID_THIN_TRANSLUCENT) return float3(1.0f, 0.8f, 0.3f);
    else if (ShadingModelID == SHADINGMODELID_STRATA) return float3(1.0f, 1.0f, 0.0f);
//// Add ShadingModel:ToonLit 2022/09/17 ////
    else if (ShadingModelID == SHADINGMODELID_TOON_LIT) return float3(0.3f, 0.3f, 0.3f);
//// Add ShadingModel:ToonLit 2022/09/17 ////
    else return float3(1.0f, 1.0f, 1.0f); // White
#else
    switch(ShadingModelID)
        case SHADINGMODELID_SINGLELAYERWATER: return float3(0.5f, 0.5f, 1.0f);
        case SHADINGMODELID_THIN_TRANSLUCENT: return float3(1.0f, 0.8f, 0.3f);
        case SHADINGMODELID_STRATA: return float3(1.0f, 1.0f, 0.0f);
	//// Add ShadingModel:ToonLit 2022/09/17 ////
        case SHADINGMODELID_TOON_LIT: return float3(0.3f, 0.3f, 0.3f);
	//// Add ShadingModel:ToonLit 2022/09/17 ////
        default: return float3(1.0f, 1.0f, 1.0f); // White
    }
#endif

\Private\ShadingModels.ush

ToonLitモデルのシェーディング計算箇所です。

    return Lighting;
}

//// Add ShadingModel:ToonLit 2022/09/17 ////
FDirectLighting ToonLitBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
    BxDFContext Context;
    FDirectLighting Lighting;

    bool bHasAnisotropy = false;

    float NoV, VoH, NoH;
    {
        Init(Context, N, V, L);

        NoV = Context.NoV;
        VoH = Context.VoH;
        NoH = Context.NoH;

        SphereMaxNoH(Context, AreaLight.SphereSinAlpha, true);
    }

    Context.NoV = saturate(abs( Context.NoV ) + 1e-5);
    
    Lighting.Diffuse = Diffuse_Lambert(GBuffer.DiffuseColor) * AreaLight.FalloffColor * Falloff;
    Lighting.Specular = 0.0f;
    Lighting.Transmission = 0.0f;
    
    return Lighting;
}
//// Add ShadingModel:ToonLit 2022/09/17 ////

FDirectLighting IntegrateBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
    switch( GBuffer.ShadingModelID )
            return ClothBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
        case SHADINGMODELID_EYE:
            return EyeBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
    //// Add ShadingModel:ToonLit 2022/09/17 ////
        case SHADINGMODELID_TOON_LIT:
            return ToonLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
    //// Add ShadingModel:ToonLit 2022/09/17 ////
        default:
            return (FDirectLighting)0;
    }

\Private\SkyLightingDiffuseShared.ush

本来スカイライトパスでは法線を考慮しますが、トゥーン表現とは微妙に相性が悪いのでベタ塗りにしています。

        DiffuseColor += ClothFuzz * GBuffer.CustomData.a;
    }

//// Add ShadingModel:ToonLit 2022/09/17 ////
    BRANCH
    if (GBuffer.ShadingModelID == SHADINGMODELID_TOON_LIT)
    {
        Lighting = Diffuse_Lambert(DiffuseColor) * View.SkyLightColor.rgb;
    }
    else
    {
        // Compute the preconvolved incoming lighting with the bent normal direction
        float3 DiffuseLookup = GetSkySHDiffuse(SkyVisData.SkyDiffuseLookUpNormal) * View.SkyLightColor.rgb;

        // And accumulate the lighting
        Lighting += (SkyVisData.SkyDiffuseLookUpMul * DiffuseLookup + SkyVisData.SkyDiffuseLookUpAdd) * DiffuseColor * DiffuseWeight;
    }
//// Add ShadingModel:ToonLit 2022/09/17 ////
    Lighting *= View.PreExposure;

    //Lighting = (Texture2DSampleLevel(BentNormalAOTexture, BentNormalAOSampler, UV, 0).xyz);

追加したシェーディングモデルを使ってみる

マテリアルのシェーディングモデルをToon Litに変更して、ベースカラーに出力させたいテクスチャなどを繋げます。

Unlitシェーダやポストエフェクトは違い、AdditionalLightの反映や影(Shadow)が落ちているのも確認できました。




おわり

お疲れさまでした!!!