虚幻引擎RDG计算管线调用流程

前言

这是我在学习 Unreal 渲染依赖图(RDG)过程中的踩坑与总结笔记(一),本篇主要记录在虚幻引擎中调用计算着色器的实践过程。

Unreal版本为5.7。

配置Unreal工程

创建Unreal工程

创建一个空白C++虚幻工程,将项目名称设置为LearnRDG

配置uproject

打开工程文件夹下的LearnRDG.uproject,将Modules内的LoadingPharseDefault修改为PostConfigInit

1
2
3
4
5
6
7
8
9
10
11
"Modules": [
{
"Name": "LearnRDG",
"Type": "Runtime",
// "LoadingPharse": "Default",
"LoadingPhase": "PostConfigInit",
"AdditionalDependencies": [
"Engine"
]
}
]

修改LearnRDG.Build.cs

打开LearnRDG.Build.cs,添加RHIRenderCoreProjects依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnrealBuildTool;

public class LearnRDG : ModuleRules
{
public LearnRDG(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

PrivateDependencyModuleNames.AddRange(new string[] { "RHI", "RenderCore", "Projects" });
}
}

修改LearnRDG.cpp

官方文档中提到,Unreal 的着色器文件(.usf / .ush)通常存放在 Engine/ShadersEngine/Plugins 目录下。

为了便于管理,我们希望将自定义着色器放置在工程目录中,因此首先在工程根目录下创建一个 Shaders 文件夹。

1
2
3
4
LearnRDG
|- Content
|- Sources
|- Shaders // Shaders文件夹和Content|Sources处于同一目录下

随后打开LearnRDG.cpp文件,将内容替换为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "LearnRDG.h"
#include "Modules/ModuleManager.h"
#include "Misc/Paths.h"
#include "ShaderCore.h"

class FLearnRDGGameModule : public FDefaultGameModuleImpl
{
public:
virtual void StartupModule() override
{
const FString ShaderDir = FPaths::Combine(FPaths::ProjectDir(), TEXT("Shaders"));
AddShaderSourceDirectoryMapping(TEXT("/MyShaders"), ShaderDir);
}

virtual void ShutdownModule() override
{
}
};

IMPLEMENT_PRIMARY_GAME_MODULE(FLearnRDGGameModule, LearnRDG, "LearnRDG");

FLearnRDGGameModuleStartupModule() 中,通过相关代码构建虚拟路径映射。完成映射后,即可使用 /MyShaders 来指代工程目录下的 /Shaders 文件夹,从而像使用引擎内置着色器路径一样引用自定义着色器。

关闭虚幻编辑器,编译Visual Studio工程。
一定要关闭编辑器后再编译,不然会遇到许多莫名其妙的问题(后续每次编译都需要先关闭虚幻编译器)。

在 UE 中使用 RDG 构建计算管线

编写计算着色器的usf文件

在工程目录下的Shaders创建一个名为ComputeShader.usf的文件,编写以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "/Engine/Public/Platform.ush"

float Scale;
float Translate;

StructuredBuffer<float> InputBuffer;
RWStructuredBuffer<float> OutputBuffer;

[numthreads(1, 1, 1)]
void MainCS(
    uint3 DispatchThreadId : SV_DispatchThreadID,
    uint GroupIndex : SV_GroupIndex)
{
    OutputBuffer[DispatchThreadId.x] = InputBuffer[DispatchThreadId.x] * Scale + Translate;
}

(示例计算着色器来自:Unreal Shader Tutorial - Chinese Ver. | SirEnri’s Homepage

创建计算着色器的Global Shader

打开UE编辑器,选择 工具->新建C++文件,父类选择 ,命名为LearnRDGShader

删除LearnRDGShader.h#include "CoreMinimal.h"以下的所有代码,删除LearnRDGShader.cpp中的所有代码。

LearnRDGShader.cpp中编写以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "LearnRDGShader.h"
#include "GlobalShader.h"
#include "RenderGraphBuilder.h"
#include "ShaderParameterStruct.h"
#include "RenderGraphDefinitions.h"
#include "RenderGraphUtils.h"
#include "RHIGPUReadback.h"
#include "ScreenPass.h"
#include "PixelShaderUtils.h"

class LEARNRDG_API FTestCS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FTestCS);
SHADER_USE_PARAMETER_STRUCT(FTestCS, FGlobalShader);

BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER(float, Scale)
SHADER_PARAMETER(float, Translate)
SHADER_PARAMETER_RDG_BUFFER_SRV(StructuredBuffer<float>, InputBuffer)
SHADER_PARAMETER_RDG_BUFFER_UAV(RWStructuredBuffer<float>, OutputBuffer)
END_SHADER_PARAMETER_STRUCT()

static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return true;
}
};

IMPLEMENT_GLOBAL_SHADER(FTestCS, "/MyShaders/ComputeShader.usf", "MainCS", SF_Compute);

着色器类的声明参考UE源码,BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )内部的宏定义需要与HLSL源文件中的着色器输入一一对应。

随后调用IMPLEMENT_GLOBAL_SHADER,把 .usf 里的计算着色器入口函数,注册成 Unreal 可识别、可编译、可使用的全局着色器类型。

EPIC官方文档:虚幻引擎中的渲染依赖图 | 虚幻引擎 5.7 文档 | Epic Developer Community

使用 FRDGBuilder 添加计算 Pass

LearnRDGShader.h添加函数声明:

1
2
3
4
namespace LearnRDGShader
{
void AddTestComputePass(FRDGBuilder& GraphBuilder, float Scale, float Translate, FRHIGPUBufferReadback* Readback) ;
}

其中FRDGBuilder是UE的RDG构建器,可以通过这个构建在渲染图中添加计算通道。ScaleTranslate用于配置Hlsl中的参数。FRHIGPUBufferReadback用于回读计算着色器的输出结果到CPU。

LearnRDGShader.cpp中实现函数:

1
2
void TestShader::AddTestComputePass(FRDGBuilder& GraphBuilder, float Scale, float Translate, FRHIGPUBufferReadback* Readback)
{}

首先获取之前注册的计算着色器:

1
2
3
4
5
6
void TestShader::AddTestComputePass(FRDGBuilder& GraphBuilder, float Scale, float Translate, FRHIGPUBufferReadback* Readback)
{
//Start Edit
TShaderMapRef<FTestCS> ComputeShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));
//End Edit
}

随后需要配置计算着色器的参数,包括两个 float 类型的标量参数、一个作为输入的 SRV 缓冲区,以及一个作为输出的 UAV 缓冲区。

这些参数通过 FTestCS::FParameters 结构体进行描述,并在创建 Pass 时将对应的资源与数值绑定到该结构体实例上。

现在配置float类型的标量参数

1
2
3
4
5
6
7
8
9
void TestShader::AddTestComputePass(FRDGBuilder& GraphBuilder, float Scale, float Translate, FRHIGPUBufferReadback* Readback)
{
TShaderMapRef<FTestCS> ComputeShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));
//Start Edit
FTestCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FTestCS::FParameters>();
PassParameters->Scale = Scale;
PassParameters->Translate = Translate;
//End Edit
}

配置缓冲区稍微复杂,参考UE官方文档和源码,配置步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void TestShader::AddTestComputePass(FRDGBuilder& GraphBuilder, float Scale, float Translate, FRHIGPUBufferReadback* Readback)
{
TShaderMapRef<FTestCS> ComputeShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));

FTestCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FTestCS::FParameters>();
PassParameters->Scale = Scale;
PassParameters->Translate = Translate;

//Start Edit
constexpr uint32 BufferElementNum = 2;
constexpr uint32 BufferSize = sizeof(float) * BufferElementNum;

TArray<float> InitialData; //随便创建一些初始数据
InitialData.Init(10.0f, BufferElementNum);

FRDGBufferRef InRDGBuffer = CreateStructuredBuffer(
GraphBuilder, TEXT("TestCS.InRDGBuffer"), sizeof(float), BufferElementNum,
InitialData.GetData(), BufferSize
);

