通过这个小练习,完成后应该了解到的是:C++ 负责定义逻辑与接口,而 UE 编辑器则负责创建与摆放对象、调整参数以及最终 Shader 的实现。同时,也能够学习 UE C++ 怎么把变量暴露到编辑器、怎么创建组件、怎么引用资产、怎么生成动态材质实例、怎么给蓝图提供接口

这个练习也非常简单:通过 C++ 创建一个 Actor 对象,让它出现在 UE 的编辑器里,并且我们能够通过编辑器的 Details 面板来调整这个 Actor 的颜色、粗糙度、自发光强度等等

注:这里我使用的是 UE5.6 版本

创建一个 C++ 类#

点击编辑器上方的 工具 -> 新建 C++ 类 -> Actor,命名为“MaterialShowcaseActor”,之后创建

此时 UE 会自动生成两个文件:MaterialShowcaseActor.h 和 MaterialShowcaseActor.cpp,与大多数类一样,前者用于声明,后者用于定义,模板如下:

 1#pragma once
 2#include "CoreMinimal.h"
 3#include "GameFramework/Actor.h"
 4#include "MaterialShowcaseActor.generated.h"
 5
 6UCLASS()
 7// 注意这里的类名是项目的名字 + "_API"
 8class UE5_MATERIAL_LAB_API AMaterialShowcaseActor : public AActor
 9{
10    GENERATED_BODY()
11
12public:	
13    // Sets default values for this actor's properties
14    AMaterialShowcaseActor();
15
16protected:
17    // Called when the game starts or when spawned
18    virtual void BeginPlay() override;
19
20public:	
21    // Called every frame
22    virtual void Tick(float DeltaTime) override;
23};
 1#include "MaterialShowcaseActor.h"
 2#include "Components/StaticMeshComponent.h"
 3#include "Materials/MaterialInstanceDynamic.h"
 4
 5// Sets default values
 6AMaterialShowcaseActor::AMaterialShowcaseActor()
 7{
 8    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
 9    PrimaryActorTick.bCanEverTick = true;
10}
11
12// Called when the game starts or when spawned
13void AMaterialShowcaseActor::BeginPlay()
14{
15    Super::BeginPlay();
16}
17
18// Called every frame
19void AMaterialShowcaseActor::Tick(float DeltaTime)
20{
21    Super::Tick(DeltaTime);
22}

可以看到,模板代码中包含一些奇奇怪怪的宏与类(比如 GENERATED_BODY()、AActor……),这些可以参见之前写的 UE 反射系统 与 UE Gameplay Framework(一)。总之,关于这些东西是什么以及他们的作用,这里不再赘述

修改 .h 和 .cpp 文件#

在 MaterialShowcaseActor.h 之中添加不同的属性与之后会用到的函数

 1#pragma once
 2
 3#include "CoreMinimal.h"
 4#include "GameFramework/Actor.h"
 5#include "MaterialShowcaseActor.generated.h"
 6
 7UCLASS()
 8class UE5_MATERIAL_LAB_API AMaterialShowcaseActor : public AActor
 9{
10    GENERATED_BODY()
11
12public:	
13    // Sets default values for this actor's properties
14    AMaterialShowcaseActor();
15
16protected:
17    // Called when the game starts or when spawned
18    virtual void BeginPlay() override;
19
20/* ------------ 新增部分 ------------ */
21    // Actor 被放置、Details 面板中的参数被修改时调用
22    virtual void OnConstruction(const FTransform& Transform) override;
23/* --------------------------------- */
24
25public:	
26    // Called every frame
27    virtual void Tick(float DeltaTime) override;
28
29/* ------------ 新增部分 ------------ */
30public:
31    // 模型组件:编辑器中可见,但不可替换;类型为 Components
32    UPROPERTY(VisibleAnywhere, Category = "Components")
33    TObjectPtr<UStaticMeshComponent> MeshComponent;
34
35    // 原始材质引用:可编辑;类型为 Material
36    UPROPERTY(EditAnywhere, Category = "Material")
37    TObjectPtr<UMaterialInterface> SourceMaterial;
38
39    // 基础色,后面的几个参数都可在 Details 面板中编辑
40    UPROPERTY(EditAnywhere, Category = "Material")
41    FLinearColor BaseColor = FLinearColor::White;
42
43    // 粗糙度
44    UPROPERTY(EditAnywhere, Category = "Material", meta = (ClampMin = "0.0", ClampMax = "1.0"))
45    float Roughness = 0.5f;
46
47    // 自发光强度
48    UPROPERTY(EditAnywhere, Category = "Material", meta = (ClampMin = "0.0"))
49    float EmissiveStrength = 0.0f;
50
51    // 动态的材质实例
52    // 这是因为我们不希望在运行时对源材质进行修改(会导致整个材质类的重新编译,同时也会影响所有使用这个材质的物体)
53    // 以此需要在运行时创建一个动态实例,在这之上的修改只会作用于当前 Actor
54    UPROPERTY()
55    TObjectPtr<UMaterialInstanceDynamic> MaterialInstance;
56
57    // 一个辅助函数,用于将 C++ 变量同步到材质
58    void UpdateMaterialInstance();
59/* --------------------------------- */
60};

