第6章 URP阴影系统

Unity URP渲染管线教程 - 第6章

第6章 URP阴影系统

6.1 级联阴影贴图(Cascade Shadow Maps)

级联阴影贴图是URP中解决大范围场景阴影质量问题的核心技术。

6.1.1 CSM原理与配置

级联阴影基础概念:

级联阴影将视锥体分成多个级联区域,每个区域使用独立的阴影贴图,近处使用高分辨率,远处使用低分辨率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
using UnityEngine;
using UnityEngine.Rendering.Universal;

public class CascadeShadowManager : MonoBehaviour
{
[Header("Shadow Settings")]
public Light directionalLight;
public UniversalRenderPipelineAsset urpAsset;

[Header("Cascade Configuration")]
[Range(1, 4)] public int cascadeCount = 4;
public float[] cascadeRatios = { 0.067f, 0.2f, 0.467f, 1.0f };

[Header("Quality Settings")]
public ShadowResolution shadowResolution = ShadowResolution._2048;
public float shadowDistance = 150f;

void Start()
{
ConfigureCascades();
}

void ConfigureCascades()
{
if (urpAsset == null)
{
Debug.LogError("URP Asset not assigned!");
return;
}

// 配置阴影级联数量
urpAsset.shadowCascadeCount = cascadeCount;

// 设置阴影距离
urpAsset.shadowDistance = shadowDistance;

// 设置主光源阴影分辨率
urpAsset.mainLightShadowmapResolution = (int)shadowResolution;

// 配置级联分割比例
if (cascadeCount == 2)
{
urpAsset.cascade2Split = cascadeRatios[0];
}
else if (cascadeCount == 4)
{
urpAsset.cascade4Split = new Vector3(
cascadeRatios[0],
cascadeRatios[1],
cascadeRatios[2]
);
}

Debug.Log($"Configured {cascadeCount} shadow cascades");
Debug.Log($"Shadow distance: {shadowDistance}m");
Debug.Log($"Resolution: {shadowResolution}");
}

// 动态调整级联距离
public void SetCascadeDistances(float near, float mid, float far)
{
if (cascadeCount != 4) return;

float totalDistance = shadowDistance;

cascadeRatios[0] = near / totalDistance;
cascadeRatios[1] = mid / totalDistance;
cascadeRatios[2] = far / totalDistance;
cascadeRatios[3] = 1.0f;

urpAsset.cascade4Split = new Vector3(
cascadeRatios[0],
cascadeRatios[1],
cascadeRatios[2]
);

Debug.Log($"Updated cascade distances: {near}m, {mid}m, {far}m");
}

// 根据场景类型预设配置
public void ApplyPreset(CascadePreset preset)
{
switch (preset)
{
case CascadePreset.Indoor:
cascadeCount = 2;
shadowDistance = 50f;
SetCascadeDistances(10f, 0, 0);
break;

case CascadePreset.Outdoor:
cascadeCount = 4;
shadowDistance = 150f;
SetCascadeDistances(10f, 30f, 70f);
break;

case CascadePreset.OpenWorld:
cascadeCount = 4;
shadowDistance = 300f;
SetCascadeDistances(20f, 60f, 150f);
break;

case CascadePreset.Mobile:
cascadeCount = 1;
shadowDistance = 30f;
shadowResolution = ShadowResolution._1024;
break;
}

ConfigureCascades();
Debug.Log($"Applied {preset} preset");
}
}

public enum CascadePreset
{
Indoor,
Outdoor,
OpenWorld,
Mobile
}

6.1.2 级联可视化调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
using UnityEngine;
using UnityEngine.Rendering;

public class CascadeVisualizer : MonoBehaviour
{
public Camera mainCamera;
public bool showCascades = true;
public Color[] cascadeColors =
{
new Color(1, 0, 0, 0.3f), // 红色 - Cascade 0
new Color(0, 1, 0, 0.3f), // 绿色 - Cascade 1
new Color(0, 0, 1, 0.3f), // 蓝色 - Cascade 2
new Color(1, 1, 0, 0.3f) // 黄色 - Cascade 3
};

private Material cascadeDebugMaterial;

void Start()
{
CreateDebugMaterial();
}

void CreateDebugMaterial()
{
Shader shader = Shader.Find("Hidden/CascadeDebug");
if (shader != null)
{
cascadeDebugMaterial = new Material(shader);
}
}

void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (showCascades && cascadeDebugMaterial != null)
{
// 传递级联颜色
cascadeDebugMaterial.SetColorArray("_CascadeColors", cascadeColors);

Graphics.Blit(source, destination, cascadeDebugMaterial);
}
else
{
Graphics.Blit(source, destination);
}
}

void OnDrawGizmos()
{
if (!showCascades || mainCamera == null) return;

// 绘制级联分割线
DrawCascadeSplits();
}

