Rdg 07 串接Pass和资源

2022-11-09
5分钟阅读时长
UE5
RDG

通常为了实现一个功能/效果,需要多个Pass配合,一般都是采用串接的方式,即先执行一个Pass,再执行其他Pass。多个Pass之间一定会有资源的传递才有意义。

Mip生成案例

一个典型的例子就是生成Mipmap的功能。它采用了两个Pass:Compute Pass 和 Raster Pass。

Compute Pass是由Compute Shader计算的,Raster Pass是由Vertex Shader和Pixel Shader一起渲染的。

入口函数

先看一个已经Deprecated的执行的入口, (虽然已经过时,但可以学到一些知识):

void FGenerateMips::Execute(FRHICommandListImmediate& RHICmdList, FRHITexture* Texture, TSharedPtr<FGenerateMipsStruct>& ExternalMipsStructCache, FGenerateMipsParams Params, bool bAllowRenderBasedGeneration)
{
	TRefCountPtr<IPooledRenderTarget> PooledRenderTarget = CreateRenderTarget(Texture, TEXT("MipGeneration"));

	FMemMark MemMark(FMemStack::Get());
	FRDGBuilder GraphBuilder(RHICmdList);
	Execute(GraphBuilder, GraphBuilder.RegisterExternalTexture(PooledRenderTarget), Params, bAllowRenderBasedGeneration ? EGenerateMipsPass::Raster : EGenerateMipsPass::Compute);
	GraphBuilder.Execute();
}

执行函数传入了一个FRHITexture*类型的纹理,然后调用 CreateRenderTarget 创建了一个未被追踪的池化的Render Target对象,它是TRefCountPtr<IPooledRenderTarget>类型。

之后创建了一个新的RDG,调用了RDG版本的Execute,最后调用GraphBuilder的Execute()函数执行。这里唯一资源就是这个纹理。

再观察一下RDG版本的执行入口(其中一个):

void FGenerateMips::Execute(FRDGBuilder& GraphBuilder, FRDGTextureRef Texture, FRHISamplerState* Sampler, EGenerateMipsPass Pass)
{
	if (Pass == EGenerateMipsPass::AutoDetect)
	{
		Pass = WillFormatSupportCompute(Texture->Desc.Format) ? EGenerateMipsPass::Compute : EGenerateMipsPass::Raster;
	}

	if (Pass == EGenerateMipsPass::Compute)
	{
		ExecuteCompute(GraphBuilder, Texture, Sampler);
	}
	else
	{
		ExecuteRaster(GraphBuilder, Texture, Sampler);
	}
}

基本上就是调用了ExecuteCompute和ExecuteRaster两个函数。

Compute Pass

简化以后的Compute Pass:

class FGenerateMipsCS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FGenerateMipsCS)
    SHADER_USE_PARAMETER_STRUCT(FGenerateMipsCS, FGlobalShader)

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER(FVector2D, TexelSize)
        SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, MipInSRV)
        SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, MipOutUAV)
        SHADER_PARAMETER_SAMPLER(SamplerState, MipSampler)
    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FGenerateMipsCS, "/Engine/Private/ComputeGenerateMips.usf", "MainCS", SF_Compute);

void FGenerateMips::ExecuteCompute(FRDGBuilder& GraphBuilder, FRDGTexture* Texture, FRHISamplerState* Sampler)
{
    TShaderMapRef<FGenerateMipsCS> ComputeShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));

    // Loop through each level of the mips that require creation and add a dispatch pass per level.
    for (uint32 MipLevel = 1, MipCount = TextureDesc.NumMips; MipLevel < MipCount; ++MipLevel)
    {
        const FIntPoint DestTextureSize(
            FMath::Max(TextureDesc.Extent.X >> MipLevel, 1),
            FMath::Max(TextureDesc.Extent.Y >> MipLevel, 1));

        FGenerateMipsCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FGenerateMipsCS::FParameters>();
        PassParameters->TexelSize  = FVector2D(1.0f / DestTextureSize.X, 1.0f / DestTextureSize.Y);
        PassParameters->MipInSRV   = GraphBuilder.CreateSRV(FRDGTextureSRVDesc::CreateForMipLevel(Texture, MipLevel - 1));
        PassParameters->MipOutUAV  = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(Texture, MipLevel));
        PassParameters->MipSampler = Sampler;

        FComputeShaderUtils::AddPass(
            GraphBuilder,
            RDG_EVENT_NAME("GenerateMips DestMipLevel=%d", MipLevel),
            ComputeShader,
            PassParameters,
            FComputeShaderUtils::GetGroupCount(DestTextureSize, FComputeShaderUtils::kGolden2DGroupSize));
    }
}

可以看到Compute Shader有一个SRV参数,一个UAV参数。其中SRV参数就是将原始贴图或者上一次生成的Mip等级贴图作为输入,而UAV则是本次要生成的贴图等级(可以注意MipInSRV和MipOutUAV后面的MipLevel数值关系)。

