最近拿 UE 玩一些渲染发现新加的 RDG(RenderDependencyGraph)非常好用,所以来分享一下。至于什么是 RDG 简单介绍一下吧,在当今游戏渲染流程里面可能会有很多的特性比如 SSR,SSAO,Volumetric 等等。它们之间的引用关系比如 SSAO 需要 GBuffer 的 Normal 而 SSR 需要 Normal 和 Microface,而这两个 Pass 之间又隔了不知道多少个 Pass 甚至隔了前后帧,这个时候手动的维护上下层 Pass 的引用关系和资源的释放就显得无比麻烦,因此 RG 应运而生。这个架构思想最初是由寒霜引擎分享的,基本可以理解为一个系统自动管理你每个资源的创建和销毁以及 Pass 之间的资源引用和复用,以及剔除掉没有连接到 FinalColor 的 Pass,做到不浪费任何一个系统资源从而达到极致的解耦和系统资源占用。
首先 RDG 里面有两个最基本也是最重要的组件,RDGBuilder 和 RDGResource,前者一般用于创建渲染资源比如 RT 和 Buffer 等以及添加一个 RenderPass 到这个 Graph 内,后者为可被 RDG 管理的资源所有的 RDGBuffer 和 RDGTexture 等都继承自它。
一、执行一个 Graph 的基本步骤
-
使用一个 RHICmdList 来初始化一个 Graph(这个 CmdList 即为整个 Graph 的 CommandList),用法为
FRDGBuilder GraphBuilder(RHICmdList)。 -
使用
GraphBuilder.AllocParameters()来分配一个当前 Pass 的参数结构体。 -
使用
GraphBuilder.AddPass()结合当前 Pass 参数和一个 Lambda 表达式来创建一个 Pass,Lambda 表达式主要传递一些 Pass 的参数比如用到的 Shader 指针。 -
在 AddPass 内执行当前 Pass 的计算,如果是 CS 就直接 Dispatch,如果是 VS/PS 就设置一些 PSO 等内容然后 Draw 即可。
-
使用
GraphBuilder.QueueTex/Buffer()来把 RGPass 内的结果提取到外部的一个 PoolRT 或者 RHIBuffer 内。并使用GraphBuilder.Execute()去真正执行整个 Graph 的所有 Pass,是的前面一大堆只是 DeferredContext。
二、Pass Resource
渲染资源这里也不同于以往的 PoolRT 来做了,而是统一使用 GraphBuilder.CreateTexture() 或者 CreateBuffer() 来创建需要的资源。它只记录资源描述符。当需要资源时通过这个 GraphBuilder 来完成分配它将会跟踪这个资源的生命周期,并且在后面的 Pass 不再引用它时自动释放和复用。并且需要注意的是一个 GraphPass 内的资源有可能不是由 Graph 创建的,这个时候就需要使用 GraphBuilder.RegisterExternalBuffer/Texture 来把某个 PoolRT 或者 RHIBuffer 转成 RDGResource 才能使用。同样的把一个 RDGResource 转成 PoolRT 或者 RHIBuffer 的方法则是 GraphBuilder.QueueExternalBuffer/Texture,感觉这两对更适合叫 ImportResource 和 ExportResource。如下图。
完整的 RDG 使用示例(资源创建 + Compute Dispatch):
void UInfinityOceanDisplacement::Render(ERHIFeatureLevel::Type FeatureLevel, FRHICommandListImmediate& CommandList, float SystemTime)
{
FRDGBuilder GraphBuilder(CommandList);
RDG_EVENT_SCOPE(GraphBuilder, "InfinityOceanDisplacement");
const FIntVector NumGroups = FIntVector(1, SizeXY, 1);
// UniformBuffer
FOceanUniformBuffer OceanUniform;
{ ... }
TUniformBufferRef<FOceanUniformBuffer> OceanUniformRef = TUniformBufferRef<FOceanUniformBuffer>::CreateUniformBufferImmediate(OceanUniform, UniformBuffer_SingleFrame);
// StructuredBuffer
FRDGBufferRef Ocean_GaussBuffer = GraphBuilder.RegisterExternalBuffer(GaussBufferRHI, TEXT("GaussBuffer"));
FRDGBufferRef Ocean_HZeroBuffer = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateStructuredDesc(sizeof(FVector2D), (SizeXY + 1) * (SizeXY + 1)), TEXT("HZeroBuffer"));
// Dispatch Compute HZero
TShaderMapRef<FOceanShader_HZero> OceanShader_HZero(GetGlobalShaderMap(FeatureLevel));
{
FOceanShader_HZero::FParameters* HZeroParameter = GraphBuilder.AllocParameters<FOceanShader_HZero::FParameters>();
{
HZeroParameter->UniformBuffer = OceanUniformRef;
HZeroParameter->Input_SRV_GaussBuffer = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(Ocean_GaussBuffer));
HZeroParameter->Output_UAV_HZeroBuffer = GraphBuilder.CreateUAV(FRDGBufferUAVDesc(Ocean_HZeroBuffer));
}
FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("ComputeOceanHZero"), *OceanShader_HZero, HZeroParameter, NumGroups);
}
}
三、Pass Parameters
当增加一个 RGPass 它必须带有 Shader 参数,可以是任何 Shader 参数比如 UniformBuffer,Texture 等。且参数使用 GraphBuilder.AllocParameters() 来分配保留所有参数的结构体,因为 Lambda 执行被延迟确保了正确的生命周期。参数采用宏的形式来声明。且参数结构体的声明最好的方法是内联,直接在每个 Pass 的 ShaderClass 内声明好结构。
首先得在 Shader 里使用宏 SHADER_USE_PARAMETERSTRUCT(FYouShader, ShaderType) 设置 Shader 需要使用 Parameter。然后需要实现一个 FParameter 的宏包裹的结构体里面声明该 Pass 需要用到的所有参数,参数基本上都是靠新的 RDG 系列宏来声明。需要注意一点的是对于 UniformBuffer 需要使用 StructRef 来引用一层,可以理解为 Parameter 结构体里面还有一个结构体。如下所示。
Shader 类声明与 Parameter 定义:
class FOceanShader_HZero : public FGlobalShader
{
DECLARE_SHADER_TYPE(FOceanShader_HZero, Global);
SHADER_USE_PARAMETER_STRUCT(FOceanShader_HZero, FGlobalShader);
public:
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER_STRUCT_REF(FOceanUniformBuffer, UniformBuffer)
SHADER_PARAMETER_RDG_BUFFER_SRV(StructuredBuffer<float2>, Input_SRV_GaussBuffer)
SHADER_PARAMETER_RDG_BUFFER_UAV(RWStructuredBuffer<float2>, Output_UAV_HZeroBuffer)
END_SHADER_PARAMETER_STRUCT()
static bool ShouldCache(EShaderPlatform Platform);
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters);
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment);
virtual bool Serialize(FArchive& Ar);
};
UniformBuffer 参数声明:
BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT_WITH_CONSTRUCTOR(FOceanUniformBuffer, )
SHADER_PARAMETER(uint32, Resolution)
SHADER_PARAMETER(uint32, Resolution_PlusOne)
SHADER_PARAMETER(uint32, Resolution_PlusOne_Squared_MinusOne)
SHADER_PARAMETER(uint32, ResolutionHalf)
SHADER_PARAMETER(uint32, ResolutionHalf_PlusOne)
SHADER_PARAMETER(uint32, ResolutionLog2_Add32)
SHADER_PARAMETER(float, Time)
SHADER_PARAMETER(float, FrequencyScale)
SHADER_PARAMETER(float, LinearScale)
SHADER_PARAMETER(float, WindScale)
SHADER_PARAMETER(float, RootScale)
SHADER_PARAMETER(float, PowerScale)
SHADER_PARAMETER(float, ChoppyScale)
SHADER_PARAMETER(FVector4, WindDir)
END_GLOBAL_SHADER_PARAMETER_STRUCT()
因为我这儿是 CS 所以没有什么 RenderTarget 的绑定概念,如果是 VS/PS 的话需要用到 RT Bind Slots 只需要在 FParameter 结构里面声明一行 RENDER_TARGET_BINDING_SLOTS() 的宏,然后在 Graph 里面就可以直接 Bind MultiRenderTarget 和 DepthStencilTarget 了。
RT Bind Slots 示例(VS/PS 场景):
class FTemporalAAPS : public FGlobalShader
{
DECLARE_GLOBAL_SHADER(FTemporalAAPS);
SHADER_USE_PARAMETER_STRUCT(FTemporalAAPS, FGlobalShader);
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER_STRUCT_INCLUDE(FTAAShaderParameters, CommonParameters)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
};
RenderTarget 绑定:
FTemporalAAPS::FParameters* PassParameters = GraphBuilder.AllocParameters<FTemporalAAPS::FParameters>();
PassParameters->CommonParameters = CommonShaderParameters;
PassParameters->RenderTargets[0] = FRenderTargetBinding(
Outputs.SceneColor,
ERenderTargetLoadAction::ENoAction,
ERenderTargetStoreAction::EStore);
if (Outputs.SceneMetadata)
{
PassParameters->RenderTargets[1] = FRenderTargetBinding(
Outputs.SceneMetadata,
ERenderTargetLoadAction::ENoAction,
ERenderTargetStoreAction::EStore);
}
DepthStencil 绑定:
if (bUseResponsiveStencilTest)
{
PassParameters->RenderTargets.DepthStencil = FDepthStencilBinding(
SceneBlackboard.SceneDepthBuffer,
ERenderTargetLoadAction::ENoAction, ERenderTargetStoreAction::ENoAction,
ERenderTargetLoadAction::ELoad, ERenderTargetStoreAction::EStore,
FExclusiveDepthStencil::DepthRead_StencilWrite);
}
且在一个 RGPass 中使用的所有资源必须在 GraphBuilder.AddPass() 的 Pass 的参数中提供。它允许 RenderGraph 了解在一个 Pass 内所使用的资源。这些资源保证在执行 Pass 的时候被分配。因此,它们只能在 GraphBuilder.AddPass() 创建的 RGPass 的 Lambda 作用域中可以访问。
四、AddPass
GraphBuilder.AddPass() 主要用来配置管线状态用于延迟执行。比如使用 FGraphicsPipelineStateInitializer 对象来配置 PSO,并调用 RHI 的 API 来进行绘制。或者使用 SetComputeShader() 来 DispatchCompute。注意此时还不会实际执行绘制而是在所有 AddPass 完成后调用 GraphBuilder.Execute() 才实际执行。而且更主要的是 SetShaderParameters() 也是在这儿做,这个函数是 UE 封装的,因为我们的一个 Pass 只能有一个 AllocParameters 所以这个东西里面是塞了 UniformBuffer SRV UAV 等等各种东西。在以前的流程里面是自己在 Shader 里封装 Shader.SetSRV/UAV/Uniform 等等的函数,而现在则只需要把所有参数塞一起并在 GraphPass 内设置即可。RDG 会自动检测参数的每一个成员和类型自动 SetUAV/SRV/Uniform。
五、最后
展示一下效果吧毕竟前面的东西都是 Code 层太硬核了 GG,光照是 Unlit 自己伪造的 Lighting。


