说明

本文先对Multiply材质节点的实现进行了简要分析,然后仿照其实现了一个自定义的四项相乘的Multiply节点

Multiply材质节点实现分析

在添加自定义的材质节点之前,先对UE内置的Multiply材质节点进行分析和学习,来进一步理解材质节点编译的过程。

Compiler中的接口及实现

// Engine\Source\Runtime\Engine\Public\MaterialCompiler.h
class FMaterialCompiler
{
    (...)
public:
    virtual int32 Mul(int32 A,int32 B) = 0;
    (...)
}

// Engine\Source\Runtime\Engine\Private\Materials\HLSLMaterialTranslator.h
class FHLSLMaterialTranslator : public FMaterialCompiler
{
    (...)
protected:
    virtual int32 Mul(int32 A, int32 B) override;
    (...)
}

// Engine\Source\Runtime\Engine\Private\Materials\HLSLMaterialTranslator.cpp
int32 FHLSLMaterialTranslator::Mul(int32 A, int32 B)
{
    // 判断操作数索引值是否有效
    if (A == INDEX_NONE || B == INDEX_NONE)
    {
        return INDEX_NONE;
    }

    // 获取Mul计算返回值类型,GetArithmeticResultType(A,B)中调用了其接受两者类型的重载版本
    // 如果两操作数的Mul运算是未定义的,则会在此步输出Error提示
    const EMaterialValueType ResultType = GetArithmeticResultType(A, B);

    if (ResultType == MCT_Unknown)
    {
        return INDEX_NONE;
    }

    // 当其中一个操作数为0或1,无需进行计算,ConstOneReturnValue记录该返回值
    int32 ConstOneReturnValue = INDEX_NONE;

    // 其中一个操作数为constant zero
    if (IsExpressionConstantValue(A, 0.0f) || IsExpressionConstantValue(B, 0.0f))
    {
        // 返回0(返回的也是索引,其对应的数据类型就是前面的ResultType,ConstArithmeticResultValue中实际也是调用GetArithmeticResultType获得结果的类型)
        return ConstArithmeticResultValue(A, B, 0.0);
    }
    // 若A操作数的值为1,结果就是B操作数,但必须使其维度与ResultType一致后才能返回
    else if (IsExpressionConstantValue(A, 1.0f))
    {
        ConstOneReturnValue = B;
    }
    // 若B操作数的值为1,结果就是A操作数,但必须使其维度与ResultType一致后才能返回
    else if (IsExpressionConstantValue(B, 1.0f))
    {
        ConstOneReturnValue = A;
    }

    // 维度调整
    if (ConstOneReturnValue != INDEX_NONE)
    {
        int32 Return = ConstOneReturnValue;
        // 下列逻辑只会在scalar x vector的情况下生效,因为非法的运算会在获取ResultType时就报错
        // 对Return循环Append一维ConstOneReturnValue值,直至Return的维度等于ResultType的维度
        while (GetNumComponents(GetParameterType(Return)) < GetNumComponents(ResultType))
        {
            Return = AppendVector(Return, ConstOneReturnValue);
        }
        // 返回索引(必要时将Return转换为LWC数据)
        return LWCCastIfNeccessary(ResultType, Return);
    }

    // 获取操作数表达式,FMaterialUniformExpression是抽象类,每个MaterialUniformExpression进行各自的实现,可以通过它们各自的方法获取操作数的类型、数值等。
    FMaterialUniformExpression* ExpressionA = GetParameterUniformExpression(A);
    FMaterialUniformExpression* ExpressionB = GetParameterUniformExpression(B);
    // 如果两个表达式非空
    if (ExpressionA && ExpressionB)
    {
        // 表达式是常量
        if (ExpressionA->IsConstant() && ExpressionB->IsConstant())
        {
            // 转换为FLinearColor类型后再进行计算
            FLinearColor ValueA, ValueB;
            GetConstParameterValue(ExpressionA, ValueA);
            GetConstParameterValue(ExpressionB, ValueB);

            FLinearColor ConstantValue = ValueA * ValueB;
            // 转回ResultType后返回
            return ConstResultValue(ResultType, ConstantValue);
        }
        // 表达式不是常量
        FAddUniformExpressionScope Scope(this);
        return AddUniformExpression(Scope, new FMaterialUniformExpressionFoldedMath(ExpressionA, ExpressionB, FMO_Mul), GetArithmeticResultType(A, B), TEXT("(%s * %s)"), *GetParameterCode(A), *GetParameterCode(B));
    }
    else
    {
        if (IsAnalyticDerivEnabled())
        {
            return DerivativeAutogen.GenerateExpressionFunc2(*this, FMaterialDerivativeAutogen::EFunc2::Mul, A, B);
        }
        else
        {
            return AddCodeChunk(GetArithmeticResultType(A, B), TEXT("(%s * %s)"), *GetParameterCode(A), *GetParameterCode(B));
        }
    }
}