void DrawCascadeSplits()
{
// 这里绘制级联分割的可视化表示
Vector3 camPos = mainCamera.transform.position;
Vector3 camForward = mainCamera.transform.forward;

UniversalRenderPipelineAsset urpAsset =
GraphicsSettings.renderPipelineAsset as UniversalRenderPipelineAsset;

if (urpAsset == null) return;

float shadowDistance = urpAsset.shadowDistance;
int cascadeCount = urpAsset.shadowCascadeCount;

for (int i = 0; i < cascadeCount; i++)
{
Gizmos.color = cascadeColors[i];

float cascadeEnd = GetCascadeEnd(urpAsset, i, shadowDistance);
Vector3 endPos = camPos + camForward * cascadeEnd;

// 绘制级联边界
Gizmos.DrawWireSphere(endPos, 2f);
Gizmos.DrawLine(camPos, endPos);
}
}

float GetCascadeEnd(UniversalRenderPipelineAsset asset, int cascadeIndex, float maxDistance)
{
if (asset.shadowCascadeCount == 1)
return maxDistance;
else if (asset.shadowCascadeCount == 2)
return cascadeIndex == 0 ? maxDistance * asset.cascade2Split : maxDistance;
else if (asset.shadowCascadeCount == 4)
{
Vector3 splits = asset.cascade4Split;
switch (cascadeIndex)
{
case 0: return maxDistance * splits.x;
case 1: return maxDistance * splits.y;
case 2: return maxDistance * splits.z;
case 3: return maxDistance;
}
}

return maxDistance;
}
}

对应的调试Shader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
Shader "Hidden/CascadeDebug"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}

SubShader
{
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"

struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};

struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 positionWS : TEXCOORD1;
};

TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

half4 _CascadeColors[4];

Varyings vert(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.positionWS = TransformObjectToWorld(input.positionOS.xyz);
output.uv = input.uv;
return output;
}

half4 frag(Varyings input) : SV_Target
{
half4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);

// 获取阴影级联索引
float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS);
int cascadeIndex = ComputeCascadeIndex(input.positionWS);

// 混合级联颜色
if (cascadeIndex >= 0 && cascadeIndex < 4)
{
color.rgb = lerp(color.rgb, _CascadeColors[cascadeIndex].rgb, 0.5);
}

return color;
}
ENDHLSL
}
}
}

6.2 阴影距离与分辨率配置

6.2.1 动态阴影距离调整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
using UnityEngine;
using UnityEngine.Rendering.Universal;

public class DynamicShadowDistance : MonoBehaviour
{
[Header("References")]
public UniversalRenderPipelineAsset urpAsset;
public Camera mainCamera;

[Header("Distance Settings")]
public float minShadowDistance = 30f;
public float maxShadowDistance = 150f;
public float transitionSpeed = 2f;

[Header("Performance Settings")]
public bool adaptToFramerate = true;
public float targetFramerate = 60f;
public float framerateThreshold = 5f;

private float currentShadowDistance;
private float targetShadowDistance;

void Start()
{
if (urpAsset != null)
{
currentShadowDistance = urpAsset.shadowDistance;
targetShadowDistance = currentShadowDistance;
}
}

void Update()
{
if (urpAsset == null) return;

// 根据性能自动调整
if (adaptToFramerate)
{
AdaptToPerformance();
}

// 平滑过渡到目标距离
if (Mathf.Abs(currentShadowDistance - targetShadowDistance) > 0.1f)
{
currentShadowDistance = Mathf.Lerp(
currentShadowDistance,
targetShadowDistance,
Time.deltaTime * transitionSpeed
);

urpAsset.shadowDistance = currentShadowDistance;
}
}

void AdaptToPerformance()
{
float currentFPS = 1.0f / Time.deltaTime;

if (currentFPS < targetFramerate - framerateThreshold)
{
// 降低阴影距离
targetShadowDistance = Mathf.Max(minShadowDistance, targetShadowDistance * 0.9f);
}
else if (currentFPS > targetFramerate + framerateThreshold)
{
// 增加阴影距离
targetShadowDistance = Mathf.Min(maxShadowDistance, targetShadowDistance * 1.1f);
}
}

// 基于相机移动速度调整
public void AdjustBasedOnCameraSpeed()
{
if (mainCamera == null) return;

float cameraSpeed = mainCamera.velocity.magnitude;

// 移动快时减少阴影距离,移动慢时增加
if (cameraSpeed > 10f)
{
targetShadowDistance = minShadowDistance;
}
else if (cameraSpeed < 1f)
{
targetShadowDistance = maxShadowDistance;
}
else
{
float t = Mathf.InverseLerp(10f, 1f, cameraSpeed);
targetShadowDistance = Mathf.Lerp(minShadowDistance, maxShadowDistance, t);
}
}

// 手动设置阴影距离
public void SetShadowDistance(float distance)
{
targetShadowDistance = Mathf.Clamp(distance, minShadowDistance, maxShadowDistance);
}
}

6.2.2 分辨率动态缩放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
using UnityEngine;
using UnityEngine.Rendering.Universal;

