From c401653b696987592627c86efadbad31d7ae3210 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Mon, 29 Jun 2026 14:46:46 +0100 Subject: [PATCH 1/4] Intersector helper for Open Brush SelectionWidget --- Runtime/GsplatRenderer.cs | 12 +++- Runtime/GsplatRendererImpl.cs | 67 +++++++++++++++++ Runtime/GsplatSettings.cs | 12 ++++ Runtime/Shaders/GsplatIntersect.compute | 75 ++++++++++++++++++++ Runtime/Shaders/GsplatIntersect.compute.meta | 8 +++ 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 Runtime/Shaders/GsplatIntersect.compute create mode 100644 Runtime/Shaders/GsplatIntersect.compute.meta diff --git a/Runtime/GsplatRenderer.cs b/Runtime/GsplatRenderer.cs index a257ce6..f4c17c1 100644 --- a/Runtime/GsplatRenderer.cs +++ b/Runtime/GsplatRenderer.cs @@ -87,6 +87,16 @@ public GsplatCutout[] Cutouts public void ComputeDepth(CommandBuffer cmd, Matrix4x4 matrixMv) => m_renderer.ComputeDepth(cmd, matrixMv); + public bool TryIntersectSphere(Vector3 centerWorld, float radiusWorld, out float score) + { + score = -1.0f; + if (!Valid || m_renderer == null) + return false; + + return m_renderer.TryIntersectSphere(transform, centerWorld, radiusWorld, + 1.0f - SplatDownscaleFactor, out score); + } + void OnEnable() { GsplatSorter.Instance.RegisterGsplat(this); @@ -171,4 +181,4 @@ public void Update() } } } -} \ No newline at end of file +} diff --git a/Runtime/GsplatRendererImpl.cs b/Runtime/GsplatRendererImpl.cs index efe457d..c23b2d1 100644 --- a/Runtime/GsplatRendererImpl.cs +++ b/Runtime/GsplatRendererImpl.cs @@ -25,11 +25,19 @@ public class GsplatRendererImpl public GraphicsBuffer OrderSizeBuffer { get; private set; } public GraphicsBuffer BoundsBuffer { get; private set; } public ISorterResource SorterResource { get; private set; } + GraphicsBuffer m_intersectionHitBuffer; + readonly uint[] m_intersectionHit = new uint[1]; static readonly int k_orderBuffer = Shader.PropertyToID("_OrderBuffer"); + static readonly int k_packedSplatsBuffer = Shader.PropertyToID("_PackedSplatsBuffer"); + static readonly int k_positionBuffer = Shader.PropertyToID("_PositionBuffer"); + static readonly int k_scaleBuffer = Shader.PropertyToID("_ScaleBuffer"); static readonly int k_matrixM = Shader.PropertyToID("_MATRIX_M"); static readonly int k_splatInstanceSize = Shader.PropertyToID("_SplatInstanceSize"); static readonly int k_splatCount = Shader.PropertyToID("_SplatCount"); + static readonly int k_sphereCenter = Shader.PropertyToID("_SphereCenter"); + static readonly int k_sphereRadius = Shader.PropertyToID("_SphereRadius"); + static readonly int k_hitBuffer = Shader.PropertyToID("_HitBuffer"); static readonly int k_gammaToLinear = Shader.PropertyToID("_GammaToLinear"); static readonly int k_shDegree = Shader.PropertyToID("_SHDegree"); static readonly int k_brightness = Shader.PropertyToID("_Brightness"); @@ -65,6 +73,63 @@ public void RecreateResources(uint splatCount) public void ComputeDepth(CommandBuffer cmd, Matrix4x4 matrixMv) => m_gsplatAsset.ComputeDepth(cmd, matrixMv, SorterResource, GsplatResource); + public bool TryIntersectSphere(Transform transform, Vector3 centerWorld, float radiusWorld, + float scaleFactor, out float score) + { + score = -1.0f; + ComputeShader cs = GsplatSettings.Instance.IntersectionShader; + if (!cs || GsplatResource == null || GsplatResource.UploadedCount == 0) + return false; + + int kernel; + if (GsplatResource is GsplatResourceSpark spark) + { + kernel = cs.FindKernel("IntersectSpark"); + cs.SetBuffer(kernel, k_packedSplatsBuffer, spark.PackedSplatsBuffer); + } + else if (GsplatResource is GsplatResourceUncompressed uncompressed) + { + kernel = cs.FindKernel("IntersectUncompressed"); + cs.SetBuffer(kernel, k_positionBuffer, uncompressed.PositionBuffer); + cs.SetBuffer(kernel, k_scaleBuffer, uncompressed.ScaleBuffer); + } + else + { + return false; + } + + EnsureIntersectionBuffer(); + m_intersectionHit[0] = 0; + m_intersectionHitBuffer.SetData(m_intersectionHit); + + Matrix4x4 worldToLocal = transform.worldToLocalMatrix; + Vector3 centerLocal = worldToLocal.MultiplyPoint3x4(centerWorld); + float radiusLocal = Mathf.Max( + worldToLocal.MultiplyVector(Vector3.right * radiusWorld).magnitude, + Mathf.Max( + worldToLocal.MultiplyVector(Vector3.up * radiusWorld).magnitude, + worldToLocal.MultiplyVector(Vector3.forward * radiusWorld).magnitude)); + + cs.SetInt(k_splatCount, (int)GsplatResource.UploadedCount); + cs.SetVector(k_sphereCenter, centerLocal); + cs.SetFloat(k_sphereRadius, radiusLocal); + cs.SetFloat(k_scaleFactor, scaleFactor); + cs.SetBuffer(kernel, k_hitBuffer, m_intersectionHitBuffer); + cs.Dispatch(kernel, (int)GsplatUtils.DivRoundUp(GsplatResource.UploadedCount, 256), 1, 1); + + m_intersectionHitBuffer.GetData(m_intersectionHit); + if (m_intersectionHit[0] == 0) + return false; + + score = 1.0f; + return true; + } + + void EnsureIntersectionBuffer() + { + m_intersectionHitBuffer ??= new GraphicsBuffer(GraphicsBuffer.Target.Structured, 1, sizeof(uint)); + } + Bounds ExtractBounds() { uint[] boundsData = new uint[6]; @@ -181,6 +246,8 @@ public void Dispose() OrderSizeBuffer = null; BoundsBuffer?.Dispose(); BoundsBuffer = null; + m_intersectionHitBuffer?.Dispose(); + m_intersectionHitBuffer = null; } public void ForceRefresh() diff --git a/Runtime/GsplatSettings.cs b/Runtime/GsplatSettings.cs index db1195c..aeb7a28 100644 --- a/Runtime/GsplatSettings.cs +++ b/Runtime/GsplatSettings.cs @@ -68,6 +68,7 @@ public static GsplatSettings Instance } public ComputeShader ComputeShader; + public ComputeShader IntersectionShader; public GsplatGlobalMaterial GlobalMaterial; [Tooltip( @@ -113,6 +114,9 @@ public Version Version static GsplatGlobalMaterial DefaultGlobalMaterial => AssetDatabase.LoadAssetAtPath( GsplatUtils.k_PackagePath + "Runtime/Materials/GsplatGlobal.asset"); + static ComputeShader DefaultIntersectionShader => AssetDatabase.LoadAssetAtPath( + GsplatUtils.k_PackagePath + "Runtime/Shaders/GsplatIntersect.compute"); + static GsplatMaterial[] DefaultMaterials { get @@ -132,6 +136,7 @@ public void Reset() { Version = GsplatUtils.k_Version; ComputeShader = DefaultComputeShader; + IntersectionShader = DefaultIntersectionShader; GlobalMaterial = DefaultGlobalMaterial; Materials = DefaultMaterials; SplatInstanceSize = 128; @@ -177,6 +182,13 @@ void CreateMeshInstance() void OnValidate() { +#if UNITY_EDITOR + if (!IntersectionShader) + { + IntersectionShader = DefaultIntersectionShader; + EditorUtility.SetDirty(this); + } +#endif if (ComputeShader != m_prevComputeShader) { GsplatSorter.Instance.InitSorter(ComputeShader); diff --git a/Runtime/Shaders/GsplatIntersect.compute b/Runtime/Shaders/GsplatIntersect.compute new file mode 100644 index 0000000..9be24b2 --- /dev/null +++ b/Runtime/Shaders/GsplatIntersect.compute @@ -0,0 +1,75 @@ +// Copyright (c) 2026 Yize Wu +// SPDX-License-Identifier: MIT + +#pragma kernel IntersectSpark +#pragma kernel IntersectUncompressed + +uint _SplatCount; +float3 _SphereCenter; +float _SphereRadius; +float _ScaleFactor; + +StructuredBuffer _PackedSplatsBuffer; +StructuredBuffer _PositionBuffer; +StructuredBuffer _ScaleBuffer; +RWStructuredBuffer _HitBuffer; + +static const float LN_SCALE_MIN = -12.0; +static const float LN_SCALE_MAX = 9.0; + +void UnpackSparkSplat(uint4 packedData, out float alpha, out float3 center, out float3 scale) +{ + uint word0 = packedData.x; + uint word1 = packedData.y; + uint word2 = packedData.z; + uint word3 = packedData.w; + + alpha = float((word0 >> 24u) & 0xFFU) / 255.0; + center = float3(f16tof32(word1 & 0xFFFFU), f16tof32((word1 >> 16u) & 0xFFFFU), + f16tof32(word2 & 0xFFFFU)); + + uint3 uScale = uint3(word3 & 0xFFU, (word3 >> 8u) & 0xFFU, (word3 >> 16u) & 0xFFU); + float lnScaleScale = (LN_SCALE_MAX - LN_SCALE_MIN) / 254.0; + scale = float3( + (uScale.x == 0u) ? 0.0 : exp(LN_SCALE_MIN + float(uScale.x - 1u) * lnScaleScale), + (uScale.y == 0u) ? 0.0 : exp(LN_SCALE_MIN + float(uScale.y - 1u) * lnScaleScale), + (uScale.z == 0u) ? 0.0 : exp(LN_SCALE_MIN + float(uScale.z - 1u) * lnScaleScale) + ); +} + +bool IntersectsSplatSphere(float3 center, float3 scale, float alpha) +{ + if (alpha <= 0.0) + return false; + + float splatRadius = max(max(scale.x, scale.y), scale.z) * _ScaleFactor * 2.0; + float radius = _SphereRadius + splatRadius; + return dot(center - _SphereCenter, center - _SphereCenter) <= radius * radius; +} + +[numthreads(256, 1, 1)] +void IntersectSpark(uint3 id : SV_DispatchThreadID) +{ + uint idx = id.x; + if (idx >= _SplatCount || _HitBuffer[0] != 0) + return; + + float alpha; + float3 center; + float3 scale; + UnpackSparkSplat(_PackedSplatsBuffer[idx], alpha, center, scale); + + if (IntersectsSplatSphere(center, scale, alpha)) + InterlockedAdd(_HitBuffer[0], 1); +} + +[numthreads(256, 1, 1)] +void IntersectUncompressed(uint3 id : SV_DispatchThreadID) +{ + uint idx = id.x; + if (idx >= _SplatCount || _HitBuffer[0] != 0) + return; + + if (IntersectsSplatSphere(_PositionBuffer[idx], _ScaleBuffer[idx], 1.0)) + InterlockedAdd(_HitBuffer[0], 1); +} diff --git a/Runtime/Shaders/GsplatIntersect.compute.meta b/Runtime/Shaders/GsplatIntersect.compute.meta new file mode 100644 index 0000000..26f019f --- /dev/null +++ b/Runtime/Shaders/GsplatIntersect.compute.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7c42c1f524804e47a50f194942e0ab31 +ComputeShaderImporter: + externalObjects: {} + currentAPIMask: 4 + userData: + assetBundleName: + assetBundleVariant: From cb914be4f807ffac3e48c5e34401ebaa4da31417 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Fri, 17 Jul 2026 14:19:30 +0100 Subject: [PATCH 2/4] Respect cutouts in sphere intersections --- Runtime/GsplatRendererImpl.cs | 10 ++++++---- Runtime/Shaders/GsplatIntersect.compute | 11 +++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Runtime/GsplatRendererImpl.cs b/Runtime/GsplatRendererImpl.cs index c23b2d1..bb9c541 100644 --- a/Runtime/GsplatRendererImpl.cs +++ b/Runtime/GsplatRendererImpl.cs @@ -78,7 +78,7 @@ public bool TryIntersectSphere(Transform transform, Vector3 centerWorld, float r { score = -1.0f; ComputeShader cs = GsplatSettings.Instance.IntersectionShader; - if (!cs || GsplatResource == null || GsplatResource.UploadedCount == 0) + if (!cs || GsplatResource == null || GsplatResource.UploadedCount == 0 || m_remainingCount == 0) return false; int kernel; @@ -98,6 +98,8 @@ public bool TryIntersectSphere(Transform transform, Vector3 centerWorld, float r return false; } + cs.SetBuffer(kernel, k_orderBuffer, SorterResource.OrderBuffer); + EnsureIntersectionBuffer(); m_intersectionHit[0] = 0; m_intersectionHitBuffer.SetData(m_intersectionHit); @@ -110,12 +112,12 @@ public bool TryIntersectSphere(Transform transform, Vector3 centerWorld, float r worldToLocal.MultiplyVector(Vector3.up * radiusWorld).magnitude, worldToLocal.MultiplyVector(Vector3.forward * radiusWorld).magnitude)); - cs.SetInt(k_splatCount, (int)GsplatResource.UploadedCount); + cs.SetInt(k_splatCount, (int)m_remainingCount); cs.SetVector(k_sphereCenter, centerLocal); cs.SetFloat(k_sphereRadius, radiusLocal); cs.SetFloat(k_scaleFactor, scaleFactor); cs.SetBuffer(kernel, k_hitBuffer, m_intersectionHitBuffer); - cs.Dispatch(kernel, (int)GsplatUtils.DivRoundUp(GsplatResource.UploadedCount, 256), 1, 1); + cs.Dispatch(kernel, (int)GsplatUtils.DivRoundUp(m_remainingCount, 256), 1, 1); m_intersectionHitBuffer.GetData(m_intersectionHit); if (m_intersectionHit[0] == 0) @@ -353,4 +355,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/Shaders/GsplatIntersect.compute b/Runtime/Shaders/GsplatIntersect.compute index 9be24b2..911deb6 100644 --- a/Runtime/Shaders/GsplatIntersect.compute +++ b/Runtime/Shaders/GsplatIntersect.compute @@ -12,6 +12,7 @@ float _ScaleFactor; StructuredBuffer _PackedSplatsBuffer; StructuredBuffer _PositionBuffer; StructuredBuffer _ScaleBuffer; +StructuredBuffer _OrderBuffer; RWStructuredBuffer _HitBuffer; static const float LN_SCALE_MIN = -12.0; @@ -50,9 +51,10 @@ bool IntersectsSplatSphere(float3 center, float3 scale, float alpha) [numthreads(256, 1, 1)] void IntersectSpark(uint3 id : SV_DispatchThreadID) { - uint idx = id.x; - if (idx >= _SplatCount || _HitBuffer[0] != 0) + uint orderIdx = id.x; + if (orderIdx >= _SplatCount || _HitBuffer[0] != 0) return; + uint idx = _OrderBuffer[orderIdx]; float alpha; float3 center; @@ -66,9 +68,10 @@ void IntersectSpark(uint3 id : SV_DispatchThreadID) [numthreads(256, 1, 1)] void IntersectUncompressed(uint3 id : SV_DispatchThreadID) { - uint idx = id.x; - if (idx >= _SplatCount || _HitBuffer[0] != 0) + uint orderIdx = id.x; + if (orderIdx >= _SplatCount || _HitBuffer[0] != 0) return; + uint idx = _OrderBuffer[orderIdx]; if (IntersectsSplatSphere(_PositionBuffer[idx], _ScaleBuffer[idx], 1.0)) InterlockedAdd(_HitBuffer[0], 1); From b60d58b9e828c798557a18f2a303a64e7dd93fa2 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Fri, 17 Jul 2026 14:20:26 +0100 Subject: [PATCH 3/4] Ignore transparent uncompressed splats --- Runtime/GsplatRendererImpl.cs | 2 ++ Runtime/Shaders/GsplatIntersect.compute | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Runtime/GsplatRendererImpl.cs b/Runtime/GsplatRendererImpl.cs index bb9c541..69471b6 100644 --- a/Runtime/GsplatRendererImpl.cs +++ b/Runtime/GsplatRendererImpl.cs @@ -32,6 +32,7 @@ public class GsplatRendererImpl static readonly int k_packedSplatsBuffer = Shader.PropertyToID("_PackedSplatsBuffer"); static readonly int k_positionBuffer = Shader.PropertyToID("_PositionBuffer"); static readonly int k_scaleBuffer = Shader.PropertyToID("_ScaleBuffer"); + static readonly int k_colorBuffer = Shader.PropertyToID("_ColorBuffer"); static readonly int k_matrixM = Shader.PropertyToID("_MATRIX_M"); static readonly int k_splatInstanceSize = Shader.PropertyToID("_SplatInstanceSize"); static readonly int k_splatCount = Shader.PropertyToID("_SplatCount"); @@ -92,6 +93,7 @@ public bool TryIntersectSphere(Transform transform, Vector3 centerWorld, float r kernel = cs.FindKernel("IntersectUncompressed"); cs.SetBuffer(kernel, k_positionBuffer, uncompressed.PositionBuffer); cs.SetBuffer(kernel, k_scaleBuffer, uncompressed.ScaleBuffer); + cs.SetBuffer(kernel, k_colorBuffer, uncompressed.ColorBuffer); } else { diff --git a/Runtime/Shaders/GsplatIntersect.compute b/Runtime/Shaders/GsplatIntersect.compute index 911deb6..54f57a8 100644 --- a/Runtime/Shaders/GsplatIntersect.compute +++ b/Runtime/Shaders/GsplatIntersect.compute @@ -12,6 +12,7 @@ float _ScaleFactor; StructuredBuffer _PackedSplatsBuffer; StructuredBuffer _PositionBuffer; StructuredBuffer _ScaleBuffer; +StructuredBuffer _ColorBuffer; StructuredBuffer _OrderBuffer; RWStructuredBuffer _HitBuffer; @@ -73,6 +74,6 @@ void IntersectUncompressed(uint3 id : SV_DispatchThreadID) return; uint idx = _OrderBuffer[orderIdx]; - if (IntersectsSplatSphere(_PositionBuffer[idx], _ScaleBuffer[idx], 1.0)) + if (IntersectsSplatSphere(_PositionBuffer[idx], _ScaleBuffer[idx], _ColorBuffer[idx].a)) InterlockedAdd(_HitBuffer[0], 1); } From 1060e5905f6e8000d046f243b6675e48002b079f Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Fri, 17 Jul 2026 14:22:15 +0100 Subject: [PATCH 4/4] Test sphere intersections in world space --- Runtime/GsplatRendererImpl.cs | 18 ++++++++++-------- Runtime/Shaders/GsplatIntersect.compute | 5 ++++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Runtime/GsplatRendererImpl.cs b/Runtime/GsplatRendererImpl.cs index 69471b6..e21728c 100644 --- a/Runtime/GsplatRendererImpl.cs +++ b/Runtime/GsplatRendererImpl.cs @@ -38,6 +38,7 @@ public class GsplatRendererImpl static readonly int k_splatCount = Shader.PropertyToID("_SplatCount"); static readonly int k_sphereCenter = Shader.PropertyToID("_SphereCenter"); static readonly int k_sphereRadius = Shader.PropertyToID("_SphereRadius"); + static readonly int k_maxObjectScale = Shader.PropertyToID("_MaxObjectScale"); static readonly int k_hitBuffer = Shader.PropertyToID("_HitBuffer"); static readonly int k_gammaToLinear = Shader.PropertyToID("_GammaToLinear"); static readonly int k_shDegree = Shader.PropertyToID("_SHDegree"); @@ -106,18 +107,19 @@ public bool TryIntersectSphere(Transform transform, Vector3 centerWorld, float r m_intersectionHit[0] = 0; m_intersectionHitBuffer.SetData(m_intersectionHit); - Matrix4x4 worldToLocal = transform.worldToLocalMatrix; - Vector3 centerLocal = worldToLocal.MultiplyPoint3x4(centerWorld); - float radiusLocal = Mathf.Max( - worldToLocal.MultiplyVector(Vector3.right * radiusWorld).magnitude, + Matrix4x4 localToWorld = transform.localToWorldMatrix; + float maxObjectScale = Mathf.Max( + localToWorld.MultiplyVector(Vector3.right).magnitude, Mathf.Max( - worldToLocal.MultiplyVector(Vector3.up * radiusWorld).magnitude, - worldToLocal.MultiplyVector(Vector3.forward * radiusWorld).magnitude)); + localToWorld.MultiplyVector(Vector3.up).magnitude, + localToWorld.MultiplyVector(Vector3.forward).magnitude)); cs.SetInt(k_splatCount, (int)m_remainingCount); - cs.SetVector(k_sphereCenter, centerLocal); - cs.SetFloat(k_sphereRadius, radiusLocal); + cs.SetVector(k_sphereCenter, centerWorld); + cs.SetFloat(k_sphereRadius, radiusWorld); cs.SetFloat(k_scaleFactor, scaleFactor); + cs.SetFloat(k_maxObjectScale, maxObjectScale); + cs.SetMatrix(k_matrixM, localToWorld); cs.SetBuffer(kernel, k_hitBuffer, m_intersectionHitBuffer); cs.Dispatch(kernel, (int)GsplatUtils.DivRoundUp(m_remainingCount, 256), 1, 1); diff --git a/Runtime/Shaders/GsplatIntersect.compute b/Runtime/Shaders/GsplatIntersect.compute index 54f57a8..c86bd5a 100644 --- a/Runtime/Shaders/GsplatIntersect.compute +++ b/Runtime/Shaders/GsplatIntersect.compute @@ -8,6 +8,8 @@ uint _SplatCount; float3 _SphereCenter; float _SphereRadius; float _ScaleFactor; +float _MaxObjectScale; +float4x4 _MATRIX_M; StructuredBuffer _PackedSplatsBuffer; StructuredBuffer _PositionBuffer; @@ -44,7 +46,8 @@ bool IntersectsSplatSphere(float3 center, float3 scale, float alpha) if (alpha <= 0.0) return false; - float splatRadius = max(max(scale.x, scale.y), scale.z) * _ScaleFactor * 2.0; + center = mul(_MATRIX_M, float4(center, 1.0)).xyz; + float splatRadius = max(max(scale.x, scale.y), scale.z) * _ScaleFactor * 2.0 * _MaxObjectScale; float radius = _SphereRadius + splatRadius; return dot(center - _SphereCenter, center - _SphereCenter) <= radius * radius; }