需要注意的是,为什么FHLSLMaterialTranslator::Mul的形参和返回值都是int32,但是其节点可以支持float型的运算呢?

事实上这里的形参A和B并非是两个参与运算的操作数的数值本身,而是索引值。这个索引值代表的是他们在TArray<FShaderCodeChunk>* CurrentScopeChunks中的索引,它们各自的类型、表达式数值都记录在对应的struct FShaderCodeChunk(也就是表达式对应的Shader代码块的信息,将在调用GetMaterialShaderCode时用于填充Shader代码)中。

FHLSLMaterialTranslator::Mul中操作数的数据类型、数值等都要通过操作数的索引值取得对应的FShaderCodeChunk,然后从中获取(GetArithmeticResultType, IsExpressionConstantValue, GetParameterUniformExpression等方法传入索引值的版本中都是这样做的)。

FHLSLMaterialTranslator::Mul的返回值则是需要创建计算结果对应的FShaderCodeChunk并Add到TArray<FShaderCodeChunk>* CurrentScopeChunks中,然后最终返回的也是它的索引值。

UMaterialExpressionMultiply

材质表达式定义

// Engine\Source\Runtime\Engine\Classes\Materials\MaterialExpressionMultiply.h

UCLASS(MinimalAPI)
class UMaterialExpressionMultiply : public UMaterialExpression
{
    GENERATED_UCLASS_BODY()

    UPROPERTY(meta = (RequiredInput = "false", ToolTip = "Defaults to 'ConstA' if not specified"))
    FExpressionInput A;

    UPROPERTY(meta = (RequiredInput = "false", ToolTip = "Defaults to 'ConstB' if not specified"))
    FExpressionInput B;

    /** only used if A is not hooked up */
    UPROPERTY(EditAnywhere, Category=MaterialExpressionMultiply, meta=(OverridingInputProperty = "A"))
    float ConstA;

    /** only used if B is not hooked up */
    UPROPERTY(EditAnywhere, Category=MaterialExpressionMultiply, meta=(OverridingInputProperty = "B"))
    float ConstB;


    //~ Begin UMaterialExpression Interface
#if WITH_EDITOR
    virtual int32 Compile(class FMaterialCompiler* Compiler, int32 OutputIndex) override;
    virtual void GetCaption(TArray<FString>& OutCaptions) const override;
    virtual FText GetKeywords() const override {return FText::FromString(TEXT("*"));}
    virtual bool GenerateHLSLExpression(FMaterialHLSLGenerator& Generator, UE::HLSLTree::FScope& Scope, int32 OutputIndex, UE::HLSLTree::FExpression const*& OutExpression) const override;
#endif // WITH_EDITOR
    //~ End UMaterialExpression Interface
};

MaterialExpression.cpp中MaterialExpressionMultiply的接口实现:

// Engine\Source\Runtime\Engine\Private\Materials\MaterialExpression.cpp
//
// UMaterialExpressionMultiply
//
UMaterialExpressionMultiply::UMaterialExpressionMultiply(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    // Structure to hold one-time initialization
    struct FConstructorStatics
    {
        FText NAME_Math;
        FConstructorStatics()
            : NAME_Math(LOCTEXT( "Math", "Math" ))
        {
        }
    };
    static FConstructorStatics ConstructorStatics;

    ConstA = 0.0f;
    ConstB = 1.0f;

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

#if WITH_EDITOR
int32 UMaterialExpressionMultiply::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
    // if the input is hooked up, use it, otherwise use the internal constant
    int32 Arg1 = A.GetTracedInput().Expression ? A.Compile(Compiler) : Compiler->Constant(ConstA);
    // if the input is hooked up, use it, otherwise use the internal constant
    int32 Arg2 = B.GetTracedInput().Expression ? B.Compile(Compiler) : Compiler->Constant(ConstB);

    return Compiler->Mul(Arg1, Arg2);
}

