LOADING

加载过慢请开启缓存 浏览器默认开启

Unity延迟管线Shader代码梳理(其二)

接着 GBuffer 阶段,阅读 ClusterDeferred 的 LightPass,理解透明度混合、模板测试以及直接光照的累加过程。

在看这篇文章之前,最好先看下Unity延迟管线Shader代码梳理(其一),并对延迟渲染有一定的了解,上一篇主要讲了延迟渲染管线的GbufferPass,这一篇主要是LightPass。

1.LightPass

unity的延迟渲染主要分为两种,第一种就是最常见的延迟渲染模式,第二中叫ClusterDeferred,它是讲屏幕分成若干小方块,再做延迟渲染的一种方式,在多光源的情况下会比原先的延迟渲染有更好的性能。这里主要是讲Unity的ClusterDeferred。 ClusterDeferred的光照Pass写在”Hidden/Universal Render Pipeline/ClusterDeferred”下,只有个三个Pass,分别是: - Deferred Clustered Lights (Lit):用于最常用的PBR光照计算 - Deferred Clustered Lights (SimpleLit):用于轻量级的光照计算 - Fog:雾效计算 这里我们主要关心 Deferred Clustered Lights (Lit),他的代码如下:

// 0 - Clustered Lights - Lit  
Pass  
{  
    Name "Deferred Clustered Lights (Lit)"  
  
    // -------------------------------------  
    // Render State Commands    ZTest NotEqual  
    ZWrite Off    Cull Off    Blend One SrcAlpha, Zero One  
  
    // -------------------------------------  
    // Stencil Settings    Stencil {  
        Ref [_LitStencilRef]  
        ReadMask [_LitStencilReadMask]  
        WriteMask [_LitStencilWriteMask]  
        Comp Equal  
        Pass Keep        Fail Keep        ZFail Keep    }  
  
    HLSLPROGRAM  
    #pragma target 4.5  
  
    // Deferred Rendering Path does not support the OpenGL-based graphics API:  
    // Desktop OpenGL, OpenGL ES 3.0, WebGL 2.0.    #pragma exclude_renderers gles3 glcore  
  
    // -------------------------------------  
    // Shader Stages    #pragma vertex VertexFullScreen  
    #pragma fragment DeferredShadingClustered  
  
    // -------------------------------------  
    // Defines    #define _CLUSTER_LIGHT_LOOP  
    #define _LIT  
    // -------------------------------------  
    // Universal Pipeline keywords    #pragma multi_compile_fragment _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN  
    #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS  
    #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH  
    #pragma multi_compile_fragment _ LIGHTMAP_SHADOW_MIXING  
    #pragma multi_compile_fragment _ SHADOWS_SHADOWMASK  
    #pragma multi_compile_fragment _ _GBUFFER_NORMALS_OCT  
    #pragma multi_compile_fragment _ _DEFERRED_MIXED_LIGHTING  
    #pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION  
    #pragma multi_compile_fragment _ _RENDER_PASS_ENABLED  
    #pragma multi_compile_fragment _ _LIGHT_COOKIES  
    #pragma multi_compile _ _LIGHT_LAYERS  
    #include_with_pragmas "Packages/com.unity.render-pipelines.core/ShaderLibrary/FoveatedRenderingKeywords.hlsl"  
  
    // -------------------------------------  
    // Includes    #include "Packages/com.unity.render-pipelines.universal/Shaders/Utils/ClusterDeferred.hlsl"  
  
    ENDHLSL  
}

1.1透明度混合

上一篇文章说过,Gbuffer阶段的第四个Gbuffer与延迟渲染的光照阶段共同使用同一个渲染目标,中间也没经历过清除,我们看目前这个Pass的混合模式:Blend One SrcAlpha, Zero One,而LightPass的最后Return是Half4(color,1),说明实际上它所作的就是单纯的将LightPass阶段的RGB输出值与环境光相加。

1.2模板测试

Unity的延迟渲染是基于模板测试的,延迟渲染其中一个缺点就是只能使用一个光照模型,而Unity则是用模板测试来解决这个问题。在Lit.shader中,基于SubShader设定的”UniversalMaterialType” = “Lit”会为每个使用这个shader的模型设定一个模板值,然后这个模板值会一路来到光照Pass阶段,再由模板测试来决定这个Pass是否渲染这个像素,这样就可以通过模板测试和多个Pass让延迟渲染也可以使用多个光照模型了。

1.3片元着色器

先提一嘴顶点着色器吧,顶点着色器内容不多,几乎就是拿屏幕UV而已。 片元着色器的内容是本文重点,它主要是为了实现多光源的PBR计算。

从头看起:UNITY_SETUP_INSTANCE_ID(input);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); 这部分依旧Instance ID与VR相关处理。没什么好讲