public class DynamicShadowResolution : MonoBehaviour
{
public UniversalRenderPipelineAsset urpAsset;

[Header("Resolution Settings")]
public int[] availableResolutions = { 512, 1024, 2048, 4096 };
public int currentResolutionIndex = 2; // Default: 2048

[Header("Auto Adjustment")]
public bool autoAdjust = true;
public float performanceCheckInterval = 1.0f;

private float lastCheckTime;
private int targetResolutionIndex;

void Start()
{
if (urpAsset != null)
{
ApplyResolution(currentResolutionIndex);
}

targetResolutionIndex = currentResolutionIndex;
}

void Update()
{
if (!autoAdjust || urpAsset == null) return;

if (Time.time - lastCheckTime >= performanceCheckInterval)
{
lastCheckTime = Time.time;
CheckPerformance();
}

// 平滑切换分辨率
if (currentResolutionIndex != targetResolutionIndex)
{
currentResolutionIndex = targetResolutionIndex;
ApplyResolution(currentResolutionIndex);
}
}

void CheckPerformance()
{
float fps = 1.0f / Time.deltaTime;

if (fps < 30f && currentResolutionIndex > 0)
{
// 降低分辨率
targetResolutionIndex = currentResolutionIndex - 1;
Debug.Log($"FPS低,降低阴影分辨率到: {availableResolutions[targetResolutionIndex]}");
}
else if (fps > 55f && currentResolutionIndex < availableResolutions.Length - 1)
{
// 提高分辨率
targetResolutionIndex = currentResolutionIndex + 1;
Debug.Log($"FPS高,提高阴影分辨率到: {availableResolutions[targetResolutionIndex]}");
}
}

void ApplyResolution(int index)
{
if (index < 0 || index >= availableResolutions.Length) return;

int resolution = availableResolutions[index];

// 设置主光源阴影分辨率
urpAsset.mainLightShadowmapResolution = resolution;

// 设置附加光源阴影分辨率
urpAsset.additionalLightsShadowmapResolution = Mathf.Max(512, resolution / 2);

Debug.Log($"Shadow resolution set to: {resolution}");
}

// 手动设置分辨率
public void SetResolution(ShadowResolution resolution)
{
urpAsset.mainLightShadowmapResolution = (int)resolution;
}

// 预设配置
public void ApplyQualityPreset(QualityPreset preset)
{
switch (preset)
{
case QualityPreset.Low:
currentResolutionIndex = 0; // 512
urpAsset.shadowCascadeCount = 1;
break;

case QualityPreset.Medium:
currentResolutionIndex = 1; // 1024
urpAsset.shadowCascadeCount = 2;
break;

case QualityPreset.High:
currentResolutionIndex = 2; // 2048
urpAsset.shadowCascadeCount = 4;
break;

case QualityPreset.Ultra:
currentResolutionIndex = 3; // 4096
urpAsset.shadowCascadeCount = 4;
break;
}

ApplyResolution(currentResolutionIndex);
}
}

public enum QualityPreset
{
Low,
Medium,
High,
Ultra
}

6.3 Shadow Bias参数调优

Shadow Bias用于解决阴影痤疮(shadow acne)和Peter Panning问题。

6.3.1 Bias参数详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
using UnityEngine;

public class ShadowBiasController : MonoBehaviour
{
[Header("Light Reference")]
public Light targetLight;

[Header("Bias Settings")]
[Range(0, 2)] public float depthBias = 0.05f;
[Range(0, 3)] public float normalBias = 0.4f;
[Range(0.001f, 10f)] public float nearPlane = 0.2f;

[Header("Auto Adjustment")]
public bool autoAdjustBias = false;
public float targetSurfaceOffset = 0.01f;

void Start()
{
if (targetLight == null)
{
targetLight = GetComponent<Light>();
}

ApplyBiasSettings();
}

void Update()
{
if (autoAdjustBias)
{
AutoAdjustBias();
}
}

void ApplyBiasSettings()
{
if (targetLight == null) return;

targetLight.shadowBias = depthBias;
targetLight.shadowNormalBias = normalBias;
targetLight.shadowNearPlane = nearPlane;

Debug.Log($"Applied Shadow Bias - Depth: {depthBias}, Normal: {normalBias}");
}

void AutoAdjustBias()
{
// 基于场景几何自动调整Bias
// 这是一个简化的示例

RaycastHit hit;
Vector3 lightDir = -targetLight.transform.forward;

if (Physics.Raycast(transform.position, Vector3.down, out hit, 100f))
{
// 根据表面角度调整Normal Bias
float surfaceAngle = Vector3.Angle(hit.normal, -lightDir);
normalBias = Mathf.Lerp(0.1f, 2.0f, surfaceAngle / 90f);

ApplyBiasSettings();
}
}

// 预设配置
public void ApplyPreset(BiasPreset preset)
{
switch (preset)
{
case BiasPreset.Default:
depthBias = 0.05f;
normalBias = 0.4f;
nearPlane = 0.2f;
break;

case BiasPreset.FlatSurfaces:
depthBias = 0.01f;
normalBias = 0.1f;
nearPlane = 0.2f;
break;

case BiasPreset.Vegetation:
depthBias = 0.1f;
normalBias = 1.0f;
nearPlane = 0.5f;
break;

case BiasPreset.Terrain:
depthBias = 0.2f;
normalBias = 0.8f;
nearPlane = 1.0f;
break;

case BiasPreset.Characters:
depthBias = 0.03f;
normalBias = 0.3f;
nearPlane = 0.1f;
break;
}

ApplyBiasSettings();
Debug.Log($"Applied {preset} bias preset");
}

// 调试可视化
void OnValidate()
{
ApplyBiasSettings();
}
}