FRDGBufferSRVRef InSRV = GraphBuilder.CreateSRV(InRDGBuffer);

FRDGBufferRef OutRDGBuffer = GraphBuilder.CreateBuffer(
FRDGBufferDesc::CreateStructuredDesc(sizeof(float), BufferElementNum),
TEXT("TestCS.OutRDGBuffer")
);
FRDGBufferUAVRef OutUAV = GraphBuilder.CreateUAV(OutRDGBuffer);

PassParameters->InputBuffer = InSRV;
PassParameters->OutputBuffer = OutUAV;
//End Edit
}

由于需要在创建计算 Pass 之前从 CPU 侧初始化 GPU 缓冲区数据,这里使用 UE 提供的辅助函数 CreateStructuredBuffer 来创建输入缓冲区 InRDGBuffer。该函数会负责创建 RDG Buffer,并将初始数据上传至 GPU。

对于输出缓冲区,由于不需要 CPU 侧提供初始数据,直接通过 FRDGBuilder 的成员函数 CreateBuffer 创建即可(实际上,CreateStructuredBuffer 的内部实现同样是基于 CreateBuffer 创建临时缓冲区)。

需要注意的是,在创建 FRDGBufferRef 之后,还需要分别创建对应的 FRDGBufferSRVRefFRDGBufferUAVRef 才能在着色器中进行访问。这一设计与 Vulkan 中 VkBufferVkBufferView 的关系类似:前者表示资源本体,后者则描述了资源的具体访问方式。

随后添加计算Pass并回读数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void TestShader::AddTestComputePass(FRDGBuilder& GraphBuilder, float Scale, float Translate, FRHIGPUBufferReadback* Readback)
{
TShaderMapRef<FTestCS> ComputeShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));

FTestCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FTestCS::FParameters>();
PassParameters->Scale = Scale;
PassParameters->Translate = Translate;

constexpr uint32 BufferElementNum = 2;
constexpr uint32 BufferSize = sizeof(float) * BufferElementNum;

TArray<float> InitialData; //随便创建一些初始数据
InitialData.Init(10.0f, BufferElementNum);

FRDGBufferRef InRDGBuffer = CreateStructuredBuffer(
GraphBuilder, TEXT("TestCS.InRDGBuffer"), sizeof(float), BufferElementNum,
InitialData.GetData(), BufferSize
);

FRDGBufferSRVRef InSRV = GraphBuilder.CreateSRV(InRDGBuffer);

FRDGBufferRef OutRDGBuffer = GraphBuilder.CreateBuffer(
FRDGBufferDesc::CreateStructuredDesc(sizeof(float), BufferElementNum),
TEXT("TestCS.OutRDGBuffer")
);
FRDGBufferUAVRef OutUAV = GraphBuilder.CreateUAV(OutRDGBuffer);

PassParameters->InputBuffer = InSRV;
PassParameters->OutputBuffer = OutUAV;
//Start Edit
GraphBuilder.AddPass(
RDG_EVENT_NAME("MyAddCS"),
PassParameters,
ERDGPassFlags::Compute,
[PassParameters, ComputeShader, OutRDGBuffer, Readback](FRHICommandList& RHICmdList)
{
FComputeShaderUtils::Dispatch(RHICmdList, ComputeShader, *PassParameters, FIntVector(1, 1, 1));
}
);

AddEnqueueCopyPass(GraphBuilder, Readback, OutRDGBuffer, BufferSize);
//End Edit
}

通过 FRDGBuilder 的成员函数 AddPass 向 Render Graph 中添加一个计算 Pass,并在传入的 lambda 函数中定义该 Pass 的执行逻辑。

从使用方式上看,这一过程与 Vulkan 中通过 vkCmd 系列函数向命令缓冲区提交操作较为相似,

最后,通过 UE 提供的辅助函数 AddEnqueueCopyPass 将计算结果回读到 CPU。需要注意的是,数据回读必须通过单独的 Pass 来完成,而不能直接在前面计算 Pass 的 lambda 函数中进行。

