PaperSloth’s diary

主にゲーム開発関連についての記事を書きます。

UE4 Material Expressionの気持ちが知りたい

環境

UE4.27.0

概要

Material Expressionの実装が知りたくて追っていました。
docを読むだけで使い方は分かるものの、具体的な実装までは分からないことが多いためです。
Material FunctionなんかはEditorから実装が見れてお手軽ですね!

Material Expressionの例
図のような緑のノード。ダブルクリックしても実装が見れません。
f:id:PaperSloth:20220407192256p:plain
Desaturationのdocは以下。
Color Expressions | Unreal Engine Documentation

Material Functionの例
図のような青のノード。ダブルクリックすると実装が見れます。
f:id:PaperSloth:20220407192344p:plain

f:id:PaperSloth:20220407192354p:plain


Material Expression自体はかなりの種類があります。
詳細は以下のdocに目を通ると良いでしょう。(以下はUE5のdocです)
Unreal Engine Material Expressions Reference | Unreal Engine Documentation


Material Expressionはどこから見れるの?

1. HLSL Codeから追う

Material EditorのWindow -> Shader Code -> HLSL Codeを開けばMaterial 全体のHLSLが見れます。
f:id:PaperSloth:20220407192905p:plain

適当なEditorにコピペして「desaturation」を検索しますがヒットしません。
これは困った。
f:id:PaperSloth:20220407193107p:plain

実際にはちゃんと実装されているんですが、それは後程解説します。
因みにIncludeされている .ush ですが、これらはUnreal Shader Fileの略称(だと思います)で、色々と便利な関数等が登録されています。

2. MaterialExpression.cpp を追う

実際のMaterial Expressionの実装についてはMaterialExpression.cpp を追うのが一番確実でしょう。
Engine\Source\Runtime\Engine\Private\Materials\MaterialExpression.cpp 内に記述があります。

Desaturationの実装については、UMaterialExpressionDesaturation() を追っていきます。

UMaterialExpressionDesaturation::UMaterialExpressionDesaturation(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    // Structure to hold one-time initialization
    struct FConstructorStatics
    {
        FText NAME_Color;
        FText NAME_Utility;
        FConstructorStatics()
            : NAME_Color(LOCTEXT( "Color", "Color" ))
            , NAME_Utility(LOCTEXT( "Utility", "Utility" ))
        {
        }
    };
    static FConstructorStatics ConstructorStatics;

    LuminanceFactors = FLinearColor(0.3f, 0.59f, 0.11f, 0.0f);

#if WITH_EDITORONLY_DATA
    MenuCategories.Add(ConstructorStatics.NAME_Color);
    MenuCategories.Add(ConstructorStatics.NAME_Utility);
#endif
}

#if WITH_EDITOR
int32 UMaterialExpressionDesaturation::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
    if(!Input.GetTracedInput().Expression)
        return Compiler->Errorf(TEXT("Missing Desaturation input"));

    int32 Color = Compiler->ForceCast(Input.Compile(Compiler), MCT_Float3, MFCF_ExactMatch|MFCF_ReplicateValue),
            Grey = Compiler->Dot(Color,Compiler->Constant3(LuminanceFactors.R,LuminanceFactors.G,LuminanceFactors.B));

    if(Fraction.GetTracedInput().Expression)
        return Compiler->Lerp(Color,Grey,Fraction.Compile(Compiler));
    else
        return Grey;
}
#endif // WITH_EDITOR

パッと見は何をしているのかよくわからないかもしれませんが、実装自体は非常にシンプルです。
UMaterialExpressionDesaturation::Compile() 内の一部分だけを見ればよいです。

int32 UMaterialExpressionDesaturation::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
    ...
    int32 Color = Compiler->ForceCast(Input.Compile(Compiler), MCT_Float3, MFCF_ExactMatch|MFCF_ReplicateValue),
            Grey = Compiler->Dot(Color,Compiler->Constant3(LuminanceFactors.R,LuminanceFactors.G,LuminanceFactors.B));

    if(Fraction.GetTracedInput().Expression)
        return Compiler->Lerp(Color,Grey,Fraction.Compile(Compiler));
    else
        return Grey;
}

入力されたColorにDot() でLuminanceFactorsを指定し
入力されたFractionがある場合にはLerpして、ない場合にはDot() の結果を返しています。
LuminanceFactors はUMaterialExpressionDesaturation() で定義されており、カラーからモノトーンな色に変換するための適当な係数です。

LuminanceFactors = FLinearColor(0.3f, 0.59f, 0.11f, 0.0f);

3. HLSL Codeでは追えないの?

1. でコピーしたHLSLをもう一度見てみます。
3000行近いコードが生成されて何がなんだか……という感じなのですが、実際に見るべきは
CalcPixelMaterialInputs() 内を見れば良いです。