public enum BiasPreset
{
Default,
FlatSurfaces,
Vegetation,
Terrain,
Characters
}

6.3.2 Bias问题诊断工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
using UnityEngine;
using System.Collections.Generic;

public class ShadowBiasDiagnostic : MonoBehaviour
{
public Light mainLight;
public LayerMask shadowCasterLayer = -1;

[Header("Diagnostic Settings")]
public int sampleCount = 100;
public float maxTestDistance = 50f;

[Header("Results")]
public int acneCount = 0;
public int peterPanningCount = 0;
public List<Vector3> problemAreas = new List<Vector3>();

public void RunDiagnostic()
{
acneCount = 0;
peterPanningCount = 0;
problemAreas.Clear();

Camera cam = Camera.main;
if (cam == null) return;

// 在视野内随机采样点
for (int i = 0; i < sampleCount; i++)
{
Vector3 screenPoint = new Vector3(
Random.Range(0f, Screen.width),
Random.Range(0f, Screen.height),
Random.Range(cam.nearClipPlane, maxTestDistance)
);

Ray ray = cam.ScreenPointToRay(screenPoint);

if (Physics.Raycast(ray, out RaycastHit hit, maxTestDistance, shadowCasterLayer))
{
CheckForShadowIssues(hit.point, hit.normal);
}
}

Debug.Log($"=== Shadow Bias Diagnostic ===");
Debug.Log($"Samples: {sampleCount}");
Debug.Log($"Shadow Acne Issues: {acneCount}");
Debug.Log($"Peter Panning Issues: {peterPanningCount}");
Debug.Log($"Problem Areas: {problemAreas.Count}");

if (acneCount > sampleCount * 0.1f)
{
Debug.LogWarning("High shadow acne detected! Increase Depth Bias or Normal Bias");
}

if (peterPanningCount > sampleCount * 0.1f)
{
Debug.LogWarning("Peter Panning detected! Decrease Depth Bias or Normal Bias");
}
}

void CheckForShadowIssues(Vector3 point, Vector3 normal)
{
// 检测Shadow Acne
// 在表面附近多次采样,检查阴影闪烁
float shadowVariance = 0f;
int samples = 5;

for (int i = 0; i < samples; i++)
{
Vector3 offset = normal * (i * 0.001f);
// 这里应该采样阴影贴图,简化示例
float shadow = SampleShadowAtPoint(point + offset);
shadowVariance += Mathf.Abs(shadow - 0.5f);
}

if (shadowVariance > 1.0f)
{
acneCount++;
problemAreas.Add(point);
}

// 检测Peter Panning
// 检查阴影是否与物体分离
Vector3 lightDir = -mainLight.transform.forward;
Ray shadowRay = new Ray(point + normal * 0.01f, -lightDir);

if (!Physics.Raycast(shadowRay, 0.5f, shadowCasterLayer))
{
peterPanningCount++;
problemAreas.Add(point);
}
}

float SampleShadowAtPoint(Vector3 point)
{
// 简化的阴影采样
// 实际应该从阴影贴图采样
return Random.value;
}

void OnDrawGizmos()
{
// 可视化问题区域
Gizmos.color = Color.red;
foreach (Vector3 pos in problemAreas)
{
Gizmos.DrawWireSphere(pos, 0.1f);
}
}
}

6.4 软阴影实现原理

6.4.1 软阴影配置与优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
using UnityEngine;
using UnityEngine.Rendering.Universal;