void UMaterialExpressionMultiply::GetCaption(TArray<FString>& OutCaptions) const
{
    FString ret = TEXT("Multiply");

    FExpressionInput ATraced = A.GetTracedInput();
    FExpressionInput BTraced = B.GetTracedInput();

    if(!ATraced.Expression || !BTraced.Expression)
    {
        ret += TEXT("(");
        ret += ATraced.Expression ? TEXT(",") : FString::Printf( TEXT("%.4g,"), ConstA);
        ret += BTraced.Expression ? TEXT(")") : FString::Printf( TEXT("%.4g)"), ConstB);
    }

    OutCaptions.Add(ret);
}
#endif // WITH_EDITOR


// Engine\Source\Runtime\Engine\Private\Materials\MaterialExpressionHLSL.cpp

bool UMaterialExpressionMultiply::GenerateHLSLExpression(FMaterialHLSLGenerator& Generator, UE::HLSLTree::FScope& Scope, int32 OutputIndex, UE::HLSLTree::FExpression const*& OutExpression) const
{
    const UE::HLSLTree::FExpression* Lhs = A.AcquireHLSLExpressionOrConstant(Generator, Scope, ConstA);
    const UE::HLSLTree::FExpression* Rhs = B.AcquireHLSLExpressionOrConstant(Generator, Scope, ConstB);
    if (!Lhs || !Rhs)
    {
        return false;
    }
    OutExpression = Generator.GetTree().NewMul(Lhs, Rhs);
    return true;
}

添加四项相乘的Multiply材质节点

在实现Morphling模型材质(该材质制作的教程:UE5材质宝典—水元素材质案例)的过程中,对于模型顶点的增加扰动的部分,其最终的输出使用了3个Multiply,这实际上可以使用一个四项相乘的Multiply完成。

MorphingMaterialBlueprint

因此,尝试添加一个接受四项输入的Multiply节点。

在Compiler中添加接口和实现

在MaterialExpression的Compile中会调用Compiler的对应接口从而完成编译,因此先在Compiler中完成接口和实现的添加。

FMaterialCompiler

此处不写成纯虚函数,因为继承自FMaterialCompiler的子类不止FHLSLMaterialTranslator,写成纯虚函数可能导致其他子类报错。

// Engine\Source\Runtime\Engine\Public\MaterialCompiler.h

class FMaterialCompiler
{
    (...)
public:
    /********************* 4-input Multiply *********************/
    virtual int32 My4InputMultiply(int32 A, int32 B, int32 C, int32 D) { return 1; }
    /************************************************************/
    (...)
}

FHLSLMaterialTranslator

FHlSLMaterialTranslator.h中声明

// Engine\Source\Runtime\Engine\Private\Materials\HLSLMaterialTranslator.h

class FHLSLMaterialTranslator : public FMaterialCompiler
{
    (...)
public:
    /********************* 4-input Multiply *********************/
    virtual int32 My4InputMultiply(int32 A, int32 B, int32 C, int32 D) override;
    /************************************************************/
    (...)
}

FHlSLMaterialTranslator.cpp中实现

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

// 输入输出都是操作数的索引,因此可以直接在其中调用Mul,实现四数相乘

int32 FHLSLMaterialTranslator::My4InputMultiply(int32 A, int32 B, int32 C, int32 D)
{
    return Mul(Mul(A, B), Mul(C, D));
}

添加MaterialExpression

新建四数相乘的材质表达式的类型:
直接参考MaterialExpressionMultiply.h修改

// Engine\Source\Runtime\Engine\Classes\Materials\MaterialExpressionMy4InputMultiply.h
// ilyaxu 2024.6.30

#pragma once

#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "MaterialExpressionIO.h"
#include "Materials/MaterialExpression.h"
#include "MaterialExpressionMy4InputMultiply.generated.h"

UCLASS(MinimalAPI)
class UMaterialExpressionMy4InputMultiply : public UMaterialExpression
{
    GENERATED_UCLASS_BODY()

    UPROPERTY(meta = (RequiredInput = "false", ToolTip = "Defaults to 'ConstA' if not specified"))
    FExpressionInput A;

    UPROPERTY(meta = (RequiredInput = "false", ToolTip = "Defaults to 'ConstB' if not specified"))
    FExpressionInput B;

    UPROPERTY(meta = (RequiredInput = "false", ToolTip = "Defaults to 'ConstC' if not specified"))
    FExpressionInput C;