这是因为在 RDG 的构建阶段,Pass 仅用于描述执行逻辑,此时相关计算 Pass 可能尚未执行完成,资源依赖和状态尚未满足;只有通过额外的拷贝 Pass,才能确保在计算结果生成之后再进行数据回读。

在 Actor 的 BeginPlay() 中调用

新建一个 C++ 类,父类选择 Actor,并命名为 ComputeRDGHelper
随后在 ComputeRDGHelper 类的 BeginPlay() 生命周期函数中,调用前面实现的用于向 RDG 添加计算 Pass 的函数。

ComputeRDGHelper.h的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "RHIGPUReadback.h"
#include "ComputeRDGHelper.generated.h"

UCLASS()
class LEARNRDG_API AComputeRDGHelper : public AActor
{
GENERATED_BODY()

public:
// Sets default values for this actor's properties
AComputeRDGHelper();

protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "TestShader")
float ScaleValue = 1.0f;

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "TestShader")
float TranslateValue = 2.0f;

TSharedPtr<FRHIGPUBufferReadback, ESPMode::ThreadSafe> Readback;

public:
// Called every frame
virtual void Tick(float DeltaTime) override;

void AddTestComputePass(float Scale, float Translate) const;
};

ComputeRDGHelper.cpp的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include "ComputeRDGHelper.h"
#include "RenderGraphBuilder.h"
#include "LeranRDGShader.h"

// Sets default values
AComputeRDGHelper::AComputeRDGHelper()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void AComputeRDGHelper::BeginPlay()
{
Super::BeginPlay();

check(GEngine != nullptr);

GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, TEXT("BeginPlay()!"));

Readback = MakeShared<FRHIGPUBufferReadback>(TEXT("MyAddReadback"));

AddTestComputePass(ScaleValue, TranslateValue);
}

// Called every frame
void AComputeRDGHelper::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);

if (!Readback.IsValid())
return;

if (!Readback->IsReady())
return;

TSharedPtr<FRHIGPUBufferReadback, ESPMode::ThreadSafe> Local = Readback;
Readback.Reset();

ENQUEUE_RENDER_COMMAND(PollReadback)(
[Local](FRHICommandListImmediate& RHICmdList)
{
const uint32 NumBytes = sizeof(float) * 2;
const void* DataPtr = Local->Lock(NumBytes);
const float* Values = static_cast<const float*>(DataPtr);

const float A = Values[0];
const float B = Values[1];

Local->Unlock();

AsyncTask(ENamedThreads::GameThread, [A, B]()
{
UE_LOG(LogTemp, Log, TEXT("Readback: %f %f"), A, B);
});
}
);
}

void AComputeRDGHelper::AddTestComputePass(float Scale, float Translate) const
{
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, TEXT("AddTestComputePass"));

ENQUEUE_RENDER_COMMAND(AddComputePass)(
[Scale, Translate, ReadbackPtr = Readback.Get()](FRHICommandListImmediate& RHICmdList)
{
FRDGBuilder GraphBuilder(RHICmdList);

TestShader::AddTestComputePass(GraphBuilder, Scale, Translate, ReadbackPtr);

GraphBuilder.Execute();
}
);

GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, TEXT("AddTestComputePassFinish"));
}


与 RDG 相关的操作必须在渲染线程Render Thread中执行,如果在游戏线程Game Thread中直接调用,将导致程序崩溃。因此需要使用 ENQUEUE_RENDER_COMMAND() 将相关逻辑封装并提交到渲染线程执行。

在编辑器中创建蓝图

为刚刚编写的 C++ 类创建对应的 UE 蓝图,将其拖入场景中并点击 Simulate 进行运行。

随后查看输出目录,可以发现计算结果已成功从 GPU 回读到 CPU。


虚幻引擎RDG计算管线调用流程
https://onikatahoshio.github.io/2026/01/24/Unreal/RDG渲染依赖图/01-RDG 计算管线调用流程/
作者
OnikataHoshio
发布于
2026年1月24日
许可协议