public class SoftShadowController : MonoBehaviour
{
public Light targetLight;
public UniversalRenderPipelineAsset urpAsset;

[Header("Soft Shadow Settings")]
public bool useSoftShadows = true;
public SoftShadowQuality quality = SoftShadowQuality.Medium;

[Header("PCF Settings")]
[Range(1, 9)] public int pcfSampleCount = 5;
[Range(0.1f, 5f)] public float pcfRadius = 1.0f;

void Start()
{
ConfigureSoftShadows();
}

void ConfigureSoftShadows()
{
if (targetLight == null || urpAsset == null) return;

// 设置阴影类型
targetLight.shadows = useSoftShadows ? LightShadows.Soft : LightShadows.Hard;

// 在URP Asset中启用软阴影
urpAsset.supportsSoftShadows = useSoftShadows;

// 应用质量设置
ApplyQualitySetting(quality);

Debug.Log($"Soft Shadows: {(useSoftShadows ? "Enabled" : "Disabled")}");
Debug.Log($"Quality: {quality}");
}

void ApplyQualitySetting(SoftShadowQuality shadowQuality)
{
switch (shadowQuality)
{
case SoftShadowQuality.Low:
pcfSampleCount = 3;
pcfRadius = 0.5f;
break;

case SoftShadowQuality.Medium:
pcfSampleCount = 5;
pcfRadius = 1.0f;
break;

case SoftShadowQuality.High:
pcfSampleCount = 7;
pcfRadius = 1.5f;
break;

case SoftShadowQuality.Ultra:
pcfSampleCount = 9;
pcfRadius = 2.0f;
break;
}
}

// 切换软硬阴影
public void ToggleSoftShadows()
{
useSoftShadows = !useSoftShadows;
ConfigureSoftShadows();
}

// 设置质量
public void SetQuality(SoftShadowQuality newQuality)
{
quality = newQuality;
ConfigureSoftShadows();
}
}

public enum SoftShadowQuality
{
Low,
Medium,
High,
Ultra
}

6.4.2 自定义软阴影Shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
Shader "Custom/SoftShadowReceiver"
{
Properties
{
_BaseMap ("Base Texture", 2D) = "white" {}
_BaseColor ("Color", Color) = (1,1,1,1)
_ShadowSoftness ("Shadow Softness", Range(0, 5)) = 1.0
}

SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
}

Pass
{
Name "ForwardLit"
Tags { "LightMode" = "UniversalForward" }

HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag

#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _SHADOWS_SOFT

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);

CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
half _ShadowSoftness;
CBUFFER_END

struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
};

struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 positionWS : TEXCOORD1;
float3 normalWS : TEXCOORD2;
};

Varyings vert(Attributes input)
{
Varyings output;

VertexPositionInputs positionInputs = GetVertexPositionInputs(input.positionOS.xyz);
VertexNormalInputs normalInputs = GetVertexNormalInputs(input.normalOS);

output.positionCS = positionInputs.positionCS;
output.positionWS = positionInputs.positionWS;
output.normalWS = normalInputs.normalWS;
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);

return output;
}

// 自定义PCF软阴影采样
half CustomSoftShadow(float3 positionWS, float softness)
{
float4 shadowCoord = TransformWorldToShadowCoord(positionWS);

#ifdef _SHADOWS_SOFT
// PCF采样
half shadow = 0.0;
float2 texelSize = 1.0 / _MainLightShadowmapSize.xy * softness;

// 5x5 PCF核心
for (int x = -2; x <= 2; x++)
{
for (int y = -2; y <= 2; y++)
{
float2 offset = float2(x, y) * texelSize;
float4 sampleCoord = shadowCoord;
sampleCoord.xy += offset;
shadow += MainLightRealtimeShadow(sampleCoord);
}
}
shadow /= 25.0; // 平均值
#else
// 硬阴影
half shadow = MainLightRealtimeShadow(shadowCoord);
#endif

return shadow;
}

half4 frag(Varyings input) : SV_Target
{
// 采样基础纹理
half4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor;

// 获取主光源
Light mainLight = GetMainLight();

// 计算自定义软阴影
half shadow = CustomSoftShadow(input.positionWS, _ShadowSoftness);

// 应用光照
half3 normalWS = normalize(input.normalWS);
half NdotL = saturate(dot(normalWS, mainLight.direction));

half3 lighting = mainLight.color * NdotL * shadow;
half3 finalColor = baseColor.rgb * lighting;

return half4(finalColor, baseColor.a);
}
ENDHLSL
}

UsePass "Universal Render Pipeline/Lit/ShadowCaster"
}
}

6.5 附加光源阴影处理

6.5.1 附加光源阴影配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
using UnityEngine;
using UnityEngine.Rendering.Universal;

