Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Runtime/GsplatRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -171,4 +181,4 @@ public void Update()
}
}
}
}
}
75 changes: 74 additions & 1 deletion Runtime/GsplatRendererImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,21 @@ 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_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");
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");
static readonly int k_brightness = Shader.PropertyToID("_Brightness");
Expand Down Expand Up @@ -65,6 +75,67 @@ 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 || m_remainingCount == 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);
cs.SetBuffer(kernel, k_colorBuffer, uncompressed.ColorBuffer);
}
else
{
return false;
}

cs.SetBuffer(kernel, k_orderBuffer, SorterResource.OrderBuffer);

EnsureIntersectionBuffer();
m_intersectionHit[0] = 0;
m_intersectionHitBuffer.SetData(m_intersectionHit);

Matrix4x4 localToWorld = transform.localToWorldMatrix;
float maxObjectScale = Mathf.Max(
localToWorld.MultiplyVector(Vector3.right).magnitude,
Mathf.Max(
localToWorld.MultiplyVector(Vector3.up).magnitude,
localToWorld.MultiplyVector(Vector3.forward).magnitude));

cs.SetInt(k_splatCount, (int)m_remainingCount);
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);

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];
Expand Down Expand Up @@ -181,6 +252,8 @@ public void Dispose()
OrderSizeBuffer = null;
BoundsBuffer?.Dispose();
BoundsBuffer = null;
m_intersectionHitBuffer?.Dispose();
m_intersectionHitBuffer = null;
}

public void ForceRefresh()
Expand Down Expand Up @@ -286,4 +359,4 @@ public void Render(Transform transform, int layer, bool gammaToLinear = false, i
Mathf.CeilToInt(m_remainingCount / (float)GsplatSettings.Instance.SplatInstanceSize));
}
}
}
}
12 changes: 12 additions & 0 deletions Runtime/GsplatSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public static GsplatSettings Instance
}

public ComputeShader ComputeShader;
public ComputeShader IntersectionShader;
public GsplatGlobalMaterial GlobalMaterial;

[Tooltip(
Expand Down Expand Up @@ -113,6 +114,9 @@ public Version Version
static GsplatGlobalMaterial DefaultGlobalMaterial => AssetDatabase.LoadAssetAtPath<GsplatGlobalMaterial>(
GsplatUtils.k_PackagePath + "Runtime/Materials/GsplatGlobal.asset");

static ComputeShader DefaultIntersectionShader => AssetDatabase.LoadAssetAtPath<ComputeShader>(
GsplatUtils.k_PackagePath + "Runtime/Shaders/GsplatIntersect.compute");

static GsplatMaterial[] DefaultMaterials
{
get
Expand All @@ -132,6 +136,7 @@ public void Reset()
{
Version = GsplatUtils.k_Version;
ComputeShader = DefaultComputeShader;
IntersectionShader = DefaultIntersectionShader;
GlobalMaterial = DefaultGlobalMaterial;
Materials = DefaultMaterials;
SplatInstanceSize = 128;
Expand Down Expand Up @@ -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);
Expand Down
82 changes: 82 additions & 0 deletions Runtime/Shaders/GsplatIntersect.compute
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) 2026 Yize Wu
// SPDX-License-Identifier: MIT

#pragma kernel IntersectSpark
#pragma kernel IntersectUncompressed

uint _SplatCount;
float3 _SphereCenter;
float _SphereRadius;
float _ScaleFactor;
float _MaxObjectScale;
float4x4 _MATRIX_M;

StructuredBuffer<uint4> _PackedSplatsBuffer;
StructuredBuffer<float3> _PositionBuffer;
StructuredBuffer<float3> _ScaleBuffer;
StructuredBuffer<float4> _ColorBuffer;
StructuredBuffer<uint> _OrderBuffer;
RWStructuredBuffer<uint> _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;

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;
}

[numthreads(256, 1, 1)]
void IntersectSpark(uint3 id : SV_DispatchThreadID)
{
uint orderIdx = id.x;
if (orderIdx >= _SplatCount || _HitBuffer[0] != 0)
return;
uint idx = _OrderBuffer[orderIdx];

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 orderIdx = id.x;
if (orderIdx >= _SplatCount || _HitBuffer[0] != 0)
return;
uint idx = _OrderBuffer[orderIdx];

if (IntersectsSplatSphere(_PositionBuffer[idx], _ScaleBuffer[idx], _ColorBuffer[idx].a))
InterlockedAdd(_HitBuffer[0], 1);
}
8 changes: 8 additions & 0 deletions Runtime/Shaders/GsplatIntersect.compute.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.