RDG 03 AddPass

2022-11-02
6分钟阅读时长
UE5
RDG

1 原型

RDG流程中,添加一个Pass的方法是 FRDGBuilder::AddPass(),其应用的最广泛的一个重载的原型如下:

template <typename ParameterStructType, typename ExecuteLambdaType>
FRDGPassRef FRDGBuilder::AddPass(
	FRDGEventName&& Name,
	const ParameterStructType* ParameterStruct,
	ERDGPassFlags Flags,
	ExecuteLambdaType&& ExecuteLambda)
{
	return AddPassInternal(Forward<FRDGEventName>(Name), ParameterStructType::FTypeInfo::GetStructMetadata(), ParameterStruct, Flags, Forward<ExecuteLambdaType>(ExecuteLambda));
}

2 调用入口

2.1 系统内的Render函数

如果直接修改引擎代码,调用该函数的位置一般是在 FDeferredShadingSceneRenderer 的 Render函数中,这个函数中可以直接访问到 GraphBuilder ,它是一个FRDGBuilder对象,我们可以直接通过该对象调用 AddPass方法:

 GraphBuilder.AddPass(...)

2.2 渲染委托

避免修改引擎源码,我们可以调用渲染委托,通过 RegisterPostOpaqueRenderDelegate 和 RegisterOverlayRenderDelegate 两个方法添加渲染的函数。例如:

RenderHandle = GetRendererModule().RegisterOverlayRenderDelegate(FPostOpaqueRenderDelegate::CreateRaw(this, &FRadiationRenderer::Render));

该函数必须包含一个参数: FPostOpaqueRenderParameters& InParameters, 通过这个参数我们就可以获取到GraphBuilder:

FRDGBuilder& GraphBuilder = *InParameters.GraphBuilder;

3 参数分析

它接受4个参数,下面逐个说明。

3.1 Pass名称

第一个参数是该Pass的名称,主要用于进行图形调试,例如使用RenderDoc这样的工具,可以看到这个Pass的名称。一般使用 RDG_EVENT_NAME(“EventName”) 来定义,双引号中间填写该Pass的名称。

3.2 Pass参数

第二个参数是该Pass的参数,注意这个Pass的参数可以是PixelShader的参数, 也可以是VertexShader和PixelShader共享的参数,如果是一个纯Compute Shader Pass,则可以是ComputeShader的参数。 我们可以看到如下不同的应用情形:

  1. 直接使用PixelShader的参数作为Pass的参数
  2. VertexShader和PixelShader共享一组Shader参数(从一个父类派生),将这组Shader参数传入。如 FShaderDrawSymbolsVS和 FShaderDrawSymbolsPS都是从 FShaderDrawSymbols派生。

3.3 Pass标记

第三个参数是一个标记,它的定义:

enum class ERDGPassFlags : uint8
{
	/** Pass没有任何输入输出的绑定,仅用于无参的AddPass函数 */
	None = 0,
	/** Pass在图形管线上使用了光栅化操作*/
	Raster = 1 << 0,
	/** Pass在图形管线上使用了compute操作*/
	Compute = 1 << 1,
	/** Pass在异步计算管线中使用了compute操作*/
	AsyncCompute = 1 << 2,
	/** Pass在图形管线上使用了复制命令 */
	Copy = 1 << 3,
	/** Pass和其生产者永不被剔除。在输出不能被Graph追踪的情况下是必须的 */
	NeverCull = 1 << 4,
	/** 忽略Render Pass的开始和结束,让用户去调用。只有与Raster结合时才有用。会在当前Pass上禁用Pass合并。	 */
	SkipRenderPass = 1 << 5,
	/**	Pass将永远不会与其他Pass合并 */
	NeverMerge = 1 << 6,
	/** Pass将永远不会离开渲染线程 */
	NeverParallel = 1 << 7,
	/** Pass uses copy commands but writes to a staging resource. Pass使用复制命令但是写入一个 Staging 资源 */
	Readback = Copy | NeverCull
};
ENUM_CLASS_FLAGS(ERDGPassFlags);

一个Pass可以有多个标记,用竖线 | 进行分割。

一个Pass的标记至少要包含 Copy,Compute,AsyncCompute, Raster之一。

如果一个Pass包含了Raster标记,则必须绑定RenderTarget,否则将出现报错:

Pass ‘XXX’ is set to ‘Raster’ but is missing render target binding slots.

绑定RenderTarget的方法见下文。

3.4 Lambda函数

Lambda函数首先要将需要用到的参数添加到捕获列表中, 函数参数为 FRHICommandListImmediate& RHICmdList。

在函数体内,我们需要声明一个 FGraphicsPipelineStateInitializer 对象,通过该对象对Pass进行必要的设置。

之后还要设置各个Shader的参数,这个操作通常采用 SetShaderParameters 函数来完成。