void CalcPixelMaterialInputs(in out FMaterialPixelParameters Parameters, in out FPixelMaterialInputs PixelMaterialInputs)
{
    // Initial calculations (required for Normal)

    // The Normal is a special case as it might have its own expressions and also be used to calculate other inputs, so perform the assignment here
    PixelMaterialInputs.Normal = MaterialFloat3(0.00000000,0.00000000,1.00000000);


    // Note that here MaterialNormal can be in world space or tangent space
    float3 MaterialNormal = GetMaterialNormal(Parameters, PixelMaterialInputs);

#if MATERIAL_TANGENTSPACENORMAL
#if SIMPLE_FORWARD_SHADING
    Parameters.WorldNormal = float3(0, 0, 1);
#endif

#if FEATURE_LEVEL >= FEATURE_LEVEL_SM4
    // Mobile will rely on only the final normalize for performance
    MaterialNormal = normalize(MaterialNormal);
#endif

    // normalizing after the tangent space to world space conversion improves quality with sheared bases (UV layout to WS causes shrearing)
    // use full precision normalize to avoid overflows
    Parameters.WorldNormal = TransformTangentNormalToWorld(Parameters.TangentToWorld, MaterialNormal);

#else //MATERIAL_TANGENTSPACENORMAL

    Parameters.WorldNormal = normalize(MaterialNormal);

#endif //MATERIAL_TANGENTSPACENORMAL

#if MATERIAL_TANGENTSPACENORMAL
    // flip the normal for backfaces being rendered with a two-sided material
    Parameters.WorldNormal *= Parameters.TwoSidedSign;
#endif

    Parameters.ReflectionVector = ReflectionAboutCustomWorldNormal(Parameters, Parameters.WorldNormal, false);

#if !PARTICLE_SPRITE_FACTORY
    Parameters.Particle.MotionBlurFade = 1.0f;
#endif // !PARTICLE_SPRITE_FACTORY

    // Now the rest of the inputs
    MaterialFloat3 Local0 = lerp(MaterialFloat3(0.00000000,0.00000000,0.00000000),Material.VectorExpressions[1].rgb,MaterialFloat(Material.ScalarExpressions[0].x));
    MaterialFloat Local1 = MaterialStoreTexCoordScale(Parameters, Parameters.TexCoords[0].xy, 0);
    MaterialFloat4 Local2 = ProcessMaterialLinearColorTextureLookup(Texture2DSampleBias(Material.Texture2D_0, Material.Texture2D_0Sampler,Parameters.TexCoords[0].xy,View.MaterialTextureMipBias));
    MaterialFloat Local3 = MaterialStoreTexSample(Parameters, Local2, 0);
    MaterialFloat Local4 = dot(Local2.rgb, MaterialFloat3(0.30000001,0.58999997,0.11000000));
    MaterialFloat3 Local5 = lerp(Local2.rgb,MaterialFloat3(Local4,Local4,Local4),MaterialFloat(0.75000000));

    PixelMaterialInputs.EmissiveColor = Local0;
    PixelMaterialInputs.Opacity = 1.00000000;
    PixelMaterialInputs.OpacityMask = 1.00000000;
    PixelMaterialInputs.BaseColor = Local5;
    PixelMaterialInputs.Metallic = 0.00000000;
    PixelMaterialInputs.Specular = 0.50000000;
    PixelMaterialInputs.Roughness = 0.50000000;
    PixelMaterialInputs.Anisotropy = 0.00000000;
    PixelMaterialInputs.Tangent = MaterialFloat3(1.00000000,0.00000000,0.00000000);
    PixelMaterialInputs.Subsurface = 0;
    PixelMaterialInputs.AmbientOcclusion = 1.00000000;
    PixelMaterialInputs.Refraction = 0;
    PixelMaterialInputs.PixelDepthOffset = 0.00000000;
    PixelMaterialInputs.ShadingModel = 1;


#if MATERIAL_USES_ANISOTROPY
    Parameters.WorldTangent = CalculateAnisotropyTangent(Parameters, PixelMaterialInputs);
#else
    Parameters.WorldTangent = 0;
#endif
}

またしても長くて何がなんだか……という感じなのですが、実際に見るべき箇所はたったの2行です。

MaterialFloat Local4 = dot(Local2.rgb, MaterialFloat3(0.30000001,0.58999997,0.11000000));
MaterialFloat3 Local5 = lerp(Local2.rgb,MaterialFloat3(Local4,Local4,Local4),MaterialFloat(0.75000000));

この処理をよく見るとDesaturation() の記述はないものの、実行している処理は先程MaterialExpression.cpp で見たものと同じです。
入力されたColor に対してdot() で LuminanceFactors = FLinearColor(0.3f, 0.59f, 0.11f, 0.0f);と同じ係数を使用して
入力されたFractionに対してLerp() を使用しているだけです。

まとめ

MathExpressionの実装がどうなっているのか知りたくて色々と追ってみましたが
上記のようにEngine側のCodeとHLSL Codeから追うことができます。

何かしらの参考になれば幸いです!
以上です。