//------------------------------------------------------------------------------------------------------------------ // Volumetric Lights // Created by Kronnect //------------------------------------------------------------------------------------------------------------------ using System; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; namespace VolumetricLights { public partial class VolumetricLight : MonoBehaviour { #region Shadow support const string SHADOW_CAM_NAME = "OcclusionCam"; Camera cam; RenderTexture rt; int camStartFrameCount; Vector3 lastCamPos; Quaternion lastCamRot; bool usesReversedZBuffer; static Matrix4x4 textureScaleAndBias; Matrix4x4 shadowMatrix; bool camTransformChanged; bool shouldOrientToCamera; RenderTexture shadowCubemap; readonly static Vector3[] camFaceDirections = { Vector3.right, Vector3.left, Vector3.up, Vector3.down, Vector3.forward, Vector3.back }; Material copyDepthIntoCubemap; int currentCubemapFace; [NonSerialized] public RenderTexture translucentMap; [NonSerialized] public RTHandle translucencyMapHandle; public bool usesCubemap { get { return shadowBakeMode != ShadowBakeMode.HalfSphere && generatedType == LightType.Point; } } bool usesTranslucency { get { return shadowTranslucency && (generatedType == LightType.Spot || generatedType == LightType.Rectangle || generatedType == LightType.Disc); } } void CheckShadows () { // called during initialization to grab a reference of existing cam if (cam == null) { Transform childCam = transform.Find(SHADOW_CAM_NAME); if (childCam != null) { cam = childCam.GetComponent(); if (cam == null) { // corrupted cam object? DestroyImmediate(childCam.gameObject); } } } } void ShadowsDisable () { // called from OnDisable if (cam != null) { cam.enabled = false; } } void ShadowsDispose () { // called from OnDestroy if (cam != null) { cam.targetTexture = null; cam.enabled = false; } DisposeRTs(); } void DisposeRTs () { if (rt != null) { rt.Release(); DestroyImmediate(rt); } if (shadowCubemap != null) { shadowCubemap.Release(); DestroyImmediate(shadowCubemap); } if (translucentMap != null) { translucentMap.Release(); DestroyImmediate(translucentMap); } } void ShadowsSupportCheck () { // called from UpdateMaterials bool usesCookie = cookieTexture != null && lightComp.type == LightType.Spot; if (!enableShadows && !usesCookie) { ShadowsDispose(); return; } usesReversedZBuffer = SystemInfo.usesReversedZBuffer; // Setup texture scale and bias matrix textureScaleAndBias = Matrix4x4.identity; textureScaleAndBias.m00 = 0.5f; textureScaleAndBias.m11 = 0.5f; textureScaleAndBias.m22 = 0.5f; textureScaleAndBias.m03 = 0.5f; textureScaleAndBias.m13 = 0.5f; textureScaleAndBias.m23 = 0.5f; if (cam == null) { Transform childCam = transform.Find(SHADOW_CAM_NAME); if (childCam != null) { cam = childCam.GetComponent(); if (cam == null) { DestroyImmediate(childCam.gameObject); } } if (cam == null) { GameObject camObj = new GameObject(SHADOW_CAM_NAME, typeof(Camera)); camObj.transform.SetParent(transform, false); cam = camObj.GetComponent(); cam.depthTextureMode = DepthTextureMode.None; cam.clearFlags = CameraClearFlags.Depth; cam.allowHDR = false; cam.allowMSAA = false; } } UniversalAdditionalCameraData camData = cam.GetComponent(); if (camData == null) { camData = cam.gameObject.AddComponent(); } if (camData != null) { camData.dithering = false; camData.renderPostProcessing = false; camData.renderShadows = false; camData.requiresColorTexture = false; camData.requiresDepthTexture = false; camData.stopNaN = false; camData.volumeLayerMask = 0; camData.allowXRRendering = false; #if UNITY_2021_3_OR_NEWER CheckAndAssignDepthRenderer(camData); #endif } cam.depth = -100; cam.nearClipPlane = shadowNearDistance; cam.orthographicSize = Mathf.Max(generatedAreaWidth, generatedAreaHeight); // custom properties per light type switch (generatedType) { case LightType.Spot: cam.transform.localRotation = Quaternion.identity; cam.orthographic = false; cam.fieldOfView = generatedSpotAngle; break; case LightType.Point: cam.orthographic = false; if (shadowBakeMode != ShadowBakeMode.HalfSphere) { cam.fieldOfView = 90f; } else { cam.fieldOfView = 160f; } break; case LightType.Rectangle: case LightType.Disc: cam.transform.localRotation = Quaternion.identity; cam.orthographic = true; cam.orthographicSize *= generatedAreaFrustumMultiplier; break; } if (rt != null && rt.width != (int)shadowResolution) { if (cam.targetTexture == rt) { cam.targetTexture = null; } DisposeRTs(); } if (rt == null) { rt = new RenderTexture((int)shadowResolution, (int)shadowResolution, 16, RenderTextureFormat.Depth, RenderTextureReadWrite.Linear); rt.antiAliasing = 1; rt.useMipMap = false; rt.filterMode = FilterMode.Bilinear; rt.wrapMode = TextureWrapMode.Clamp; } if (usesTranslucency && translucentMap == null) { translucentMap = new RenderTexture((int)shadowResolution, (int)shadowResolution, 0, RenderTextureFormat.ARGBHalf); translucentMap.antiAliasing = 1; translucentMap.useMipMap = false; translucentMap.wrapMode = TextureWrapMode.Clamp; } if (usesCubemap && shadowCubemap == null) { shadowCubemap = new RenderTexture((int)shadowResolution, (int)shadowResolution, 0, RenderTextureFormat.RHalf, RenderTextureReadWrite.Linear); shadowCubemap.dimension = TextureDimension.Cube; shadowCubemap.antiAliasing = 1; shadowCubemap.useMipMap = false; } fogMat.SetVector(ShaderParams.ShadowIntensity, new Vector4(shadowIntensity, 1f - shadowIntensity, 0, 0)); fogMat.SetVector(ShaderParams.ShadowColor, shadowColor); if ((shadowCullingMask & 2) != 0) { shadowCullingMask &= ~2; // exclude transparent FX layer } cam.cullingMask = shadowCullingMask; cam.targetTexture = rt; if (enableShadows) { shouldOrientToCamera = true; ScheduleShadowCapture(); } else { cam.enabled = false; } } public bool CheckCompatiblePipelineArchitecture () { UniversalRenderPipelineAsset pipe = (UniversalRenderPipelineAsset)GraphicsSettings.currentRenderPipeline; if (pipe == null) return false; // Skip custom depth renderer for Forward+/Deferred+ rendering path as it's incompatible bool isCompatible = true; #if UNITY_2022_2_OR_NEWER if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Direct3D12) { for (int i = 0; i < pipe.m_RendererDataList.Length; i++) { UniversalRendererData rendererData = pipe.m_RendererDataList[i] as UniversalRendererData; if (rendererData != null) { if (rendererData.renderingMode == RenderingMode.ForwardPlus) { isCompatible = false; break; } #if UNITY_6000_1_OR_NEWER else if (rendererData.renderingMode == RenderingMode.DeferredPlus) { isCompatible = false; break; } #endif } } } #endif return isCompatible; } #if UNITY_2021_3_OR_NEWER UniversalRendererData depthRendererData; void CheckAndAssignDepthRenderer (UniversalAdditionalCameraData camData) { UniversalRenderPipelineAsset pipe = (UniversalRenderPipelineAsset)GraphicsSettings.currentRenderPipeline; if (pipe == null) return; // Skip custom depth renderer for Forward+/Deferred+ on DX12 rendering path as it's incompatible bool skipCustomRenderer = !CheckCompatiblePipelineArchitecture(); if (skipCustomRenderer) { // Use default renderer for Forward+ to avoid shadow issues camData.SetRenderer(-1); // -1 uses the default renderer return; } if (depthRendererData == null) { depthRendererData = Resources.Load("Shaders/VolumetricLightsDepthRenderer"); if (depthRendererData == null) { Debug.LogError("Volumetric Lights Depth Renderer asset not found."); return; } depthRendererData.postProcessData = null; } int depthRendererIndex = -1; for (int k = 0; k < pipe.m_RendererDataList.Length; k++) { if (pipe.m_RendererDataList[k] == depthRendererData) { depthRendererIndex = k; break; } } if (depthRendererIndex < 0) { depthRendererIndex = pipe.m_RendererDataList.Length; System.Array.Resize(ref pipe.m_RendererDataList, depthRendererIndex + 1); pipe.m_RendererDataList[depthRendererIndex] = depthRendererData; #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty(pipe); #endif } camData.SetRenderer(depthRendererIndex); } #endif /// /// Updates shadows on this volumetric light /// public void ScheduleShadowCapture () { if (cam == null) return; if (usesCubemap) { if (copyDepthIntoCubemap == null) { copyDepthIntoCubemap = new Material(Shader.Find("Hidden/VolumetricLights/CopyDepthIntoCubemap")); } copyDepthIntoCubemap.SetVector(ShaderParams.LightPos, cam.transform.position); RenderTexture active = RenderTexture.active; int renderFaceCount = shadowBakeMode == ShadowBakeMode.CubemapOneFacePerFrame && shadowBakeInterval == ShadowBakeInterval.EveryFrame ? 1 : 6; for (int k = 0; k < renderFaceCount; k++) { int cubemapFace = currentCubemapFace % 6; cam.transform.forward = camFaceDirections[cubemapFace]; cam.Render(); copyDepthIntoCubemap.SetMatrix(ShaderParams.InvVPMatrix, cam.cameraToWorldMatrix * GL.GetGPUProjectionMatrix(cam.projectionMatrix, false).inverse); copyDepthIntoCubemap.SetTexture(ShaderParams.ShadowTexture, rt, RenderTextureSubElement.Depth); Graphics.SetRenderTarget(shadowCubemap, 0, (CubemapFace)cubemapFace); Graphics.Blit(rt, copyDepthIntoCubemap); currentCubemapFace++; } cam.enabled = false; RenderTexture.active = active; fogMat.SetTexture(ShaderParams.ShadowCubemap, shadowCubemap); if (enableDustParticles && particleMaterial != null) { particleMaterial.SetTexture(ShaderParams.ShadowCubemap, shadowCubemap); } if (!fogMat.IsKeywordEnabled(ShaderParams.SKW_SHADOWS_CUBEMAP)) { fogMat.EnableKeyword(ShaderParams.SKW_SHADOWS_CUBEMAP); } } else { cam.enabled = true; camStartFrameCount = Time.frameCount; string shadowKeyword = usesTranslucency ? ShaderParams.SKW_SHADOWS_TRANSLUCENCY : ShaderParams.SKW_SHADOWS; if (!fogMat.IsKeywordEnabled(shadowKeyword)) { fogMat.EnableKeyword(shadowKeyword); } } } void SetupShadowMatrix () { if (usesCubemap) return; ComputeShadowTransform(cam.projectionMatrix, cam.worldToCameraMatrix); fogMat.SetMatrix(ShaderParams.ShadowMatrix, shadowMatrix); fogMat.SetTexture(ShaderParams.ShadowTexture, cam.targetTexture, RenderTextureSubElement.Depth); fogMat.SetTexture(ShaderParams.TranslucencyTexture, translucentMap); if (enableDustParticles && particleMaterial != null) { particleMaterial.SetMatrix(ShaderParams.ShadowMatrix, shadowMatrix); particleMaterial.SetTexture(ShaderParams.ShadowTexture, cam.targetTexture, RenderTextureSubElement.Depth); particleMaterial.SetVector(ShaderParams.ShadowIntensity, new Vector4(shadowIntensity, 1f - shadowIntensity, 0, 0)); particleMaterial.SetTexture(ShaderParams.TranslucencyTexture, translucentMap); } } void ShadowsUpdate () { // called from Update bool usesCookie = cookieTexture != null && lightComp.type == LightType.Spot; if (!enableShadows && !usesCookie) return; if (cam == null) return; int frameCount = Time.frameCount; if (!meshRenderer.isVisible && frameCount - camStartFrameCount > 5) { if (cam.enabled) { ShadowsDisable(); } return; } Transform camTransform = cam.transform; cam.farClipPlane = generatedRange; if (generatedType == LightType.Point) { if (shadowBakeMode != ShadowBakeMode.HalfSphere) { } else if (shadowOrientation == ShadowOrientation.ToCamera) { if (enableShadows && mainCamera != null) { // if it's a point light, check if the orientation is target camera and if the angle has changed too much force a shadow update if (shadowBakeInterval != ShadowBakeInterval.EveryFrame) { if (Vector3.Angle(camTransform.forward, mainCamera.position - lastCamPos) > 45) { shouldOrientToCamera = true; ScheduleShadowCapture(); } } if (shouldOrientToCamera || shadowBakeInterval == ShadowBakeInterval.EveryFrame) { shouldOrientToCamera = false; camTransform.LookAt(mainCamera.position); } } } else { camTransform.forward = shadowDirection; } } camTransformChanged = false; bool rotationChanged = lastCamRot != camTransform.rotation; if (lastCamPos != camTransform.position || (rotationChanged && !shadowBakeIgnoreRotationChange)) { camTransformChanged = true; lastCamPos = camTransform.position; lastCamRot = camTransform.rotation; } if (enableShadows) { ShadowCamUpdate(); } if (camTransformChanged || rotationChanged || usesCookie || cam.enabled) { SetupShadowMatrix(); } } void ShadowCamUpdate () { if (shadowAutoToggle) { float maxDistSqr = shadowDistanceDeactivation * shadowDistanceDeactivation; if (distanceToCameraSqr > maxDistSqr) { if (cam.enabled) { ShadowsDisable(); if (fogMat.IsKeywordEnabled(ShaderParams.SKW_SHADOWS)) { fogMat.DisableKeyword(ShaderParams.SKW_SHADOWS); } if (fogMat.IsKeywordEnabled(ShaderParams.SKW_SHADOWS_TRANSLUCENCY)) { fogMat.DisableKeyword(ShaderParams.SKW_SHADOWS_TRANSLUCENCY); } if (fogMat.IsKeywordEnabled(ShaderParams.SKW_SHADOWS_CUBEMAP)) { fogMat.DisableKeyword(ShaderParams.SKW_SHADOWS_CUBEMAP); } } return; } } if (shadowBakeInterval == ShadowBakeInterval.OnStart) { if (!cam.enabled && camTransformChanged) { ScheduleShadowCapture(); } else if (Time.frameCount > camStartFrameCount + 1) { cam.enabled = false; } } else if (!cam.enabled) { ScheduleShadowCapture(); } } void ComputeShadowTransform (Matrix4x4 proj, Matrix4x4 view) { // Currently CullResults ComputeDirectionalShadowMatricesAndCullingPrimitives doesn't // apply z reversal to projection matrix. We need to do it manually here. if (usesReversedZBuffer) { proj.m20 = -proj.m20; proj.m21 = -proj.m21; proj.m22 = -proj.m22; proj.m23 = -proj.m23; } Matrix4x4 worldToShadow = proj * view; // Apply texture scale and offset to save a MAD in shader. shadowMatrix = textureScaleAndBias * worldToShadow; } #endregion } }