public class AdditionalLightsShadowManager : MonoBehaviour
{
public UniversalRenderPipelineAsset urpAsset;

[Header("Additional Lights Shadow Settings")]
public bool enableAdditionalShadows = true;
public int maxShadowedLights = 4;
public ShadowResolution additionalShadowResolution = ShadowResolution._512;

[Header("Per-Light Settings")]
public Light[] additionalLights;

void Start()
{
ConfigureAdditionalShadows();
}

void ConfigureAdditionalShadows()
{
if (urpAsset == null)
{
Debug.LogError("URP Asset not assigned!");
return;
}

// 启用附加光源阴影
urpAsset.supportsAdditionalLightShadows = enableAdditionalShadows;

// 设置最大阴影数量
urpAsset.maxAdditionalLightsCount = Mathf.Max(maxShadowedLights, 4);

// 设置附加光源阴影分辨率
urpAsset.additionalLightsShadowmapResolution = (int)additionalShadowResolution;

// 配置每个附加光源
ConfigureIndividualLights();

Debug.Log($"Additional Lights Shadows: {(enableAdditionalShadows ? "Enabled" : "Disabled")}");
Debug.Log($"Max Shadowed Lights: {maxShadowedLights}");
Debug.Log($"Shadow Resolution: {additionalShadowResolution}");
}

void ConfigureIndividualLights()
{
if (additionalLights == null || additionalLights.Length == 0) return;

for (int i = 0; i < additionalLights.Length; i++)
{
Light light = additionalLights[i];
if (light == null) continue;

// 配置阴影设置
if (i < maxShadowedLights)
{
light.shadows = LightShadows.Soft;
light.shadowStrength = 1.0f;

// 根据光源类型调整参数
if (light.type == LightType.Spot)
{
light.shadowBias = 0.05f;
light.shadowNormalBias = 0.4f;
}
else if (light.type == LightType.Point)
{
light.shadowBias = 0.1f;
light.shadowNormalBias = 0.8f;
}
}
else
{
// 超出限制的光源不投射阴影
light.shadows = LightShadows.None;
}
}
}

// 动态启用/禁用阴影
public void ToggleLightShadow(int lightIndex, bool enabled)
{
if (lightIndex < 0 || lightIndex >= additionalLights.Length) return;

Light light = additionalLights[lightIndex];
if (light != null)
{
light.shadows = enabled ? LightShadows.Soft : LightShadows.None;
}
}

// 根据距离自动管理阴影
public void UpdateShadowsBasedOnDistance(Camera camera)
{
if (camera == null) return;

Vector3 cameraPos = camera.transform.position;

// 按距离排序光源
System.Array.Sort(additionalLights, (a, b) =>
{
if (a == null) return 1;
if (b == null) return -1;

float distA = Vector3.Distance(cameraPos, a.transform.position);
float distB = Vector3.Distance(cameraPos, b.transform.position);
return distA.CompareTo(distB);
});

// 只为最近的N个光源启用阴影
for (int i = 0; i < additionalLights.Length; i++)
{
if (additionalLights[i] == null) continue;

bool shouldCastShadow = i < maxShadowedLights;
additionalLights[i].shadows = shouldCastShadow ? LightShadows.Soft : LightShadows.None;
}
}
}

6.5.2 点光源阴影优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
using UnityEngine;

public class PointLightShadowOptimizer : MonoBehaviour
{
[Header("Point Light Settings")]
public Light pointLight;

[Header("Shadow Optimization")]
public bool useViewportCulling = true;
public float cullDistance = 30f;

[Header("Dynamic Quality")]
public bool dynamicQuality = true;
public float nearQualityDistance = 5f;
public float farQualityDistance = 20f;

private Camera mainCamera;

void Start()
{
mainCamera = Camera.main;

if (pointLight == null)
{
pointLight = GetComponent<Light>();
}

if (pointLight.type != LightType.Point)
{
Debug.LogWarning("This script is designed for Point Lights!");
}
}

void Update()
{
if (mainCamera == null || pointLight == null) return;

float distance = Vector3.Distance(mainCamera.transform.position, transform.position);

// 视口剔除
if (useViewportCulling)
{
bool inView = IsLightInView();
pointLight.shadows = (inView && distance < cullDistance) ? LightShadows.Soft : LightShadows.None;
}

// 动态质量调整
if (dynamicQuality && pointLight.shadows != LightShadows.None)
{
AdjustShadowQuality(distance);
}
}

bool IsLightInView()
{
Vector3 viewportPoint = mainCamera.WorldToViewportPoint(transform.position);

// 考虑光源范围
float rangeInViewport = pointLight.range / Vector3.Distance(mainCamera.transform.position, transform.position);

return viewportPoint.x >= -rangeInViewport && viewportPoint.x <= 1 + rangeInViewport &&
viewportPoint.y >= -rangeInViewport && viewportPoint.y <= 1 + rangeInViewport &&
viewportPoint.z > 0;
}

void AdjustShadowQuality(float distance)
{
if (distance < nearQualityDistance)
{
// 高质量阴影
pointLight.shadowStrength = 1.0f;
pointLight.shadowBias = 0.05f;
pointLight.shadowNormalBias = 0.4f;
}
else if (distance < farQualityDistance)
{
// 中等质量
float t = (distance - nearQualityDistance) / (farQualityDistance - nearQualityDistance);
pointLight.shadowStrength = Mathf.Lerp(1.0f, 0.6f, t);
pointLight.shadowBias = Mathf.Lerp(0.05f, 0.15f, t);
}
else
{
// 低质量或禁用
pointLight.shadowStrength = 0.5f;
pointLight.shadows = LightShadows.None;
}
}
}

6.6 移动平台阴影优化

6.6.1 移动端阴影配置策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
using UnityEngine;
using UnityEngine.Rendering.Universal;

