虚幻引擎RDG图形管线调用流程

前言

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

虚幻版本5.7

本篇主要介绍如何通过 AddPass() 调用图形管线,Unreal 引擎的配置与上一篇相同。

在 UE 中使用 RDG 构建图形管线

编写顶点和像素着色器的usf文件

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

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
#include "/Engine/Public/Platform.ush"

struct VS_INPUT
{
    float2 Position : ATTRIBUTE0;
    float2 TexCoord : ATTRIBUTE1;
};

struct VS_OUTPUT
{
    float4 Position : SV_POSITION;
    float2 TexCoord : TEXCOORD0;
};

void MainVS(
    in VS_INPUT Input,
    out VS_OUTPUT Output
)
{
    Output.Position = float4(Input.Position, 0.0, 1.0);
    Output.TexCoord = Input.TexCoord;
}

float BValue;

void MainPS(
    in VS_OUTPUT PS_Input,
    out float4 OutColor : SV_Target0
)
{
    OutColor = float4(PS_Input.TexCoord.x, PS_Input.TexCoord.y, BValue, 1.0);
}

.usf 文件中,顶点输入必须通过 ATTRIBUTE 语义与顶点缓冲进行绑定。除系统语义外,着色器阶段之间只能使用 TEXCOORDCOLOR 作为中间语义。

创建顶点和像素着色器的Global Shader

接下来我们将回到上一篇中的 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
31
32
33
34
35
36
class LEARNRDG_API FTestVS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FTestVS);
SHADER_USE_PARAMETER_STRUCT(FTestVS, FGlobalShader);

BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )

END_SHADER_PARAMETER_STRUCT()

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

IMPLEMENT_GLOBAL_SHADER(FTestVS, "/MyShaders/GraphicsShader.usf", "MainVS", SF_Vertex);

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

BEGIN_SHADER_PARAMETER_STRUCT(FParameters,)
SHADER_PARAMETER(float, BValue)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()

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

IMPLEMENT_GLOBAL_SHADER(FTestPS, "/MyShaders/GraphicsShader.usf", "MainPS", SF_Pixel);

这段代码的作用是将 .usf 文件中定义的着色器注册为 Unreal 引擎可识别和使用的全局着色器。

使用FRDGBuilder添加计算Pass

LearnRDGShader.h中添加函数声明

1
2
3
4
5
namespace LearnRDGShader
{
void AddTestGraphicsPass(FRDGBuilder& GraphBuilder, FRDGTextureRef RenderTarget, float BValue);
}

这里的 FRDGTextureRef 作为我们的渲染目标使用,本质上是一块存放在显存中的纹理缓冲区。

LearnRDGShader.cpp中实现函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void LearnRDGShader::AddTestGraphicsPass(FRDGBuilder& GraphBuilder, FRDGTextureRef RenderTarget, float BValue)
{
FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);

TShaderMapRef<FTestVS> VertexShader(ShaderMap);
TShaderMapRef<FTestPS> PixelShader(ShaderMap);

FTestPS::FParameters* PassParameters = GraphBuilder.AllocParameters<FTestPS::FParameters>();
PassParameters->BValue = BValue;
PassParameters->RenderTargets[0] = FRenderTargetBinding(RenderTarget, ERenderTargetLoadAction::EClear);

