using System; using System.Collections.Generic; using Unity.Collections; namespace UnityEngine.Rendering.Universal.Internal { /// /// Renders a shadow map atlas for additional shadow-casting Lights. /// public class AdditionalLightsShadowCasterPass : ScriptableRenderPass { private static class AdditionalShadowsConstantBuffer { public static int _AdditionalLightsWorldToShadow; public static int _AdditionalShadowParams; public static int _AdditionalShadowOffset0; public static int _AdditionalShadowOffset1; public static int _AdditionalShadowOffset2; public static int _AdditionalShadowOffset3; public static int _AdditionalShadowmapSize; } public static int m_AdditionalShadowsBufferId; public static int m_AdditionalShadowsIndicesId; bool m_UseStructuredBuffer; const int k_ShadowmapBufferBits = 16; private RenderTargetHandle m_AdditionalLightsShadowmap; RenderTexture m_AdditionalLightsShadowmapTexture; int m_ShadowmapWidth; int m_ShadowmapHeight; ShadowSliceData[] m_AdditionalLightSlices = null; // Shader data for UBO path Matrix4x4[] m_AdditionalLightsWorldToShadow = null; Vector4[] m_AdditionalLightsShadowParams = null; // Shader data for SSBO ShaderInput.ShadowData[] m_AdditionalLightsShadowData = null; List m_AdditionalShadowCastingLightIndices = new List(); List m_AdditionalShadowCastingLightIndicesMap = new List(); bool m_SupportsBoxFilterForShadows; const string m_ProfilerTag = "Render Additional Shadows"; ProfilingSampler m_ProfilingSampler = new ProfilingSampler(m_ProfilerTag); public AdditionalLightsShadowCasterPass(RenderPassEvent evt) { renderPassEvent = evt; AdditionalShadowsConstantBuffer._AdditionalLightsWorldToShadow = Shader.PropertyToID("_AdditionalLightsWorldToShadow"); AdditionalShadowsConstantBuffer._AdditionalShadowParams = Shader.PropertyToID("_AdditionalShadowParams"); AdditionalShadowsConstantBuffer._AdditionalShadowOffset0 = Shader.PropertyToID("_AdditionalShadowOffset0"); AdditionalShadowsConstantBuffer._AdditionalShadowOffset1 = Shader.PropertyToID("_AdditionalShadowOffset1"); AdditionalShadowsConstantBuffer._AdditionalShadowOffset2 = Shader.PropertyToID("_AdditionalShadowOffset2"); AdditionalShadowsConstantBuffer._AdditionalShadowOffset3 = Shader.PropertyToID("_AdditionalShadowOffset3"); AdditionalShadowsConstantBuffer._AdditionalShadowmapSize = Shader.PropertyToID("_AdditionalShadowmapSize"); m_AdditionalLightsShadowmap.Init("_AdditionalLightsShadowmapTexture"); m_AdditionalShadowsBufferId = Shader.PropertyToID("_AdditionalShadowsBuffer"); m_AdditionalShadowsIndicesId = Shader.PropertyToID("_AdditionalShadowsIndices"); m_UseStructuredBuffer = RenderingUtils.useStructuredBuffer; m_SupportsBoxFilterForShadows = Application.isMobilePlatform || SystemInfo.graphicsDeviceType == GraphicsDeviceType.Switch; if (!m_UseStructuredBuffer) { // Preallocated a fixed size. CommandBuffer.SetGlobal* does allow this data to grow. int maxLights = UniversalRenderPipeline.maxVisibleAdditionalLights; m_AdditionalLightsWorldToShadow = new Matrix4x4[maxLights]; m_AdditionalLightsShadowParams = new Vector4[maxLights]; } } public bool Setup(ref RenderingData renderingData) { Clear(); m_ShadowmapWidth = renderingData.shadowData.additionalLightsShadowmapWidth; m_ShadowmapHeight = renderingData.shadowData.additionalLightsShadowmapHeight; var visibleLights = renderingData.lightData.visibleLights; int additionalLightsCount = renderingData.lightData.additionalLightsCount; if (m_AdditionalLightSlices == null || m_AdditionalLightSlices.Length < additionalLightsCount) m_AdditionalLightSlices = new ShadowSliceData[additionalLightsCount]; if (m_AdditionalLightsShadowData == null || m_AdditionalLightsShadowData.Length < additionalLightsCount) m_AdditionalLightsShadowData = new ShaderInput.ShadowData[additionalLightsCount]; int validShadowCastingLights = 0; bool supportsSoftShadows = renderingData.shadowData.supportsSoftShadows; for (int i = 0; i < visibleLights.Length && m_AdditionalShadowCastingLightIndices.Count < additionalLightsCount; ++i) { VisibleLight shadowLight = visibleLights[i]; // Skip main directional light as it is not packed into the shadow atlas if (i == renderingData.lightData.mainLightIndex) continue; int shadowCastingLightIndex = m_AdditionalShadowCastingLightIndices.Count; bool isValidShadowSlice = false; if (renderingData.cullResults.GetShadowCasterBounds(i, out var bounds)) { // We need to iterate the lights even though additional lights are disabled because // cullResults.GetShadowCasterBounds() does the fence sync for the shadow culling jobs. if (!renderingData.shadowData.supportsAdditionalLightShadows) { continue; } if (IsValidShadowCastingLight(ref renderingData.lightData, i)) { bool success = ShadowUtils.ExtractSpotLightMatrix(ref renderingData.cullResults, ref renderingData.shadowData, i, out var shadowTransform, out m_AdditionalLightSlices[shadowCastingLightIndex].viewMatrix, out m_AdditionalLightSlices[shadowCastingLightIndex].projectionMatrix); if (success) { m_AdditionalShadowCastingLightIndices.Add(i); var light = shadowLight.light; float shadowStrength = light.shadowStrength; float softShadows = (supportsSoftShadows && light.shadows == LightShadows.Soft) ? 1.0f : 0.0f; Vector4 shadowParams = new Vector4(shadowStrength, softShadows, 0.0f, 0.0f); if (m_UseStructuredBuffer) { m_AdditionalLightsShadowData[shadowCastingLightIndex].worldToShadowMatrix = shadowTransform; m_AdditionalLightsShadowData[shadowCastingLightIndex].shadowParams = shadowParams; } else { m_AdditionalLightsWorldToShadow[shadowCastingLightIndex] = shadowTransform; m_AdditionalLightsShadowParams[shadowCastingLightIndex] = shadowParams; } isValidShadowSlice = true; validShadowCastingLights++; } } } if (m_UseStructuredBuffer) { // When using StructuredBuffers all the valid shadow casting slices data // are stored in a the ShadowData buffer and then we setup a index map to // map from light indices to shadow buffer index. A index map of -1 means // the light is not a valid shadow casting light and there's no data for it // in the shadow buffer. int indexMap = (isValidShadowSlice) ? shadowCastingLightIndex : -1; m_AdditionalShadowCastingLightIndicesMap.Add(indexMap); } else if (!isValidShadowSlice) { // When NOT using structured buffers we have no performant way to sample the // index map as int[]. Unity shader compiler converts int[] to float4[] to force memory alignment. // This makes indexing int[] arrays very slow. So, in order to avoid indexing shadow lights we // setup slice data and reserve shadow map space even for invalid shadow slices. // The data is setup with zero shadow strength. This has the same visual effect of no shadow // attenuation contribution from this light. // This makes sampling shadow faster but introduces waste in shadow map atlas. // The waste increases with the amount of additional lights to shade. // Therefore Universal RP try to keep the limit at sane levels when using uniform buffers. Matrix4x4 identity = Matrix4x4.identity; m_AdditionalShadowCastingLightIndices.Add(i); m_AdditionalLightsWorldToShadow[shadowCastingLightIndex] = identity; m_AdditionalLightsShadowParams[shadowCastingLightIndex] = Vector4.zero; m_AdditionalLightSlices[shadowCastingLightIndex].viewMatrix = identity; m_AdditionalLightSlices[shadowCastingLightIndex].projectionMatrix = identity; } } // Lights that need to be rendered in the shadow map atlas if (validShadowCastingLights == 0) return false; int atlasWidth = renderingData.shadowData.additionalLightsShadowmapWidth; int atlasHeight = renderingData.shadowData.additionalLightsShadowmapHeight; int sliceResolution = ShadowUtils.GetMaxTileResolutionInAtlas(atlasWidth, atlasHeight, validShadowCastingLights); // In the UI we only allow for square shadow map atlas. Here we check if we can fit // all shadow slices into half resolution of the atlas and adjust height to have tighter packing. int maximumSlices = (m_ShadowmapWidth / sliceResolution) * (m_ShadowmapHeight / sliceResolution); if (validShadowCastingLights <= (maximumSlices / 2)) m_ShadowmapHeight /= 2; int shadowSlicesPerRow = (atlasWidth / sliceResolution); float oneOverAtlasWidth = 1.0f / m_ShadowmapWidth; float oneOverAtlasHeight = 1.0f / m_ShadowmapHeight; int sliceIndex = 0; int shadowCastingLightsBufferCount = m_AdditionalShadowCastingLightIndices.Count; Matrix4x4 sliceTransform = Matrix4x4.identity; sliceTransform.m00 = sliceResolution * oneOverAtlasWidth; sliceTransform.m11 = sliceResolution * oneOverAtlasHeight; for (int i = 0; i < shadowCastingLightsBufferCount; ++i) { // we can skip the slice if strength is zero. Some slices with zero // strength exists when using uniform array path. if (!m_UseStructuredBuffer && Mathf.Approximately(m_AdditionalLightsShadowParams[i].x, 0.0f)) continue; m_AdditionalLightSlices[i].offsetX = (sliceIndex % shadowSlicesPerRow) * sliceResolution; m_AdditionalLightSlices[i].offsetY = (sliceIndex / shadowSlicesPerRow) * sliceResolution; m_AdditionalLightSlices[i].resolution = sliceResolution; sliceTransform.m03 = m_AdditionalLightSlices[i].offsetX * oneOverAtlasWidth; sliceTransform.m13 = m_AdditionalLightSlices[i].offsetY * oneOverAtlasHeight; // We bake scale and bias to each shadow map in the atlas in the matrix. // saves some instructions in shader. if (m_UseStructuredBuffer) m_AdditionalLightsShadowData[i].worldToShadowMatrix = sliceTransform * m_AdditionalLightsShadowData[i].worldToShadowMatrix; else m_AdditionalLightsWorldToShadow[i] = sliceTransform * m_AdditionalLightsWorldToShadow[i]; sliceIndex++; } return true; } public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) { m_AdditionalLightsShadowmapTexture = ShadowUtils.GetTemporaryShadowTexture(m_ShadowmapWidth, m_ShadowmapHeight, k_ShadowmapBufferBits); ConfigureTarget(new RenderTargetIdentifier(m_AdditionalLightsShadowmapTexture)); ConfigureClear(ClearFlag.All, Color.black); } /// public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (renderingData.shadowData.supportsAdditionalLightShadows) RenderAdditionalShadowmapAtlas(ref context, ref renderingData.cullResults, ref renderingData.lightData, ref renderingData.shadowData); } public override void FrameCleanup(CommandBuffer cmd) { if (cmd == null) throw new ArgumentNullException("cmd"); if (m_AdditionalLightsShadowmapTexture) { RenderTexture.ReleaseTemporary(m_AdditionalLightsShadowmapTexture); m_AdditionalLightsShadowmapTexture = null; } } void Clear() { m_AdditionalShadowCastingLightIndices.Clear(); m_AdditionalShadowCastingLightIndicesMap.Clear(); m_AdditionalLightsShadowmapTexture = null; } void RenderAdditionalShadowmapAtlas(ref ScriptableRenderContext context, ref CullingResults cullResults, ref LightData lightData, ref ShadowData shadowData) { NativeArray visibleLights = lightData.visibleLights; bool additionalLightHasSoftShadows = false; CommandBuffer cmd = CommandBufferPool.Get(m_ProfilerTag); using (new ProfilingScope(cmd, m_ProfilingSampler)) { bool anyShadowSliceRenderer = false; int shadowSlicesCount = m_AdditionalShadowCastingLightIndices.Count; for (int i = 0; i < shadowSlicesCount; ++i) { // we do the shadow strength check here again here because when using // the uniform array path we might have zero strength shadow lights. // In that case we need the shadow data buffer but we can skip // rendering them to shadowmap. if (!m_UseStructuredBuffer && Mathf.Approximately(m_AdditionalLightsShadowParams[i].x, 0.0f)) continue; // Index of the VisibleLight int shadowLightIndex = m_AdditionalShadowCastingLightIndices[i]; VisibleLight shadowLight = visibleLights[shadowLightIndex]; ShadowSliceData shadowSliceData = m_AdditionalLightSlices[i]; var settings = new ShadowDrawingSettings(cullResults, shadowLightIndex); Vector4 shadowBias = ShadowUtils.GetShadowBias(ref shadowLight, shadowLightIndex, ref shadowData, shadowSliceData.projectionMatrix, shadowSliceData.resolution); ShadowUtils.SetupShadowCasterConstantBuffer(cmd, ref shadowLight, shadowBias); ShadowUtils.RenderShadowSlice(cmd, ref context, ref shadowSliceData, ref settings); additionalLightHasSoftShadows |= shadowLight.light.shadows == LightShadows.Soft; anyShadowSliceRenderer = true; } // We share soft shadow settings for main light and additional lights to save keywords. // So we check here if pipeline supports soft shadows and either main light or any additional light has soft shadows // to enable the keyword. // TODO: In PC and Consoles we can upload shadow data per light and branch on shader. That will be more likely way faster. bool mainLightHasSoftShadows = shadowData.supportsMainLightShadows && lightData.mainLightIndex != -1 && visibleLights[lightData.mainLightIndex].light.shadows == LightShadows.Soft; bool softShadows = shadowData.supportsSoftShadows && (mainLightHasSoftShadows || additionalLightHasSoftShadows); CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.AdditionalLightShadows, anyShadowSliceRenderer); CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.SoftShadows, softShadows); if (anyShadowSliceRenderer) SetupAdditionalLightsShadowReceiverConstants(cmd, ref shadowData, softShadows); } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } void SetupAdditionalLightsShadowReceiverConstants(CommandBuffer cmd, ref ShadowData shadowData, bool softShadows) { int shadowLightsCount = m_AdditionalShadowCastingLightIndices.Count; float invShadowAtlasWidth = 1.0f / shadowData.additionalLightsShadowmapWidth; float invShadowAtlasHeight = 1.0f / shadowData.additionalLightsShadowmapHeight; float invHalfShadowAtlasWidth = 0.5f * invShadowAtlasWidth; float invHalfShadowAtlasHeight = 0.5f * invShadowAtlasHeight; cmd.SetGlobalTexture(m_AdditionalLightsShadowmap.id, m_AdditionalLightsShadowmapTexture); if (m_UseStructuredBuffer) { NativeArray shadowBufferData = new NativeArray(shadowLightsCount, Allocator.Temp); for (int i = 0; i < shadowLightsCount; ++i) { ShaderInput.ShadowData data; data.worldToShadowMatrix = m_AdditionalLightsShadowData[i].worldToShadowMatrix; data.shadowParams = m_AdditionalLightsShadowData[i].shadowParams; shadowBufferData[i] = data; } var shadowBuffer = ShaderData.instance.GetShadowDataBuffer(shadowLightsCount); shadowBuffer.SetData(shadowBufferData); var shadowIndicesMapBuffer = ShaderData.instance.GetShadowIndicesBuffer(m_AdditionalShadowCastingLightIndicesMap.Count); shadowIndicesMapBuffer.SetData(m_AdditionalShadowCastingLightIndicesMap, 0, 0, m_AdditionalShadowCastingLightIndicesMap.Count); cmd.SetGlobalBuffer(m_AdditionalShadowsBufferId, shadowBuffer); cmd.SetGlobalBuffer(m_AdditionalShadowsIndicesId, shadowIndicesMapBuffer); shadowBufferData.Dispose(); } else { cmd.SetGlobalMatrixArray(AdditionalShadowsConstantBuffer._AdditionalLightsWorldToShadow, m_AdditionalLightsWorldToShadow); cmd.SetGlobalVectorArray(AdditionalShadowsConstantBuffer._AdditionalShadowParams, m_AdditionalLightsShadowParams); } if (softShadows) { if (m_SupportsBoxFilterForShadows) { cmd.SetGlobalVector(AdditionalShadowsConstantBuffer._AdditionalShadowOffset0, new Vector4(-invHalfShadowAtlasWidth, -invHalfShadowAtlasHeight, 0.0f, 0.0f)); cmd.SetGlobalVector(AdditionalShadowsConstantBuffer._AdditionalShadowOffset1, new Vector4(invHalfShadowAtlasWidth, -invHalfShadowAtlasHeight, 0.0f, 0.0f)); cmd.SetGlobalVector(AdditionalShadowsConstantBuffer._AdditionalShadowOffset2, new Vector4(-invHalfShadowAtlasWidth, invHalfShadowAtlasHeight, 0.0f, 0.0f)); cmd.SetGlobalVector(AdditionalShadowsConstantBuffer._AdditionalShadowOffset3, new Vector4(invHalfShadowAtlasWidth, invHalfShadowAtlasHeight, 0.0f, 0.0f)); } // Currently only used when !SHADER_API_MOBILE but risky to not set them as it's generic // enough so custom shaders might use it. cmd.SetGlobalVector(AdditionalShadowsConstantBuffer._AdditionalShadowmapSize, new Vector4(invShadowAtlasWidth, invShadowAtlasHeight, shadowData.additionalLightsShadowmapWidth, shadowData.additionalLightsShadowmapHeight)); } } bool IsValidShadowCastingLight(ref LightData lightData, int i) { if (i == lightData.mainLightIndex) return false; VisibleLight shadowLight = lightData.visibleLights[i]; // Directional and Point light shadows are not supported in the shadow map atlas if (shadowLight.lightType == LightType.Point || shadowLight.lightType == LightType.Directional) return false; Light light = shadowLight.light; return light != null && light.shadows != LightShadows.None && !Mathf.Approximately(light.shadowStrength, 0.0f); } } }