public class MobileShadowOptimizer : MonoBehaviour
{
public UniversalRenderPipelineAsset urpAsset;

[Header("Mobile Shadow Settings")]
public MobileOptimizationLevel optimizationLevel = MobileOptimizationLevel.Balanced;

[Header("Device Detection")]
public bool autoDetectDevice = true;

void Start()
{
if (autoDetectDevice)
{
DetectAndApplySettings();
}
else
{
ApplyMobileSettings(optimizationLevel);
}
}

void DetectAndApplySettings()
{
#if UNITY_ANDROID || UNITY_IOS
// 移动平台
int memorySize = SystemInfo.systemMemorySize;
int gpuMemory = SystemInfo.graphicsMemorySize;

if (memorySize > 6000 && gpuMemory > 2000)
{
// 高端移动设备
optimizationLevel = MobileOptimizationLevel.Balanced;
}
else if (memorySize > 3000)
{
// 中端设备
optimizationLevel = MobileOptimizationLevel.Performance;
}
else
{
// 低端设备
optimizationLevel = MobileOptimizationLevel.MaxPerformance;
}
#else
// PC平台
optimizationLevel = MobileOptimizationLevel.Quality;
#endif

ApplyMobileSettings(optimizationLevel);

Debug.Log($"Auto-detected device tier: {optimizationLevel}");
Debug.Log($"Memory: {SystemInfo.systemMemorySize}MB, GPU Memory: {SystemInfo.graphicsMemorySize}MB");
}

public void ApplyMobileSettings(MobileOptimizationLevel level)
{
if (urpAsset == null)
{
Debug.LogError("URP Asset not assigned!");
return;
}

switch (level)
{
case MobileOptimizationLevel.MaxPerformance:
ConfigureMaxPerformance();
break;

case MobileOptimizationLevel.Performance:
ConfigurePerformance();
break;

case MobileOptimizationLevel.Balanced:
ConfigureBalanced();
break;

case MobileOptimizationLevel.Quality:
ConfigureQuality();
break;
}

Debug.Log($"Applied mobile shadow settings: {level}");
}

void ConfigureMaxPerformance()
{
// 极限性能优化 - 适用于低端设备
urpAsset.shadowDistance = 15f;
urpAsset.shadowCascadeCount = 1;
urpAsset.mainLightShadowmapResolution = 512;
urpAsset.supportsSoftShadows = false;
urpAsset.supportsAdditionalLightShadows = false;

// 禁用所有附加光源阴影
Light[] lights = FindObjectsOfType<Light>();
foreach (Light light in lights)
{
if (light.type != LightType.Directional)
{
light.shadows = LightShadows.None;
}
else
{
light.shadows = LightShadows.Hard;
}
}
}

void ConfigurePerformance()
{
// 性能优先 - 适用于中低端设备
urpAsset.shadowDistance = 30f;
urpAsset.shadowCascadeCount = 1;
urpAsset.mainLightShadowmapResolution = 1024;
urpAsset.supportsSoftShadows = false;
urpAsset.supportsAdditionalLightShadows = false;

SetMainLightShadows(LightShadows.Hard);
}

void ConfigureBalanced()
{
// 平衡模式 - 适用于中端设备
urpAsset.shadowDistance = 50f;
urpAsset.shadowCascadeCount = 2;
urpAsset.cascade2Split = 0.25f;
urpAsset.mainLightShadowmapResolution = 1024;
urpAsset.supportsSoftShadows = true;
urpAsset.supportsAdditionalLightShadows = false;

SetMainLightShadows(LightShadows.Soft);
}

void ConfigureQuality()
{
// 质量优先 - 适用于高端设备或PC
urpAsset.shadowDistance = 80f;
urpAsset.shadowCascadeCount = 4;
urpAsset.cascade4Split = new Vector3(0.067f, 0.2f, 0.467f);
urpAsset.mainLightShadowmapResolution = 2048;
urpAsset.supportsSoftShadows = true;
urpAsset.supportsAdditionalLightShadows = true;
urpAsset.additionalLightsShadowmapResolution = 512;

SetMainLightShadows(LightShadows.Soft);
}

void SetMainLightShadows(LightShadows shadowType)
{
Light[] lights = FindObjectsOfType<Light>();
foreach (Light light in lights)
{
if (light.type == LightType.Directional)
{
light.shadows = shadowType;
}
}
}

// 运行时性能监控和自动调整
public void MonitorAndAdjust()
{
float fps = 1.0f / Time.deltaTime;

if (fps < 25f && optimizationLevel > MobileOptimizationLevel.MaxPerformance)
{
// 降低质量
optimizationLevel--;
ApplyMobileSettings(optimizationLevel);
Debug.Log($"FPS低,降低阴影质量到: {optimizationLevel}");
}
else if (fps > 55f && optimizationLevel < MobileOptimizationLevel.Quality)
{
// 提高质量
optimizationLevel++;
ApplyMobileSettings(optimizationLevel);
Debug.Log($"FPS高,提高阴影质量到: {optimizationLevel}");
}
}
}

public enum MobileOptimizationLevel
{
MaxPerformance, // 最高性能(低端设备)
Performance, // 性能优先(中低端)
Balanced, // 平衡(中端)
Quality // 质量优先(高端/PC)
}

