虚幻 – 材质蓝图节点源码实现分析

引擎: UE4.26.2 构建版本

事情的起因是写体素构建器的时候,想生成个球,就想到抄材质的 SphereMask 节点。

这里以 SphereMask 节点为例,分析材质蓝图节点的定义。

UMaterialExpression

材质节点的 C++ 父类为 UMaterialExpression ,定义在 MaterialExpression.h 头文件中,所有原生材质节点均为此类的子类,此类主要成员如下。

virtual int32 Compile(class FMaterialCompiler* Compiler, int32 OutputIndex);

编译节点,用于生成对应的 HLSL 代码,需要子类覆盖。

virtual void GetCaption(TArray<FString>& OutCaptions) const;
virtual FText GetKeywords() const;

获取节点的标题和关键词,选择性覆盖。

virtual TArray<FExpressionOutput>& GetOutputs();
virtual const TArray<FExpressionInput*> GetInputs();
virtual FExpressionInput* GetInput(int32 InputIndex);
virtual FName GetInputName(int32 InputIndex) const;

用于系统获取节点的输入输出信息,具有通过反射获取信息的默认实现,选择性覆盖。

UMaterialExpressionSphereMask

UMaterialExpressionSphereMask 是 UMaterialExpression 的直接子类,定义于 MaterialExpressionSphereMask.h 但是与其他系统节点共享实现文件,位于 MaterialExpressions.cpp 。

输入输出引脚定义

UPROPERTY()
FExpressionInput A;

UPROPERTY()
FExpressionInput B;

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

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

参数缺省定义

在 Radius 和 Hardness 引脚没有连接时,使用缺省定义的常量。

UPROPERTY(EditAnywhere, Category=MaterialExpressionSphereMask, meta=(OverridingInputProperty = "Radius"), DisplayName = "Radius")
float AttenuationRadius;

UPROPERTY(EditAnywhere, Category=MaterialExpressionSphereMask, meta=(UIMin = "0.0", UIMax = "100.0", ClampMin = "0.0", ClampMax = "100.0", OverridingInputProperty = "Hardness", DisplayName = "Hardness"))
float HardnessPercent;

编译函数实现

virtual int32 Compile(class FMaterialCompiler* Compiler, int32 OutputIndex) override
{
    if(!A.GetTracedInput().Expression)
    {
        return Compiler->Errorf(TEXT("Missing input A"));
    }
    else if(!B.GetTracedInput().Expression)
    {
        return Compiler->Errorf(TEXT("Missing input B"));
    }
    else
    {
        int32 Arg1 = A.Compile(Compiler);
        int32 Arg2 = B.Compile(Compiler);
        int32 Distance = CompileHelperLength(Compiler, Arg1, Arg2);

        int32 ArgInvRadius;
        if(Radius.GetTracedInput().Expression)
        {
            ArgInvRadius = Compiler->Div(Compiler->Constant(1.0f), Compiler->Max(Compiler->Constant(0.00001f), Radius.Compile(Compiler)));
        }
        else
        {
            ArgInvRadius = Compiler->Constant(1.0f / FMath::Max(0.00001f, AttenuationRadius));
        }

        int32 NormalizeDistance = Compiler->Mul(Distance, ArgInvRadius);

        int32 ArgInvHardness;
        if(Hardness.GetTracedInput().Expression)
        {
            int32 Softness = Compiler->Sub(Compiler->Constant(1.0f), Hardness.Compile(Compiler));

            ArgInvHardness = Compiler->Div(Compiler->Constant(1.0f), Compiler->Max(Softness, Compiler->Constant(0.00001f)));
        }
        else
        {
            float InvHardness = 1.0f / FMath::Max(1.0f - HardnessPercent * 0.01f, 0.00001f);

            ArgInvHardness = Compiler->Constant(InvHardness);
        }

        int32 NegNormalizedDistance = Compiler->Sub(Compiler->Constant(1.0f), NormalizeDistance);
        int32 MaskUnclamped = Compiler->Mul(NegNormalizedDistance, ArgInvHardness);

        return CompileHelperSaturate(Compiler, MaskUnclamped);
    }
}

最重要的函数,用于生产 SphereMask 节点的 HLSL 代码,开头的两个判断分支检查 A 和 B 输入是否存在,不存在则会终止编译并输出错误信息,检查无误后开始生成代码,其中有两个判断是检查 Radius 和 Hardness 是否需要使用缺省选项,下面按照 A 输入 UV , B 为 0.5 定值, Radius 缺省为 0.25 , Hardness 缺省为 0.0 分析。

生成的 HLSL 代码为:

MaterialFloat2 Local0 = (Parameters.TexCoords[0].xy - 0.50000000);
MaterialFloat Local1 = dot(Local0, Local0);
MaterialFloat Local2 = sqrt(Local1);
MaterialFloat Local3 = (Local2 * 4.00000000);
MaterialFloat Local4 = (1.00000000 - Local3);
MaterialFloat Local5 = (Local4 * 1.00000000);
MaterialFloat Local6 = min(max(Local5,0.00000000),1.00000000);

对应 C++ 代码分析:

int32 Distance = CompileHelperLength(Compiler, Arg1, Arg2);
MaterialFloat2 Local0 = (Parameters.TexCoords[0].xy - 0.50000000);
MaterialFloat Local1 = dot(Local0, Local0);
MaterialFloat Local2 = sqrt(Local1);

int32 NormalizeDistance = Compiler->Mul(Distance, ArgInvRadius);
MaterialFloat Local3 = (Local2 * 4.00000000);

int32 NegNormalizedDistance = Compiler->Sub(Compiler->Constant(1.0f), NormalizeDistance);
MaterialFloat Local4 = (1.00000000 - Local3);

int32 MaskUnclamped = Compiler->Mul(NegNormalizedDistance, ArgInvHardness);
MaterialFloat Local5 = (Local4 * 1.00000000);

return CompileHelperSaturate(Compiler, MaskUnclamped);
MaterialFloat Local6 = min(max(Local5,0.00000000),1.00000000);

易得,材质编译器用 int32 储存 HLSL 变量的索引,按照调用顺序生成 HLSL 代码。

类与函数

  • FExpressionInput – 用于定义材质节点输入
  • FExpressionInput::Compile – 编译输入
  • FMaterialCompiler – 材质编译器
  • FMaterialCompiler::Add – 加编译
  • FMaterialCompiler::Sub – 减编译
  • FMaterialCompiler::Mul – 乘编译
  • FMaterialCompiler::Div – 除编译
  • CompileHelperLength – 取长度编译
  • CompileHelperSaturate – Clamp 编译

发表回复