所以总结Compute Shader的作用,就是提取上一级别的Mip作为源,将其压缩作为本级别的Mip。

Raster Pass

再阅读简化后的Raster Pass:

class FGenerateMipsVS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FGenerateMipsVS);
};

IMPLEMENT_GLOBAL_SHADER(FGenerateMipsVS, "/Engine/Private/ComputeGenerateMips.usf", "MainVS", SF_Vertex);

class FGenerateMipsPS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FGenerateMipsPS);
    SHADER_USE_PARAMETER_STRUCT(FGenerateMipsPS, FGlobalShader);

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER(FVector2D, HalfTexelSize)
        SHADER_PARAMETER(float, Level)
        SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, MipInSRV)
        SHADER_PARAMETER_SAMPLER(SamplerState, MipSampler)
        RENDER_TARGET_BINDING_SLOTS()
    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FGenerateMipsPS, "/Engine/Private/ComputeGenerateMips.usf", "MainPS", SF_Pixel);

void FGenerateMips::ExecuteRaster(FRDGBuilder& GraphBuilder, FRDGTexture* Texture, FRHISamplerState* Sampler)
{
    auto ShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);
    TShaderMapRef<FGenerateMipsVS> VertexShader(ShaderMap);
    TShaderMapRef<FGenerateMipsPS> PixelShader(ShaderMap);

    for (uint32 MipLevel = 1, MipCount = Texture->Desc.NumMips; MipLevel < MipCount; ++MipLevel)
    {
        const uint32 InputMipLevel = MipLevel - 1;

        const FIntPoint DestTextureSize(
            FMath::Max(Texture->Desc.Extent.X >> MipLevel, 1),
            FMath::Max(Texture->Desc.Extent.Y >> MipLevel, 1));

        FGenerateMipsPS::FParameters* PassParameters = GraphBuilder.AllocParameters<FGenerateMipsPS::FParameters>();
        PassParameters->HalfTexelSize = FVector2D(0.5f / DestTextureSize.X, 0.5f / DestTextureSize.Y);
        PassParameters->Level = InputMipLevel;
        PassParameters->MipInSRV = GraphBuilder.CreateSRV(FRDGTextureSRVDesc::CreateForMipLevel(Texture, InputMipLevel));
        PassParameters->MipSampler = Sampler;
        PassParameters->RenderTargets[0] = FRenderTargetBinding(Texture, ERenderTargetLoadAction::ELoad, MipLevel);

        FPixelShaderUtils::AddFullscreenPass(
            GraphBuilder,
            ShaderMap,
            RDG_EVENT_NAME("GenerateMips DestMipLevel=%d", MipLevel),
            PixelShader,
            PassParameters,
            FIntRect(FIntPoint::ZeroValue, DestTextureSize));
    }
}

Pixel Shader有一个SRV,它的作用就是读取Texture的某个Mip级别。

Raster Pass 通过

PassParameters->RenderTargets[0] = FRenderTargetBinding(Texture, ERenderTargetLoadAction::ELoad, MipLevel);

绑定了Texture的某个Mip级别作为该Pass的渲染目标。

所以两个Pass先手串联起来,对一个Texture资源的不同Mip等级进行读取和储存,就完成了计算一组MipMap的工作。

更复杂的一个案例

这个案例中,同样要使用一个ComputePass和一个Raster Pass,但是区别是Compute Pass并不是每帧都要执行,而Raster Pass是每帧都要执行。

Compute Pass的作用是读取外部数据,计算一个立体贴图,计算量比较大,因此我们应该尽量避免频繁执行这个Pass,只是再外部数据发生变化的时候执行一次即可。

Raster Pass的作用是读取Compute Pass计算出来的立体贴图作为SRV,在屏幕上进行绘制。

Compute Shader

class FMyCS : public FGlobalShader
{
public:
	DECLARE_GLOBAL_SHADER(FMyCS)
	SHADER_USE_PARAMETER_STRUCT(FMyCS, FGlobalShader)

	BEGIN_SHADER_PARAMETER_STRUCT(FParameters,)
		SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture3D, OutUAV)
		SHADER_PARAMETER_RDG_UNIFORM_BUFFER(FRadiationUniformParameters, UniformBuffer)
		SHADER_PARAMETER_RDG_BUFFER_SRV(StructuredBuffer<RadiationData>, InDataBuffer)
		END_SHADER_PARAMETER_STRUCT()

	static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
	{
		return RHISupportsComputeShaders(Parameters.Platform);
	}
};

IMPLEMENT_GLOBAL_SHADER(FMyCS, "/Engine/Private/MyPassShader.usf", "MainCS", SF_Compute);

这里要关注的重点是 SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture3D, OutUAV) 这个参数,我们最后计算出来的立体贴图就是它。

Compute Pass

