From 319abb5887b53cac293f8f12a2142877936207ae Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Mon, 29 Jun 2026 10:46:05 +0100 Subject: [PATCH 01/12] Add optional splat depth prepass --- Runtime/GsplatGlobalRenderer.cs | 4 +- Runtime/GsplatRendererImpl.cs | 4 +- Runtime/GsplatSettings.cs | 3 + Runtime/Shaders/Gsplat.shader | 109 +++++++++++++++++++++++++++- Runtime/Shaders/GsplatGlobal.shader | 91 +++++++++++++++++++++++ 5 files changed, 208 insertions(+), 3 deletions(-) diff --git a/Runtime/GsplatGlobalRenderer.cs b/Runtime/GsplatGlobalRenderer.cs index f7375e0..22ab948 100644 --- a/Runtime/GsplatGlobalRenderer.cs +++ b/Runtime/GsplatGlobalRenderer.cs @@ -111,6 +111,7 @@ struct RendererParams static readonly int k_rendererParamsProp = Shader.PropertyToID("_RendererParams"); static readonly int k_totalSplatCount = Shader.PropertyToID("_TotalSplatCount"); static readonly int k_splatInstanceSize = Shader.PropertyToID("_SplatInstanceSize"); + static readonly int k_depthPrepassAlphaCutoff = Shader.PropertyToID("_DepthPrepassAlphaCutoff"); public void InitGlobal(GsplatGlobalMaterial globalMaterial) { @@ -479,6 +480,7 @@ void Render() m_globalPropertyBlock.SetBuffer(k_rendererParamsProp, m_rendererParamsBuffer); m_globalPropertyBlock.SetInteger(k_totalSplatCount, (int)m_totalRemainingCount); m_globalPropertyBlock.SetInteger(k_splatInstanceSize, (int)GsplatSettings.Instance.SplatInstanceSize); + m_globalPropertyBlock.SetFloat(k_depthPrepassAlphaCutoff, GsplatSettings.Instance.DepthPrepassAlphaCutoff); if (m_globalSHBands >= 1) m_globalPropertyBlock.SetBuffer(k_globalSH1Buffer, m_globalSH1Buffer); @@ -537,4 +539,4 @@ public void DisposeGlobalBuffers() m_builtSplatCounts = null; } } -} \ No newline at end of file +} diff --git a/Runtime/GsplatRendererImpl.cs b/Runtime/GsplatRendererImpl.cs index efe457d..ac53065 100644 --- a/Runtime/GsplatRendererImpl.cs +++ b/Runtime/GsplatRendererImpl.cs @@ -34,6 +34,7 @@ public class GsplatRendererImpl static readonly int k_shDegree = Shader.PropertyToID("_SHDegree"); static readonly int k_brightness = Shader.PropertyToID("_Brightness"); static readonly int k_scaleFactor = Shader.PropertyToID("_ScaleFactor"); + static readonly int k_depthPrepassAlphaCutoff = Shader.PropertyToID("_DepthPrepassAlphaCutoff"); uint m_framesBeforeRecomputeSort = 0; uint m_sortsBeforeRecomputeCutouts = 0; @@ -272,6 +273,7 @@ public void Render(Transform transform, int layer, bool gammaToLinear = false, i m_propertyBlock.SetInteger(k_shDegree, Math.Min(m_gsplatAsset.SHBands, shDegree)); m_propertyBlock.SetFloat(k_brightness, brightness); m_propertyBlock.SetFloat(k_scaleFactor, scaleFactor); + m_propertyBlock.SetFloat(k_depthPrepassAlphaCutoff, GsplatSettings.Instance.DepthPrepassAlphaCutoff); m_propertyBlock.SetMatrix(k_matrixM, transform.localToWorldMatrix); uint order = Math.Clamp(renderOrder, 0, GsplatSettings.Instance.MaxRenderOrder - 1); @@ -286,4 +288,4 @@ public void Render(Transform transform, int layer, bool gammaToLinear = false, i Mathf.CeilToInt(m_remainingCount / (float)GsplatSettings.Instance.SplatInstanceSize)); } } -} \ No newline at end of file +} diff --git a/Runtime/GsplatSettings.cs b/Runtime/GsplatSettings.cs index db1195c..93e4e9f 100644 --- a/Runtime/GsplatSettings.cs +++ b/Runtime/GsplatSettings.cs @@ -76,6 +76,8 @@ public static GsplatSettings Instance public uint SplatInstanceSize; public uint UploadBatchSize; + [Tooltip("Alpha cutoff used by the optional depth prepass. Values above 1 disable depth writes.")] + [Range(0f, 1.1f)] public float DepthPrepassAlphaCutoff = 1.1f; [Range(1, 20)] public uint MaxRenderOrder; public bool DisplayBoundingBoxes; @@ -136,6 +138,7 @@ public void Reset() Materials = DefaultMaterials; SplatInstanceSize = 128; UploadBatchSize = 100000; + DepthPrepassAlphaCutoff = 1.1f; MaxRenderOrder = 1; DisplayBoundingBoxes = false; CameraTranslationRefreshTreshold = 0.2f; diff --git a/Runtime/Shaders/Gsplat.shader b/Runtime/Shaders/Gsplat.shader index 4c8e655..eb91813 100644 --- a/Runtime/Shaders/Gsplat.shader +++ b/Runtime/Shaders/Gsplat.shader @@ -12,6 +12,113 @@ Shader "Gsplat/Standard" "Queue"="Transparent" } + Pass + { + Name "DepthPrepass" + ZWrite On + ZTest LEqual + ColorMask 0 + Cull Off + + HLSLPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma require compute + #pragma multi_compile SH_BANDS_0 SH_BANDS_1 SH_BANDS_2 SH_BANDS_3 SH_BANDS_4 + #pragma multi_compile UNCOMPRESSED SPARK + + #include "UnityCG.cginc" + #include "Gsplat.hlsl" + #ifdef UNCOMPRESSED + #include "GsplatUncompressed.hlsl" + #endif + #ifdef SPARK + #include "GsplatSpark.hlsl" + #endif + + int _SplatCount; + int _SplatInstanceSize; + float4x4 _MATRIX_M; + float _ScaleFactor; + float _DepthPrepassAlphaCutoff; + StructuredBuffer _OrderBuffer; + + struct appdata + { + float4 vertex : POSITION; + #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) + uint instanceID : SV_InstanceID; + #endif + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + bool InitSource(appdata v, out SplatSource source) + { + #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) + source.order = v.instanceID * _SplatInstanceSize + asuint(v.vertex.z); + #else + source.order = unity_InstanceID * _SplatInstanceSize + asuint(v.vertex.z); + #endif + + if (source.order >= _SplatCount) + return false; + + source.id = _OrderBuffer[source.order]; + source.cornerUV = float2(v.vertex.x, v.vertex.y) * _ScaleFactor; + return true; + } + + struct v2f + { + float2 uv : TEXCOORD0; + float alpha : TEXCOORD1; + float4 vertex : SV_POSITION; + UNITY_VERTEX_OUTPUT_STEREO + }; + + v2f vert(appdata v) + { + v2f o; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_OUTPUT(v2f, o); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); + o.vertex = discardVec; + + SplatSource source; + if (!InitSource(v, source)) + return o; + + SplatCenter center; + SplatCorner corner; + float4 color; + if (!InitSplatData(source, mul(UNITY_MATRIX_V, _MATRIX_M), center, corner, color)) + return o; + + ClipCorner(corner, color.w); + + o.vertex = center.proj + float4(corner.offset.x, _ProjectionParams.x * corner.offset.y, 0, 0); + o.alpha = color.a; + o.uv = corner.uv; + return o; + } + + float4 frag(v2f i) : SV_Target + { + float A = dot(i.uv, i.uv); + if (A > 1.0) discard; + + float2 absUV = abs(i.uv); + float maxUV = max(absUV.x, absUV.y); + + float falloff = -exp((maxUV - _ScaleFactor * 1.16) * 25 * _ScaleFactor); + float alpha = (exp(-A * 4.0) + falloff) * i.alpha; + + if (alpha < _DepthPrepassAlphaCutoff) discard; + return 0; + } + ENDHLSL + } + Pass { ZWrite Off @@ -132,4 +239,4 @@ Shader "Gsplat/Standard" } } -} \ No newline at end of file +} diff --git a/Runtime/Shaders/GsplatGlobal.shader b/Runtime/Shaders/GsplatGlobal.shader index 5c7f9dc..6d0a1a2 100644 --- a/Runtime/Shaders/GsplatGlobal.shader +++ b/Runtime/Shaders/GsplatGlobal.shader @@ -17,6 +17,97 @@ Shader "Gsplat/Global" "Queue"="Transparent" } + Pass + { + Name "DepthPrepass" + ZWrite On + ZTest LEqual + ColorMask 0 + Cull Off + + HLSLPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma require compute + #pragma multi_compile SH_BANDS_0 SH_BANDS_1 SH_BANDS_2 SH_BANDS_3 SH_BANDS_4 + + #include "UnityCG.cginc" + + int _SplatInstanceSize; + float _DepthPrepassAlphaCutoff; + + #include "GsplatSparkGlobal.hlsl" + + struct appdata + { + float4 vertex : POSITION; + #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) + uint instanceID : SV_InstanceID; + #endif + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct v2f + { + float2 uv : TEXCOORD0; + nointerpolation uint rendererId : TEXCOORD1; + float alpha : TEXCOORD2; + float4 vertex : SV_POSITION; + UNITY_VERTEX_OUTPUT_STEREO + }; + + v2f vert(appdata v) + { + v2f o; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_OUTPUT(v2f, o); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); + o.vertex = discardVec; + + #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) + uint instanceId = v.instanceID; + #else + uint instanceId = unity_InstanceID; + #endif + + GlobalSplatSource source; + if (!InitGlobalSource(instanceId, v.vertex.xyz, source)) + return o; + + SplatCenter center; + SplatCorner corner; + float4 color; + if (!InitGlobalSplatData(source, center, corner, color)) + return o; + + ClipCorner(corner, color.a); + + o.vertex = center.proj + float4(corner.offset.x, _ProjectionParams.x * corner.offset.y, 0, 0); + o.alpha = color.a; + o.uv = corner.uv; + o.rendererId = source.rendererId; + return o; + } + + float4 frag(v2f i) : SV_Target + { + RendererParams p = _RendererParams[i.rendererId]; + + float A = dot(i.uv, i.uv); + if (A > 1.0) discard; + + float2 absUV = abs(i.uv); + float maxUV = max(absUV.x, absUV.y); + + float falloff = -exp((maxUV - p.scaleFactor * 1.16) * 25 * p.scaleFactor); + float alpha = (exp(-A * 4.0) + falloff) * i.alpha; + + if (alpha < _DepthPrepassAlphaCutoff) discard; + return 0; + } + ENDHLSL + } + Pass { ZWrite Off From 1e2e11f939aaf6ecec108d6f22bbc2a60850d982 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Mon, 29 Jun 2026 12:38:41 +0100 Subject: [PATCH 02/12] Depth compositing fixes --- Editor/GsplatSettingsProvider.cs | 3 +- Runtime/GsplatGlobalMaterial.cs | 35 ++++- Runtime/GsplatGlobalRenderer.cs | 81 +++++++++++- Runtime/GsplatMaterial.cs | 30 +++++ Runtime/GsplatRenderer.cs | 29 ++++- Runtime/GsplatRendererImpl.cs | 87 +++++++++++++ Runtime/GsplatSorter.cs | 79 ++++++++++++ Runtime/Materials/GsplatGlobal.asset | 1 + Runtime/Materials/GsplatGlobalDepth.mat | 31 +++++ Runtime/Materials/GsplatGlobalDepth.mat.meta | 8 ++ Runtime/Materials/GsplatSpark.asset | 1 + Runtime/Materials/GsplatSparkDepth.mat | 32 +++++ Runtime/Materials/GsplatSparkDepth.mat.meta | 8 ++ Runtime/Materials/GsplatUncompressed.asset | 1 + Runtime/Materials/GsplatUncompressedDepth.mat | 32 +++++ .../GsplatUncompressedDepth.mat.meta | 8 ++ Runtime/SRP/GsplatURPFeature.cs | 80 ++++++++++++ Runtime/Shaders/Gsplat.shader | 107 ---------------- Runtime/Shaders/GsplatDepth.shader | 121 ++++++++++++++++++ Runtime/Shaders/GsplatDepth.shader.meta | 9 ++ Runtime/Shaders/GsplatGlobal.shader | 91 ------------- Runtime/Shaders/GsplatGlobalDepth.shader | 105 +++++++++++++++ Runtime/Shaders/GsplatGlobalDepth.shader.meta | 9 ++ 23 files changed, 784 insertions(+), 204 deletions(-) create mode 100644 Runtime/Materials/GsplatGlobalDepth.mat create mode 100644 Runtime/Materials/GsplatGlobalDepth.mat.meta create mode 100644 Runtime/Materials/GsplatSparkDepth.mat create mode 100644 Runtime/Materials/GsplatSparkDepth.mat.meta create mode 100644 Runtime/Materials/GsplatUncompressedDepth.mat create mode 100644 Runtime/Materials/GsplatUncompressedDepth.mat.meta create mode 100644 Runtime/Shaders/GsplatDepth.shader create mode 100644 Runtime/Shaders/GsplatDepth.shader.meta create mode 100644 Runtime/Shaders/GsplatGlobalDepth.shader create mode 100644 Runtime/Shaders/GsplatGlobalDepth.shader.meta diff --git a/Editor/GsplatSettingsProvider.cs b/Editor/GsplatSettingsProvider.cs index 31cf304..ce82796 100644 --- a/Editor/GsplatSettingsProvider.cs +++ b/Editor/GsplatSettingsProvider.cs @@ -26,6 +26,7 @@ public override void OnGUI(string searchContext) EditorGUILayout.PropertyField(m_gsplatSettings.FindProperty(nameof(GsplatSettings.ComputeShader))); EditorGUILayout.PropertyField(m_gsplatSettings.FindProperty(nameof(GsplatSettings.SplatInstanceSize))); EditorGUILayout.PropertyField(m_gsplatSettings.FindProperty(nameof(GsplatSettings.UploadBatchSize))); + EditorGUILayout.PropertyField(m_gsplatSettings.FindProperty(nameof(GsplatSettings.DepthPrepassAlphaCutoff))); EditorGUILayout.PropertyField(m_gsplatSettings.FindProperty(nameof(GsplatSettings.MaxRenderOrder))); EditorGUILayout.PropertyField( m_gsplatSettings.FindProperty(nameof(GsplatSettings.CameraTranslationRefreshTreshold))); @@ -73,4 +74,4 @@ public static SettingsProvider CreateGsplatSettingsProvider() return provider; } } -} \ No newline at end of file +} diff --git a/Runtime/GsplatGlobalMaterial.cs b/Runtime/GsplatGlobalMaterial.cs index d50f215..a124e27 100644 --- a/Runtime/GsplatGlobalMaterial.cs +++ b/Runtime/GsplatGlobalMaterial.cs @@ -9,10 +9,12 @@ namespace Gsplat public class GsplatGlobalMaterial : ScriptableObject { public Material DefaultMaterial; + public Material DefaultDepthMaterial; public ComputeShader MergeShader; public ComputeShader CopyBufferShader; Material[] m_materials; // indexed by SH band (0–4) + Material[] m_depthMaterials; // indexed by SH band (0–4) /// /// Returns five Material instances — one per SH band level — with the appropriate @@ -41,8 +43,35 @@ public Material[] Materials } } - public void Reset() => m_materials = null; + public Material[] DepthMaterials + { + get + { + if (m_depthMaterials != null && m_depthMaterials[0] != null) + return m_depthMaterials; + + m_depthMaterials = new Material[5]; + for (int i = 0; i < 5; i++) + { + m_depthMaterials[i] = new Material(DefaultDepthMaterial); + m_depthMaterials[i].DisableKeyword("SH_BANDS_0"); + m_depthMaterials[i].DisableKeyword("SH_BANDS_1"); + m_depthMaterials[i].DisableKeyword("SH_BANDS_2"); + m_depthMaterials[i].DisableKeyword("SH_BANDS_3"); + m_depthMaterials[i].DisableKeyword("SH_BANDS_4"); + m_depthMaterials[i].EnableKeyword($"SH_BANDS_{i}"); + } + + return m_depthMaterials; + } + } + + public void Reset() + { + m_materials = null; + m_depthMaterials = null; + } - public bool Valid() => MergeShader && CopyBufferShader && DefaultMaterial; + public bool Valid() => MergeShader && CopyBufferShader && DefaultMaterial && DefaultDepthMaterial; } -} \ No newline at end of file +} diff --git a/Runtime/GsplatGlobalRenderer.cs b/Runtime/GsplatGlobalRenderer.cs index 22ab948..d65181c 100644 --- a/Runtime/GsplatGlobalRenderer.cs +++ b/Runtime/GsplatGlobalRenderer.cs @@ -5,6 +5,9 @@ using System.Linq; using UnityEngine; using UnityEngine.Rendering; +#if UNITY_6000_0_OR_NEWER +using UnityEngine.Rendering.RenderGraphModule; +#endif namespace Gsplat { @@ -159,7 +162,8 @@ public void Update(List activeGsplats) if (m_totalSplatCount == 0) return; UpdateRendererTransforms(activeGsplats); UpdateRendererParams(activeGsplats); - Render(); + if (!GraphicsSettings.currentRenderPipeline) + Render(); } public void DispatchMerge(CommandBuffer cmd, List activeGsplats) @@ -501,6 +505,81 @@ void Render() Graphics.RenderMeshPrimitives(rp, GsplatSettings.Instance.Mesh, 0, instances); } + public void RenderColor(CommandBuffer cmd) + { + if (m_globalBuffersDirty || m_globalOrderBuffer == null || m_totalRemainingCount == 0) return; + + BindGlobalDrawProperties(); + + int instances = Mathf.CeilToInt(m_totalRemainingCount / (float)GsplatSettings.Instance.SplatInstanceSize); + cmd.DrawMeshInstancedProcedural(GsplatSettings.Instance.Mesh, 0, m_globalMaterial.Materials[m_globalSHBands], + 0, instances, m_globalPropertyBlock); + } + + public void RenderDepthPrepass(CommandBuffer cmd) + { + if (GsplatSettings.Instance.DepthPrepassAlphaCutoff > 1.0f) + return; + + if (m_globalBuffersDirty || m_globalOrderBuffer == null || m_totalRemainingCount == 0) return; + + BindGlobalDrawProperties(); + + int instances = Mathf.CeilToInt(m_totalRemainingCount / (float)GsplatSettings.Instance.SplatInstanceSize); + cmd.DrawMeshInstancedProcedural(GsplatSettings.Instance.Mesh, 0, + m_globalMaterial.DepthMaterials[m_globalSHBands], 0, instances, m_globalPropertyBlock); + } + +#if UNITY_6000_0_OR_NEWER + public void RenderColor(RasterCommandBuffer cmd) + { + if (m_globalBuffersDirty || m_globalOrderBuffer == null || m_totalRemainingCount == 0) return; + + BindGlobalDrawProperties(); + + int instances = Mathf.CeilToInt(m_totalRemainingCount / (float)GsplatSettings.Instance.SplatInstanceSize); + cmd.DrawMeshInstancedProcedural(GsplatSettings.Instance.Mesh, 0, m_globalMaterial.Materials[m_globalSHBands], + 0, instances, m_globalPropertyBlock); + } + + public void RenderDepthPrepass(RasterCommandBuffer cmd) + { + if (GsplatSettings.Instance.DepthPrepassAlphaCutoff > 1.0f) + return; + + if (m_globalBuffersDirty || m_globalOrderBuffer == null || m_totalRemainingCount == 0) return; + + BindGlobalDrawProperties(); + + int instances = Mathf.CeilToInt(m_totalRemainingCount / (float)GsplatSettings.Instance.SplatInstanceSize); + cmd.DrawMeshInstancedProcedural(GsplatSettings.Instance.Mesh, 0, + m_globalMaterial.DepthMaterials[m_globalSHBands], 0, instances, m_globalPropertyBlock); + } +#endif + + void BindGlobalDrawProperties() + { + m_globalPropertyBlock ??= new MaterialPropertyBlock(); + m_globalPropertyBlock.Clear(); + m_globalPropertyBlock.SetBuffer(k_globalOrderBuffer, m_globalOrderBuffer); + m_globalPropertyBlock.SetBuffer(k_globalPackedBuffer, m_globalPackedBuffer); + m_globalPropertyBlock.SetBuffer(k_rendererOffsetsProp, m_rendererOffsetsBuffer); + m_globalPropertyBlock.SetBuffer(k_rendererTransformsProp, m_rendererTransformsBuffer); + m_globalPropertyBlock.SetBuffer(k_rendererParamsProp, m_rendererParamsBuffer); + m_globalPropertyBlock.SetInteger(k_totalSplatCount, (int)m_totalRemainingCount); + m_globalPropertyBlock.SetInteger(k_splatInstanceSize, (int)GsplatSettings.Instance.SplatInstanceSize); + m_globalPropertyBlock.SetFloat(k_depthPrepassAlphaCutoff, GsplatSettings.Instance.DepthPrepassAlphaCutoff); + + if (m_globalSHBands >= 1) + m_globalPropertyBlock.SetBuffer(k_globalSH1Buffer, m_globalSH1Buffer); + if (m_globalSHBands >= 2) + m_globalPropertyBlock.SetBuffer(k_globalSH2Buffer, m_globalSH2Buffer); + if (m_globalSHBands >= 3) + m_globalPropertyBlock.SetBuffer(k_globalSH3Buffer, m_globalSH3Buffer); + if (m_globalSHBands >= 4) + m_globalPropertyBlock.SetBuffer(k_globalSH4Buffer, m_globalSH4Buffer); + } + // ----------------------------------------------------------------------- // Cleanup diff --git a/Runtime/GsplatMaterial.cs b/Runtime/GsplatMaterial.cs index da028bc..0d9600a 100644 --- a/Runtime/GsplatMaterial.cs +++ b/Runtime/GsplatMaterial.cs @@ -9,11 +9,14 @@ namespace Gsplat public class GsplatMaterial : ScriptableObject { public Material DefaultMaterial; + public Material DefaultDepthMaterial; private Material[][] m_material; + private Material[][] m_depthMaterial; public void Reset() { m_material = null; + m_depthMaterial = null; } public Material[][] Materials // materials generated with SH bands from 0 to 4 and custom renderOrders @@ -43,6 +46,33 @@ public Material[][] Materials // materials generated with SH bands from 0 to 4 a } } + public Material[][] DepthMaterials + { + get + { + if (m_depthMaterial != null && m_depthMaterial[0][0] != null) + return m_depthMaterial; + + m_depthMaterial = new Material[5][]; + for (var i = 0; i < 5; ++i) + { + m_depthMaterial[i] = new Material[GsplatSettings.Instance.MaxRenderOrder]; + for (var j = 0; j < GsplatSettings.Instance.MaxRenderOrder; ++j) + { + m_depthMaterial[i][j] = new Material(DefaultDepthMaterial); + m_depthMaterial[i][j].DisableKeyword($"SH_BANDS_0"); + m_depthMaterial[i][j].DisableKeyword($"SH_BANDS_1"); + m_depthMaterial[i][j].DisableKeyword($"SH_BANDS_2"); + m_depthMaterial[i][j].DisableKeyword($"SH_BANDS_3"); + m_depthMaterial[i][j].DisableKeyword($"SH_BANDS_4"); + m_depthMaterial[i][j].EnableKeyword($"SH_BANDS_{i}"); + m_depthMaterial[i][j].renderQueue = 3000 + j; + } + } + return m_depthMaterial; + } + } + public ComputeShader CalcDepthShader; public ComputeShader InitOrderShader; } diff --git a/Runtime/GsplatRenderer.cs b/Runtime/GsplatRenderer.cs index a257ce6..4543122 100644 --- a/Runtime/GsplatRenderer.cs +++ b/Runtime/GsplatRenderer.cs @@ -5,6 +5,9 @@ using UnityEditor; using UnityEngine; using UnityEngine.Rendering; +#if UNITY_6000_0_OR_NEWER +using UnityEngine.Rendering.RenderGraphModule; +#endif namespace Gsplat { @@ -87,6 +90,30 @@ public GsplatCutout[] Cutouts public void ComputeDepth(CommandBuffer cmd, Matrix4x4 matrixMv) => m_renderer.ComputeDepth(cmd, matrixMv); + public void RenderColor(CommandBuffer cmd, Camera camera) + { + m_renderer.RenderColor(cmd, transform, GammaToLinear, SHDegree, Brightness, + 1.0f - SplatDownscaleFactor, RenderOrder); + } + + public void RenderDepthPrepass(CommandBuffer cmd, Camera camera) + { + m_renderer.RenderDepthPrepass(cmd, transform, 1.0f - SplatDownscaleFactor, RenderOrder); + } + +#if UNITY_6000_0_OR_NEWER + public void RenderColor(RasterCommandBuffer cmd, Camera camera) + { + m_renderer.RenderColor(cmd, transform, GammaToLinear, SHDegree, Brightness, + 1.0f - SplatDownscaleFactor, RenderOrder); + } + + public void RenderDepthPrepass(RasterCommandBuffer cmd, Camera camera) + { + m_renderer.RenderDepthPrepass(cmd, transform, 1.0f - SplatDownscaleFactor, RenderOrder); + } +#endif + void OnEnable() { GsplatSorter.Instance.RegisterGsplat(this); @@ -165,7 +192,7 @@ public void Update() 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) + if (!GraphicsSettings.currentRenderPipeline && !GsplatSorter.Instance.GlobalRenderEnabled) m_renderer.Render(transform, gameObject.layer, GammaToLinear, SHDegree, Brightness, 1.0f - SplatDownscaleFactor, RenderOrder); } diff --git a/Runtime/GsplatRendererImpl.cs b/Runtime/GsplatRendererImpl.cs index ac53065..76272d8 100644 --- a/Runtime/GsplatRendererImpl.cs +++ b/Runtime/GsplatRendererImpl.cs @@ -5,6 +5,9 @@ using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; +#if UNITY_6000_0_OR_NEWER +using UnityEngine.Rendering.RenderGraphModule; +#endif using Vector3 = UnityEngine.Vector3; namespace Gsplat @@ -287,5 +290,89 @@ public void Render(Transform transform, int layer, bool gammaToLinear = false, i Graphics.RenderMeshPrimitives(rp, GsplatSettings.Instance.Mesh, 0, Mathf.CeilToInt(m_remainingCount / (float)GsplatSettings.Instance.SplatInstanceSize)); } + + public void RenderDepthPrepass(CommandBuffer cmd, Transform transform, float scaleFactor = 1.0f, + uint renderOrder = 0) + { + if (m_remainingCount <= 0 || GsplatSettings.Instance.DepthPrepassAlphaCutoff > 1.0f) + return; + + m_propertyBlock.SetInteger(k_splatCount, (int)m_remainingCount); + m_propertyBlock.SetInteger(k_splatInstanceSize, (int)GsplatSettings.Instance.SplatInstanceSize); + m_propertyBlock.SetFloat(k_scaleFactor, scaleFactor); + m_propertyBlock.SetFloat(k_depthPrepassAlphaCutoff, GsplatSettings.Instance.DepthPrepassAlphaCutoff); + m_propertyBlock.SetMatrix(k_matrixM, transform.localToWorldMatrix); + + uint order = Math.Clamp(renderOrder, 0, GsplatSettings.Instance.MaxRenderOrder - 1); + int instances = Mathf.CeilToInt(m_remainingCount / (float)GsplatSettings.Instance.SplatInstanceSize); + cmd.DrawMeshInstancedProcedural(GsplatSettings.Instance.Mesh, 0, + m_gsplatAsset.GsplatMaterial.DepthMaterials[m_gsplatAsset.SHBands][order], 0, instances, + m_propertyBlock); + } + +#if UNITY_6000_0_OR_NEWER + public void RenderDepthPrepass(RasterCommandBuffer cmd, Transform transform, float scaleFactor = 1.0f, + uint renderOrder = 0) + { + if (m_remainingCount <= 0 || GsplatSettings.Instance.DepthPrepassAlphaCutoff > 1.0f) + return; + + m_propertyBlock.SetInteger(k_splatCount, (int)m_remainingCount); + m_propertyBlock.SetInteger(k_splatInstanceSize, (int)GsplatSettings.Instance.SplatInstanceSize); + m_propertyBlock.SetFloat(k_scaleFactor, scaleFactor); + m_propertyBlock.SetFloat(k_depthPrepassAlphaCutoff, GsplatSettings.Instance.DepthPrepassAlphaCutoff); + m_propertyBlock.SetMatrix(k_matrixM, transform.localToWorldMatrix); + + uint order = Math.Clamp(renderOrder, 0, GsplatSettings.Instance.MaxRenderOrder - 1); + int instances = Mathf.CeilToInt(m_remainingCount / (float)GsplatSettings.Instance.SplatInstanceSize); + cmd.DrawMeshInstancedProcedural(GsplatSettings.Instance.Mesh, 0, + m_gsplatAsset.GsplatMaterial.DepthMaterials[m_gsplatAsset.SHBands][order], 0, instances, + m_propertyBlock); + } +#endif + + public void RenderColor(CommandBuffer cmd, Transform transform, bool gammaToLinear = false, int shDegree = 3, + float brightness = 1.0f, float scaleFactor = 1.0f, uint renderOrder = 0) + { + if (m_remainingCount <= 0) + return; + + m_propertyBlock.SetInteger(k_splatCount, (int)m_remainingCount); + m_propertyBlock.SetInteger(k_gammaToLinear, gammaToLinear ? 1 : 0); + m_propertyBlock.SetInteger(k_splatInstanceSize, (int)GsplatSettings.Instance.SplatInstanceSize); + m_propertyBlock.SetInteger(k_shDegree, Math.Min(m_gsplatAsset.SHBands, shDegree)); + m_propertyBlock.SetFloat(k_brightness, brightness); + m_propertyBlock.SetFloat(k_scaleFactor, scaleFactor); + m_propertyBlock.SetFloat(k_depthPrepassAlphaCutoff, GsplatSettings.Instance.DepthPrepassAlphaCutoff); + m_propertyBlock.SetMatrix(k_matrixM, transform.localToWorldMatrix); + + uint order = Math.Clamp(renderOrder, 0, GsplatSettings.Instance.MaxRenderOrder - 1); + int instances = Mathf.CeilToInt(m_remainingCount / (float)GsplatSettings.Instance.SplatInstanceSize); + cmd.DrawMeshInstancedProcedural(GsplatSettings.Instance.Mesh, 0, m_gsplatAsset.Materials[order], 0, + instances, m_propertyBlock); + } + +#if UNITY_6000_0_OR_NEWER + public void RenderColor(RasterCommandBuffer cmd, Transform transform, bool gammaToLinear = false, + int shDegree = 3, float brightness = 1.0f, float scaleFactor = 1.0f, uint renderOrder = 0) + { + if (m_remainingCount <= 0) + return; + + m_propertyBlock.SetInteger(k_splatCount, (int)m_remainingCount); + m_propertyBlock.SetInteger(k_gammaToLinear, gammaToLinear ? 1 : 0); + m_propertyBlock.SetInteger(k_splatInstanceSize, (int)GsplatSettings.Instance.SplatInstanceSize); + m_propertyBlock.SetInteger(k_shDegree, Math.Min(m_gsplatAsset.SHBands, shDegree)); + m_propertyBlock.SetFloat(k_brightness, brightness); + m_propertyBlock.SetFloat(k_scaleFactor, scaleFactor); + m_propertyBlock.SetFloat(k_depthPrepassAlphaCutoff, GsplatSettings.Instance.DepthPrepassAlphaCutoff); + m_propertyBlock.SetMatrix(k_matrixM, transform.localToWorldMatrix); + + uint order = Math.Clamp(renderOrder, 0, GsplatSettings.Instance.MaxRenderOrder - 1); + int instances = Mathf.CeilToInt(m_remainingCount / (float)GsplatSettings.Instance.SplatInstanceSize); + cmd.DrawMeshInstancedProcedural(GsplatSettings.Instance.Mesh, 0, m_gsplatAsset.Materials[order], 0, + instances, m_propertyBlock); + } +#endif } } diff --git a/Runtime/GsplatSorter.cs b/Runtime/GsplatSorter.cs index db476eb..c462c56 100644 --- a/Runtime/GsplatSorter.cs +++ b/Runtime/GsplatSorter.cs @@ -6,6 +6,9 @@ using System.Linq; using UnityEngine; using UnityEngine.Rendering; +#if UNITY_6000_0_OR_NEWER +using UnityEngine.Rendering.RenderGraphModule; +#endif namespace Gsplat { @@ -18,6 +21,12 @@ public interface IGsplat public bool Valid { get; } public bool ComputeSortRequired { get; } public void ComputeDepth(CommandBuffer cmd, Matrix4x4 matrixMv); + public void RenderColor(CommandBuffer cmd, Camera camera); + public void RenderDepthPrepass(CommandBuffer cmd, Camera camera); +#if UNITY_6000_0_OR_NEWER + public void RenderColor(RasterCommandBuffer cmd, Camera camera); + public void RenderDepthPrepass(RasterCommandBuffer cmd, Camera camera); +#endif // Used by GsplatSorter to populate the global packed buffer. public GsplatResource GsplatResource { get; } @@ -235,6 +244,76 @@ public void DispatchSort(CommandBuffer cmd, Camera camera) m_globalRenderer.DispatchMerge(cmd, m_activeGsplats); } + public void RenderDepthPrepass(CommandBuffer cmd, Camera camera) + { + if (GlobalRenderEnabled) + { + m_globalRenderer.RenderDepthPrepass(cmd); + return; + } + + foreach (var gs in m_activeGsplats) + { + if (gs.RemainingCount <= 0) continue; + var layer = (gs as Component)?.gameObject.layer ?? 0; + if (camera && (camera.cullingMask & (1 << layer)) == 0) continue; + gs.RenderDepthPrepass(cmd, camera); + } + } + + public void RenderColor(CommandBuffer cmd, Camera camera) + { + if (GlobalRenderEnabled) + { + m_globalRenderer.RenderColor(cmd); + return; + } + + foreach (var gs in m_activeGsplats) + { + if (gs.RemainingCount <= 0) continue; + var layer = (gs as Component)?.gameObject.layer ?? 0; + if (camera && (camera.cullingMask & (1 << layer)) == 0) continue; + gs.RenderColor(cmd, camera); + } + } + +#if UNITY_6000_0_OR_NEWER + public void RenderDepthPrepass(RasterCommandBuffer cmd, Camera camera) + { + if (GlobalRenderEnabled) + { + m_globalRenderer.RenderDepthPrepass(cmd); + return; + } + + foreach (var gs in m_activeGsplats) + { + if (gs.RemainingCount <= 0) continue; + var layer = (gs as Component)?.gameObject.layer ?? 0; + if (camera && (camera.cullingMask & (1 << layer)) == 0) continue; + gs.RenderDepthPrepass(cmd, camera); + } + } + + public void RenderColor(RasterCommandBuffer cmd, Camera camera) + { + if (GlobalRenderEnabled) + { + m_globalRenderer.RenderColor(cmd); + return; + } + + foreach (var gs in m_activeGsplats) + { + if (gs.RemainingCount <= 0) continue; + var layer = (gs as Component)?.gameObject.layer ?? 0; + if (camera && (camera.cullingMask & (1 << layer)) == 0) continue; + gs.RenderColor(cmd, camera); + } + } +#endif + // Called by GsplatPlayerLoopHook once per frame, before Unity's PostLateUpdate phase public void Update() { diff --git a/Runtime/Materials/GsplatGlobal.asset b/Runtime/Materials/GsplatGlobal.asset index 410a6e5..b2fa2f8 100644 --- a/Runtime/Materials/GsplatGlobal.asset +++ b/Runtime/Materials/GsplatGlobal.asset @@ -13,5 +13,6 @@ MonoBehaviour: m_Name: GsplatGlobal m_EditorClassIdentifier: DefaultMaterial: {fileID: 2100000, guid: e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8, type: 2} + DefaultDepthMaterial: {fileID: 2100000, guid: 4d8e9f104a5b6c7d8e1f0a9b6c5d4e3f, type: 2} MergeShader: {fileID: 7200000, guid: a1e5c8f2b4d7903e6f2a8c1d5e9b3f70, type: 3} CopyBufferShader: {fileID: 7200000, guid: 3f7a1c4e8b2d9f506e1a4c7d3b8f2e90, type: 3} diff --git a/Runtime/Materials/GsplatGlobalDepth.mat b/Runtime/Materials/GsplatGlobalDepth.mat new file mode 100644 index 0000000..0b17742 --- /dev/null +++ b/Runtime/Materials/GsplatGlobalDepth.mat @@ -0,0 +1,31 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: GsplatGlobalDepth + m_Shader: {fileID: 4800000, guid: 9a5c8e1f2b3d4706a4f9c0d1e2b3a456, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: + - SH_BANDS_3 + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: [] + m_Ints: [] + m_Floats: [] + m_Colors: [] + m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Runtime/Materials/GsplatGlobalDepth.mat.meta b/Runtime/Materials/GsplatGlobalDepth.mat.meta new file mode 100644 index 0000000..b1b6fb3 --- /dev/null +++ b/Runtime/Materials/GsplatGlobalDepth.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4d8e9f104a5b6c7d8e1f0a9b6c5d4e3f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Materials/GsplatSpark.asset b/Runtime/Materials/GsplatSpark.asset index 2e3acc6..f78cdba 100644 --- a/Runtime/Materials/GsplatSpark.asset +++ b/Runtime/Materials/GsplatSpark.asset @@ -13,5 +13,6 @@ MonoBehaviour: m_Name: GsplatSpark m_EditorClassIdentifier: DefaultMaterial: {fileID: 2100000, guid: 0fd25878460c14a46946f5bffc5ee42f, type: 2} + DefaultDepthMaterial: {fileID: 2100000, guid: 3c7d8e9f104a5b6c8d2e1f0a9b6c5d4e, type: 2} CalcDepthShader: {fileID: 7200000, guid: f234fe86176948548be7fad55f5d6147, type: 3} InitOrderShader: {fileID: 7200000, guid: 9f8e041cb7bfae141b2e6ee80402bf45, type: 3} diff --git a/Runtime/Materials/GsplatSparkDepth.mat b/Runtime/Materials/GsplatSparkDepth.mat new file mode 100644 index 0000000..c77b955 --- /dev/null +++ b/Runtime/Materials/GsplatSparkDepth.mat @@ -0,0 +1,32 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: GsplatSparkDepth + m_Shader: {fileID: 4800000, guid: 8f4d7a3b9c2e4615a0b1c6d8e9f20314, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: + - SH_BANDS_0 + - SPARK + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: [] + m_Ints: [] + m_Floats: [] + m_Colors: [] + m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Runtime/Materials/GsplatSparkDepth.mat.meta b/Runtime/Materials/GsplatSparkDepth.mat.meta new file mode 100644 index 0000000..3cc9523 --- /dev/null +++ b/Runtime/Materials/GsplatSparkDepth.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3c7d8e9f104a5b6c8d2e1f0a9b6c5d4e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Materials/GsplatUncompressed.asset b/Runtime/Materials/GsplatUncompressed.asset index 0e6be49..de044d1 100644 --- a/Runtime/Materials/GsplatUncompressed.asset +++ b/Runtime/Materials/GsplatUncompressed.asset @@ -13,5 +13,6 @@ MonoBehaviour: m_Name: GsplatUncompressed m_EditorClassIdentifier: DefaultMaterial: {fileID: 2100000, guid: 0065a348838e5474f802212446cf1ddd, type: 2} + DefaultDepthMaterial: {fileID: 2100000, guid: 2b6c7d8e9f104a5b8c3d2e1f0a9b6c5d, type: 2} CalcDepthShader: {fileID: 7200000, guid: 03396f0af8c54a3093503c0c27511df3, type: 3} InitOrderShader: {fileID: 7200000, guid: 71f302cf0e7c6254e9bae3e5c94b77d6, type: 3} diff --git a/Runtime/Materials/GsplatUncompressedDepth.mat b/Runtime/Materials/GsplatUncompressedDepth.mat new file mode 100644 index 0000000..16ba628 --- /dev/null +++ b/Runtime/Materials/GsplatUncompressedDepth.mat @@ -0,0 +1,32 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: GsplatUncompressedDepth + m_Shader: {fileID: 4800000, guid: 8f4d7a3b9c2e4615a0b1c6d8e9f20314, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: + - SH_BANDS_0 + - UNCOMPRESSED + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: [] + m_Ints: [] + m_Floats: [] + m_Colors: [] + m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Runtime/Materials/GsplatUncompressedDepth.mat.meta b/Runtime/Materials/GsplatUncompressedDepth.mat.meta new file mode 100644 index 0000000..f479b6d --- /dev/null +++ b/Runtime/Materials/GsplatUncompressedDepth.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2b6c7d8e9f104a5b8c3d2e1f0a9b6c5d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SRP/GsplatURPFeature.cs b/Runtime/SRP/GsplatURPFeature.cs index 91fefb2..a1b5ef4 100644 --- a/Runtime/SRP/GsplatURPFeature.cs +++ b/Runtime/SRP/GsplatURPFeature.cs @@ -46,12 +46,86 @@ public override void Execute(ScriptableRenderContext context, ref RenderingData #endif } + class GsplatDepthPrepass : ScriptableRenderPass + { +#if UNITY_6000_0_OR_NEWER + class PassData + { + public UniversalCameraData CameraData; + } + + public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) + { + UniversalResourceData resourceData = frameData.Get(); + using var builder = renderGraph.AddRasterRenderPass("Gsplat.DepthPrepass", + out PassData passData); + passData.CameraData = frameData.Get(); + builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture, AccessFlags.ReadWrite); + builder.AllowPassCulling(false); + builder.SetRenderFunc(static (PassData data, RasterGraphContext context) => + { + GsplatSorter.Instance.RenderDepthPrepass(context.cmd, data.CameraData.camera); + }); + } +#else + public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) + { + var cmd = CommandBufferPool.Get("Gsplat.DepthPrepass"); + GsplatSorter.Instance.RenderDepthPrepass(cmd, renderingData.cameraData.camera); + context.ExecuteCommandBuffer(cmd); + CommandBufferPool.Release(cmd); + } +#endif + } + + class GsplatColorPass : ScriptableRenderPass + { +#if UNITY_6000_0_OR_NEWER + class PassData + { + public UniversalCameraData CameraData; + } + + public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) + { + UniversalResourceData resourceData = frameData.Get(); + using var builder = renderGraph.AddRasterRenderPass("Gsplat.Color", out PassData passData); + passData.CameraData = frameData.Get(); + builder.SetRenderAttachment(resourceData.activeColorTexture, 0, AccessFlags.ReadWrite); + builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture, AccessFlags.Read); + builder.AllowPassCulling(false); + builder.SetRenderFunc(static (PassData data, RasterGraphContext context) => + { + GsplatSorter.Instance.RenderColor(context.cmd, data.CameraData.camera); + }); + } +#else + public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) + { + var cmd = CommandBufferPool.Get("Gsplat.Color"); + GsplatSorter.Instance.RenderColor(cmd, renderingData.cameraData.camera); + context.ExecuteCommandBuffer(cmd); + CommandBufferPool.Release(cmd); + } +#endif + } + GsplatRenderPass m_pass; + GsplatColorPass m_colorPass; + GsplatDepthPrepass m_depthPrepass; bool m_hasGsplats; public override void Create() { m_pass = new GsplatRenderPass { renderPassEvent = RenderPassEvent.BeforeRenderingTransparents }; + m_colorPass = new GsplatColorPass + { + renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents + 1) + }; + m_depthPrepass = new GsplatDepthPrepass + { + renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents + 2) + }; } public override void OnCameraPreCull(ScriptableRenderer renderer, in CameraData cameraData) @@ -66,7 +140,11 @@ public override void OnCameraPreCull(ScriptableRenderer renderer, in CameraData public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (GsplatSorter.Instance.Valid && GsplatSettings.Instance.Valid && m_hasGsplats) + { renderer.EnqueuePass(m_pass); + renderer.EnqueuePass(m_colorPass); + renderer.EnqueuePass(m_depthPrepass); + } } protected override void Dispose(bool disposing) @@ -76,6 +154,8 @@ protected override void Dispose(bool disposing) m_pass.CommandBuffer = null; #endif m_pass = null; + m_colorPass = null; + m_depthPrepass = null; } } } diff --git a/Runtime/Shaders/Gsplat.shader b/Runtime/Shaders/Gsplat.shader index eb91813..8c5ffa9 100644 --- a/Runtime/Shaders/Gsplat.shader +++ b/Runtime/Shaders/Gsplat.shader @@ -12,113 +12,6 @@ Shader "Gsplat/Standard" "Queue"="Transparent" } - Pass - { - Name "DepthPrepass" - ZWrite On - ZTest LEqual - ColorMask 0 - Cull Off - - HLSLPROGRAM - #pragma vertex vert - #pragma fragment frag - #pragma require compute - #pragma multi_compile SH_BANDS_0 SH_BANDS_1 SH_BANDS_2 SH_BANDS_3 SH_BANDS_4 - #pragma multi_compile UNCOMPRESSED SPARK - - #include "UnityCG.cginc" - #include "Gsplat.hlsl" - #ifdef UNCOMPRESSED - #include "GsplatUncompressed.hlsl" - #endif - #ifdef SPARK - #include "GsplatSpark.hlsl" - #endif - - int _SplatCount; - int _SplatInstanceSize; - float4x4 _MATRIX_M; - float _ScaleFactor; - float _DepthPrepassAlphaCutoff; - StructuredBuffer _OrderBuffer; - - struct appdata - { - float4 vertex : POSITION; - #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) - uint instanceID : SV_InstanceID; - #endif - UNITY_VERTEX_INPUT_INSTANCE_ID - }; - - bool InitSource(appdata v, out SplatSource source) - { - #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) - source.order = v.instanceID * _SplatInstanceSize + asuint(v.vertex.z); - #else - source.order = unity_InstanceID * _SplatInstanceSize + asuint(v.vertex.z); - #endif - - if (source.order >= _SplatCount) - return false; - - source.id = _OrderBuffer[source.order]; - source.cornerUV = float2(v.vertex.x, v.vertex.y) * _ScaleFactor; - return true; - } - - struct v2f - { - float2 uv : TEXCOORD0; - float alpha : TEXCOORD1; - float4 vertex : SV_POSITION; - UNITY_VERTEX_OUTPUT_STEREO - }; - - v2f vert(appdata v) - { - v2f o; - UNITY_SETUP_INSTANCE_ID(v); - UNITY_INITIALIZE_OUTPUT(v2f, o); - UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); - o.vertex = discardVec; - - SplatSource source; - if (!InitSource(v, source)) - return o; - - SplatCenter center; - SplatCorner corner; - float4 color; - if (!InitSplatData(source, mul(UNITY_MATRIX_V, _MATRIX_M), center, corner, color)) - return o; - - ClipCorner(corner, color.w); - - o.vertex = center.proj + float4(corner.offset.x, _ProjectionParams.x * corner.offset.y, 0, 0); - o.alpha = color.a; - o.uv = corner.uv; - return o; - } - - float4 frag(v2f i) : SV_Target - { - float A = dot(i.uv, i.uv); - if (A > 1.0) discard; - - float2 absUV = abs(i.uv); - float maxUV = max(absUV.x, absUV.y); - - float falloff = -exp((maxUV - _ScaleFactor * 1.16) * 25 * _ScaleFactor); - float alpha = (exp(-A * 4.0) + falloff) * i.alpha; - - if (alpha < _DepthPrepassAlphaCutoff) discard; - return 0; - } - ENDHLSL - } - Pass { ZWrite Off diff --git a/Runtime/Shaders/GsplatDepth.shader b/Runtime/Shaders/GsplatDepth.shader new file mode 100644 index 0000000..d0b108b --- /dev/null +++ b/Runtime/Shaders/GsplatDepth.shader @@ -0,0 +1,121 @@ +// Copyright (c) 2026 Keir Rice +// SPDX-License-Identifier: MIT + +Shader "Gsplat/DepthOnly" +{ + Properties {} + SubShader + { + Tags + { + "RenderType"="Transparent" + "Queue"="Transparent" + } + + Pass + { + ZWrite On + ZTest LEqual + ColorMask 0 + Cull Off + + HLSLPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma require compute + #pragma multi_compile SH_BANDS_0 SH_BANDS_1 SH_BANDS_2 SH_BANDS_3 SH_BANDS_4 + #pragma multi_compile UNCOMPRESSED SPARK + + #include "UnityCG.cginc" + #include "Gsplat.hlsl" + #ifdef UNCOMPRESSED + #include "GsplatUncompressed.hlsl" + #endif + #ifdef SPARK + #include "GsplatSpark.hlsl" + #endif + + int _SplatCount; + int _SplatInstanceSize; + float4x4 _MATRIX_M; + float _ScaleFactor; + float _DepthPrepassAlphaCutoff; + StructuredBuffer _OrderBuffer; + + struct appdata + { + float4 vertex : POSITION; + #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) + uint instanceID : SV_InstanceID; + #endif + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + bool InitSource(appdata v, out SplatSource source) + { + #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) + source.order = v.instanceID * _SplatInstanceSize + asuint(v.vertex.z); + #else + source.order = unity_InstanceID * _SplatInstanceSize + asuint(v.vertex.z); + #endif + + if (source.order >= _SplatCount) + return false; + + source.id = _OrderBuffer[source.order]; + source.cornerUV = float2(v.vertex.x, v.vertex.y) * _ScaleFactor; + return true; + } + + struct v2f + { + float2 uv : TEXCOORD0; + float alpha : TEXCOORD1; + float4 vertex : SV_POSITION; + UNITY_VERTEX_OUTPUT_STEREO + }; + + v2f vert(appdata v) + { + v2f o; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_OUTPUT(v2f, o); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); + o.vertex = discardVec; + + SplatSource source; + if (!InitSource(v, source)) + return o; + + SplatCenter center; + SplatCorner corner; + float4 color; + if (!InitSplatData(source, mul(UNITY_MATRIX_V, _MATRIX_M), center, corner, color)) + return o; + + ClipCorner(corner, color.w); + + o.vertex = center.proj + float4(corner.offset.x, _ProjectionParams.x * corner.offset.y, 0, 0); + o.alpha = color.a; + o.uv = corner.uv; + return o; + } + + float4 frag(v2f i) : SV_Target + { + float A = dot(i.uv, i.uv); + if (A > 1.0) discard; + + float2 absUV = abs(i.uv); + float maxUV = max(absUV.x, absUV.y); + + float falloff = -exp((maxUV - _ScaleFactor * 1.16) * 25 * _ScaleFactor); + float alpha = (exp(-A * 4.0) + falloff) * i.alpha; + + if (alpha < _DepthPrepassAlphaCutoff) discard; + return 0; + } + ENDHLSL + } + } +} diff --git a/Runtime/Shaders/GsplatDepth.shader.meta b/Runtime/Shaders/GsplatDepth.shader.meta new file mode 100644 index 0000000..a49701a --- /dev/null +++ b/Runtime/Shaders/GsplatDepth.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 8f4d7a3b9c2e4615a0b1c6d8e9f20314 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Shaders/GsplatGlobal.shader b/Runtime/Shaders/GsplatGlobal.shader index 6d0a1a2..5c7f9dc 100644 --- a/Runtime/Shaders/GsplatGlobal.shader +++ b/Runtime/Shaders/GsplatGlobal.shader @@ -17,97 +17,6 @@ Shader "Gsplat/Global" "Queue"="Transparent" } - Pass - { - Name "DepthPrepass" - ZWrite On - ZTest LEqual - ColorMask 0 - Cull Off - - HLSLPROGRAM - #pragma vertex vert - #pragma fragment frag - #pragma require compute - #pragma multi_compile SH_BANDS_0 SH_BANDS_1 SH_BANDS_2 SH_BANDS_3 SH_BANDS_4 - - #include "UnityCG.cginc" - - int _SplatInstanceSize; - float _DepthPrepassAlphaCutoff; - - #include "GsplatSparkGlobal.hlsl" - - struct appdata - { - float4 vertex : POSITION; - #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) - uint instanceID : SV_InstanceID; - #endif - UNITY_VERTEX_INPUT_INSTANCE_ID - }; - - struct v2f - { - float2 uv : TEXCOORD0; - nointerpolation uint rendererId : TEXCOORD1; - float alpha : TEXCOORD2; - float4 vertex : SV_POSITION; - UNITY_VERTEX_OUTPUT_STEREO - }; - - v2f vert(appdata v) - { - v2f o; - UNITY_SETUP_INSTANCE_ID(v); - UNITY_INITIALIZE_OUTPUT(v2f, o); - UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); - o.vertex = discardVec; - - #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) - uint instanceId = v.instanceID; - #else - uint instanceId = unity_InstanceID; - #endif - - GlobalSplatSource source; - if (!InitGlobalSource(instanceId, v.vertex.xyz, source)) - return o; - - SplatCenter center; - SplatCorner corner; - float4 color; - if (!InitGlobalSplatData(source, center, corner, color)) - return o; - - ClipCorner(corner, color.a); - - o.vertex = center.proj + float4(corner.offset.x, _ProjectionParams.x * corner.offset.y, 0, 0); - o.alpha = color.a; - o.uv = corner.uv; - o.rendererId = source.rendererId; - return o; - } - - float4 frag(v2f i) : SV_Target - { - RendererParams p = _RendererParams[i.rendererId]; - - float A = dot(i.uv, i.uv); - if (A > 1.0) discard; - - float2 absUV = abs(i.uv); - float maxUV = max(absUV.x, absUV.y); - - float falloff = -exp((maxUV - p.scaleFactor * 1.16) * 25 * p.scaleFactor); - float alpha = (exp(-A * 4.0) + falloff) * i.alpha; - - if (alpha < _DepthPrepassAlphaCutoff) discard; - return 0; - } - ENDHLSL - } - Pass { ZWrite Off diff --git a/Runtime/Shaders/GsplatGlobalDepth.shader b/Runtime/Shaders/GsplatGlobalDepth.shader new file mode 100644 index 0000000..66b7e07 --- /dev/null +++ b/Runtime/Shaders/GsplatGlobalDepth.shader @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Keir Rice +// SPDX-License-Identifier: MIT + +Shader "Gsplat/GlobalDepthOnly" +{ + Properties {} + SubShader + { + Tags + { + "RenderType"="Transparent" + "Queue"="Transparent" + } + + Pass + { + ZWrite On + ZTest LEqual + ColorMask 0 + Cull Off + + HLSLPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma require compute + #pragma multi_compile SH_BANDS_0 SH_BANDS_1 SH_BANDS_2 SH_BANDS_3 SH_BANDS_4 + + #include "UnityCG.cginc" + + int _SplatInstanceSize; + float _DepthPrepassAlphaCutoff; + + #include "GsplatSparkGlobal.hlsl" + + struct appdata + { + float4 vertex : POSITION; + #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) + uint instanceID : SV_InstanceID; + #endif + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct v2f + { + float2 uv : TEXCOORD0; + nointerpolation uint rendererId : TEXCOORD1; + float alpha : TEXCOORD2; + float4 vertex : SV_POSITION; + UNITY_VERTEX_OUTPUT_STEREO + }; + + v2f vert(appdata v) + { + v2f o; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_OUTPUT(v2f, o); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); + o.vertex = discardVec; + + #if !defined(UNITY_INSTANCING_ENABLED) && !defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(UNITY_STEREO_INSTANCING_ENABLED) + uint instanceId = v.instanceID; + #else + uint instanceId = unity_InstanceID; + #endif + + GlobalSplatSource source; + if (!InitGlobalSource(instanceId, v.vertex.xyz, source)) + return o; + + SplatCenter center; + SplatCorner corner; + float4 color; + if (!InitGlobalSplatData(source, center, corner, color)) + return o; + + ClipCorner(corner, color.a); + + o.vertex = center.proj + float4(corner.offset.x, _ProjectionParams.x * corner.offset.y, 0, 0); + o.alpha = color.a; + o.uv = corner.uv; + o.rendererId = source.rendererId; + return o; + } + + float4 frag(v2f i) : SV_Target + { + RendererParams p = _RendererParams[i.rendererId]; + + float A = dot(i.uv, i.uv); + if (A > 1.0) discard; + + float2 absUV = abs(i.uv); + float maxUV = max(absUV.x, absUV.y); + + float falloff = -exp((maxUV - p.scaleFactor * 1.16) * 25 * p.scaleFactor); + float alpha = (exp(-A * 4.0) + falloff) * i.alpha; + + if (alpha < _DepthPrepassAlphaCutoff) discard; + return 0; + } + ENDHLSL + } + } +} diff --git a/Runtime/Shaders/GsplatGlobalDepth.shader.meta b/Runtime/Shaders/GsplatGlobalDepth.shader.meta new file mode 100644 index 0000000..94ec8a4 --- /dev/null +++ b/Runtime/Shaders/GsplatGlobalDepth.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 9a5c8e1f2b3d4706a4f9c0d1e2b3a456 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: From 6ba248aed047203ba8f82cfdda933359559ff625 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Sun, 12 Jul 2026 16:59:28 +0100 Subject: [PATCH 03/12] Restore HDRP splat rendering --- Runtime/SRP/GsplatHDRPPass.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Runtime/SRP/GsplatHDRPPass.cs b/Runtime/SRP/GsplatHDRPPass.cs index e2df33c..5c21f4c 100644 --- a/Runtime/SRP/GsplatHDRPPass.cs +++ b/Runtime/SRP/GsplatHDRPPass.cs @@ -11,7 +11,10 @@ public class GsplatHDRPPass : CustomPass protected override void Execute(CustomPassContext ctx) { if (GsplatSorter.Instance.Valid && GsplatSettings.Instance.Valid && GsplatSorter.Instance.GatherGsplatsForCamera(ctx.hdCamera.camera)) + { GsplatSorter.Instance.DispatchSort(ctx.cmd, ctx.hdCamera.camera); + GsplatSorter.Instance.RenderColor(ctx.cmd, ctx.hdCamera.camera); + } } } } From db51bdd0ffbb4817ab1d603d3a9e70bd4b73dcd7 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Mon, 29 Jun 2026 13:06:36 +0100 Subject: [PATCH 04/12] Avoid invalid reads in global splat merge --- Runtime/Shaders/GsplatMergeOrderBuffers.compute | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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. From ca5135120dbc8e5e20035ecdd01ce0c8a3e9192e Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Sun, 12 Jul 2026 17:00:58 +0100 Subject: [PATCH 05/12] Respect camera masks in global rendering --- Runtime/GsplatGlobalRenderer.cs | 5 ++++- Runtime/GsplatSorter.cs | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Runtime/GsplatGlobalRenderer.cs b/Runtime/GsplatGlobalRenderer.cs index f7375e0..fcf10f9 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] as Component)?.gameObject.layer ?? 0; 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); diff --git a/Runtime/GsplatSorter.cs b/Runtime/GsplatSorter.cs index db476eb..4f0d3ec 100644 --- a/Runtime/GsplatSorter.cs +++ b/Runtime/GsplatSorter.cs @@ -165,6 +165,12 @@ bool CanRenderGlobally() 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_activeGsplats[0] as Component)?.gameObject.layer ?? 0; + if (m_activeGsplats.Any(gs => ((gs as Component)?.gameObject.layer ?? 0) != renderLayer)) + return false; + return true; } From d0e35bbae7ffeb1533f58969b748fc9c53dbf495 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Thu, 16 Jul 2026 16:49:40 +0100 Subject: [PATCH 06/12] Honor layers for custom IGsplat implementations --- Runtime/GsplatGlobalRenderer.cs | 4 ++-- Runtime/GsplatSorter.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Runtime/GsplatGlobalRenderer.cs b/Runtime/GsplatGlobalRenderer.cs index fcf10f9..8f23768 100644 --- a/Runtime/GsplatGlobalRenderer.cs +++ b/Runtime/GsplatGlobalRenderer.cs @@ -157,7 +157,7 @@ public void Update(List activeGsplats) { EnsureGlobalBuffers(activeGsplats); if (m_totalSplatCount == 0) return; - m_renderLayer = (activeGsplats[0] as Component)?.gameObject.layer ?? 0; + m_renderLayer = activeGsplats[0].transform.gameObject.layer; UpdateRendererTransforms(activeGsplats); UpdateRendererParams(activeGsplats); Render(); @@ -540,4 +540,4 @@ public void DisposeGlobalBuffers() m_builtSplatCounts = null; } } -} \ No newline at end of file +} diff --git a/Runtime/GsplatSorter.cs b/Runtime/GsplatSorter.cs index 4f0d3ec..19dc2fa 100644 --- a/Runtime/GsplatSorter.cs +++ b/Runtime/GsplatSorter.cs @@ -167,8 +167,8 @@ bool CanRenderGlobally() // 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_activeGsplats[0] as Component)?.gameObject.layer ?? 0; - if (m_activeGsplats.Any(gs => ((gs as Component)?.gameObject.layer ?? 0) != renderLayer)) + var renderLayer = m_activeGsplats[0].transform.gameObject.layer; + if (m_activeGsplats.Any(gs => gs.transform.gameObject.layer != renderLayer)) return false; return true; From d5ac39f68fbc11d684914f2713e2037def4a22bc Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Thu, 16 Jul 2026 16:52:19 +0100 Subject: [PATCH 07/12] Choose one render path after runtime changes --- Runtime/GsplatRenderer.cs | 17 +++++++++++------ Runtime/GsplatSorter.cs | 19 +++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Runtime/GsplatRenderer.cs b/Runtime/GsplatRenderer.cs index a257ce6..97396fe 100644 --- a/Runtime/GsplatRenderer.cs +++ b/Runtime/GsplatRenderer.cs @@ -163,12 +163,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 19dc2fa..53f534a 100644 --- a/Runtime/GsplatSorter.cs +++ b/Runtime/GsplatSorter.cs @@ -244,15 +244,22 @@ public void DispatchSort(CommandBuffer cmd, Camera camera) // 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); + + GlobalRenderEnabled = m_globalRenderer.Valid && GsplatSettings.Instance.EnableGlobalSort && + m_activeGsplats.Count >= 2 && CanRenderGlobally(); + if (GlobalRenderEnabled) + { + m_globalRenderer.Update(m_activeGsplats); + 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) From 08f7dbeb9c134b1c7fce65e8c61cc398b85ebd3d Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Fri, 17 Jul 2026 15:40:42 +0100 Subject: [PATCH 08/12] Honor RenderOrder in explicit draws --- Runtime/GsplatSorter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Runtime/GsplatSorter.cs b/Runtime/GsplatSorter.cs index c462c56..e7d90c7 100644 --- a/Runtime/GsplatSorter.cs +++ b/Runtime/GsplatSorter.cs @@ -269,7 +269,7 @@ public void RenderColor(CommandBuffer cmd, Camera camera) return; } - foreach (var gs in m_activeGsplats) + foreach (var gs in m_activeGsplats.OrderBy(gs => (gs as GsplatRenderer)?.RenderOrder ?? 0)) { if (gs.RemainingCount <= 0) continue; var layer = (gs as Component)?.gameObject.layer ?? 0; @@ -304,7 +304,7 @@ public void RenderColor(RasterCommandBuffer cmd, Camera camera) return; } - foreach (var gs in m_activeGsplats) + foreach (var gs in m_activeGsplats.OrderBy(gs => (gs as GsplatRenderer)?.RenderOrder ?? 0)) { if (gs.RemainingCount <= 0) continue; var layer = (gs as Component)?.gameObject.layer ?? 0; From 404cea974f980dff299291812faa78b60c328594 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Fri, 17 Jul 2026 15:41:48 +0100 Subject: [PATCH 09/12] Respect camera masks in global draws --- Runtime/GsplatSorter.cs | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/Runtime/GsplatSorter.cs b/Runtime/GsplatSorter.cs index e7d90c7..5a6d4e0 100644 --- a/Runtime/GsplatSorter.cs +++ b/Runtime/GsplatSorter.cs @@ -174,9 +174,23 @@ bool CanRenderGlobally() return false; } + // One merged draw cannot represent multiple Unity layers. Use per-renderer draws + // for mixed layers so each camera can apply its culling mask independently. + var renderLayer = m_activeGsplats[0].transform.gameObject.layer; + if (m_activeGsplats.Any(gs => gs.transform.gameObject.layer != renderLayer)) + return false; + return true; } + bool CameraRendersGlobalLayer(Camera camera) + { + if (!camera) + return true; + var layer = m_activeGsplats[0].transform.gameObject.layer; + return (camera.cullingMask & (1 << layer)) != 0; + } + public void MarkGlobalBuffersDirty() => m_globalRenderer.MarkGlobalBuffersDirty(); void InitialClearCmdBuffer(Camera cam) @@ -248,14 +262,15 @@ public void RenderDepthPrepass(CommandBuffer cmd, Camera camera) { if (GlobalRenderEnabled) { - m_globalRenderer.RenderDepthPrepass(cmd); + if (CameraRendersGlobalLayer(camera)) + m_globalRenderer.RenderDepthPrepass(cmd); return; } foreach (var gs in m_activeGsplats) { if (gs.RemainingCount <= 0) continue; - var layer = (gs as Component)?.gameObject.layer ?? 0; + var layer = gs.transform.gameObject.layer; if (camera && (camera.cullingMask & (1 << layer)) == 0) continue; gs.RenderDepthPrepass(cmd, camera); } @@ -265,14 +280,15 @@ public void RenderColor(CommandBuffer cmd, Camera camera) { if (GlobalRenderEnabled) { - m_globalRenderer.RenderColor(cmd); + if (CameraRendersGlobalLayer(camera)) + m_globalRenderer.RenderColor(cmd); return; } foreach (var gs in m_activeGsplats.OrderBy(gs => (gs as GsplatRenderer)?.RenderOrder ?? 0)) { if (gs.RemainingCount <= 0) continue; - var layer = (gs as Component)?.gameObject.layer ?? 0; + var layer = gs.transform.gameObject.layer; if (camera && (camera.cullingMask & (1 << layer)) == 0) continue; gs.RenderColor(cmd, camera); } @@ -283,14 +299,15 @@ public void RenderDepthPrepass(RasterCommandBuffer cmd, Camera camera) { if (GlobalRenderEnabled) { - m_globalRenderer.RenderDepthPrepass(cmd); + if (CameraRendersGlobalLayer(camera)) + m_globalRenderer.RenderDepthPrepass(cmd); return; } foreach (var gs in m_activeGsplats) { if (gs.RemainingCount <= 0) continue; - var layer = (gs as Component)?.gameObject.layer ?? 0; + var layer = gs.transform.gameObject.layer; if (camera && (camera.cullingMask & (1 << layer)) == 0) continue; gs.RenderDepthPrepass(cmd, camera); } @@ -300,14 +317,15 @@ public void RenderColor(RasterCommandBuffer cmd, Camera camera) { if (GlobalRenderEnabled) { - m_globalRenderer.RenderColor(cmd); + if (CameraRendersGlobalLayer(camera)) + m_globalRenderer.RenderColor(cmd); return; } foreach (var gs in m_activeGsplats.OrderBy(gs => (gs as GsplatRenderer)?.RenderOrder ?? 0)) { if (gs.RemainingCount <= 0) continue; - var layer = (gs as Component)?.gameObject.layer ?? 0; + var layer = gs.transform.gameObject.layer; if (camera && (camera.cullingMask & (1 << layer)) == 0) continue; gs.RenderColor(cmd, camera); } From 6894a4ca47546edc2b351cf372b24db12504509f Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Fri, 17 Jul 2026 15:42:39 +0100 Subject: [PATCH 10/12] Run splat passes before URP transparents --- Runtime/SRP/GsplatURPFeature.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Runtime/SRP/GsplatURPFeature.cs b/Runtime/SRP/GsplatURPFeature.cs index a1b5ef4..3bd62ce 100644 --- a/Runtime/SRP/GsplatURPFeature.cs +++ b/Runtime/SRP/GsplatURPFeature.cs @@ -117,14 +117,19 @@ public override void Execute(ScriptableRenderContext context, ref RenderingData public override void Create() { - m_pass = new GsplatRenderPass { renderPassEvent = RenderPassEvent.BeforeRenderingTransparents }; - m_colorPass = new GsplatColorPass + // Record all splat work before URP's transparent draw pass. The depth pass must run + // before color so its writes can also depth-test the transparents that follow. + m_pass = new GsplatRenderPass { - renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents + 1) + renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents - 3) }; m_depthPrepass = new GsplatDepthPrepass { - renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents + 2) + renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents - 2) + }; + m_colorPass = new GsplatColorPass + { + renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents - 1) }; } From aeb81b356182729cde0b83b401cc16948975ac81 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Fri, 17 Jul 2026 15:44:51 +0100 Subject: [PATCH 11/12] Write splat depth in HDRP --- Runtime/SRP/GsplatHDRPPass.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Runtime/SRP/GsplatHDRPPass.cs b/Runtime/SRP/GsplatHDRPPass.cs index 5c21f4c..fbc4879 100644 --- a/Runtime/SRP/GsplatHDRPPass.cs +++ b/Runtime/SRP/GsplatHDRPPass.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MIT #if GSPLAT_ENABLE_HDRP +using UnityEngine.Rendering; using UnityEngine.Rendering.HighDefinition; namespace Gsplat @@ -13,6 +14,8 @@ protected override void Execute(CustomPassContext ctx) if (GsplatSorter.Instance.Valid && GsplatSettings.Instance.Valid && GsplatSorter.Instance.GatherGsplatsForCamera(ctx.hdCamera.camera)) { GsplatSorter.Instance.DispatchSort(ctx.cmd, ctx.hdCamera.camera); + CoreUtils.SetRenderTarget(ctx.cmd, ctx.cameraColorBuffer, ctx.cameraDepthBuffer); + GsplatSorter.Instance.RenderDepthPrepass(ctx.cmd, ctx.hdCamera.camera); GsplatSorter.Instance.RenderColor(ctx.cmd, ctx.hdCamera.camera); } } From c0bb5811474dbd7121516a40b2608b441b304a84 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Sat, 18 Jul 2026 15:11:39 +0100 Subject: [PATCH 12/12] Fix splat blending with depth prepass --- Runtime/SRP/GsplatURPFeature.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Runtime/SRP/GsplatURPFeature.cs b/Runtime/SRP/GsplatURPFeature.cs index 3bd62ce..aebcba5 100644 --- a/Runtime/SRP/GsplatURPFeature.cs +++ b/Runtime/SRP/GsplatURPFeature.cs @@ -117,19 +117,19 @@ public override void Execute(ScriptableRenderContext context, ref RenderingData public override void Create() { - // Record all splat work before URP's transparent draw pass. The depth pass must run - // before color so its writes can also depth-test the transparents that follow. + // Record all splat work before URP's transparent draw pass. Render splat color before + // splat depth so overlapping transparent splats can blend before the depth write. m_pass = new GsplatRenderPass { renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents - 3) }; m_depthPrepass = new GsplatDepthPrepass { - renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents - 2) + renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents - 1) }; m_colorPass = new GsplatColorPass { - renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents - 1) + renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingTransparents - 2) }; }