Shadow Bias
有关Shadow Bias的介绍我只推荐看这篇知乎文章自适应Shadow Bias算法,里面介绍了有关Shadow Bias的几乎所有需要了解的信息。同时这篇文章也指出了Unity目前正在使用的Shadow Caster Vertex Based Bias方法的不足之处。
如果是平时使用URP的话,Unity使用的在绘制Shadow Caster Pass时将ShadowBias添加到顶点的偏移上的方法,虽然称不上尽善尽美,但是也完全够用了。但是在二次元角色渲染的时候,为了营造丝袜勒肉的效果,会在腿部的模型和丝袜的模型交接的区域做一个向内凹陷的效果。这个区域两侧的模型是正常闭合的,其法线是相对的,此时如果使用Unity默认的使用ShadowBias去调整顶点位置的方式,ShadowBias中的NormalBias就会导致这个区域两侧的模型朝着法线的反方向偏移,导致这个区域的阴影在某些角度时会出现漏洞,可以在文章封面图的左边看到明显的瑕疵,右边则是在顶点着色器中使用了ShadowBias,看上去的效果就比较正常了,而且角色的阴影和角色的模型的大小也基本保持一致。当然了,把NormalBias设置成0就不会有这个问题了,但是失去了NormalBias则会带来其他角度的阴影的瑕疵。
本文使用的是Unity2022.3.43f1c1,URP版本是14.0.11。
关于Shadows.hlsl的碎碎念
我讨厌Unity URP Package里的Shadows.hlsl,因为它使用了一个LerpWhiteTo
的方法,这个方法定义在CoreRP的CommonMaterial.hlsl里,而不知道为什么Shadows.hlsl并没有包含这个CommonMaterial,这就导致了我只想单独使用Shadows.hlsl时,必须还得手动包含一遍CommonMaterial.hlsl,我觉得这很不合理。
更要命的是,如果在hlsl中想要使用URP的Light结构体,就起码得包含RealtimeLights.hlsl,这个hlsl又包含了Shadows.hlsl。索性我就直接从这些文件中摘取了一套自己的uber_lights.hlsl和uber_shadows.hlsl,这也方便后续做修改。
对于URP的修改
虽然我的想法是尽量做到即插即用,也就是说尽量不去修改URP默认的代码,但是很遗憾为了将ShadowBias移动到片元着色器里,还是得稍作修改。
URP设置ShadowBias是在绘制级联阴影的每一个slice的时候,将其作为参数传给顶点着色器的,也就是说虽然顶点着色器中的参数只有_ShadowBias
一个,实际上Unity会使用级联阴影的层级数个ShadowBias,我们需要将其保存起来,在渲染LitForwardPass的时候,根据当前像素的世界坐标找到对应的级联阴影的层级,再查找对应的ShadowBias进行计算。
MainLightShadowCasterPass.cs
主要是在MainLightShadowCasterPass.cs的RenderMainLightCascadeShadowmap
方法中,计算得到了当前层级的ShadowBias之后,将其保存在一个数组中留待后续使用。
for (int cascadeIndex = 0; cascadeIndex < m_ShadowCasterCascadesCount; ++cascadeIndex)
{
settings.splitData = m_CascadeSlices[cascadeIndex].splitData;
Vector4 shadowBias = ShadowUtils.GetShadowBias(ref shadowLight, shadowLightIndex, ref renderingData.shadowData, m_CascadeSlices[cascadeIndex].projectionMatrix, m_CascadeSlices[cascadeIndex].resolution);
m_MainLightShadowBiases[cascadeIndex] = shadowBias; // Shadow Biases for fragment shader.
ShadowUtils.SetupShadowCasterConstantBuffer(cmd, ref shadowLight, shadowBias);
CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.CastingPunctualLightShadow, false);
ShadowUtils.RenderShadowSlice(cmd, ref context, ref m_CascadeSlices[cascadeIndex],
ref settings, m_CascadeSlices[cascadeIndex].projectionMatrix, m_CascadeSlices[cascadeIndex].viewMatrix);
}
ShadowUtils.cs
比较神秘的是,Unity还额外设置了全局的DepthBias,由于我们直接在片元着色器里计算ShadowBias,这个地方可以不进行设置。如果不注释掉这一行,当我们将DepthBias和NormalBias都设置成0时,我们也几乎不能观察到条纹状的阴影瑕疵,这会影响我们对实际使用的ShadowBias的判断。
public static void RenderShadowSlice(CommandBuffer cmd, ref ScriptableRenderContext context,
ref ShadowSliceData shadowSliceData, ref ShadowDrawingSettings settings,
Matrix4x4 proj, Matrix4x4 view)
{
// cmd.SetGlobalDepthBias(1.0f, 2.5f); // these values match HDRP defaults (see https://github.com/Unity-Technologies/Graphics/blob/9544b8ed2f98c62803d285096c91b44e9d8cbc47/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/HDShadowAtlas.cs#L197 )
cmd.SetViewport(new Rect(shadowSliceData.offsetX, shadowSliceData.offsetY, shadowSliceData.resolution, shadowSliceData.resolution));
cmd.SetViewProjectionMatrices(view, proj);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
context.DrawShadows(ref settings);
cmd.DisableScissorRect();
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
cmd.SetGlobalDepthBias(0.0f, 0.0f); // Restore previous depth bias values
}
uber_shadow.hlsl
这里节选了比较关键的部分,大部分是和Unity的Shadows.hlsl一致的,但是需要注意片元中的ApplyShadowBias的方向和顶点中的ApplyShadowBias的方向是相反的,顶点需要远离光源,而片元则需要靠近光源,法线也是同理。采样器使用的是sampler_LinearClampCompare
,似乎是将双线性对应的四个像素点的深度和当前深度进行比较之后,再对比较的结果进行线性插值,能够得到一些小小的渐变,配合PCF就能得到比较好看的软阴影了。
至于要不要使用PCSS,我的想法是并不完全必要。首先PCSS的计算比较复杂,采样数比较多,对性能会有影响,如果使用Dither的方法的话,在使用二次元渲染经常使用的Ramp图时不一定能够得到理想的结果,而使用Ramp图制作皮肤在受光和阴影交接处的次表面散射效果时,需要比较大的阴影变化范围才能做出比较好看的散射效果,使用了PCSS在被较近物体遮挡时就没有很大的半影区域了。
float3 ApplyShadowBias(float3 positionWS, float3 normalWS, float3 lightDirection, float2 shadowBias)
{
float invNdotL = 1.0 - saturate(dot(lightDirection, normalWS));
float scale = invNdotL * shadowBias.y;
// normal bias is negative since we want to apply an inset normal offset
positionWS = -lightDirection * shadowBias.xxx + positionWS;
positionWS = -normalWS * scale.xxx + positionWS;
return positionWS;
}
half MainLightShadow(float3 positionWS, float3 normalWS)
{
#if !defined(MAIN_LIGHT_CALCULATE_SHADOWS)
return 1.0f;
#else
#ifdef _MAIN_LIGHT_SHADOWS_CASCADE
int cascadeIndex = (int)ComputeCascadeIndex(positionWS);
#else
int cascadeIndex = 0;
#endif
float3 lightDirWS = _LightDirection;
float2 shadowBias = _MainLightShadowBiases[cascadeIndex].xy;
positionWS = ApplyShadowBias(positionWS, normalWS, lightDirWS, shadowBias);
float4 shadowCoord = mul(_MainLightWorldToShadow[cascadeIndex], float4(positionWS, 1.0));
half shadow = SampleShadowmap(_MainLightShadowmapTexture, sampler_LinearClampCompare, shadowCoord, _MainLightShadowmapSize);
return shadow;
#endif
}
后记
非常简单的一篇小文章,写这篇文章的主要目的其实是告诉大家我还在做新的东西。。。文章中使用的角色是少前2追放里的绛雨,这里也放一个小小的图透。祝大家国庆节快乐捏!