然后是拿屏幕UV:

float2 screen_uv = (input.screenUV.xy / input.screenUV.z);  
  
#if defined(SUPPORTS_FOVEATED_RENDERING_NON_UNIFORM_RASTER)  
float2 undistorted_screen_uv = screen_uv;  
UNITY_BRANCH if (_FOVEATED_RENDERING_NON_UNIFORM_RASTER)  
{  
    screen_uv = input.positionCS.xy * _ScreenSize.zw;  
}  
#endif

由于这个阶段是对整个屏幕处理的,所以拿屏幕UV这事还是非常重要,我们会利用这个UV对各种信息进行采样(其实主要指的就是各种Gbuffer),这里的宏主要是为了VR做支持,得到他们专用的屏幕空间UV。

GBufferData gBufferData = UnpackGBuffers(input.positionCS.xy); GBufferData这个结构体被定义在GBufferCommon.hlsl中,可以看一下:

struct GBufferData  
{  
    half3 baseColor;  
    half smoothness;  
    // For Lit materials, if !(materialFlags & kMaterialFlagSpecularSetup) then specularColor.r contains metallic.  
    half3 specularColor;  
    half occlusion;  
    float3 normalWS;  
    uint materialFlags;  
    float depth;  
  
    // Only assigned if GBUFFER_FEATURE_SHADOWMASK is defined!  
    // Otherwise, defaults to (1, 1, 1, 1)    half4 shadowMask;  
  
    // Only assigned if GBUFFER_FEATURE_RENDERING_LAYERS is defined!  
    // Otherwise, defaults to 0xFFFF (Everything)    uint meshRenderingLayers;  
};

其中的内容无非就是为了从Gbuffer取出各种变量而准备了各种数据。 然后是UnpackGBuffers:

GBufferData UnpackGBuffers(uint2 unCoord2)  
{  
    half4 gBuffer0;  
    half4 gBuffer1;  
    half4 gBuffer2;  
    float depth;  
    uint renderingLayers;  
    half4 shadowMask;  
  
    LoadGBuffers(unCoord2, gBuffer0, gBuffer1, gBuffer2, depth, renderingLayers, shadowMask);  
  
    GBufferData gBufferData;  
    ZERO_INITIALIZE(GBufferData, gBufferData);  
  
    gBufferData.baseColor = gBuffer0.rgb;  
    gBufferData.materialFlags = UnpackGBufferMaterialFlags(gBuffer0.a);  
    gBufferData.specularColor = gBuffer1.rgb;  
    gBufferData.occlusion = gBuffer1.a;  
    gBufferData.normalWS = normalize(UnpackGBufferNormal(gBuffer2.rgb));  
    gBufferData.smoothness = gBuffer2.a;  
    gBufferData.depth = depth;  
    gBufferData.shadowMask = shadowMask;  
    gBufferData.meshRenderingLayers = renderingLayers;  
  
    return gBufferData;  
}

这段代码首先用LoadGBuffers函数采样Gbuffer,再传递给gBufferData。 再看下LoadGBuffers吧:

