目标
在材质编辑器的节点中,有的节点可以双击点开,并呈现出内部的节点:
他们是材质函数(MaterialFunction),每一种对应了一个资源。
而有的节点无法打开:
他们每一种都对应了一个C++类:UMaterialExpression 。(可在\Engine\Source\Runtime\Engine\Classes\Materials
路径下找到大量其子类的定义,在\Engine\Source\Runtime\Engine\Private\Materials\MaterialExpressions.cpp
中找到实现)
本篇的目标是:
- 简单观察
UMaterialExpression
类的最重要内容 - 尝试创建一个
UMaterialExpression
的子类,即扩展一个UE4材质节点。在本篇中,我创建的是一个可以接受多个(可变化数目)引脚的Add节点。
讨论:何时需要用C++扩展一个UE4材质节点
要想自定义节点逻辑,其实可以使用材质函数,或者Custom节点。如果选择用C++扩展材质节点,实际上是相对更费时费力的。
目前我实际上也没有遇到太多必须用C++扩展材质节点的情况。我目前能想象到的需要的情况是:
- 对输入有特殊要求的,比如输入的引脚数目是变化的,正如本篇试图创建的节点。
- 对一个已有的C++节点的逻辑进行修改。这个节点由于是C++的,所以想修改它也只能从C++入手。
UMaterialExpression
class ENGINE_API UMaterialExpression : public UObject
我想,它最重要的接口要属Compile
了,(注释中“Abs expression”的Abs可能有误。。。)
/**
* Create the new shader code chunk needed for the Abs expression
*
* @param Compiler - UMaterial compiler that knows how to handle this expression.
* @return Index to the new FMaterialCompiler::CodeChunk entry for this expression
*/
virtual int32 Compile(class FMaterialCompiler* Compiler, int32 OutputIndex) { return INDEX_NONE; }
在这个接口中,shader代码被拼接,例如对于Add表达式:
int32 UMaterialExpressionAdd::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->Add(Arg1, Arg2);
}
也就是说:
C++代码中并不写shader计算的逻辑,而是写shader代码拼写的逻辑。而实际的拼写,则由FMaterialCompiler
完成
FMaterialCompiler
两个子类:
实际发挥作用的是FHLSLMaterialTranslator
。
例如对于Add
:
virtual int32 Add(int32 A,int32 B) override
{
if(A == INDEX_NONE || B == INDEX_NONE)
{
return INDEX_NONE;
}
const uint64 Hash = CityHash128to64({ GetParameterHash(A), GetParameterHash(B) });
if(GetParameterUniformExpression(A) && GetParameterUniformExpression(B))
{
return AddUniformExpressionWithHash(Hash, new FMaterialUniformExpressionFoldedMath(GetParameterUniformExpression(A),GetParameterUniformExpression(B),FMO_Add),GetArithmeticResultType(A,B),TEXT("(%s + %s)"),*GetParameterCode(A),*GetParameterCode(B));
}
else
{
return AddCodeChunkWithHash(Hash, GetArithmeticResultType(A,B),TEXT("(%s + %s)"),*GetParameterCode(A),*GetParameterCode(B));
}
}
扩展一个UE4材质节点
我选择将扩展的UE4节点放到插件中
0. 创建插件
以空白为模板创建插件
1. 创建一个新的 UMaterialExpression 子类(从Add节点拷贝)
为了从一个方便的起点开始编辑,我选择拷贝Add这个节点的代码,然后改名。
拷贝后并改名的结果:
MaterialExpressionYaksueTest.h
:
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "MaterialExpressionIO.h"
#include "Materials/MaterialExpression.h"
#include "MaterialExpressionYaksueTest.generated.h"
UCLASS(MinimalAPI)
class UMaterialExpressionYaksueTest : 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=MaterialExpressionYaksueTest, meta=(OverridingInputProperty = "A"))
float ConstA;
/** only used if B is not hooked up */
UPROPERTY(EditAnywhere, Category=MaterialExpressionYaksueTest, 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("+"));}
#endif // WITH_EDITOR
//~ End UMaterialExpression Interface
};
MaterialExpressionYaksueTest.cpp
:
#include "MaterialExpressionYaksueTest.h"
#include "MaterialCompiler.h"
#if WITH_EDITOR
int32 UMaterialExpressionYaksueTest::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->Add(Arg1, Arg2);
}
void UMaterialExpressionYaksueTest::GetCaption(TArray<FString>& OutCaptions) const
{
FString ret = TEXT("YaksueTest");
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
UMaterialExpressionYaksueTest::UMaterialExpressionYaksueTest(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
ConstA = 0.0f;
ConstB = 1.0f;
}
编译代码后,便可以在材质编辑器内看到一个新的名为“YaksueTest”的节点,目前它的功能和Add节点一样:
(注意我使用了“DebugScalarValues”节点来输出数字)
2. 改变输入的方式,使其可以接收可变的输入
ValueCount
代表输入的数目:
UPROPERTY(EditAnywhere, Category=MaterialExpressionYaksueTest)
int ValueCount;
AddingValues
容纳所有的输入
UPROPERTY()
TArray<FExpressionInput> AddingValues;
不需要在UPROPERTY
宏内指定AddingValues
可编辑,因为节点的输入通过重载GetInputs()
函数来指定:
//重载此函数,根据ValueCount来决定输入的个数
virtual const TArray<FExpressionInput*> GetInputs() override;
const TArray<FExpressionInput*> UMaterialExpressionYaksueTest::GetInputs()
{
//设定列表长度
AddingValues.SetNum(ValueCount);
//加入所有的输入:
TArray<FExpressionInput*> Result;
for (int32 i = 0; i < AddingValues.Num(); i++)
Result.Add(&AddingValues[i]);
return Result;
}
而当输入引脚数目发生变化后需要刷新一下节点的UI,所以需要重载PostEditChangeProperty
函数:(参考了UMaterialExpressionLandscapeLayerBlend::PostEditChangeProperty
)
//在ValueCount变化时需要刷新节点的引脚UI
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
void UMaterialExpressionYaksueTest::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
//刷新节点的引脚UI
if (UMaterialGraphNode* MatGraphNode = Cast<UMaterialGraphNode>(GraphNode))
MatGraphNode->RecreateAndLinkNode();
}
(由于使用了节点界面的功能,所以PrivateDependencyModuleNames
需要加入"UnrealEd"
)
而Compile函数,则累加所有的输入:
int32 UMaterialExpressionYaksueTest::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
//如果没有任何输入,则直接返回一个常量:0
if (AddingValues.Num() == 0)
return Compiler->Constant(0);
//当前表达式:
int32 Current = AddingValues[0].Compile(Compiler);
//累加之后的所有表达式
for (int i = 1; i < AddingValues.Num(); i++)
Current = Compiler->Add(Current, AddingValues[i].Compile(Compiler));
//返回最后的表达式
return Current;
}
效果:
首先,输入的数目可以变化:
累加的效果也正确:
最终代码
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "MaterialExpressionIO.h"
#include "Materials/MaterialExpression.h"
#include "MaterialExpressionYaksueTest.generated.h"
UCLASS(MinimalAPI)
class UMaterialExpressionYaksueTest : public UMaterialExpression
{
GENERATED_UCLASS_BODY()
//输入的数目:
UPROPERTY(EditAnywhere, Category=MaterialExpressionYaksueTest)
int ValueCount;
//输入引脚的列表
UPROPERTY()
TArray<FExpressionInput> AddingValues;
//~ 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("+"));}
//重载此函数,根据ValueCount来决定输入的个数
virtual const TArray<FExpressionInput*> GetInputs() override;
//在ValueCount变化时需要刷新节点的引脚UI
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif // WITH_EDITOR
//~ End UMaterialExpression Interface
};
#include "MaterialExpressionYaksueTest.h"
#include "MaterialCompiler.h"
#if WITH_EDITOR
#include "MaterialGraph/MaterialGraphNode.h"
#endif
#if WITH_EDITOR
int32 UMaterialExpressionYaksueTest::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
//如果没有任何输入,则直接返回一个常量:0
if (AddingValues.Num() == 0)
return Compiler->Constant(0);
//当前表达式:
int32 Current = AddingValues[0].Compile(Compiler);
//累加之后的所有表达式
for (int i = 1; i < AddingValues.Num(); i++)
Current = Compiler->Add(Current, AddingValues[i].Compile(Compiler));
//返回最后的表达式
return Current;
}
void UMaterialExpressionYaksueTest::GetCaption(TArray<FString>& OutCaptions) const
{
FString ret = TEXT("YaksueTest");
OutCaptions.Add(ret);
}
const TArray<FExpressionInput*> UMaterialExpressionYaksueTest::GetInputs()
{
//设定列表长度
AddingValues.SetNum(ValueCount);
//加入所有的输入:
TArray<FExpressionInput*> Result;
for (int32 i = 0; i < AddingValues.Num(); i++)
Result.Add(&AddingValues[i]);
return Result;
}
void UMaterialExpressionYaksueTest::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
//刷新节点的引脚UI
if (UMaterialGraphNode* MatGraphNode = Cast<UMaterialGraphNode>(GraphNode))
MatGraphNode->RecreateAndLinkNode();
}
#endif // WITH_EDITOR
UMaterialExpressionYaksueTest::UMaterialExpressionYaksueTest(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
ValueCount = 3;
}