说明
本文先对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完成。
因此,尝试添加一个接受四项输入的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
效果与先前没有差别,My4InputMultiply材质节点测试无误