├── LICENSE ├── License.txt ├── ReadMe.MD ├── TemporalVarianceShadowOptimizerURP.cs ├── VarianceComputeURP_Atlas.compute └── sunny.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 LexCapp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 David Cappleman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /ReadMe.MD: -------------------------------------------------------------------------------- 1 | TVSO (Temporal Variance Shadow Optimizer for URP) 2 | 3 | Dynamic, intelligent shadow cascade optimization for Unity URP. 4 | 5 | Features 6 | 7 | GPU-powered shadow variance detection 8 | 9 | Dynamic adjustment of URP cascade splits 10 | 11 | Smarter cascade management based on scene activity 12 | 13 | Lightweight compute shader + AsyncGPUReadback 14 | 15 | Plug-and-Play: Just add it to your main camera 16 | 17 | Debugging overlays and variance logging 18 | 19 | Why Use TVSO? 20 | 21 | In URP, shadow cascades are static: one-size-fits-all, even when your scene varies wildly. 22 | 23 | TVSO fixes that. 24 | 25 | TVSO detects dynamic areas and tightens cascades for sharper shadows 26 | 27 | Detects static areas and relaxes cascades for better performance 28 | 29 | No manual tweaking. No heavy scripting overhead. 30 | 31 | Result: 32 | 33 | Smoother framerate 34 | 35 | Lower GPU load 36 | 37 | Cleaner visuals when it matters most 38 | 39 | Initial Real-World Test Results 40 | 41 | Test 42 | 43 | FPS in heavy city scenes 44 | ---------------------------------------------------------------------------- 45 | 46 | Before TVSO 47 | 29–37 FPS 48 | 49 | Frame times 50 | 24–29ms 51 | 52 | Stability 53 | Wide swings 54 | 55 | Garbage Collection impact 56 | Noticeable spikes 57 | 58 | ---------------------------------------------------------------------------- 59 | 60 | After TVSO 61 | 30–45 FPS 62 | 63 | Frame times 64 | 21–27ms 65 | 66 | Stability 67 | Smoother recovery 68 | 69 | Garbage Collection impact 70 | Softer recovery 71 | 72 | TVSO uses an intelligent threshold (default: 10%) to trigger re-optimization only when needed. 73 | 74 | ---------------------------------------------------------------------------- 75 | 76 | Setup 77 | 78 | Import the TemporalVarianceShadowOptimizerURP.cs script. 79 | 80 | Assign the VarianceComputeURP_Atlas.compute shader. 81 | 82 | Add the TemporalVarianceShadowOptimizerURP component to your Main Camera. 83 | 84 | Configure sample count (default: 16) and frame dispatch settings (default: every 3 frames). 85 | 86 | (Optional) Enable Debug Mode to see live variance logs. 87 | 88 | Roadmap 89 | 90 | Threshold auto-tuning and learning mode 91 | 92 | Shadow bias and normal bias optimization 93 | 94 | Realtime debug heatmap overlay 95 | 96 | HDRP and Built-In Render Pipeline support (future) 97 | 98 | 'Aggressive Mode' for hard-tuning cascade saves 99 | 100 | 101 | Credits 102 | 103 | Created by: 104 | 105 | David Alex Cappleman 106 | 107 | Original System Design and Implementation: David Alex Cappleman 108 | 109 | License 110 | 111 | This project is licensed under the MIT License. Feel free to use, modify, and contribute! 112 | 113 | MIT License 114 | Copyright (c) 2025 115 | 116 | TVSO isn't just about "better shadows." It's about smarter shadows. 117 | 118 | Smoother. Faster. Sharper. Adaptive. Tasty. Shadows 119 | 120 | Powered by variance. 121 | 122 | -------------------------------------------------------------------------------- /TemporalVarianceShadowOptimizerURP.cs: -------------------------------------------------------------------------------- 1 | // TemporalVarianceShadowOptimizerURP.cs 2 | using UnityEngine; 3 | using UnityEngine.Rendering; 4 | using UnityEngine.Rendering.Universal; 5 | using UnityEngine.Experimental.Rendering; // AsyncGPUReadback 6 | 7 | [RequireComponent(typeof(Camera))] 8 | public class TemporalVarianceShadowOptimizerURP : MonoBehaviour 9 | { 10 | [Header("Compute & Timing")] 11 | [Tooltip("ComputeShader with CS_VarianceURP kernel")] 12 | public ComputeShader varianceCompute; 13 | [Tooltip("Frames between dispatches")] 14 | public int framesPerDispatch = 3; 15 | [Tooltip("Random samples per cascade")] 16 | public int sampleCount = 16; 17 | 18 | [Header("Variance Thresholds")] 19 | [Range(0f,1f)] public float lowToMid = 0.05f; 20 | [Range(0f,1f)] public float midToHigh = 0.10f; 21 | 22 | [Header("Debug & Logging")] 23 | [Tooltip("Enable verbose logs")] 24 | public bool debugMode = true; 25 | 26 | // Internals 27 | RenderTexture varianceTex; 28 | AsyncGPUReadbackRequest pendingRequest; 29 | bool requestInFlight; 30 | bool atlasWarned; 31 | int kernelIndex; 32 | int frameCounter; 33 | uint[] cascadeState = new uint[4]; 34 | Vector4 lastVariances; 35 | Camera cam; 36 | 37 | void OnEnable() 38 | { 39 | // Only run under URP 40 | if (!(GraphicsSettings.currentRenderPipeline is UniversalRenderPipelineAsset)) 41 | { 42 | Debug.LogWarning("[TVSO] Not running under URP; disabling."); 43 | enabled = false; 44 | return; 45 | } 46 | 47 | cam = GetComponent(); 48 | 49 | // Find compute kernel 50 | if (varianceCompute == null || !varianceCompute.HasKernel("CS_VarianceURP")) 51 | { 52 | Debug.LogError("[TVSO] ComputeShader or CS_VarianceURP kernel missing; disabling."); 53 | enabled = false; 54 | return; 55 | } 56 | kernelIndex = varianceCompute.FindKernel("CS_VarianceURP"); 57 | 58 | // Create a 1×1 full-float RT for 4-component variance 59 | varianceTex = new RenderTexture(1, 1, 0, RenderTextureFormat.ARGBFloat) 60 | { 61 | enableRandomWrite = true 62 | }; 63 | varianceTex.Create(); 64 | 65 | // Hook after URP finishes each camera render 66 | RenderPipelineManager.endCameraRendering += OnEndCameraRendering; 67 | 68 | atlasWarned = false; 69 | if (debugMode) 70 | Debug.Log($"[TVSO] Initialized (kernel={kernelIndex}, samples={sampleCount})"); 71 | } 72 | 73 | void OnDisable() 74 | { 75 | RenderPipelineManager.endCameraRendering -= OnEndCameraRendering; 76 | varianceTex?.Release(); 77 | } 78 | 79 | void OnEndCameraRendering(ScriptableRenderContext ctx, Camera camera) 80 | { 81 | if (camera != cam) 82 | return; 83 | 84 | frameCounter++; 85 | 86 | // Throttle 87 | if (requestInFlight || (frameCounter % framesPerDispatch) != 0) 88 | return; 89 | 90 | // Grab URP's shadow atlas (2D RenderTexture) 91 | var atlas = Shader.GetGlobalTexture("_MainLightShadowmapTexture") as RenderTexture; 92 | if (atlas == null) 93 | { 94 | if (debugMode && !atlasWarned) 95 | { 96 | Debug.LogWarning("[TVSO] Shadow atlas not yet bound; waiting."); 97 | atlasWarned = true; 98 | } 99 | return; 100 | } 101 | 102 | // Dispatch compute 103 | varianceCompute.SetTexture(kernelIndex, "_MainLightShadowmapTexture", atlas); 104 | varianceCompute.SetTexture(kernelIndex, "_VarianceOut", varianceTex); 105 | varianceCompute.SetInt("_SampleCount", sampleCount); 106 | varianceCompute.Dispatch(kernelIndex, 1, 1, 1); 107 | 108 | // <-- Use the Texture overload with mip 0 109 | pendingRequest = AsyncGPUReadback.Request(varianceTex, 0, OnCompleteReadback); 110 | requestInFlight = true; 111 | 112 | if (debugMode) 113 | Debug.Log($"[TVSO] Dispatched variance compute @frame {Time.frameCount}"); 114 | } 115 | 116 | void OnCompleteReadback(AsyncGPUReadbackRequest req) 117 | { 118 | requestInFlight = false; 119 | 120 | if (req.hasError) 121 | { 122 | if (debugMode) Debug.LogWarning("[TVSO] GPU readback error."); 123 | return; 124 | } 125 | 126 | var data = req.GetData(); 127 | if (data.Length == 0) 128 | { 129 | if (debugMode) Debug.LogWarning("[TVSO] Readback returned no data; skipping."); 130 | return; 131 | } 132 | 133 | lastVariances = data[0]; 134 | if (debugMode) 135 | { 136 | Debug.LogFormat( 137 | "[TVSO] Variances → {0:F4}, {1:F4}, {2:F4}, {3:F4}", 138 | lastVariances.x, lastVariances.y, 139 | lastVariances.z, lastVariances.w 140 | ); 141 | } 142 | 143 | ApplyVariance(lastVariances); 144 | } 145 | 146 | void ApplyVariance(Vector4 var4) 147 | { 148 | float[] v = { var4.x, var4.y, var4.z, var4.w }; 149 | bool anyChange = false; 150 | 151 | for (int i = 0; i < 4; i++) 152 | { 153 | uint prev = cascadeState[i], next = prev; 154 | if (prev == 0 && v[i] > lowToMid) next = 1; 155 | else if (prev == 1 && v[i] > midToHigh) next = 2; 156 | else if (prev == 2 && v[i] < midToHigh * 0.8f) next = 1; 157 | else if (prev == 1 && v[i] < lowToMid * 0.8f) next = 0; 158 | 159 | if (next != prev) 160 | { 161 | cascadeState[i] = next; 162 | ToggleKeyword(i, next); 163 | anyChange = true; 164 | if (debugMode) 165 | Debug.Log($"[TVSO] Cascade[{i}] {prev}→{next}"); 166 | } 167 | } 168 | 169 | if (anyChange) 170 | { 171 | RecomputeSplits(); 172 | if (debugMode) 173 | { 174 | var s = QualitySettings.shadowCascade4Split; 175 | Debug.LogFormat("[TVSO] New splits → {0:F2}, {1:F2}, {2:F2}", s.x, s.y, s.z); 176 | } 177 | } 178 | } 179 | 180 | void ToggleKeyword(int cascade, uint state) 181 | { 182 | string kw = $"SHADOWS_CASCADE_LOW_DETAIL_{cascade}"; 183 | if (state == 0) Shader.EnableKeyword(kw); 184 | else Shader.DisableKeyword(kw); 185 | } 186 | 187 | void RecomputeSplits() 188 | { 189 | float[] baseSplits = { 0.1f, 0.3f, 0.6f }; 190 | Vector3 splits = Vector3.zero; 191 | for (int i = 0; i < 3; i++) 192 | { 193 | float s = baseSplits[i]; 194 | if (cascadeState[i] == 0) s *= 1.2f; 195 | else if (cascadeState[i] == 2) s *= 0.9f; 196 | splits[i] = Mathf.Clamp01(s); 197 | } 198 | QualitySettings.shadowCascade4Split = splits; 199 | } 200 | 201 | // Optional on-screen debug 202 | void OnGUI() 203 | { 204 | if (!debugMode) return; 205 | GUILayout.BeginArea(new Rect(10, 10, 240, 100), "TVSO", GUI.skin.window); 206 | GUILayout.Label($"Low→Mid Thr: {lowToMid:F3}"); 207 | GUILayout.Label($"Mid→High Thr: {midToHigh:F3}"); 208 | GUILayout.Label( 209 | $"Vars: {lastVariances.x:F4}, {lastVariances.y:F4}\n" + 210 | $" {lastVariances.z:F4}, {lastVariances.w:F4}" 211 | ); 212 | GUILayout.EndArea(); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /VarianceComputeURP_Atlas.compute: -------------------------------------------------------------------------------- 1 | // VarianceComputeURP.compute 2 | #pragma kernel CS_VarianceURP 3 | #include "UnityCG.cginc" 4 | 5 | // URP packs cascades into a single 2D atlas (quad layout). 6 | Texture2D _MainLightShadowmapTexture; 7 | RWTexture2D _VarianceOut; 8 | int _SampleCount; 9 | 10 | [numthreads(1,1,1)] 11 | void CS_VarianceURP(uint3 id : SV_DispatchThreadID) 12 | { 13 | uint width, height; 14 | _MainLightShadowmapTexture.GetDimensions(width, height); 15 | 16 | // Each cascade is in one quadrant: 2×2 grid 17 | uint quadW = width / 2; 18 | uint quadH = height / 2; 19 | 20 | float4 variances = float4(0,0,0,0); 21 | 22 | for (uint c = 0; c < 4; ++c) 23 | { 24 | // quadrant origin 25 | uint baseX = (c % 2) * quadW; 26 | uint baseY = (c / 2) * quadH; 27 | 28 | float mean = 0; 29 | float meanSq = 0; 30 | 31 | for (uint s = 0; s < _SampleCount; ++s) 32 | { 33 | uint seed = c * 0x9E3779B9u + s * 0x85EBCA6Bu; 34 | float u = frac(sin((float)seed) * 43758.5453123); 35 | float v = frac(cos((float)(seed + 1u)) * 96331.1571); 36 | 37 | uint x = baseX + (uint)(u * (quadW - 1)); 38 | uint y = baseY + (uint)(v * (quadH - 1)); 39 | 40 | float d = _MainLightShadowmapTexture.Load(int3(x, y, 0), 0); 41 | mean += d; 42 | meanSq += d * d; 43 | } 44 | 45 | mean /= _SampleCount; 46 | meanSq /= _SampleCount; 47 | variances[c] = max(meanSq - mean * mean, 0); 48 | } 49 | 50 | // Write our 4 cascades’ variance into the single pixel at (0,0) 51 | _VarianceOut[int2(0,0)] = variances; 52 | } 53 | -------------------------------------------------------------------------------- /sunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LexCapp/TVSO-Temporal-Variance-Shadow-Optimizer/0c49b49a42fa09eaada74c4a2f2f96087ed6e975/sunny.png --------------------------------------------------------------------------------