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), new Color(0, 1, 0, 0.3f), new Color(0, 0, 1, 0.3f), new Color(1, 1, 0, 0.3f) }; 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; [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; urpAsset.shadowCascadeCount = 1; break; case QualityPreset.Medium: currentResolutionIndex = 1; urpAsset.shadowCascadeCount = 2; break; case QualityPreset.High: currentResolutionIndex = 2; urpAsset.shadowCascadeCount = 4; break; case QualityPreset.Ultra: currentResolutionIndex = 3; 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() { RaycastHit hit; Vector3 lightDir = -targetLight.transform.forward; if (Physics.Raycast(transform.position, Vector3.down, out hit, 100f)) { 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) { 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); } 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; 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); }); 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 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() { 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 }
|
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() { #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阴影系统的各个方面,从级联阴影贴图到移动平台优化。关键要点:
- 级联阴影贴图 - 合理配置级联数量和分割比例,平衡质量和性能
- 阴影距离和分辨率 - 根据场景规模和目标平台动态调整
- Shadow Bias调优 - 解决阴影痤疮和Peter Panning问题,不同场景使用不同预设
- 软阴影 - 理解PCF原理,根据性能需求选择采样数量
- 附加光源阴影 - 限制数量,使用距离剔除和优先级管理
- 移动平台优化 - 针对不同设备等级应用不同配置策略
性能优化建议:
- 移动端:级联数1-2,分辨率512-1024,禁用附加光源阴影
- PC中端:级联数2-4,分辨率1024-2048,限制附加光源阴影
- PC高端:级联数4,分辨率2048-4096,支持多个附加光源阴影
- 使用阴影LOD系统,根据距离动态调整质量
- 合并静态阴影投射器减少Draw Call
调试技巧:
- 使用级联可视化工具检查分割是否合理
- 运行Shadow Bias诊断工具找出问题区域
- 监控阴影相关性能指标(Shadow Pass时间)
下一章预告: 将深入讲解URP后处理系统,包括Volume框架、各种后处理效果和自定义后处理的实现方法。