323 lines
14 KiB
Markdown
323 lines
14 KiB
Markdown
|
## 级联阴影
|
|||
|
阴影贴图的缺点:阴影边缘的锯齿严重。原因是阴影贴图的分辨率低,在对阴影贴图采样时,多个不同的顶点对同一个像素采样,导致生成锯齿。为了解决这种问题,我们使用多张阴影贴图,离相机近的地方使用精细的阴影贴图,离相机远的地方使用粗糙的阴影贴图,这样不仅优化了阴影效果,还保证了渲染效率因此,级联阴影的关键就是生成和使用不同精细度的阴影贴图
|
|||
|
|
|||
|
阴影贴图的原理:在灯光方向架一台摄像机,获取深度图,然后再正常渲染自己的场景,再在正常渲染场景的时候把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 级联阴影贴图
|
|||
|
现在终于得到阴影,但它们看起来很糟糕。不应被阴影化的表面最终会被形成像素化带的阴影伪影所覆盖。这些是由于阴影贴图的有限分辨率导致的自我阴影化。使用不同的分辨率会更改伪影模式,但不会消除它们。
|
|||
|

|
|||
|
|
|||
|
#### 球形剔除
|
|||
|
Unity通过为其创建一个选择球来确定每个级联覆盖的区域。由于阴影投影是正交的且呈正方形,因此它们最终会紧密契合其剔除球,但还会覆盖周围的一些空间。这就是为什么可以在剔除区域之外看到一些阴影的原因。同样,光的方向与球无关,因此所有定向光最终都使用相同的剔除球。
|
|||
|
|
|||
|
剔除球是ComputeDirectionalShadowMatricesAndCullingPrimitives函数中计算出的`ShadowSplitData`分离出的数据。传递`_CascadeCount`级联数以及`_CascadeCullingSpheres`以及剔除球的位置数据。
|
|||
|
|
|||
|
创建`ShadowData`结构体以传递级联级别以及阴影硬度。将会在`GetLighting`中通过像素的世界坐标是否在球体内来计算`ShadowData`结构体中的级联级别。
|
|||
|
|
|||
|
#### 最大距离
|
|||
|
此时阴影会在超过最后一个剔除球后消失,为了解决这个问题,会设置一个最大距离,超过最大距离阴影才会消失。具体操作是在`GetShadowData`中比较像素深度以及阴影最大距离值,如果超过则将`strength`设置为0。
|
|||
|
|
|||
|
#### 给阴影添加衰减与级联渐变
|
|||
|
阴影衰减见git,
|
|||
|

|
|||
|
|
|||
|
级联衰减公式:
|
|||
|

|
|||
|
其中f为下面式子的倒数:
|
|||
|

|
|||
|
在`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<sphere.w)
|
|||
|
{
|
|||
|
if(i== _CascadeCount-1)
|
|||
|
{
|
|||
|
data.strength*=FadedShadowStrength(distanceSqr,_CascadeData[i].x,_ShadowDistanceFade.z);
|
|||
|
}
|
|||
|
break;;
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
##### 过渡抖动
|
|||
|
提高级联过渡效果。在`LitPassFragment`中给像素计算抖动值
|
|||
|
```c#
|
|||
|
surface.dither=InterleavedGradientNoise(input.positionCS.xy,0);
|
|||
|
```
|
|||
|
当使用抖动混合时,如果我们不在上一个级联中,则当混合值小于抖动值时,跳到下一个级联。
|
|||
|
```c#
|
|||
|
#if defined(_CASCADE_BLEND_DITHER)
|
|||
|
else if(data.cascadeBlend < surfaceWS.dither)
|
|||
|
{
|
|||
|
i+=1;
|
|||
|
}
|
|||
|
#endif
|
|||
|
```
|
|||
|
##### 其他功能效果
|
|||
|
- 透明度
|
|||
|
- 阴影模式
|
|||
|
- 裁切阴影
|
|||
|
- 抖动阴影
|
|||
|
- 无阴影
|
|||
|
- 不受光阴影投射器
|
|||
|
- 接受阴影
|