14 KiB
级联阴影
阴影贴图的缺点:阴影边缘的锯齿严重。原因是阴影贴图的分辨率低,在对阴影贴图采样时,多个不同的顶点对同一个像素采样,导致生成锯齿。为了解决这种问题,我们使用多张阴影贴图,离相机近的地方使用精细的阴影贴图,离相机远的地方使用粗糙的阴影贴图,这样不仅优化了阴影效果,还保证了渲染效率因此,级联阴影的关键就是生成和使用不同精细度的阴影贴图
阴影贴图的原理:在灯光方向架一台摄像机,获取深度图,然后再正常渲染自己的场景,再在正常渲染场景的时候把fragment转换到光源空间,把它和之前渲染的Shadowmap中高度深度作比较,看它是否在影子里,如果是就返回0,不是就返回1
主要的步骤是取得场景中灯光的设置并且传递到Shader中,之后在RenderDirectionalShadows
中取得GetTemporaryRT
在设置完绘制属性后(将渲染结果传递至RT而不是摄像机上),通过RenderDirectionalShadows
绘制各个方向光的阴影。
RenderDirectionalShadows
主要是通过cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives
来计算视图矩阵、投影矩阵与ShadowSplitData结构,该函数第一个参数是可见光指数。接下来的三个参数是两个整数和一个Vector3,它们控制阴影级联。稍后我们将处理级联,因此现在使用零,一和零向量。然后是纹理尺寸,我们需要使用平铺尺寸。第六个参数是靠近平面的阴影,我们现在将其忽略并将其设置为零。之后操作为:
shadowSettings.splitData = splitData;
//设置矩阵
buffer.SetViewProjectionMatrices(viewMatrix,projectionMatrix);
ExecuteBuffer();
//根据ShadowDrawingSettings绘制阴影
context.DrawShadows(ref shadowSettings);
之后开始往Lit.Shader
添加绘制阴影Pass
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只负责裁剪
#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
类:
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类
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);
。
渲染
阴影图集
TEXTURE2D_SHADOW(_DirectionalShadowAtlas);
#define SHADOW_SAMPLER sampler_linear_clamp_compare
SAMPLER_CMP(SHADOW_SAMPLER);
3 级联阴影贴图
现在终于得到阴影,但它们看起来很糟糕。不应被阴影化的表面最终会被形成像素化带的阴影伪影所覆盖。这些是由于阴影贴图的有限分辨率导致的自我阴影化。使用不同的分辨率会更改伪影模式,但不会消除它们。
球形剔除
Unity通过为其创建一个选择球来确定每个级联覆盖的区域。由于阴影投影是正交的且呈正方形,因此它们最终会紧密契合其剔除球,但还会覆盖周围的一些空间。这就是为什么可以在剔除区域之外看到一些阴影的原因。同样,光的方向与球无关,因此所有定向光最终都使用相同的剔除球。
剔除球是ComputeDirectionalShadowMatricesAndCullingPrimitives函数中计算出的ShadowSplitData
分离出的数据。传递_CascadeCount
级联数以及_CascadeCullingSpheres
以及剔除球的位置数据。
创建ShadowData
结构体以传递级联级别以及阴影硬度。将会在GetLighting
中通过像素的世界坐标是否在球体内来计算ShadowData
结构体中的级联级别。
最大距离
此时阴影会在超过最后一个剔除球后消失,为了解决这个问题,会设置一个最大距离,超过最大距离阴影才会消失。具体操作是在GetShadowData
中比较像素深度以及阴影最大距离值,如果超过则将strength
设置为0。
给阴影添加衰减与级联渐变
级联衰减公式:
其中f为下面式子的倒数:
在
RenderDirectionalShadows
中计算fade因子后传递给_ShadowDistanceFade
清除阴影的摩尔纹
简单的清除方法
- 最简单的方法是向阴影投射器的深度添加恒定的偏差,这虽然会产生不精确的阴影但可以消除摩尔纹。
- 另一种方法是应用斜率比例偏差,方法是对SetGlobalDepthBias的第二个参数使用非零值。
法线偏差
新建级联数据变量用来传递级级联数据,x为剔除球半径倒数,y为使用√2*2f*cullingSphere.w/tileSize
算出来的级联纹理大小,因为最坏的情况是以像素对角方向进行偏移,所以前面有乘以√2
,存入shader后乘以Normal取得偏移值,之后对surfaceWS
进行偏移。
可配置的偏差
从剔除数据中获取Light以及其shadowBias,之后传递给ShadowedDirectionalLight
。在绘制阴影前将偏移值传递给buffer.SetGlobalDepthBias(0,light.slopeScaleBiase);
;从剔除数据中获取Light以及其normalBias传递到Shader中,乘以上一步中的normalBias
。
解决阴影裁剪问题
当摄像机处于物体中间时,会出现阴影被裁剪的问题。解决方法是在顶点着色器中添加:
#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。之后对阴影进行对应次数的采样。
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中的过滤器模式来自动执行此操作。
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`
for (i=0;i<_CascadeCount;i++)
{
float4 sphere=_CascadeCullingSpheres[i];
float distanceSqr=DistanceSquared(surfaceWS.position,sphere.xyz);
if(distanceSqr<sphere.w)
{
if(i== _CascadeCount-1)
{
data.strength*=FadedShadowStrength(distanceSqr,_CascadeData[i].x,_ShadowDistanceFade.z);
}
break;;
}
}
过渡抖动
提高级联过渡效果。在LitPassFragment
中给像素计算抖动值
surface.dither=InterleavedGradientNoise(input.positionCS.xy,0);
当使用抖动混合时,如果我们不在上一个级联中,则当混合值小于抖动值时,跳到下一个级联。
#if defined(_CASCADE_BLEND_DITHER)
else if(data.cascadeBlend < surfaceWS.dither)
{
i+=1;
}
#endif
其他功能效果
- 透明度
- 阴影模式
- 裁切阴影
- 抖动阴影
- 无阴影
- 不受光阴影投射器
- 接受阴影