最后还需要设置VertexBuffer(和IndexBuffer)。

最终调用绘制命令进行绘制。

4 辅助函数

RDG包含几个有用的辅助函数,用于添加常用的Pass。应尽可能使用这些函数。

  • FComputeShaderUtils::AddPass 用于添加Compute Pass
  • FPixelShaderUtils::AddFullScreenPass 用于添加全屏像素着色器 Pass 例:
FComputeShaderUtils::AddPass(
				GraphBuilder,
				RDG_EVENT_NAME("ComputeVolumeTexture"),
				ComputeShader,
				CSParams,
				FIntVector(GroupCount, GroupCount, textureSize));

FPixelShaderUtils::AddFullscreenPass<RenderSkyAtmosphereEditorHudPS>(
				GraphBuilder, 
				View.ShaderMap, 
				RDG_EVENT_NAME("SkyAtmosphereEditor"), 
				PixelShader, 
				PassParameters, 
				View.ViewRect);

FPixelShaderUtils::AddFullscreenPass(
				GraphBuilder,
				View.ShaderMap,
				RDG_EVENT_NAME("DownsampleHZB(mip=%d) %dx%d", StartDestMip, DstSize.X, DstSize.Y),
				PixelShader,
				PassParameters,
				FIntRect(0, 0, DstSize.X, DstSize.Y));

5 不带着色器参数的Pass

BEGIN_SHADER_PARAMETER_STRUCT(FCopyTextureParameters, )

    // 声明CopySrc访问FRDGTexture*
    RDG_TEXTURE_ACCESS(Input,  ERHIAccess::CopySrc)

    // 声明CopyDest访问FRDGTexture*
    RDG_TEXTURE_ACCESS(Output, ERHIAccess::CopyDest)

END_SHADER_PARAMETER_STRUCT()

void AddCopyTexturePass(
    FRDGBuilder& GraphBuilder,
    FRDGTextureRef InputTexture,
    FRDGTextureRef OutputTexture,
    const FRHICopyTextureInfo& CopyInfo)
{
    FCopyTextureParameters* Parameters = GraphBuilder.AllocParameters<FCopyTextureParameters>();
    Parameters->Input = InputTexture;
    Parameters->Output = OutputTexture;

    GraphBuilder.AddPass(
        RDG_EVENT_NAME("CopyTexture(%s -> %s)", InputTexture->Name, OutputTexture->Name),
        Parameters,
        ERDGPassFlags::Copy,
        [InputTexture, OutputTexture, CopyInfo](FRHICommandList& RHICmdList)
    {
        RHICmdList.CopyTexture(InputTexture->GetRHI(), OutputTexture->GetRHI(), CopyInfo);
    });
}

这个Pass的两个参数都与着色器无关,只是分别指明了要复制的源以及目标。 实现了复制贴图的功能。

6 Raster Pass

注意,每一个Raster Pass都需要一个RenderTarget,RDG通过 RENDER_TARGET_BINDING_SLOTS 参数为Raster Pass暴露了固定渲染管线的RenderTarget,我们只需要给参数增加一个 RENDER_TARGET_BINDING_SLOTS()

在给参数赋值的时候做这两件事:

  • 为RenderTargets[0]赋值一个FRenderTargetBinding,设置颜色的绑定对象以及操作。
  • 为RenderTargets.DepthStencil赋值一个FDepthStencilBinding,设置深度模板的绑定对象及操作。
PSParams->RenderTargets[0] = FRenderTargetBinding(SceneColorTexture, ERenderTargetLoadAction::ELoad);
PSParams->RenderTargets.DepthStencil = FDepthStencilBinding(
	SceneDepthTexture, ERenderTargetLoadAction::ELoad, ERenderTargetLoadAction::ELoad,
	FExclusiveDepthStencil::DepthWrite_StencilWrite);

FRenderTargetBinding 构造函数

FRenderTargetBinding(FRDGTexture* InTexture, ERenderTargetLoadAction InLoadAction, uint8 InMipIndex = 0, int16 InArraySlice = -1)

第一个参数是 FRDGTextureRef类型对象,赋给SceneColorTexture即可。

第二个参数是 ERenderTargetLoadAction 枚举,有三个成员:

  • ELoad: 保留Texture已存在的部分
  • EClear: 清理Texture,采用其优化的清除值
  • ENoAction: 可能不会保留内容,这个选项在某些硬件上速度更快(如果确保所有有效像素都被写入)

FDepthStencilBinding 构造函数

FDepthStencilBinding(
		FRDGTexture* InTexture,
		ERenderTargetLoadAction InDepthLoadAction,
		ERenderTargetLoadAction InStencilLoadAction,
		FExclusiveDepthStencil InDepthStencilAccess)

第一个参数赋给SceneDepthTexture即可。

