diff --git a/com.unity.render-pipelines.high-definition/CHANGELOG.md b/com.unity.render-pipelines.high-definition/CHANGELOG.md index 1c11ea1df5b..b9c0f2ab452 100644 --- a/com.unity.render-pipelines.high-definition/CHANGELOG.md +++ b/com.unity.render-pipelines.high-definition/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added browsing of the documentation of Compositor Window - Added a complete solution for volumetric clouds for HDRP including a cloud map generation tool. - Added a Force Forward Emissive option for Lit Material that forces the Emissive contribution to render in a separate forward pass when the Lit Material is in Deferred Lit shader Mode. +- Added new API in CachedShadowManager ### Fixed - Fixed an exception when opening the color picker in the material UI (case 1307143). @@ -47,6 +48,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Fixed sub-shadow rendering for cached shadow maps. - Fixed PCSS filtering issues with cached shadow maps. +- Fixed WouldFitInAtlas that would previously return wrong results if any one face of a point light would fit (it used to return true even though the light in entirety wouldn't fit). + ### Changed - Changed Window/Render Pipeline/HD Render Pipeline Wizard to Window/Rendering/HDRP Wizard - Removed the material pass probe volumes evaluation mode. diff --git a/com.unity.render-pipelines.high-definition/Documentation~/Shadows-in-HDRP.md b/com.unity.render-pipelines.high-definition/Documentation~/Shadows-in-HDRP.md index 4624d8a5cdf..321c282079f 100644 --- a/com.unity.render-pipelines.high-definition/Documentation~/Shadows-in-HDRP.md +++ b/com.unity.render-pipelines.high-definition/Documentation~/Shadows-in-HDRP.md @@ -132,6 +132,8 @@ If the shadow atlas is full when a Light requests a spot, the cached shadow mana After a Scene loads with all the already placed Lights, if you add a new Light with cached shadows to the Scene, HDRP tries to place it in order to fill the holes in the atlas. However, depending on the order of insertion, the atlas may be fragmented and the holes available are not enough to place the Light's shadow map in. In this case, you can defragment the atlas to allow for additional Lights. To do this, pass the target atlas into the following function: `HDCachedShadowManager.instance.DefragAtlas` Note that this causes HDRP to mark all the shadow maps in the atlas as dirty which means HDRP renders them the moment their parent Light becomes visible. +It is possible to check if a light has its shadow maps has a placement in the cached shadow atlas `HDCachedShadowManager.instance.LightHasBeenPlacedInAtlas` and if it has been placed and rendered at least once with `HDCachedShadowManager.instance.LightHasBeenPlaceAndRenderedAtLeastOnce`. + ### Preserving shadow atlas placement If you disable the Light or change its **Update Mode** to **Every Frame**, the cached shadow manager unreserves the Light's shadow map's space in the cached shadow atlas and HDRP begins to render the Light's shadow map to the normal shadow atlases every frame. If the cached shadow manager needs to allocate space on the atlas for another Light, it can overwrite the space currently taken up by the original Light's shadow map. diff --git a/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/HDCachedShadowAtlas.cs b/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/HDCachedShadowAtlas.cs index 6ad6d110b52..f34fec4d4fc 100644 --- a/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/HDCachedShadowAtlas.cs +++ b/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/HDCachedShadowAtlas.cs @@ -30,16 +30,24 @@ struct CachedTransform internal Vector3 angles; // Only for area and spot } + enum SlotValue : byte + { + Free, + Occupied, + TempOccupied // Used when checking if it will fit. + } + private int m_AtlasResolutionInSlots; // Atlas Resolution / m_MinSlotSize private bool m_NeedOptimalPacking = true; // Whenever this is set to true, the pending lights are sorted before insertion. - private List m_AtlasSlots; // One entry per slot (of size m_MinSlotSize) true if occupied, false if free. + private List m_AtlasSlots; // One entry per slot (of size m_MinSlotSize) true if occupied, false if free. // Note: Some of these could be simple lists, but since we might need to search by index some of them and we want to avoid GC alloc, a dictionary is easier. // This also mean slightly worse performance, however hopefully the number of cached shadow lights is not huge at any tie. private Dictionary m_PlacedShadows; private Dictionary m_ShadowsPendingRendering; + private Dictionary m_ShadowsWithValidData; // Shadows that have been placed and rendered at least once (OnDemand shadows are not rendered unless requested explicitly). It is a dictionary for fast access by shadow index. private Dictionary m_RegisteredLightDataPendingPlacement; private Dictionary m_RecordsPendingPlacement; // Note: this is different from m_RegisteredLightDataPendingPlacement because it contains records that were allocated in the system // but they lost their spot (e.g. post defrag). They don't have a light associated anymore if not by index, so we keep a separate collection. @@ -57,6 +65,7 @@ public HDCachedShadowAtlas(ShadowMapType type) { m_PlacedShadows = new Dictionary(s_InitialCapacity); m_ShadowsPendingRendering = new Dictionary(s_InitialCapacity); + m_ShadowsWithValidData = new Dictionary(s_InitialCapacity); m_TempListForPlacement = new List(s_InitialCapacity); m_RegisteredLightDataPendingPlacement = new Dictionary(s_InitialCapacity); @@ -73,10 +82,10 @@ public override void InitAtlas(RenderPipelineResources renderPipelineResources, m_IsACacheForShadows = true; m_AtlasResolutionInSlots = HDUtils.DivRoundUp(width, m_MinSlotSize); - m_AtlasSlots = new List(m_AtlasResolutionInSlots * m_AtlasResolutionInSlots); + m_AtlasSlots = new List(m_AtlasResolutionInSlots * m_AtlasResolutionInSlots); for (int i = 0; i < m_AtlasResolutionInSlots * m_AtlasResolutionInSlots; ++i) { - m_AtlasSlots.Add(false); + m_AtlasSlots.Add(SlotValue.Free); } // Note: If changing the characteristics of the atlas via HDRP asset, the lights OnEnable will not be called again so we are missing them, however we can explicitly @@ -108,21 +117,26 @@ public void AddBlitRequestsForUpdatedShadows(HDDynamicShadowAtlas dynamicAtlas) // ------------------------------------------------------------------------------------------ private bool IsEntryEmpty(int x, int y) { - return (m_AtlasSlots[y * m_AtlasResolutionInSlots + x] == false); + return (m_AtlasSlots[y * m_AtlasResolutionInSlots + x] == SlotValue.Free); } private bool IsEntryFull(int x, int y) { - return (m_AtlasSlots[y * m_AtlasResolutionInSlots + x]); + return (m_AtlasSlots[y * m_AtlasResolutionInSlots + x]) != SlotValue.Free; + } + + private bool IsEntryTempOccupied(int x, int y) + { + return (m_AtlasSlots[y * m_AtlasResolutionInSlots + x]) == SlotValue.TempOccupied; } // Always fill slots in a square shape, for example : if x = 1 and y = 2, if numEntries = 2 it will fill {(1,2),(2,2),(1,3),(2,3)} private void FillEntries(int x, int y, int numEntries) { - MarkEntries(x, y, numEntries, true); + MarkEntries(x, y, numEntries, SlotValue.Occupied); } - private void MarkEntries(int x, int y, int numEntries, bool value) + private void MarkEntries(int x, int y, int numEntries, SlotValue value) { for (int j = y; j < y + numEntries; ++j) { @@ -149,10 +163,9 @@ private bool CheckSlotAvailability(int x, int y, int numEntries) return true; } - internal bool FindSlotInAtlas(int resolution, out int x, out int y) + internal bool FindSlotInAtlas(int resolution, bool tempFill, out int x, out int y) { int numEntries = HDUtils.DivRoundUp(resolution, m_MinSlotSize); - for (int j = 0; j < m_AtlasResolutionInSlots; ++j) { for (int i = 0; i < m_AtlasResolutionInSlots; ++i) @@ -161,6 +174,10 @@ internal bool FindSlotInAtlas(int resolution, out int x, out int y) { x = i; y = j; + + if (tempFill) + MarkEntries(x, y, numEntries, SlotValue.TempOccupied); + return true; } } @@ -171,6 +188,26 @@ internal bool FindSlotInAtlas(int resolution, out int x, out int y) return false; } + internal void FreeTempFilled(int x, int y, int resolution) + { + int numEntries = HDUtils.DivRoundUp(resolution, m_MinSlotSize); + for (int j = y; j < y + numEntries; ++j) + { + for (int i = x; i < x + numEntries; ++i) + { + if (m_AtlasSlots[j * m_AtlasResolutionInSlots + i] == SlotValue.TempOccupied) + { + m_AtlasSlots[j * m_AtlasResolutionInSlots + i] = SlotValue.Free; + } + } + } + } + + internal bool FindSlotInAtlas(int resolution, out int x, out int y) + { + return FindSlotInAtlas(resolution, false, out x, out y); + } + internal bool GetSlotInAtlas(int resolution, out int x, out int y) { if (FindSlotInAtlas(resolution, out x, out y)) @@ -241,8 +278,9 @@ internal void EvictLight(HDAdditionalLightData lightData) #endif m_PlacedShadows.Remove(shadowIdx); m_ShadowsPendingRendering.Remove(shadowIdx); + m_ShadowsWithValidData.Remove(shadowIdx); - MarkEntries((int)recordToRemove.offsetInAtlas.z, (int)recordToRemove.offsetInAtlas.w, HDUtils.DivRoundUp(recordToRemove.viewportSize, m_MinSlotSize), false); + MarkEntries((int)recordToRemove.offsetInAtlas.z, (int)recordToRemove.offsetInAtlas.w, HDUtils.DivRoundUp(recordToRemove.viewportSize, m_MinSlotSize), SlotValue.Free); m_CanTryPlacement = true; } } @@ -362,7 +400,7 @@ private bool PlaceMultipleShadows(int startIdx, int numberOfShadows) int numEntries = HDUtils.DivRoundUp(m_TempListForPlacement[startIdx].viewportSize, m_MinSlotSize); for (int j = 0; j < successfullyPlaced; ++j) { - MarkEntries(placements[j].x, placements[j].y, numEntries, false); + MarkEntries(placements[j].x, placements[j].y, numEntries, SlotValue.Free); } } @@ -446,12 +484,13 @@ internal void DefragmentAtlasAndReRender(HDShadowInitParameters initParams) for (int i = 0; i < m_AtlasResolutionInSlots * m_AtlasResolutionInSlots; ++i) { - m_AtlasSlots[i] = false; + m_AtlasSlots[i] = SlotValue.Free; } // Clear the other state lists. m_PlacedShadows.Clear(); m_ShadowsPendingRendering.Clear(); + m_ShadowsWithValidData.Clear(); m_RecordsPendingPlacement.Clear(); // We'll reset what records are pending. // Sort in order to obtain a more optimal packing. @@ -495,6 +534,33 @@ internal bool ShadowIsPendingRendering(int shadowIdx) return m_ShadowsPendingRendering.ContainsKey(shadowIdx); } + internal bool ShadowHasRenderedAtLeastOnce(int shadowIdx) + { + return m_ShadowsWithValidData.ContainsKey(shadowIdx); + } + + internal bool FullLightShadowHasRenderedAtLeastOnce(HDAdditionalLightData lightData) + { + int cachedShadowIdx = lightData.lightIdxForCachedShadows; + if (lightData.type == HDLightType.Point) + { + bool allRendered = true; + for (int i = 0; i < 6; ++i) + { + allRendered = allRendered && m_ShadowsWithValidData.ContainsKey(cachedShadowIdx + i); + } + + return allRendered; + } + return m_ShadowsWithValidData.ContainsKey(cachedShadowIdx); + } + + internal bool LightIsPlaced(HDAdditionalLightData lightData) + { + int cachedShadowIdx = lightData.lightIdxForCachedShadows; + return cachedShadowIdx >= 0 && m_PlacedShadows.ContainsKey(cachedShadowIdx); + } + internal void ScheduleShadowUpdate(HDAdditionalLightData lightData) { if (!lightData.isActiveAndEnabled) return; @@ -542,6 +608,7 @@ internal void MarkAsRendered(int shadowIdx) if (m_ShadowsPendingRendering.ContainsKey(shadowIdx)) { m_ShadowsPendingRendering.Remove(shadowIdx); + m_ShadowsWithValidData.Add(shadowIdx, shadowIdx); } } diff --git a/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/HDCachedShadowManager.cs b/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/HDCachedShadowManager.cs index d2736bd13ee..29fea17419d 100644 --- a/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/HDCachedShadowManager.cs +++ b/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/HDCachedShadowManager.cs @@ -19,9 +19,13 @@ public class HDCachedShadowManager // Data for cached directional light shadows. private const int m_MaxShadowCascades = 4; private bool[] m_DirectionalShadowPendingUpdate = new bool[m_MaxShadowCascades]; + private bool[] m_DirectionalShadowHasRendered = new bool[m_MaxShadowCascades]; private Vector3 m_CachedDirectionalForward; private Vector3 m_CachedDirectionalAngles; + // Helper array used to check what has been tmp filled. + private (int, int)[] m_TempFilled = new(int, int)[6]; + // Cached atlas internal HDCachedShadowAtlas punctualShadowAtlas; internal HDCachedShadowAtlas areaShadowAtlas; @@ -76,13 +80,34 @@ internal void PrintLightStatusInCachedAtlas() public bool WouldFitInAtlas(int shadowResolution, HDLightType lightType) { bool fits = true; - int x, y; + int x = 0; + int y = 0; if (lightType == HDLightType.Point) { + int fitted = 0; for (int i = 0; i < 6; ++i) { - fits = fits && HDShadowManager.cachedShadowManager.punctualShadowAtlas.FindSlotInAtlas(shadowResolution, out x, out y); + fits = fits && HDShadowManager.cachedShadowManager.punctualShadowAtlas.FindSlotInAtlas(shadowResolution, true, out x, out y); + if (fits) + { + m_TempFilled[fitted++] = (x, y); + } + else + { + // Free the temp filled ones. + for (int filled = 0; filled < fitted; ++filled) + { + HDShadowManager.cachedShadowManager.punctualShadowAtlas.FreeTempFilled(m_TempFilled[filled].Item1, m_TempFilled[filled].Item2, shadowResolution); + } + return false; + } + } + + // Free the temp filled ones. + for (int filled = 0; filled < fitted; ++filled) + { + HDShadowManager.cachedShadowManager.punctualShadowAtlas.FreeTempFilled(m_TempFilled[filled].Item1, m_TempFilled[filled].Item2, shadowResolution); } } @@ -95,6 +120,22 @@ public bool WouldFitInAtlas(int shadowResolution, HDLightType lightType) return fits; } + /// + /// This function verifies if the shadow map for the passed light would fit in the atlas when inserted. + /// + /// The light that we try to fit in the atlas. + /// True if the shadow map would fit in the atlas, false otherwise. If lightData does not cast shadows, false is returned. + public bool WouldFitInAtlas(HDAdditionalLightData lightData) + { + if (lightData.legacyLight.shadows != LightShadows.None) + { + var lightType = lightData.type; + var resolution = lightData.GetResolutionFromSettings(lightData.GetShadowMapType(lightType), m_InitParams); + return WouldFitInAtlas(resolution, lightType); + } + return false; + } + /// /// If a light is added after a scene is loaded, its placement in the atlas might be not optimal and the suboptimal placement might prevent a light to find a place in the atlas. /// This function will force a defragmentation of the atlas containing lights of type lightType and redistributes the shadows inside so that the placement is optimal. Note however that this will also mark the shadow maps @@ -133,6 +174,88 @@ public void ForceRegisterLight(HDAdditionalLightData lightData) RegisterLight(lightData); } + /// + /// This function verifies if the light has its shadow maps placed in the cached shadow atlas. + /// + /// The light that we want to check the placement of. + /// True if the shadow map is already placed in the atlas, false otherwise. + public bool LightHasBeenPlacedInAtlas(HDAdditionalLightData lightData) + { + var lightType = lightData.type; + if (lightType == HDLightType.Area) + return instance.areaShadowAtlas.LightIsPlaced(lightData); + if (lightType == HDLightType.Point || lightType == HDLightType.Spot) + return instance.punctualShadowAtlas.LightIsPlaced(lightData); + if (lightType == HDLightType.Directional) + return !lightData.ShadowIsUpdatedEveryFrame(); + + return false; + } + + /// + /// This function verifies if the light has its shadow maps placed in the cached shadow atlas and if it was rendered at least once. + /// + /// The light that we want to check. + /// Optional parameter required only when querying data about a directional light. It needs to match the number of cascades used by the directional light. + /// True if the shadow map is already placed in the atlas and rendered at least once, false otherwise. + public bool LightHasBeenPlaceAndRenderedAtLeastOnce(HDAdditionalLightData lightData, int numberOfCascades = 0) + { + var lightType = lightData.type; + if (lightType == HDLightType.Area) + { + return instance.areaShadowAtlas.LightIsPlaced(lightData) && instance.areaShadowAtlas.FullLightShadowHasRenderedAtLeastOnce(lightData); + } + if (lightType == HDLightType.Point || lightType == HDLightType.Spot) + { + return instance.punctualShadowAtlas.LightIsPlaced(lightData) && instance.punctualShadowAtlas.FullLightShadowHasRenderedAtLeastOnce(lightData); + } + if (lightType == HDLightType.Directional) + { + Debug.Assert(numberOfCascades <= m_MaxShadowCascades, "numberOfCascades is bigger than the maximum cascades allowed"); + bool hasRendered = true; + for (int i = 0; i < numberOfCascades; ++i) + { + hasRendered = hasRendered && m_DirectionalShadowHasRendered[i]; + } + return !lightData.ShadowIsUpdatedEveryFrame() && hasRendered; + } + + return false; + } + + /// + /// This function verifies if the light if a specific sub-shadow maps is placed in the cached shadow atlas and if it was rendered at least once. + /// + /// The light that we want to check. + /// The sub-shadow index (e.g. cascade index or point light face). It is ignored when irrelevant to the light type. + /// True if the shadow map is already placed in the atlas and rendered at least once, false otherwise. + public bool ShadowHasBeenPlaceAndRenderedAtLeastOnce(HDAdditionalLightData lightData, int shadowIndex) + { + var lightType = lightData.type; + if (lightType == HDLightType.Area) + { + return instance.areaShadowAtlas.LightIsPlaced(lightData) && instance.areaShadowAtlas.ShadowHasRenderedAtLeastOnce(lightData.lightIdxForCachedShadows); + } + if (lightType == HDLightType.Spot) + { + return instance.punctualShadowAtlas.LightIsPlaced(lightData) && instance.punctualShadowAtlas.ShadowHasRenderedAtLeastOnce(lightData.lightIdxForCachedShadows); + } + if (lightType == HDLightType.Point || lightType == HDLightType.Spot) + { + if (lightType == HDLightType.Point) + Debug.Assert(shadowIndex < 6, "Shadow Index is bigger than the available sub-shadows"); + + return instance.punctualShadowAtlas.LightIsPlaced(lightData) && instance.punctualShadowAtlas.ShadowHasRenderedAtLeastOnce(lightData.lightIdxForCachedShadows + shadowIndex); + } + if (lightType == HDLightType.Directional) + { + Debug.Assert(shadowIndex < m_MaxShadowCascades, "Shadow Index is bigger than the maximum cascades allowed"); + return !lightData.ShadowIsUpdatedEveryFrame() && m_DirectionalShadowHasRendered[shadowIndex]; + } + + return false; + } + // ------------------------------------------------------------------------------------------------------------------ private void MarkAllDirectionalShadowsForUpdate() @@ -140,6 +263,7 @@ private void MarkAllDirectionalShadowsForUpdate() for (int i = 0; i < m_MaxShadowCascades; ++i) { m_DirectionalShadowPendingUpdate[i] = true; + m_DirectionalShadowHasRendered[i] = false; } } @@ -277,7 +401,10 @@ internal void MarkShadowAsRendered(int shadowIdx, ShadowMapType shadowMapType) if (shadowMapType == ShadowMapType.AreaLightAtlas) areaShadowAtlas.MarkAsRendered(shadowIdx); if (shadowMapType == ShadowMapType.CascadedDirectional) + { m_DirectionalShadowPendingUpdate[shadowIdx] = false; + m_DirectionalShadowHasRendered[shadowIdx] = true; + } } internal void UpdateResolutionRequest(ref HDShadowResolutionRequest request, int shadowIdx, ShadowMapType shadowMapType)