GraphBuilder.AddPass(
RDG_EVENT_NAME("MyTestGraphicsPass"),
PassParameters,
ERDGPassFlags::Raster,
[\*Todo*\](FRHICommandList& RHICmdList)
{
}

和计算着色器相比,图形管线的配置会麻烦不少。我们需要依次配置图形管线的各个部分,比如顶点缓冲区、全局缓冲区,以及图形管线本身的各种选项。

接下来我们先来配置顶点缓冲区,整体流程可以分为以下几个步骤:

  1. 在 GPU 侧创建顶点缓冲区;
  2. 在 CPU 侧声明并准备具体的顶点数据;
  3. 将 CPU 端的数据上传到刚刚创建的 GPU 顶点缓冲区中;
  4. 配置对应的 FVertexDeclarationRHIRef
  5. 将其绑定到 FGraphicsPipelineStateInitializerBoundShaderState.VertexDeclarationRHI 中;
  6. 指定所需的缓冲区。

相关代码如下:

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
void LearnRDGShader::AddTestGraphicsPass(FRDGBuilder& GraphBuilder, FRDGTextureRef RenderTarget, float BValue)
{
FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);

TShaderMapRef<FTestVS> VertexShader(ShaderMap);
TShaderMapRef<FTestPS> PixelShader(ShaderMap);

FTestPS::FParameters* PassParameters = GraphBuilder.AllocParameters<FTestPS::FParameters>();
PassParameters->BValue = BValue;
PassParameters->RenderTargets[0] = FRenderTargetBinding(RenderTarget, ERenderTargetLoadAction::EClear);

//Start Edit
constexpr uint32 NumVertices = 4;
constexpr uint32 NumIndices = 6;

struct VS_INPUT
{
FVector2f Position;
FVector2f TexCoord;
};

FRDGBufferRef VertexBuffer = GraphBuilder.CreateBuffer(
FRDGBufferDesc::CreateUploadDesc(sizeof(VS_INPUT), NumVertices),
TEXT("MyVertexBuffer"));

FRDGUploadData<VS_INPUT> VSInputData(GraphBuilder, 4);
VSInputData[0] = { FVector2f(-1.0f, 1.0f), FVector2f(0.0f, 0.0f) };
VSInputData[1] = { FVector2f(1.0f, 1.0f), FVector2f(1.0f, 0.0f) };
VSInputData[2] = { FVector2f(-1.0f, -1.0f), FVector2f(0.0f, 1.0f) };
VSInputData[3] = { FVector2f(1.0f, -1.0f), FVector2f(1.0f, 1.0f) };

GraphBuilder.QueueBufferUpload(VertexBuffer, VSInputData, ERDGInitialDataFlags::NoCopy);

FRDGBufferRef IndexBuffer = GraphBuilder.CreateBuffer(
FRDGBufferDesc::CreateUploadDesc(sizeof(uint32), NumIndices),
TEXT("MyIndexBuffer"));

FRDGUploadData<int32> Indices(GraphBuilder, 6);
Indices[0] = 0;
Indices[1] = 1;
Indices[2] = 2;
Indices[3] = 2;
Indices[4] = 1;
Indices[5] = 3;

GraphBuilder.QueueBufferUpload(IndexBuffer, Indices, ERDGInitialDataFlags::NoCopy);

uint16 Stride = sizeof(VS_INPUT);
FVertexDeclarationElementList Elements;
Elements.Add(FVertexElement(0, STRUCT_OFFSET(VS_INPUT, Position), VET_Float2, 0, Stride));
Elements.Add(FVertexElement(0, STRUCT_OFFSET(VS_INPUT, TexCoord), VET_Float2, 1, Stride));
FVertexDeclarationRHIRef VertexDeclarationRHI = PipelineStateCache::GetOrCreateVertexDeclaration(Elements);

//End Edit

GraphBuilder.AddPass(
RDG_EVENT_NAME("MyTestGraphicsPass"),
PassParameters,
ERDGPassFlags::Raster,
[VertexDeclarationRHI](FRHICommandList& RHICmdList)
{
//Start Edit
FGraphicsPipelineStateInitializer GraphicsPSOInit;
GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0);

RHICmdList.SetStreamSource(0, VertexBuffer->GetRHI(), 0);
RHICmdList.DrawIndexedPrimitive(IndexBuffer->GetRHI(), 0, 0, 4, 0, 2, 1);
//End Edit
}
);
}

我们通过 FRDGBuilder 的成员函数 CreateBuffer() 来创建缓冲区,随后调用 QueueBufferUpload() 将 CPU 端的数据上传到顶点缓冲区中。

索引缓冲区的处理方式与此相同,按照同样的流程进行创建和数据上传即可。

配置管线的各种选项,包括视口大小、深度模板测试、混合方式、剔除方式、图元类型等等,完整代码如下:

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
80
81
void LearnRDGShader::AddTestGraphicsPass(FRDGBuilder& GraphBuilder, FRDGTextureRef RenderTarget, float BValue)
{
constexpr uint32 NumVertices = 4;
constexpr uint32 NumIndices = 6;
const FIntRect ViewRect(0, 0, RenderTarget->Desc.Extent.X, RenderTarget->Desc.Extent.Y);


FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);

TShaderMapRef<FTestVS> VertexShader(ShaderMap);
TShaderMapRef<FTestPS> PixelShader(ShaderMap);

struct VS_INPUT
{
FVector2f Position;
FVector2f TexCoord;
};

FRDGBufferRef VertexBuffer = GraphBuilder.CreateBuffer(
FRDGBufferDesc::CreateUploadDesc(sizeof(VS_INPUT), NumVertices),
TEXT("MyVertexBuffer"));

FRDGUploadData<VS_INPUT> VSInputData(GraphBuilder, 4);
VSInputData[0] = { FVector2f(-1.0f, 1.0f), FVector2f(0.0f, 0.0f) };
VSInputData[1] = { FVector2f(1.0f, 1.0f), FVector2f(1.0f, 0.0f) };
VSInputData[2] = { FVector2f(-1.0f, -1.0f), FVector2f(0.0f, 1.0f) };
VSInputData[3] = { FVector2f(1.0f, -1.0f), FVector2f(1.0f, 1.0f) };

GraphBuilder.QueueBufferUpload(VertexBuffer, VSInputData, ERDGInitialDataFlags::NoCopy);

FRDGBufferRef IndexBuffer = GraphBuilder.CreateBuffer(
FRDGBufferDesc::CreateUploadDesc(sizeof(uint32), NumIndices),
TEXT("MyIndexBuffer"));

FRDGUploadData<int32> Indices(GraphBuilder, 6);
Indices[0] = 0;
Indices[1] = 1;
Indices[2] = 2;
Indices[3] = 2;
Indices[4] = 1;
Indices[5] = 3;

GraphBuilder.QueueBufferUpload(IndexBuffer, Indices, ERDGInitialDataFlags::NoCopy);