// Raster Pass
if (SceneInfo != nullptr &&
    (SceneInfo->VolumeRT == nullptr || SceneInfo->IsVolumeTextureDirty()))
{
    ComputeVolumeTexture(GraphBuilder, ComputeShader, Scene, textureSize);
    SceneInfo->SetVolumeTextureDirty(false);
}
// Compute VolumeTexture,省略部分内容
void ComputeVolumeTexture(FRDGBuilder& GraphBuilder, const TShaderMapRef<FMyCS>& ComputeShader, FScene* Scene,
                          const uint32 textureSize)
{
	FRDGTextureDesc TextDesc = FRDGTextureDesc::Create3D(
		FIntVector(textureSize, textureSize, textureSize),
		EPixelFormat::PF_R32_FLOAT,
		FClearValueBinding::None,
		TexCreate_ShaderResource | TexCreate_UAV);

	// Create VolumeTexture, use this as a bridge between CS and PS
	Scene->GetRadiationSceneInfo()->VolumeTexture = GraphBuilder.
		CreateTexture(TextDesc, TEXT("RadiationVolumeTexture"));
    // ...

	FComputeShaderUtils::AddPass(
		GraphBuilder,
		RDG_EVENT_NAME("ComputeVolumeTexture"),
		ComputeShader,
		CSParams,
		FIntVector(GroupCount, GroupCount, textureSize));

	// Extract Volume Texture Resource
	GraphBuilder.QueueTextureExtraction(Scene->GetRadiationSceneInfo()->VolumeTexture,
	                                    &Scene->GetRadiationSceneInfo()->VolumeRT);
}

首先看到ComputeVolumeTexture的前面是有一个判断的,满足这个条件才进行计算。其中的条件主要是判断贴图是否已经"dirty",即是否有新的数据来了,需要重新计算了。这样我们就可以减少执行这个Pass的次数。

另外注意到最后一行,使用了QueueTextureExtraction将贴图提取到外部进行保存。保存的指针格式是 TRefCountPtr<IPooledRenderTarget>

Pixel Shader

Vertex Shader这里略去不表。看一下简化后的Pixel Shader:

class FMyPS : public FGlobalShader
{
public:
	DECLARE_GLOBAL_SHADER(FMyPS);
	SHADER_USE_PARAMETER_STRUCT(FMyPS, FGlobalShader)

	BEGIN_SHADER_PARAMETER_STRUCT(FParameters,)
		SHADER_PARAMETER_RDG_TEXTURE(Texture3D, VolumeTexture)
		SHADER_PARAMETER_SAMPLER(SamplerState, VolumeSampler)
		RENDER_TARGET_BINDING_SLOTS()
	END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FMyPS, "/Engine/Private/MyPassShader.usf", "MainPS", SF_Pixel);

Pixel Shader 使用一个类型为Texture3D的SHADER_PARAMETER_RDG_TEXTURE作为参数,目的就是载入Compute Pass计算出来的VolumeTexture。

Raster Pass

简化后的Raster Pass代码:

if (SceneInfo != nullptr && SceneInfo->VolumeRT != nullptr)
{
	// Register pooled Texture
	volumeTexture = GraphBuilder.RegisterExternalTexture(SceneInfo->VolumeRT);

    if(SceneInfo->ShouldRenderVolume())
    {
        // Render To Screen 
        if (View.IsPerspectiveProjection())
        {
            check(SceneDepthTexture);

            FMyPS::FParameters* PassParams = GraphBuilder.AllocParameters<FMyPS::FParameters>();
            PassParams->RenderTargets[0] = FRenderTargetBinding(SceneColorTexture, ERenderTargetLoadAction::ELoad);
            PassParams->RenderTargets.DepthStencil = FDepthStencilBinding(
                SceneDepthTexture, ERenderTargetLoadAction::ELoad, ERenderTargetLoadAction::ELoad,
                FExclusiveDepthStencil::DepthRead_StencilWrite);
            PassParams->VolumeSampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();        
            PassParams->VolumeTexture = volumeTexture;

            GraphBuilder.AddPass(
                RDG_EVENT_NAME("RenderToScreen"),
                PassParams,
                ERDGPassFlags::Raster,
                [this, &View, LocalParameter, VertexShader, PixelShader, PassParams, UVScale](
                    FRHICommandList& RHICmdList)
                {
                    RenderToScreen(RHICmdList, View, LocalParameter, VertexShader, PixelShader, PassParams,
                        UVScale);
                });
        }
    }
}

开头先判断VolumeRT(ComputePass中提取出来的纹理资源)是否为空,不为空的情况下,调用 RegisterExternalTexture 函数将它再注册到本Graph中。

提取的动作不是即时完成的,在Raster Pass时是不能保证VolumeRT一定不为空,因此我们需要加以判断。

本例中两个Pass很大可能不是在同一帧执行,也就不是在同一个图中(图是每一帧都要重新创建的),因此要通过两个步骤:提取、注册 来共享资源。

下一页 Rdg 06 Resources