6.6.2 阴影渲染批处理优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
using UnityEngine;
using System.Collections.Generic;

public class ShadowBatchingOptimizer : MonoBehaviour
{
[Header("Batching Settings")]
public bool enableStaticBatching = true;
public bool enableDynamicBatching = true;

[Header("Shadow Caster Optimization")]
public bool combineShadowCasters = true;
public int maxCombinedVertices = 65000;

void Start()
{
if (enableStaticBatching)
{
OptimizeStaticShadowCasters();
}

ConfigureBatching();
}

void ConfigureBatching()
{
// 设置批处理选项
// 注意:这些在URP中主要通过SRP Batcher实现

#if UNITY_EDITOR
UnityEditor.PlayerSettings.gpuSkinning = true;
#endif

Debug.Log($"Shadow batching configured");
Debug.Log($"Static Batching: {enableStaticBatching}");
Debug.Log($"Dynamic Batching: {enableDynamicBatching}");
}

void OptimizeStaticShadowCasters()
{
if (!combineShadowCasters) return;

// 查找所有静态阴影投射器
MeshRenderer[] renderers = FindObjectsOfType<MeshRenderer>();
Dictionary<Material, List<GameObject>> materialGroups = new Dictionary<Material, List<GameObject>>();

foreach (MeshRenderer renderer in renderers)
{
if (!renderer.gameObject.isStatic) continue;
if (renderer.shadowCastingMode == UnityEngine.Rendering.ShadowCastingMode.Off) continue;

Material mat = renderer.sharedMaterial;
if (mat == null) continue;

if (!materialGroups.ContainsKey(mat))
{
materialGroups[mat] = new List<GameObject>();
}

materialGroups[mat].Add(renderer.gameObject);
}

// 合并每组对象
foreach (var group in materialGroups)
{
if (group.Value.Count > 1)
{
CombineMeshes(group.Value, group.Key);
}
}
}

void CombineMeshes(List<GameObject> objects, Material material)
{
List<CombineInstance> combines = new List<CombineInstance>();
int vertexCount = 0;

foreach (GameObject obj in objects)
{
MeshFilter meshFilter = obj.GetComponent<MeshFilter>();
if (meshFilter == null || meshFilter.sharedMesh == null) continue;

// 检查顶点数限制
if (vertexCount + meshFilter.sharedMesh.vertexCount > maxCombinedVertices)
{
continue;
}

CombineInstance ci = new CombineInstance();
ci.mesh = meshFilter.sharedMesh;
ci.transform = meshFilter.transform.localToWorldMatrix;
combines.Add(ci);

vertexCount += meshFilter.sharedMesh.vertexCount;
}

if (combines.Count < 2) return;

// 创建合并对象
GameObject combined = new GameObject($"Combined_ShadowCasters_{material.name}");
combined.isStatic = true;

MeshFilter combinedMF = combined.AddComponent<MeshFilter>();
MeshRenderer combinedMR = combined.AddComponent<MeshRenderer>();

Mesh combinedMesh = new Mesh();
combinedMesh.CombineMeshes(combines.ToArray(), true, true);
combinedMesh.name = $"Combined_Shadow_Mesh_{material.name}";

combinedMF.sharedMesh = combinedMesh;
combinedMR.sharedMaterial = material;
combinedMR.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly;

// 禁用原始对象的阴影投射
foreach (GameObject obj in objects)
{
MeshRenderer renderer = obj.GetComponent<MeshRenderer>();
if (renderer != null)
{
renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
}
}

Debug.Log($"Combined {combines.Count} shadow casters into one mesh ({vertexCount} vertices)");
}
}

6.7 总结

本章详细讲解了URP阴影系统的各个方面,从级联阴影贴图到移动平台优化。关键要点:

  1. 级联阴影贴图 - 合理配置级联数量和分割比例,平衡质量和性能
  2. 阴影距离和分辨率 - 根据场景规模和目标平台动态调整
  3. Shadow Bias调优 - 解决阴影痤疮和Peter Panning问题,不同场景使用不同预设
  4. 软阴影 - 理解PCF原理,根据性能需求选择采样数量
  5. 附加光源阴影 - 限制数量,使用距离剔除和优先级管理
  6. 移动平台优化 - 针对不同设备等级应用不同配置策略

性能优化建议:

  • 移动端:级联数1-2,分辨率512-1024,禁用附加光源阴影
  • PC中端:级联数2-4,分辨率1024-2048,限制附加光源阴影
  • PC高端:级联数4,分辨率2048-4096,支持多个附加光源阴影
  • 使用阴影LOD系统,根据距离动态调整质量
  • 合并静态阴影投射器减少Draw Call

调试技巧:

  • 使用级联可视化工具检查分割是否合理
  • 运行Shadow Bias诊断工具找出问题区域
  • 监控阴影相关性能指标(Shadow Pass时间)

下一章预告: 将深入讲解URP后处理系统,包括Volume框架、各种后处理效果和自定义后处理的实现方法。