    UPROPERTY(meta = (RequiredInput = "false", ToolTip = "Defaults to 'ConstD' if not specified"))
    FExpressionInput D;

    /** only used if A is not hooked up */
    UPROPERTY(EditAnywhere, Category=MaterialExpressionMy4InputMultiply, meta = (OverridingInputProperty = "A"))
    float ConstA;

    /** only used if B is not hooked up */
    UPROPERTY(EditAnywhere, Category=MaterialExpressionMy4InputMultiply, meta=(OverridingInputProperty = "B"))
    float ConstB;

    /** only used if C is not hooked up */
    UPROPERTY(EditAnywhere, Category = MaterialExpressionMy4InputMultiply, meta = (OverridingInputProperty = "C"))
    float ConstC;

    /** only used if D is not hooked up */
    UPROPERTY(EditAnywhere, Category = MaterialExpressionMy4InputMultiply, meta = (OverridingInputProperty = "D"))
    float ConstD;


    //~ Begin UMaterialExpression Interface
#if WITH_EDITOR
    virtual int32 Compile(class FMaterialCompiler* Compiler, int32 OutputIndex) override;
    virtual void GetCaption(TArray<FString>& OutCaptions) const override;
#endif // WITH_EDITOR
    //~ End UMaterialExpression Interface
};

在MaterialExpressions.cpp中实现MaterialExpressionMy4InputMultiply材质表达式的接口:

// Engine\Source\Runtime\Engine\Private\Materials\MaterialExpressions.cpp

//
// UMaterialExpressionMy4InputMultiply
//
UMaterialExpressionMy4InputMultiply::UMaterialExpressionMy4InputMultiply(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    // Structure to hold one-time initialization
    struct FConstructorStatics
    {
        FText NAME_Math;
        FConstructorStatics()
            : NAME_Math(LOCTEXT("Math", "Math"))
        {}
    };
    static FConstructorStatics ConstructorStatics;

    ConstA = 0.0f;
    ConstB = 1.0f;
    ConstC = 0.0f;
    ConstD = 1.0f;

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

#if WITH_EDITOR
int32 UMaterialExpressionMy4InputMultiply::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
    // if the input is hooked up, use it, otherwise use the internal constant
    int32 Arg1 = A.GetTracedInput().Expression ? A.Compile(Compiler) : Compiler->Constant(ConstA);
    // if the input is hooked up, use it, otherwise use the internal constant
    int32 Arg2 = B.GetTracedInput().Expression ? B.Compile(Compiler) : Compiler->Constant(ConstB);
    // if the input is hooked up, use it, otherwise use the internal constant
    int32 Arg3 = C.GetTracedInput().Expression ? C.Compile(Compiler) : Compiler->Constant(ConstC);
    // if the input is hooked up, use it, otherwise use the internal constant
    int32 Arg4 = D.GetTracedInput().Expression ? D.Compile(Compiler) : Compiler->Constant(ConstD);

    return Compiler->My4InputMultiply(Arg1, Arg2, Arg3, Arg4);
}

void UMaterialExpressionMy4InputMultiply::GetCaption(TArray<FString>& OutCaptions) const
{
    FString ret = TEXT("My4InputMultiply");

    FExpressionInput ATraced = A.GetTracedInput();
    FExpressionInput BTraced = B.GetTracedInput();
    FExpressionInput CTraced = C.GetTracedInput();
    FExpressionInput DTraced = D.GetTracedInput();

    if (!ATraced.Expression || !BTraced.Expression || !CTraced.Expression || !DTraced.Expression)
    {
        ret += TEXT("(");
        ret += ATraced.Expression ? TEXT(",") : FString::Printf(TEXT("%.4g,"), ConstA);
        ret += BTraced.Expression ? TEXT(",") : FString::Printf(TEXT("%.4g)"), ConstB);
        ret += CTraced.Expression ? TEXT(",") : FString::Printf(TEXT("%.4g,"), ConstC);
        ret += DTraced.Expression ? TEXT(")") : FString::Printf(TEXT("%.4g)"), ConstD);
    }

    OutCaptions.Add(ret);
}
#endif // WITH_EDITOR

测试

使用My4InputMultiply替换原先的三个Multiply

test

效果与先前没有差别,My4InputMultiply材质节点测试无误

Morphing

Reference

Unreal Engine Documentation: Material Functions Overview

剖析虚幻渲染体系(09)- 材质体系

用C++扩展一个UE4材质节点