第二、三个参数分别是设定深度和模板部分的操作。

第四个参数是 FExclusiveDepthStencil 类型,它控制着每个平面是否具有读取或写入访问权限

enum Type
	{
		// 不要使用上面的单独选项,使用下面的组合
		// 4 bits are used for depth and 4 for stencil to make the hex value readable and non overlapping
		DepthNop = 0x00,
		DepthRead = 0x01,
		DepthWrite = 0x02,
		DepthMask = 0x0f,
		StencilNop = 0x00,
		StencilRead = 0x10,
		StencilWrite = 0x20,
		StencilMask = 0xf0,

		// 用这些:
		DepthNop_StencilNop = DepthNop + StencilNop,
		DepthRead_StencilNop = DepthRead + StencilNop,
		DepthWrite_StencilNop = DepthWrite + StencilNop,
		DepthNop_StencilRead = DepthNop + StencilRead,
		DepthRead_StencilRead = DepthRead + StencilRead,
		DepthWrite_StencilRead = DepthWrite + StencilRead,
		DepthNop_StencilWrite = DepthNop + StencilWrite,
		DepthRead_StencilWrite = DepthRead + StencilWrite,
		DepthWrite_StencilWrite = DepthWrite + StencilWrite,
	};

例子

颜色目标手动清除,而深度和模板目标使用硬件清除操作:

BEGIN_SHADER_PARAMETER_STRUCT(FRenderTargetParameters, RENDERCORE_API)

    // 这些绑定插槽包含颜色和深度模板目标。
    RENDER_TARGET_BINDING_SLOTS()

END_SHADER_PARAMETER_STRUCT()

void AddClearRenderTargetPass(FRDGBuilder& GraphBuilder, FRDGTexture* Texture, const FLinearColor& ClearColor, FIntRect Viewport)
{
    FRenderTargetParameters* Parameters = GraphBuilder.AllocParameters<FRenderTargetParameters>();

    Parameters->RenderTargets[0] = FRenderTargetBinding(
        Texture,
        ERenderTargetLoadAction::ENoAction // <- 不需要加载之前的render target 内容,因为我们要手动清理
    );

    GraphBuilder.AddPass(
        RDG_EVENT_NAME("ClearRenderTarget(%s) [(%d, %d), (%d, %d)] ClearQuad", Texture->Name, Viewport.Min.X, Viewport.Min.Y, Viewport.Max.X, Viewport.Max.Y),
        Parameters,
        ERDGPassFlags::Raster,
        [Parameters, ClearColor, Viewport](FRHICommandList& RHICmdList)
    {
        RHICmdList.SetViewport(Viewport.Min.X, Viewport.Min.Y, 0.0f, Viewport.Max.X, Viewport.Max.Y, 1.0f);
        DrawClearQuad(RHICmdList, ClearColor);
    });
}

void AddClearDepthStencilPass(FRDGBuilder& GraphBuilder, FRDGTextureRef Texture)
{
    auto* PassParameters = GraphBuilder.AllocParameters<FRenderTargetParameters>();

    PassParameters->RenderTargets.DepthStencil = FDepthStencilBinding(
        Texture,
        ERenderTargetLoadAction::EClear, // <- 清理深度到其优化的清理值.
        ERenderTargetLoadAction::EClear, // <- 清理模板到其优化的清理值.
        FExclusiveDepthStencil::DepthWrite_StencilWrite // <- 允许写入深度和模板
    );

    GraphBuilder.AddPass(
        RDG_EVENT_NAME("ClearDepthStencil (%s)", Texture->Name),
        PassParameters,
        ERDGPassFlags::Raster,
        [](FRHICommandList&)
    {
        // Lambda中无实际工作!RDG为我们处理渲染通道!清除通过Clear操作完成。
    });
}

7 UAV Raster Pass

RDG支持写入UAV而不是固定管线渲染目标的Pass。最简单的办法就是使用 FPixelShaderUtils::AddUAVPass 函数。它创建了一个没有绑定渲染目标的自定义的渲染Pass,并且将RHI Viewport 设置好。

BEGIN_SHADER_PARAMETER_STRUCT(FUAVRasterPassParameters, RENDERCORE_API)
    SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, Output)
END_SHADER_PARAMETER_STRUCT()

auto* PassParameters = GraphBuilder.AllocParameters<FUAVRasterPassParameters>();
PassParameters.Output = GraphBuilder.CreateUAV(OutputTexture);

// Specify the viewport rect.
FIntRect ViewportRect = ...;

FPixelShaderUtils::AddUAVPass(
    GraphBuilder,
    RDG_EVENT_NAME("Raster UAV Pass"),
    PassParameters,
    ViewportRect,
    [](FRHICommandList& RHICmdList)
{
    // Bind parameters and draw.
});