## 级联阴影 阴影贴图的缺点:阴影边缘的锯齿严重。原因是阴影贴图的分辨率低,在对阴影贴图采样时,多个不同的顶点对同一个像素采样,导致生成锯齿。为了解决这种问题,我们使用多张阴影贴图,离相机近的地方使用精细的阴影贴图,离相机远的地方使用粗糙的阴影贴图,这样不仅优化了阴影效果,还保证了渲染效率因此,级联阴影的关键就是生成和使用不同精细度的阴影贴图 阴影贴图的原理:在灯光方向架一台摄像机,获取深度图,然后再正常渲染自己的场景,再在正常渲染场景的时候把fragment转换到光源空间,把它和之前渲染的Shadowmap中高度深度作比较,看它是否在影子里,如果是就返回0,不是就返回1 主要的步骤是取得场景中灯光的设置并且传递到Shader中,之后在`RenderDirectionalShadows`中取得`GetTemporaryRT`在设置完绘制属性后(将渲染结果传递至RT而不是摄像机上),通过`RenderDirectionalShadows`绘制各个方向光的阴影。 `RenderDirectionalShadows`主要是通过`cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives`来计算视图矩阵、投影矩阵与ShadowSplitData结构,该函数第一个参数是可见光指数。接下来的三个参数是两个整数和一个Vector3,它们控制阴影级联。稍后我们将处理级联,因此现在使用零,一和零向量。然后是纹理尺寸,我们需要使用平铺尺寸。第六个参数是靠近平面的阴影,我们现在将其忽略并将其设置为零。之后操作为: ```c# shadowSettings.splitData = splitData; //设置矩阵 buffer.SetViewProjectionMatrices(viewMatrix,projectionMatrix); ExecuteBuffer(); //根据ShadowDrawingSettings绘制阴影 context.DrawShadows(ref shadowSettings); ``` 之后开始往`Lit.Shader`添加绘制阴影Pass ```c# Pass{ Tags { "LightMode" = "ShadowCaster" } ColorMask 0 HLSLPROGRAM #pragma target 3.5 #pragma shader_feature _CLIPPING #pragma multi_compile_instancing #pragma vertex ShadowCasterPassVertex #pragma fragment ShadowCasterPassFragment #include "ShadowCasterPass.hlsl" ENDHLSL } ``` 其中FragmentShader只负责裁剪 ```c# #ifndef CUSTOM_SHADOW_CASTER_PASS_INCLUDED #define CUSTOM_SHADOW_CASTER_PASS_INCLUDED #include "../ShaderLibrary/Common.hlsl" TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); UNITY_INSTANCING_BUFFER_START(UnityPerMaterial) UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST) UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor) UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff) UNITY_INSTANCING_BUFFER_END(UnityPerMaterial) struct Attributes { float3 positionOS : POSITION; float2 baseUV : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionCS : SV_POSITION; float2 baseUV : VAR_BASE_UV; UNITY_VERTEX_INPUT_INSTANCE_ID }; Varyings ShadowCasterPassVertex (Attributes input) { Varyings output; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); float3 positionWS = TransformObjectToWorld(input.positionOS); output.positionCS = TransformWorldToHClip(positionWS); float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST); output.baseUV = input.baseUV * baseST.xy + baseST.zw; return output; } void ShadowCasterPassFragment (Varyings input) { UNITY_SETUP_INSTANCE_ID(input); float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV); float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor); float4 base = baseMap * baseColor; #if defined(_CLIPPING) clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff)); #endif } #endif ``` `clip()`是HLSL内置函数,当传入数值小于0时丢弃当前像素。`_Cutoff`是一个设定的浮点值,默认为0。不是0就是1。在`ShadowCaster`Pass中是为了正确渲染Alpha物体阴影。 `ShadowCaster`Pass是用来渲染阴影贴图(灯光空间的深度贴图),如果相机空间物体表面深度大于阴影贴图中深度,则代表物体处于阴影。取得阴影值在`GetDirectionalShadowAttenuation()=>FilterDirectionalShadow()`里面,采样完阴影后`GetDirectionalShadowAttenuation()`里进行一次插值,`return lerp(1.0,shadow,directional.strength);`,最后在`GetLighting()`取得计算完的阴影值。 在`FilterDirectionalShadow()`中调用了`SAMPLE_TEXTURE2D_SHADOW`宏。`SAMPLE_TEXTURE2D_SHADOW`宏本质是`SampleCmpLevelZero()`,函数会对指定的纹理坐标进行采样,将采样的结果与传入的参数z(当前texel在光源空间的深度)进行比较,小于等于z视为通过(说明此texel没被遮挡),否则视为不通过(说明此texel位于阴影中)。 另外注意`SamplerComparisonState`是用来进行深度采样比较的采样器。需要与`SampleCmpLevelZero`一起使用。 ### 添加级联效果 由于定向光会影响最大阴影距离范围内的所有物体,因此它们的阴影贴图最终会覆盖较大的区域。由于阴影贴图使用正交投影,因此阴影贴图中的每个纹理像素都具有固定的世界空间大小。如果该尺寸太大,则清晰可见单个阴影纹理,从而导致锯齿状的阴影边缘和小的阴影可能消失。可以通过增加图集大小来缓解这种情况,但仅限于一定程度。 ### 添加设置 #### ShadowSettings类 添加`ShadowSettings`类: ```c# using UnityEngine; [System.Serializable] public class ShadowSettings { [Min(0f)] public float maxDistance = 100f; public enum TextureSize { _256=256,_512=512,_1024=1024,_2048=2048,_4096=4096,_8192=8192 } [System.Serializable] public struct Directional { public TextureSize atlasSize; } public Directional directional = new Directional { atlasSize = TextureSize._1024}; } ``` 往`CustomRenderPipelineAsset`、`CustomRenderPipeline`、`CameraRenderer` 、`Lighting`依次添加`ShadowSettings`变量以及对应函数中的形参。 #### Shadow类 ```c# public class Shadows { const int maxShadowedDirectionalLightCount = 1; int ShadowedDirectionalLightCount; const string bufferName = "shadows"; CommandBuffer buffer = new CommandBuffer { name = bufferName }; ScriptableRenderContext context; CullingResults cullingResults; ShadowSettings settings; struct ShadowedDirectionalLight { public int visibleLightIndex; } private ShadowedDirectionalLight[] ShadowedDirectionalLights = new ShadowedDirectionalLight[maxShadowedDirectionalLightCount]; public void Setup( ScriptableRenderContext context,CullingResults cullingResults, ShadowSettings settings ) { ShadowedDirectionalLightCount = 0; this.context = context; this.cullingResults = cullingResults; this.settings = settings; } void ExecuteBuffer() { context.ExecuteCommandBuffer(buffer); buffer.Clear(); } /* * 存储方向光阴影信息 * 灯光处于阴影有效且处于可见状态时,将信息存储在ShadowedDirectionalLights[]中 */ public void ReserveDirectionalShadows(Light light, int visibleLightIndex) { if (ShadowedDirectionalLightCount < maxShadowedDirectionalLightCount && light.shadows!=LightShadows.None && light.shadowStrength>0f && cullingResults.GetShadowCasterBounds(visibleLightIndex,out Bounds b)) { ShadowedDirectionalLights[ShadowedDirectionalLightCount++] = new ShadowedDirectionalLight() { visibleLightIndex = visibleLightIndex}; } } } ``` - 之后在`Lighting`类中添加`Shadow`类变量shadow,并且在`Setup`中添加`shadows.Setup(context,cullingResults,shadowSettings);`。以及在`SetupDirectionalLight`中添加`shadows.ReserveDirectionalShadows(visibleLight.light,index);`。 ### 渲染 #### 阴影图集 ```c# TEXTURE2D_SHADOW(_DirectionalShadowAtlas); #define SHADOW_SAMPLER sampler_linear_clamp_compare SAMPLER_CMP(SHADOW_SAMPLER); ``` #### 3 级联阴影贴图 现在终于得到阴影,但它们看起来很糟糕。不应被阴影化的表面最终会被形成像素化带的阴影伪影所覆盖。这些是由于阴影贴图的有限分辨率导致的自我阴影化。使用不同的分辨率会更改伪影模式,但不会消除它们。 ![](https://pic1.zhimg.com/80/v2-7b1bc2d23fcaaf10112496200536f770_720w.jpg) #### 球形剔除 Unity通过为其创建一个选择球来确定每个级联覆盖的区域。由于阴影投影是正交的且呈正方形,因此它们最终会紧密契合其剔除球,但还会覆盖周围的一些空间。这就是为什么可以在剔除区域之外看到一些阴影的原因。同样,光的方向与球无关,因此所有定向光最终都使用相同的剔除球。 剔除球是ComputeDirectionalShadowMatricesAndCullingPrimitives函数中计算出的`ShadowSplitData`分离出的数据。传递`_CascadeCount`级联数以及`_CascadeCullingSpheres`以及剔除球的位置数据。 创建`ShadowData`结构体以传递级联级别以及阴影硬度。将会在`GetLighting`中通过像素的世界坐标是否在球体内来计算`ShadowData`结构体中的级联级别。 #### 最大距离 此时阴影会在超过最后一个剔除球后消失,为了解决这个问题,会设置一个最大距离,超过最大距离阴影才会消失。具体操作是在`GetShadowData`中比较像素深度以及阴影最大距离值,如果超过则将`strength`设置为0。 #### 给阴影添加衰减与级联渐变 阴影衰减见git, ![](https://pic3.zhimg.com/80/v2-3d3ad5abdfc204e2b640ffab7329554a_720w.jpg) 级联衰减公式: ![](https://pic2.zhimg.com/80/v2-01588a58eec8923422b5ea114f2000e5_720w.jpg) 其中f为下面式子的倒数: ![](https://pic2.zhimg.com/80/v2-5739eecbfd934de957734ad3d9913ad9_720w.jpg) 在`RenderDirectionalShadows`中计算fade因子后传递给`_ShadowDistanceFade` #### 清除阴影的摩尔纹 ##### 简单的清除方法 1. 最简单的方法是向阴影投射器的深度添加恒定的偏差,这虽然会产生不精确的阴影但可以消除摩尔纹。 2. 另一种方法是应用斜率比例偏差,方法是对SetGlobalDepthBias的第二个参数使用非零值。 ##### 法线偏差 新建级联数据变量用来传递级级联数据,x为剔除球半径倒数,y为使用`√2*2f*cullingSphere.w/tileSize`算出来的级联纹理大小,因为最坏的情况是以像素对角方向进行偏移,所以前面有乘以`√2`,存入shader后乘以Normal取得偏移值,之后对`surfaceWS`进行偏移。 ##### 可配置的偏差 从剔除数据中获取Light以及其shadowBias,之后传递给`ShadowedDirectionalLight`。在绘制阴影前将偏移值传递给`buffer.SetGlobalDepthBias(0,light.slopeScaleBiase);`;从剔除数据中获取Light以及其normalBias传递到Shader中,乘以上一步中的`normalBias`。 ##### 解决阴影裁剪问题 当摄像机处于物体中间时,会出现阴影被裁剪的问题。解决方法是在顶点着色器中添加: ```c# #if UNITY_REVERSED_Z output.positionCS.z=min(output.positionCS.z,output.positionCS.w*UNITY_NEAR_CLIP_VALUE); #else output.positionCS.z=max(output.positionCS.z,output.positionCS.w*UNITY_NEAR_CLIP_VALUE); #endif ``` 原理是当物体z坐标小于近剪裁面时,将顶点挤压or贴在近剪裁面上。 对于大三角产生问题的原因不明白。解决方法是取得灯光的`ShadowNearPlane`之后传递给计算剔除球形参中。 ##### PCF过滤 到目前为止,我们仅对每个片段采样一次阴影贴图,且使用了硬阴影。阴影比较采样器使用特殊形式的双线性插值,在插值之前执行深度比较。这被称为百分比紧密过滤(percentage closer filtering 简称PCF),因为其中包含四个纹理像素,所以一般指是2×2 PCF过滤器。 - 添加2x2、3x3、5x5,3种过滤枚举、传递shadowAtlasSize到Shader,以及对应的Shader宏,并且修改`SetKeywords()`。 - 为每种过滤器设置不同的采样次数与宏设置 `DIRECTIONAL_FILTER_SETUP`为SampleShadow_ComputeSamples_Tent_xxx通过Size与positionSTS.xy计算权重以及uv。之后对阴影进行对应次数的采样。 ```c# float FilterDirectionalShadow (float3 positionSTS) { #if defined(DIRECTIONAL_FILTER_SETUP) float weights[DIRECTIONAL_FILTER_SAMPLES]; float2 positions[DIRECTIONAL_FILTER_SAMPLES]; float4 size = _ShadowAtlasSize.yyxx; DIRECTIONAL_FILTER_SETUP(size, positionSTS.xy, weights, positions); float shadow = 0; for (int i = 0; i < DIRECTIONAL_FILTER_SAMPLES; i++) { shadow += weights[i] * SampleDirectionalShadowAtlas( float3(positions[i].xy, positionSTS.z) ); } return shadow; #else return SampleDirectionalShadowAtlas(positionSTS); #endif } ``` 在`Shadows.cs`中修改`SetCascadeData()`。增大滤镜大小可使阴影更平滑,但也会导致粉刺再次出现。我们需要增加法向偏置以匹配滤波器尺寸。可以通过将纹理像素大小乘以1加上SetCascadeData中的过滤器模式来自动执行此操作。 ```c# void SetCascadeData(int index, Vector4 cullingSphere, float tileSize) { float texelSize = 2f * cullingSphere.w / tileSize; float filterSize = texelSize * ((float)settings.directional.filter + 1f); cullingSphere.w -= filterSize; cullingSphere.w *= cullingSphere.w; cascadeCullingSpheres[index] = cullingSphere; cascadeData[index] = new Vector4( 1f / cullingSphere.w, filterSize*1.4142136f); } ``` ##### 级联过渡 在`GetShadowData()`中根据距离计算fade,如果不是最后一个球就把fade赋值给cascadeBlend。最后一个球的`·`strength=strength*fade` ```c# for (i=0;i<_CascadeCount;i++) { float4 sphere=_CascadeCullingSpheres[i]; float distanceSqr=DistanceSquared(surfaceWS.position,sphere.xyz); if(distanceSqr