FTestPS::FParameters* PassParameters = GraphBuilder.AllocParameters<FTestPS::FParameters>();
PassParameters->BValue = BValue;
PassParameters->RenderTargets[0] = FRenderTargetBinding(RenderTarget, ERenderTargetLoadAction::EClear);

uint16 Stride = sizeof(VS_INPUT);
FVertexDeclarationElementList Elements;
Elements.Add(FVertexElement(0, STRUCT_OFFSET(VS_INPUT, Position), VET_Float2, 0, Stride));
Elements.Add(FVertexElement(0, STRUCT_OFFSET(VS_INPUT, TexCoord), VET_Float2, 1, Stride));
FVertexDeclarationRHIRef VertexDeclarationRHI = PipelineStateCache::GetOrCreateVertexDeclaration(Elements);

GraphBuilder.AddPass(
RDG_EVENT_NAME("MyTestGraphicsPass"),
PassParameters,
ERDGPassFlags::Raster,
[VertexShader, PixelShader, PassParameters, VertexBuffer, IndexBuffer, ViewRect, VertexDeclarationRHI](FRHICommandList& RHICmdList)
{
RHICmdList.SetViewport(ViewRect.Min.X, ViewRect.Min.Y, 0.0f, ViewRect.Max.X, ViewRect.Max.Y, 1.0f);

FGraphicsPipelineStateInitializer GraphicsPSOInit;
RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();
GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = VertexDeclarationRHI;
GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
GraphicsPSOInit.PrimitiveType = PT_TriangleList;
SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0);

SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), *PassParameters);

RHICmdList.SetStreamSource(0, VertexBuffer->GetRHI(), 0);

RHICmdList.DrawIndexedPrimitive(IndexBuffer->GetRHI(), 0, 0, 4, 0, 2, 1);
}
);
}

在Actor中的BeginPlay()中调用

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

GraphicsRDGHelper.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
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GraphicsRDGHelper.generated.h"

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

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

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

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "TestShader")
TObjectPtr<UTextureRenderTarget2D> GraphicsRDGRenderTarget;

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

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

void AddTestGraphicsPass(UTextureRenderTarget2D* RenderTarget2D, float BChannelValue) const;

};

GraphicsRDGHelper.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
// Fill out your copyright notice in the Description page of Project Settings.


#include "GraphicsRDGHelper.h"
#include "RenderGraphBuilder.h"
#include "Engine/TextureRenderTarget2D.h"
#include "LearnRDGShader.h"
// Sets default values
AGraphicsRDGHelper::AGraphicsRDGHelper()
{
// 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 AGraphicsRDGHelper::BeginPlay()
{
Super::BeginPlay();

check(GEngine != nullptr);

if (IsValid(GraphicsRDGRenderTarget))
AddTestGraphicsPass(GraphicsRDGRenderTarget.Get(), BValue);
}

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

void AGraphicsRDGHelper::AddTestGraphicsPass(UTextureRenderTarget2D* RenderTarget2D, float BChannelValue) const
{
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, TEXT("AddTestGraphicsPass"));

FTextureResource* RenderResource = RenderTarget2D->GetResource();

ENQUEUE_RENDER_COMMAND(AddTestGraphicsPass)(
[RenderResource, BChannelValue](FRHICommandListImmediate& RHICmdList)
{
FRDGBuilder GraphBuilder(RHICmdList);

FTextureRHIRef TextureRHI = RenderResource->GetTextureRHI();

FRDGTextureRef RDGTexture =
GraphBuilder.RegisterExternalTexture(
CreateRenderTarget(TextureRHI, TEXT("MyRT2D_External"))
);

LearnRDGShader::AddTestGraphicsPass(GraphBuilder, RDGTexture, BChannelValue);

GraphBuilder.Execute();
}
);

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

在编辑器中创建蓝图

接下来为刚刚编写的 C++ 类创建对应的 UE 蓝图,并新建一个 Material 和一个 RenderTarget。将该 RenderTarget 连接到 Material 的 Base Color(基础色)输入上,然后将该 Material 指定给对应的 UE 蓝图进行使用。

点击模拟,可以看到颜色已经正确地输入到纹理

AddFullScreenPass

对于仅包含全屏两个三角形的顶点数据,可以直接使用 Unreal 提供的 AddFullScreenPass 辅助函数来简化代码实现,具体用法可以参考 UE 的相关源码实现。需要注意的是,在使用该辅助函数时,像素着色器在通常情况下只能接收一个 float4 类型的“位置”输入。

一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
#include "/Engine/Public/Platform.ush"
#include "/Engine/Private/Common.ush"

float BValue;
float2 TargetSize;

void FullscreenPS(float4 InPosition : SV_POSITION,  out float4 OutColor : SV_Target0)
{
    float2 UV = InPosition.xy / TargetSize;
    OutColor = float4(UV.x, UV.y, BValue, 1.0);
}

需要注意的是,与 GLSL 中的用法不同,这里的 SV_POSITION 表示的是屏幕空间下的像素位置,而非归一化后的 [-1, 1] NDC 坐标。


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