GRAPHICS × PRODUCTION
中EN

皮了个牛

↓

计算机图形学

Unreal RenderGraph

以 Unreal RenderGraph 的实际调用顺序为线索,解释执行 Graph、Pass Resource、Pass Parameters 与 AddPass 的职责和连接,收束到一次 Graph 执行的整体理解。

2020-11-1018 分钟阅读
Unreal RenderGraph

最近拿 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 的基本步骤

  1. 使用一个 RHICmdList 来初始化一个 Graph(这个 CmdList 即为整个 Graph 的 CommandList),用法为 FRDGBuilder GraphBuilder(RHICmdList)。

  2. 使用 GraphBuilder.AllocParameters() 来分配一个当前 Pass 的参数结构体。

  3. 使用 GraphBuilder.AddPass() 结合当前 Pass 参数和一个 Lambda 表达式来创建一个 Pass,Lambda 表达式主要传递一些 Pass 的参数比如用到的 Shader 指针。

  4. 在 AddPass 内执行当前 Pass 的计算,如果是 CS 就直接 Dispatch,如果是 VS/PS 就设置一些 PSO 等内容然后 Draw 即可。

  5. 使用 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。