void LoadGBuffers(uint2 unCoord2, out half4 gBuffer0, out half4 gBuffer1, out half4 gBuffer2, out float depth,  
                  out uint renderingLayers, out half4 shadowMask)  
{  
    renderingLayers = 0xFFFFFFFF;  
    shadowMask = half4(1, 1, 1, 1);  
  
    #if defined(GBUFFER_FBFETCH_AVAILABLE)  
  
    depth    = LOAD_FRAMEBUFFER_X_INPUT(GBUFFER_IDX_R_DEPTH, unCoord2).x;  
    gBuffer0 = LOAD_FRAMEBUFFER_X_INPUT(GBUFFER_IDX_RGB_BASECOLOR_A_FLAGS, unCoord2);  
    gBuffer1 = LOAD_FRAMEBUFFER_X_INPUT(GBUFFER_IDX_RGB_SPECULAR_R_METALLIC_A_OCCLUSION, unCoord2);  
    gBuffer2 = LOAD_FRAMEBUFFER_X_INPUT(GBUFFER_IDX_RGB_NORMALS_A_SMOOTHNESS, unCoord2);  
  
    #if defined(GBUFFER_FEATURE_SHADOWMASK)  
    shadowMask = LOAD_FRAMEBUFFER_X_INPUT(GBUFFER_IDX_RGBA_SHADOWMASK, unCoord2);  
    #endif  
  
    #if defined(GBUFFER_FEATURE_RENDERING_LAYERS)  
    renderingLayers = LOAD_FRAMEBUFFER_X_INPUT(GBUFFER_IDX_R_RENDERING_LAYERS, unCoord2).x;  
    #endif  
  
    #else  
    depth    = LOAD_TEXTURE2D_X(_CameraDepthTexture, unCoord2).x;  
    gBuffer0 = LOAD_TEXTURE2D_X(GBUFFER_TEX2D_NAME(GBUFFER_IDX_RGB_BASECOLOR_A_FLAGS), unCoord2);  
    gBuffer1 = LOAD_TEXTURE2D_X(GBUFFER_TEX2D_NAME(GBUFFER_IDX_RGB_SPECULAR_R_METALLIC_A_OCCLUSION), unCoord2);  
    gBuffer2 = LOAD_TEXTURE2D_X(GBUFFER_TEX2D_NAME(GBUFFER_IDX_RGB_NORMALS_A_SMOOTHNESS), unCoord2);  
  
    #if defined(GBUFFER_FEATURE_SHADOWMASK)  
    shadowMask = LOAD_TEXTURE2D_X(GBUFFER_TEX2D_NAME(GBUFFER_IDX_AFTER(GBUFFER_IDX_RGBA_SHADOWMASK)), unCoord2);  
    #endif  
  
    #if defined(GBUFFER_FEATURE_RENDERING_LAYERS)  
    renderingLayers = LOAD_TEXTURE2D_X(GBUFFER_TEX2D_NAME(GBUFFER_IDX_AFTER(GBUFFER_IDX_R_RENDERING_LAYERS)), unCoord2).x;  
    #endif  
  
    #endif}

嗯,其中是根据各种情况以各种规格采样Gbuffer。

half3 color = 0.0;
half alpha = 1.0; 初始化返回值

#if defined(SUPPORTS_FOVEATED_RENDERING_NON_UNIFORM_RASTER)  
UNITY_BRANCH if (_FOVEATED_RENDERING_NON_UNIFORM_RASTER)  
{  
    input.positionCS.xy = undistorted_screen_uv * _ScreenSize.xy;  
}  
#endif

VR支持

float4 posWS = mul(_ScreenToWorld[SLICE_ARRAY_INDEX], float4(input.positionCS.xy, gBufferData.depth, 1.0));  
posWS.xyz *= rcp(posWS.w);

深度图还原世界坐标,由于Gbuffer有限,我们不能把所有数据都储存在里面,像世界坐标这种东西我们就可以用深度图进行还原。深度图重构世界坐标有多种方式,这里使用了最简单粗暴的逆矩阵法。

InputData inputData = (InputData)0;  
  
inputData.positionWS = posWS.xyz;  
inputData.normalWS = gBufferData.normalWS;  
inputData.viewDirectionWS = GetWorldSpaceNormalizeViewDir(posWS.xyz);  
inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS);

初始化各种中间数据用的结构体并填入数据。

AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(screen_uv);  
  
#if defined(_SCREEN_SPACE_OCCLUSION)  
    // What we want is really to apply the minimum occlusion value between the baked occlusion from surfaceDataOcclusion and real-time occlusion from SSAO.  
    // But we already applied the baked occlusion during gbuffer pass, so we have to cancel it out here.    // We must also avoid divide-by-0 that the reciprocal can generate.    half surfaceDataOcclusion = gBufferData.occlusion;  
    half occlusion = aoFactor.indirectAmbientOcclusion < surfaceDataOcclusion ? aoFactor.indirectAmbientOcclusion * rcp(surfaceDataOcclusion) : 1.0;  
    alpha = occlusion;#endif

取SSAO

// Main light  
Light mainLight = GetMainLight();  
mainLight.distanceAttenuation = 1.0;  
bool materialReceiveShadowsOff = (gBufferData.materialFlags & kMaterialFlagReceiveShadowsOff) != 0;  
UNITY_BRANCH if (!materialReceiveShadowsOff)  
{  
    #if defined(_MAIN_LIGHT_SHADOWS_SCREEN) && !defined(_SURFACE_TYPE_TRANSPARENT)  
        float4 shadowCoord = float4(screen_uv, 0.0, 1.0);  
    #elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)  
        float4 shadowCoord = TransformWorldToShadowCoord(posWS.xyz);  
    #else  
        float4 shadowCoord = float4(0, 0, 0, 0);  
    #endif  
    mainLight.shadowAttenuation = MainLightShadow(shadowCoord, posWS.xyz, gBufferData.shadowMask, _MainLightOcclusionProbes);  
}  
  
#if defined(_LIGHT_COOKIES)  
    half3 cookieColor = SampleMainLightCookie(posWS.xyz);  
    mainLight.color *= half3(cookieColor);  
#endif  
  
#if defined(_SCREEN_SPACE_OCCLUSION)  
    mainLight.shadowAttenuation *= aoFactor.directAmbientOcclusion;  
#endif

