diff --git a/Runtime/GsplatGlobalRenderer.cs b/Runtime/GsplatGlobalRenderer.cs index f7375e0..8f23768 100644 --- a/Runtime/GsplatGlobalRenderer.cs +++ b/Runtime/GsplatGlobalRenderer.cs @@ -57,6 +57,7 @@ struct RendererParams Matrix4x4[] m_rendererTransformsCache; RendererParams[] m_rendererParamsCache; MaterialPropertyBlock m_globalPropertyBlock; + int m_renderLayer; public bool Valid => m_globalMaterial && m_globalMaterial.Valid() @@ -156,6 +157,7 @@ public void Update(List activeGsplats) { EnsureGlobalBuffers(activeGsplats); if (m_totalSplatCount == 0) return; + m_renderLayer = activeGsplats[0].transform.gameObject.layer; UpdateRendererTransforms(activeGsplats); UpdateRendererParams(activeGsplats); Render(); @@ -492,7 +494,8 @@ void Render() var rp = new RenderParams(m_globalMaterial.Materials[m_globalSHBands]) { worldBounds = new Bounds(Vector3.zero, Vector3.one * 1e6f), - matProps = m_globalPropertyBlock + matProps = m_globalPropertyBlock, + layer = m_renderLayer }; int instances = Mathf.CeilToInt(m_totalRemainingCount / (float)GsplatSettings.Instance.SplatInstanceSize); @@ -537,4 +540,4 @@ public void DisposeGlobalBuffers() m_builtSplatCounts = null; } } -} \ No newline at end of file +} diff --git a/Runtime/GsplatRenderer.cs b/Runtime/GsplatRenderer.cs index a257ce6..08cb84d 100644 --- a/Runtime/GsplatRenderer.cs +++ b/Runtime/GsplatRenderer.cs @@ -34,6 +34,24 @@ public enum GsplatSortMode public bool AsyncUpload; public bool RenderBeforeUploadComplete = true; + [SerializeField] + [Tooltip("When enabled, this renderer can be merged with other compatible renderers for global depth sorting. Disable it for isolated previews or other renderers that must retain an independent draw.")] + bool m_participateInGlobalSort = true; + + public bool ParticipateInGlobalSort + { + get => m_participateInGlobalSort; + set + { + if (m_participateInGlobalSort == value) + return; + + m_participateInGlobalSort = value; + if (isActiveAndEnabled) + GsplatSorter.Instance.MarkGlobalBuffersDirty(); + } + } + [Tooltip("Does cutouts update the Gsplat world bounds? (Costly on moving cutouts)")] public bool CutoutsUpdateBounds = true; @@ -123,6 +141,7 @@ public void OnDrawGizmos() void OnValidate() { ForceRefresh(); + GsplatSorter.Instance.MarkGlobalBuffersDirty(); #if UNITY_EDITOR if (GsplatAsset && AssetDatabase.TryGetGUIDAndLocalFileIdentifier(GsplatAsset, out var guid, out long localId)) @@ -163,12 +182,17 @@ public void Update() { m_renderer.EvaluateRefreshRequired(SortMode, SortRefreshRate - 1, CutoutsRefreshRate - 1); m_renderer.DispatchInitOrder(Cutouts, transform.localToWorldMatrix, CutoutsUpdateBounds); - // When the global sorter has merged all renderers into a single draw call, - // skip the per-renderer draw — GsplatSorter.DrawAll handles rendering. - if (!GsplatSorter.Instance.GlobalRenderEnabled) - m_renderer.Render(transform, gameObject.layer, GammaToLinear, SHDegree, Brightness, - 1.0f - SplatDownscaleFactor, RenderOrder); } } + + // Called by GsplatSorter after it has selected one render path for the frame. + internal void Render() + { + if (!Valid || !GsplatSettings.Instance.Valid || !GsplatSorter.Instance.Valid) + return; + + m_renderer.Render(transform, gameObject.layer, GammaToLinear, SHDegree, Brightness, + 1.0f - SplatDownscaleFactor, RenderOrder); + } } -} \ No newline at end of file +} diff --git a/Runtime/GsplatSorter.cs b/Runtime/GsplatSorter.cs index db476eb..765434c 100644 --- a/Runtime/GsplatSorter.cs +++ b/Runtime/GsplatSorter.cs @@ -68,6 +68,8 @@ public void Dispose() readonly HashSet m_gsplats = new(); readonly HashSet m_camerasInjected = new(); readonly List m_activeGsplats = new(); + readonly List m_globalGsplats = new(); + readonly List m_fallbackGsplats = new(); readonly HashSet m_warnedUncompressed = new(); GsplatSortPass m_sortPass; public const string k_passName = "SortGsplats"; @@ -124,6 +126,8 @@ public void UnregisterGsplat(IGsplat gsplat) } m_activeGsplats.Clear(); + m_globalGsplats.Clear(); + m_fallbackGsplats.Clear(); m_commandBuffer?.Dispose(); m_commandBuffer = null; Camera.onPreCull -= OnPreCullCamera; @@ -146,7 +150,7 @@ public bool GatherGsplatsForCamera(Camera cam) bool CanRenderGlobally() { // renderer_id is packed into 8 bits ([31:24]); max 255 renderers. - if (m_activeGsplats.Count > 255) + if (m_globalGsplats.Count > 255) { Debug.LogError( "[GsplatSorter] Global merge supports at most 255 renderers. Falling back to per-renderer rendering."); @@ -154,17 +158,23 @@ bool CanRenderGlobally() } // Global merge requires every active renderer to use SPARK compression. - foreach (var gs in m_activeGsplats) + foreach (var gs in m_globalGsplats) { if (gs.GsplatResource is GsplatResourceSpark) continue; var obj = gs as UnityEngine.Object; var id = obj ? obj.GetInstanceID() : 0; if (m_warnedUncompressed.Add(id)) Debug.LogWarning( - $"[GsplatSorter] '{obj?.name}' uses an uncompressed asset; global sort requires every active renderer to use SPARK compression. Disabling global sort for this scene — all renderers fall back to per-renderer rendering."); + $"[GsplatSorter] '{obj?.name}' uses an uncompressed asset; global sort requires every participating renderer to use SPARK compression. Participating renderers will fall back to per-renderer rendering."); return false; } + // A single global draw can only have one Unity layer. Mixed-layer sets must retain + // per-renderer draws so each camera's culling mask is respected. + var renderLayer = m_globalGsplats[0].transform.gameObject.layer; + if (m_globalGsplats.Any(gs => gs.transform.gameObject.layer != renderLayer)) + return false; + return true; } @@ -232,21 +242,40 @@ public void DispatchSort(CommandBuffer cmd, Camera camera) // --- Global K-way merge --- if (GlobalRenderEnabled) - m_globalRenderer.DispatchMerge(cmd, m_activeGsplats); + m_globalRenderer.DispatchMerge(cmd, m_globalGsplats); } // Called by GsplatPlayerLoopHook once per frame, before Unity's PostLateUpdate phase public void Update() { - GlobalRenderEnabled = m_globalRenderer.Valid && GsplatSettings.Instance.EnableGlobalSort && - m_activeGsplats.Count >= 2; - if (!GlobalRenderEnabled) return; m_activeGsplats.Clear(); foreach (var gs in m_gsplats.Where(gs => gs is { isActiveAndEnabled: true, Valid: true })) m_activeGsplats.Add(gs); - GlobalRenderEnabled = GlobalRenderEnabled && CanRenderGlobally(); - if (!GlobalRenderEnabled) return; - m_globalRenderer.Update(m_activeGsplats); + + m_globalGsplats.Clear(); + m_fallbackGsplats.Clear(); + foreach (var gs in m_activeGsplats) + { + if (gs is GsplatRenderer { ParticipateInGlobalSort: false }) + m_fallbackGsplats.Add(gs); + else + m_globalGsplats.Add(gs); + } + + GlobalRenderEnabled = m_globalRenderer.Valid && GsplatSettings.Instance.EnableGlobalSort && + m_globalGsplats.Count >= 2 && CanRenderGlobally(); + if (GlobalRenderEnabled) + { + m_globalRenderer.Update(m_globalGsplats); + foreach (var renderer in m_fallbackGsplats.OfType()) + renderer.Render(); + return; + } + + // Select and submit the fallback path here as well, so a runtime transition cannot + // make GsplatRenderer.Update observe the previous frame's global-render state. + foreach (var renderer in m_activeGsplats.OfType()) + renderer.Render(); } public ISorterResource CreateSorterResource(uint count, GraphicsBuffer orderBuffer) diff --git a/Runtime/Shaders/GsplatMergeOrderBuffers.compute b/Runtime/Shaders/GsplatMergeOrderBuffers.compute index 840458b..0f85ffe 100644 --- a/Runtime/Shaders/GsplatMergeOrderBuffers.compute +++ b/Runtime/Shaders/GsplatMergeOrderBuffers.compute @@ -109,7 +109,10 @@ void MergeTwo(uint3 id : SV_DispatchThreadID) float dA = (i < _CountA) ? _DepthsA[_OffsetA + i] : FLOAT_MAX; float dB = (j < _CountB) ? _DepthsB[_OffsetB + j] : FLOAT_MAX; - _OutputOrders[_OutputOffset + p] = (dA <= dB) ? _OrdersA[_OffsetA + i] : _OrdersB[_OffsetB + j]; + if (dA <= dB) + _OutputOrders[_OutputOffset + p] = _OrdersA[_OffsetA + i]; + else + _OutputOrders[_OutputOffset + p] = _OrdersB[_OffsetB + j]; } // Intermediate merge pass — writes merged orders AND depths to scratch.