【转载】用 C++ 扩展一个 UE4 材质节点

1,093 阅读5分钟

版权声明:本文为CSDN博主「YakSue」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:blog.csdn.net/u013412391/…

目标

在材质编辑器的节点中,有的节点可以双击点开,并呈现出内部的节点:

image.png

他们是材质函数MaterialFunction),每一种对应了一个资源。

而有的节点无法打开:

image.png

他们每一种都对应了一个 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 两个子类:

image.png

实际发挥作用的是 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. 创建插件

以空白为模板创建插件

image.png

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” 节点来输出数字)

image.png

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;
}

效果

首先,输入的数目可以变化:

20201201234036404.gif

累加的效果也正确:

image.png

最终代码

#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;
}