在 MaterialShowcaseActor.cpp 中对之前声明的函数进行定义

 1#include "MaterialShowcaseActor.h"
 2#include "Components/StaticMeshComponent.h"
 3#include "Materials/MaterialInstanceDynamic.h"
 4
 5// Sets default values
 6AMaterialShowcaseActor::AMaterialShowcaseActor()
 7{
 8    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
 9    PrimaryActorTick.bCanEverTick = true;
10
11/* ------------ 新增部分 ------------ */
12    // Actor 本身只是一个容器,真正的显示则是由一个个的组件所实现
13    // 这里则是创建了一个 StaticMeshComponent
14    MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
15    // 将这个组件设置为根组件,之后的旋转、缩放等操作都以此为基础进行
16    RootComponent = MeshComponent;
17/* --------------------------------- */
18}
19
20// Called when the game starts or when spawned
21void AMaterialShowcaseActor::BeginPlay()
22{
23    Super::BeginPlay();
24
25/* ------------ 新增部分 ------------ */
26    // 如果没有在 Details 面板中填入材质,就不执行,避免空指针错误
27    if (SourceMaterial)
28    {
29        // 根据 SourceMaterial 创建一份材质实例
30        MaterialInstance = UMaterialInstanceDynamic::Create(SourceMaterial, this);
31        // 并将这个材质实例绑定到 0 号材质槽中
32        MeshComponent->SetMaterial(0, MaterialInstance);
33        // 将 C++ 中定义的参数写入材质实例
34        UpdateMaterialInstance();
35    }
36}
37
38// 这个函数用来创建动态材质与更新参数
39// 因此即使没有运行也可以在预览窗口中实时看到材质变化
40void AMaterialShowcaseActor::OnConstruction(const FTransform& Transform)
41{
42    Super::OnConstruction(Transform);
43
44    if (SourceMaterial)
45    {
46        MaterialInstance = UMaterialInstanceDynamic::Create(SourceMaterial, this);
47        MeshComponent->SetMaterial(0, MaterialInstance);
48        UpdateMaterialInstance();
49    }
50}
51
52void AMaterialShowcaseActor::UpdateMaterialInstance()
53{
54    // 如果连材质实例都不存在,自然也就没必要更新,直接退出即可
55    if (!MaterialInstance)
56    {
57        return;
58    }
59
60    // 这里是真正将 C++ 与材质连接在一起的地方
61    // 需要注意的是,这里的字符串必须与材质蓝图中的参数名完全一致
62    // 如果不一致,编译器倒是不会报错,但调整参数也不会有效果
63    MaterialInstance->SetVectorParameterValue(TEXT("BaseColor"), BaseColor);
64    MaterialInstance->SetScalarParameterValue(TEXT("Roughness"), Roughness);
65    MaterialInstance->SetScalarParameterValue(TEXT("EmissiveStrength"), EmissiveStrength);
66}
67/* --------------------------------- */
68
69// Called every frame
70void AMaterialShowcaseActor::Tick(float DeltaTime)
71{
72    Super::Tick(DeltaTime);
73}

创建材质#

在写代码时就已经可以知道,通过 C++ 类所创建的 Actor,必须得有一个材质进行承接,不然代码中的 if 语句会直接将流程跳过。

在 UE 的内容浏览器中创建一个名为“M_ShowCase”的材质,进入蓝图后创建三个参数——BaseColor、Roughness、EmissiveStrength 并进行如图连接:

M_ShowCase 材质中 BaseColor、Roughness 和 EmissiveStrength 参数的节点连接

编译#

这里既可以直接在 IDE 中进行编译,同时也可以点击 UE 编辑器下方栏中的编译按钮,之后在内容浏览器中就能找到“C++ 类 / YourProject / Public / MaterialShowcaseActor”

UE 内容浏览器中编译生成的 MaterialShowcaseActor C++ 类

设置 Actor#

将此 Actor 拖入场景中,可以看到 Actor 是空的,什么也没有,这是因为 Actor 还没有绑定组件,在右侧的 Detail 栏中随便选一个 Static Mesh 即可,之后再把上一步创建的材质拖进材质槽位。此时如果对参数进行修改,正常情况下应该是会作用到 Actor 上的

总结:#

至此,我们已经打通了 C++ 与引擎通信的第一层:

C++ class

-> UE 反射系统

-> Editor Details 面板

-> Actor 实例

-> Component

-> Material asset

-> Dynamic Material Instance

-> Runtime parameter update