Unity URP延迟渲染流程简单分析(Deferred Rendering)

前言

  大概两个月前的某个Unity项目里选择使用了URP延迟渲染管线(其实没必要,主要是因为自己想用一用、学一学,因为后续自己的项目中需要使用延迟渲染),但在写Shader实现物体表面着色时却一直有个疑惑:

  延迟渲染应该是先一个Pass将信息存储在GBuffer中,然后再一个Pass进行实际的着色,而且通常来说使用的都是同一套着色模型,而自己在前向渲染中编写使用的自定义着色的Shader挪到延迟渲染中却看起来没有什么问题。这让我不禁怀疑起自己,又或者Unity有着强大的延迟渲染管线中的Shader扩展支持。但因为延迟渲染是准备之后仔细研究的,所以疑惑一直留到了前几天。

  就在前两天因为五一假期所以在做自己的项目,于是决定研究一下上边的问题。这时突然想起来之前有看到过Unity的延迟渲染其实是混合了前向渲染的,主要是为了支持透明物体的渲染。拿多个光源照一下物体发现有光源数量限制,又翻了一下Frame Debug,果然自己写的Shader其实走的是延迟渲染后的前向渲染(Render GBuffer里当然也没有对应的信息),问题还没开始就结束了:

在这里插入图片描述

  然后大概看了看,发现如果想自己自定义着色效果而且走延迟渲染,可能还得改一改URP的代码(不过写个Shader在GBuffer Pass里动动手脚大概也能不改管线实现点效果,但感觉限制比较大),而在改之前就先要了解一下URP中的延迟渲染到底是个什么流程。

  这篇文章原本是打算学习使用一段时间后再整理一下,但最近可能要去忙其他事情,这里先写一部分,后续再补充。对Unity渲染管线了解不多,以下内容是对假期时学习的总结,仅整理大致流程方便翻找代码,可能有误,仅供参考。

GBuffer

  首先来看一下GBuffer的准备部分。
  从Universal Render Pipeline/Lit(也就是默认使用的Lit.shader)入手,在项目Project窗口Packages/Universal RP/Shaders下可以找到(右键->Show in Explorer可以看到文件的实际位置),而在这个Shader中我们能看到这么一个Pass:

	Name "GBuffer"
    Tags{"LightMode" = "UniversalGBuffer"}

  也就是在这里,ps将所需的数据写入了GBuffer,但这个Shader里并没有具体的vs或ps的实现,而是被包装在了LitGBufferPass.hlsl中:

	#pragma vertex LitGBufferPassVertex
    #pragma fragment LitGBufferPassFragment

    #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/LitGBufferPass.hlsl"

  在Shaders/LitGBufferPass.hlsl中,我们主要关注ps阶段,其中主要的部分就是通过InitializeStandardLitSurfaceData()获取了物体表面的材质信息,中间InitializeBRDFData()将其转为了BRDF参数(感觉起来这两者主要是对输入的参数进行打包、整理等),然后获取了全局光照,最后BRDFDataToGbuffer()即输出的Gbuffer数据(数据类型为FragmentOutput)。

    SurfaceData surfaceData;
    InitializeStandardLitSurfaceData(input.uv, surfaceData);

    InputData inputData;
    InitializeInputData(input, surfaceData.normalTS, inputData);
    SETUP_DEBUG_TEXTURE_DATA(inputData, input.uv, _BaseMap);

#ifdef _DBUFFER
    ApplyDecalToSurfaceData(input.positionCS, surfaceData, inputData);
#endif

    // Stripped down version of UniversalFragmentPBR().

    // in LitForwardPass GlobalIllumination (and temporarily LightingPhysicallyBased) are called inside UniversalFragmentPBR
    // in Deferred rendering we store the sum of these values (and of emission as well) in the GBuffer
    BRDFData brdfData;
    InitializeBRDFData(surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.alpha, brdfData);

    Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, inputData.shadowMask);
    MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI, inputData.shadowMask);
    half3 color = GlobalIllumination(brdfData, inputData.bakedGI, surfaceData.occlusion, inputData.positionWS, inputData.normalWS, inputData.viewDirectionWS);

    return BRDFDataToGbuffer(brdfData, inputData, surfaceData.smoothness, surfaceData.emission + color, surfaceData.occlusion);

  其中InitializeStandardLitSurfaceData()在Shaders/LitInput.shader中定义,而其输出的SurfaceData结构则定义在ShaderLibrary/SurfaceData.hlsl中;InitializeBRDFData()及其输出的结构定义在ShaderLibrary/BRDF.hlsl中;BRDFDataToGbuffer()部分(也是我们比较关心的部分)定义在ShaderLibrary/UnityGBuffer.hlsl中。
  在ShaderLibrary/UnityGBuffer.hlsl中,我们可以看到FragmentOutput的定义:

struct FragmentOutput
{
    half4 GBuffer0 : SV_Target0;
    half4 GBuffer1 : SV_Target1;
    half4 GBuffer2 : SV_Target2;
    half4 GBuffer3 : SV_Target3; // Camera color attachment

    #ifdef GBUFFER_OPTIONAL_SLOT_1
    GBUFFER_OPTIONAL_SLOT_1_TYPE GBuffer4 : SV_Target4;
    #endif
    #ifdef GBUFFER_OPTIONAL_SLOT_2
    half4 GBuffer5 : SV_Target5;
    #endif
    #ifdef GBUFFER_OPTIONAL_SLOT_3
    half4 GBuffer6 : SV_Target6;
    #endif
};

可以看到也是使用了MRT。然后找到BRDFDataToGbuffer()函数,代码以及注释写出了对GBuffer的分配情况:

	FragmentOutput output;
    output.GBuffer0 = half4(brdfData.albedo.rgb, PackMaterialFlags(materialFlags));  // diffuse           diffuse         diffuse         materialFlags   (sRGB rendertarget)
    output.GBuffer1 = half4(packedSpecular, occlusion);                              // metallic/specular specular        specular        occlusion
    output.GBuffer2 = half4(packedNormalWS, smoothness);                             // encoded-normal    encoded-normal  encoded-normal  smoothness
    output.GBuffer3 = half4(globalIllumination, 1);                                  // GI                GI              GI              [optional: see OutputAlpha()] (lighting buffer)
    ...

	return output;

到此,GBuffer的信息被写入。