这里主要是对阴影相关计算 首先通过GetMainLight拿到主光源相关的数据,然后由于是平行光,光照没有衰减,所以 mainLight.distanceAttenuation = 1.0; 是恒等于1.0的 materialReceiveShadowsOff:通过之前在Gbuffer中的标记materialFlags来判断是否需要阴影。 最后就是根据各种情况,将阴影储存在mainLight.shadowAttenuation

color += DeferredLightContribution(mainLight, inputData, gBufferData); 这个就是主要的光照计算逻辑了。 计算光照DeferredLightContribution如下:

half3 DeferredLightContribution(Light light, InputData inputData, GBufferData gBufferData)  
{  
    #if defined(_LIGHT_LAYERS)  
    UNITY_BRANCH if (!IsMatchingLightLayer(light.layerMask, gBufferData.meshRenderingLayers))  
        return half3(0.0, 0.0, 0.0);  
    #endif  
  
    #if defined(_SIMPLELIT)  
    {        SurfaceData surfaceData = GBufferDataToSurfaceData(gBufferData);  
        half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);  
        half3 diffuseColor = LightingLambert(attenuatedLightColor, light.direction, inputData.normalWS);  
        half smoothness = exp2(10 * surfaceData.smoothness + 1);  
        half3 specularColor = LightingSpecular(attenuatedLightColor, light.direction, inputData.normalWS, inputData.viewDirectionWS, half4(surfaceData.specular, 1), smoothness);  
  
        // TODO: if !defined(_SPECGLOSSMAP) && !defined(_SPECULAR_COLOR), force specularColor to 0 in gbuffer code  
        return half3(diffuseColor * surfaceData.albedo + specularColor);  
    }    #elif defined(_LIT)  
    {        #if SHADER_API_MOBILE || SHADER_API_SWITCH  
            // Specular highlights are still silenced by setting specular to 0.0 during gbuffer pass and GPU timing is still reduced.  
            bool materialSpecularHighlightsOff = false;  
        #else  
            bool materialSpecularHighlightsOff = (gBufferData.materialFlags & kMaterialFlagSpecularHighlightsOff);  
        #endif  
  
        BRDFData brdfData = GBufferDataToBRDFData(gBufferData);  
        return half3(LightingPhysicallyBased(brdfData, light, inputData.normalWS, inputData.viewDirectionWS, materialSpecularHighlightsOff));  
    }    #endif  
  
    return half3(0.0, 0.0, 0.0);  
}

大概就是通过_SIMPLELIT与_LIT判断计算简易的光照模型还是BRDF,而这两个宏被定义在ClusterDeferred.shader的各自的Pass中,关于BRDF的内容就不再细挖了,里面是层层函数包含,具体计算过程实际上就哪些,法线分布函数啊,几何遮蔽之类的。如果想要写自己的光照逻辑的话可以接着接上一个#elif ,并且在自己的Pass中定义一个宏,例如:#define _LIT

// Additional light loop  
// We do additional directional lights last because otherwise FXC complains...  
uint pixelLightCount = GetAdditionalLightsCount();  
LIGHT_LOOP_BEGIN(pixelLightCount)  
    Light light = GetAdditionalLight(lightIndex, inputData, gBufferData.shadowMask, aoFactor);  
  
    UNITY_BRANCH if (materialReceiveShadowsOff)  
    {        light.shadowAttenuation = 1.0;  
    }  
    color += DeferredLightContribution(light, inputData, gBufferData);  
LIGHT_LOOP_END  
  
UNITY_LOOP for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++)  
{  
    CLUSTER_LIGHT_LOOP_SUBTRACTIVE_LIGHT_CHECK  
    Light light = GetAdditionalLight(lightIndex, inputData, gBufferData.shadowMask, aoFactor);  
  
    UNITY_BRANCH if (materialReceiveShadowsOff)  
    {        light.shadowAttenuation = 1.0;  
    }  
    color += DeferredLightContribution(light, inputData, gBufferData);  
}

这里是最后的部分,点光源与聚光灯的渲染,之前提过,这个是ClusterDeferred,对于它我并没深度了解,不过也可以简单介绍一下差异。普通的延迟渲染对于多光源是逐个Pass来完成的,有几盏灯就需要几个Pass,而ClusterDeferred则是将这些光照计算通过循环的方式在一个Pass中完成,当然这并不是说一个Pass有多么多么好,只不过它的机制,设计,最后是这样实现而已,如果想要深入了解,就要自行去寻找ClusterDeferred的内容了。代码很简单,根据宏进行光照循环,其中光照计算的关键函数与平行光一模一样,都是DeferredLightContribution函数,每次循环都会把结果叠加到color上。 至此关于LightPass的部分也大概梳理完成了,其中一定有很多需要查缺补漏的地方,之后注意到的话也会逐一补上来。


上一篇:Unity延迟管线Shader代码梳理(其一)