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

目标

在材质编辑器的节点中,有的节点可以双击点开,并呈现出内部的节点:
在这里插入图片描述
他们是材质函数(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;
}
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
UE4是一款强大的游戏引擎,其中的材质节点是用来创建和控制游戏中物体的表面外观的。在UE4中编写材质节点需要使用C++编程语言来创建自定义的节点。 首先,我们需要创建一个用于材质编程的MaterialInstanceDynamic对象。我们可以使用以下代码来创建它: UMaterialInstanceDynamic* DynamicMaterial = UMaterialInstanceDynamic::Create(OriginalMaterial, nullptr); 接下来,我们可以通过使用DynamicMaterial来设置和控制材质的参数。例如,我们可以使用以下代码来设置节点的颜色参数: DynamicMaterial->SetVectorParameterValue(FName("Color"), FLinearColor(1.0f, 0.0f, 0.0f)); 除了设置基本的参数,我们还可以设置纹理和其他复杂的节点属性。例如,我们可以使用以下代码来设置一个纹理节点: DynamicMaterial->SetTextureParameterValue(FName("Texture"), Texture); 我们还可以使用C++代码来创建和连接各种节点的输入和输出。例如,我们可以使用以下代码来创建一个常量节点: UMaterialExpressionConstant* ConstantNode = NewObject<UMaterialExpressionConstant>(); ConstantNode->R = 0.5f; ConstantNode->G = 0.5f; ConstantNode->B = 0.5f; 然后,我们可以使用以下代码将该节点连接到材质的Diffuse输入节点上: DynamicMaterial->BaseColor.Expression = ConstantNode; 最后,我们需要将DynamicMaterial应用于我们想要渲染的物体上。我们可以使用以下代码来完成这一步骤: MeshComponent->SetMaterial(0, DynamicMaterial); 通过使用UE4中的C++编程语言,我们可以创建复杂的材质节点,从而实现更高级的渲染效果和外观。这使得我们能够在游戏中创造出令人印象深刻的图形效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值