接着 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的部分也大概梳理完成了,其中一定有很多需要查缺补漏的地方,之后注意到的话也会逐一补上来。