Lighting

  Gbuffer有了,那么使用GBuffer进行实际画面渲染的部分在哪?在Runtime/UniversalRenderer.cs的构造函数中我们可以看到这么一段代码,包括Material的赋值、new DeferredLights、new DeferredPass:

	public UniversalRenderer(UniversalRendererData data) : base(data)
	{
		...
	    //m_TileDeferredMaterial = CoreUtils.CreateEngineMaterial(data.shaders.tileDeferredPS);
	    m_StencilDeferredMaterial = CoreUtils.CreateEngineMaterial(data.shaders.stencilDeferredPS);
	
		...
		
	    if (this.renderingMode == RenderingMode.Deferred)
	    {
	        var deferredInitParams = new DeferredLights.InitParams();
	        ...
	        deferredInitParams.tileDeferredMaterial = m_TileDeferredMaterial;
	        deferredInitParams.stencilDeferredMaterial = m_StencilDeferredMaterial;
	        ...
	        m_DeferredLights = new DeferredLights(deferredInitParams, useRenderPassEnabled);
	        ...
	        m_DeferredLights.TiledDeferredShading = false;
	        
	        ...
	        
	        m_DeferredPass = new DeferredPass(RenderPassEvent.BeforeRenderingDeferredLights, m_DeferredLights);
	        ...
	    }
	
	    ...
	    
	}

  从后往前看,DeferredPass(Runtime/Passes/DeferredPass.cs)就是一个ScriptableRenderPass,在Execute中执行了DeferredLights.ExecuteDeferredPass()。而DeferredLights(Runtime/DeferredLights.cs)中ExecuteDeferredPass部分如下:

	internal void ExecuteDeferredPass(ScriptableRenderContext context, ref RenderingData renderingData)
	{
	    ...
	
	    CommandBuffer cmd = CommandBufferPool.Get();
	    using (new ProfilingScope(cmd, m_ProfilingDeferredPass))
	    {
	        ...
	        
	        RenderStencilLights(context, cmd, ref renderingData);
	
	        RenderTileLights(context, cmd, ref renderingData);
	
	        ...
	    }
	
	    ...
	
	    context.ExecuteCommandBuffer(cmd);
	    CommandBufferPool.Release(cmd);
	}

  执行了两个RenderXXX函数,但RenderTileLights()并没有实际执行,注意之前所说的UniversalRenderer的构造函数中,tileDeferredMaterial被赋了空值,TiledDeferredShading也被置为false。而RenderStencilLights()中便是我们想找到的东西:

	void RenderStencilLights(ScriptableRenderContext context, CommandBuffer cmd, ref RenderingData renderingData)
	{
	    ...
	    
	    using (new ProfilingScope(cmd, m_ProfilingSamplerDeferredStencilPass))
	    {
	        ...
	
	        if (HasStencilLightsOfType(LightType.Directional))
	            RenderStencilDirectionalLights(cmd, ref renderingData, visibleLights, renderingData.lightData.mainLightIndex);
	        if (HasStencilLightsOfType(LightType.Point))
	            RenderStencilPointLights(cmd, ref renderingData, visibleLights);
	        if (HasStencilLightsOfType(LightType.Spot))
	            RenderStencilSpotLights(cmd, ref renderingData, visibleLights);
	    }
	
	    ...
	}
	
	void RenderStencilPointLights(CommandBuffer cmd, ref RenderingData renderingData, NativeArray<VisibleLight> visibleLights)
	{
	    ...
	
	    for (int soffset = m_stencilVisLightOffsets[(int)LightType.Point]; soffset < m_stencilVisLights.Length; ++soffset)
	    {
	        ...
	
	        // Stencil pass.
	        cmd.DrawMesh(m_SphereMesh, transformMatrix, m_StencilDeferredMaterial, 0, m_StencilDeferredPasses[(int)StencilDeferredPasses.StencilVolume]);
	
	        // Lighting pass.
	        cmd.DrawMesh(m_SphereMesh, transformMatrix, m_StencilDeferredMaterial, 0, m_StencilDeferredPasses[(int)StencilDeferredPasses.PunctualLit]);
	        cmd.DrawMesh(m_SphereMesh, transformMatrix, m_StencilDeferredMaterial, 0, m_StencilDeferredPasses[(int)StencilDeferredPasses.PunctualSimpleLit]);
	    }
	
	    ...
	}
	

  所以我们想找的shader就是m_StencilDeferredMaterial所对应的shader。于是回到开头,这个m_StencilDeferredMaterial是从UniversalRenderer构造函数中的UniversalRendererData(Runtime/UniversalRendererData.cs)里来的,而这个data的实例就是我们创建URP项目使用的那几个URP管线配置文件的其中一个(也可以通过Create->Rendering->URP Universal Renderer新建一个,或者URP包里Runtime/Data/UniversalRendererData也可以看到):


  我们在UniversalRendererData.cs代码里或者点击这个界面最右上角三个小点并选择Debug,都会发现stencilDeferredPS使用的是Shaders/Utils/StencilDeferred.shader(具体光照在ShaderLibrary/Lighting.hlsl中实现)。

  到此,一切差不多都串了起来。

Stencil

  但还有一个疑问,Stencil(模板)这个词在代码中频繁出现,而TiledDeferred却并没有实际使用。实际上Unity自己在这里讲的很清楚,他们最后采用了基于模板剔除光照的延迟渲染(Stencil,就是管线中深度检测、模板检测中的模板)而非经常被讨论的分块剔除光照的延迟渲染(Tiled-Based)。(一开始我只是在网上简单翻过一些资料了解Unity的延迟渲染管线,还以为Stencil只是说Unity自己写了一个延迟渲染的模板所以叫StencilDeferred…)

  而在之前提到的RenderStencilXXXLights函数中(当然,DirectionalLight除外),我们也会发现Unity先执行了Stencil Pass,然后执行了Lighting Pass。简单来说就是在光源处先使用一个凸几何体写入Stencil,仅覆盖了光能够照到的区域,然后再只对这部分区域进行该光源光照的着色。所以其实是Unity原本实现了Stencil和Tiled-Based两种,但综合考虑(不同设备支持、性能等)最终选择了Stencil。