From 30a70c1ca7aa7c76923988558a2e8fb05a7f742d Mon Sep 17 00:00:00 2001 From: Luke Clemens Date: Sat, 16 May 2026 22:54:04 -0600 Subject: [PATCH 1/3] BVH.cs - In BVHPrecomputeHierarchyJob.Execute(), changed the line "if (SortedNodes.Length < BVHUtils.NbLeavesPerNode)" to "if (SortedNodes.Length <= 1)" . This fixes an issue where if the tree had 2 or 3 nodes, only one node was getting included. Now the early-out only triggers for 0 or 1 nodes, and lets the existing padding code handle cases with 2 and 3 nodes. --- com.trove.spatialqueries/Runtime/BVH.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/com.trove.spatialqueries/Runtime/BVH.cs b/com.trove.spatialqueries/Runtime/BVH.cs index 2fa39e9b..283c0954 100644 --- a/com.trove.spatialqueries/Runtime/BVH.cs +++ b/com.trove.spatialqueries/Runtime/BVH.cs @@ -1002,7 +1002,8 @@ public void Execute() { NodeLevelDatas.Clear(); - if (SortedNodes.Length < BVHUtils.NbLeavesPerNode) + // Special case for length 1 and 0 + if (SortedNodes.Length <= 1) { if (SortedNodes.Length > 0) { From 6412f1fd24eac18000b8ecc40d78a0cf1ac08022 Mon Sep 17 00:00:00 2001 From: Luke Clemens Date: Sat, 16 May 2026 23:47:12 -0600 Subject: [PATCH 2/3] GeometryUtils.cs - added a tEntry parameter to IntersectsRay() because it is needed by QueryRayClosest(). --- com.trove.common/Runtime/GeometryUtils.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/com.trove.common/Runtime/GeometryUtils.cs b/com.trove.common/Runtime/GeometryUtils.cs index 58022b77..3ab20de8 100644 --- a/com.trove.common/Runtime/GeometryUtils.cs +++ b/com.trove.common/Runtime/GeometryUtils.cs @@ -173,7 +173,7 @@ public bool Contains(float3 point) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IntersectsRay(float3 rayOrigin, float3 rayDirectionNormalized, float rayLength) + public bool IntersectsRay(float3 rayOrigin, float3 rayDirectionNormalized, float rayLength, out float tEntry) { float tMin = 0.0f; float tMax = float.MaxValue; @@ -195,22 +195,25 @@ public bool IntersectsRay(float3 rayOrigin, float3 rayDirectionNormalized, float if (tMin > tMax) { + tEntry = 0f; return false; } } if (tMax < 0.0f) { + tEntry = 0f; return false; } - float distance = tMin; - if (distance <= rayLength) - { - return true; - } + tEntry = tMin; + return tMin <= rayLength; + } - return false; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IntersectsRay(float3 rayOrigin, float3 rayDirectionNormalized, float rayLength) + { + return IntersectsRay(rayOrigin, rayDirectionNormalized, rayLength, out _); } } From f84d8da52deefdf87ed35bd4a0e1543c7556ec90 Mon Sep 17 00:00:00 2001 From: Luke Clemens Date: Sun, 17 May 2026 00:20:33 -0600 Subject: [PATCH 3/3] BVH.cs - added QueryRayClosest() (two overloads) and QueryRayAny(). I have not tested QueryRayAny() yet, but QueryRayClosest() seems to work as expected. It uses pruning so that every subsequent ray-AABB test uses a shrunken length, and IntersectsRay rejects any AABB whose entry is past it. This required a small change in the AABB struct's IntersectRay() function. --- com.trove.spatialqueries/Runtime/BVH.cs | 124 ++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 6 deletions(-) diff --git a/com.trove.spatialqueries/Runtime/BVH.cs b/com.trove.spatialqueries/Runtime/BVH.cs index 283c0954..73630a22 100644 --- a/com.trove.spatialqueries/Runtime/BVH.cs +++ b/com.trove.spatialqueries/Runtime/BVH.cs @@ -375,18 +375,122 @@ public unsafe bool QueryRay(float3 rayOrigin, float3 rayDirectionNor where TCollector : unmanaged, IBVHQueryCollector { collector.OnBeginQuery(); - - if (SortedNodes.Length < 1) - { + + if (SortedNodes.Length < 1) { return false; } - + Stack nodesStack = new Stack(256); int* nodesStackPtr = stackalloc int[nodesStack.Capacity]; BVHNode* nodesPtr = SortedNodes.GetUnsafeReadOnlyPtr(); TNodeData* leafDataPtr = LeafNodeDatas.GetUnsafeReadOnlyPtr(); int leafNodesCount = LeafNodeDatas.Length; + nodesStack.PushLast(nodesStackPtr, SortedNodes.Length - 1); // start at root node; + while (nodesStack.PopLast(nodesStackPtr, out int nodeIndex)) { + BVHNode node = nodesPtr[nodeIndex]; + + if (!node.AABB.IntersectsRay(rayOrigin, rayDirectionNormalized, rayLength) || !node.IsValid()) + continue; + + if (nodeIndex < leafNodesCount) { + collector.AddNode(leafDataPtr[node.DataIndex]); + } else { + for (int i = 0; i < BVHUtils.NbLeavesPerNode; i++) { + nodesStack.PushLast(nodesStackPtr, node.DataIndex + i); + } + } + } + + return collector.HasFoundResults(); + } + + // Perform a raycast and fetch only the closest entity (no collector required) + // This version also gives the closest distance. + public unsafe bool QueryRayClosest(float3 rayOrigin, float3 rayDirectionNormalized, float rayLength, + out TNodeData closestHit, out float closestDistance) + { + closestHit = default; + closestDistance = float.MaxValue; + + if (SortedNodes.Length < 1) { + return false; + } + + Stack nodesStack = new Stack(256); + int* nodesStackPtr = stackalloc int[nodesStack.Capacity]; + BVHNode* nodesPtr = SortedNodes.GetUnsafeReadOnlyPtr(); + TNodeData* leafDataPtr = LeafNodeDatas.GetUnsafeReadOnlyPtr(); + int leafNodesCount = LeafNodeDatas.Length; + + float currentMaxT = rayLength; + bool found = false; + + nodesStack.PushLast(nodesStackPtr, SortedNodes.Length - 1); + while (nodesStack.PopLast(nodesStackPtr, out int nodeIndex)) { + BVHNode node = nodesPtr[nodeIndex]; + if (!node.IsValid()) + continue; + if (!node.AABB.IntersectsRay(rayOrigin, rayDirectionNormalized, currentMaxT, out float tEntry)) + continue; + if (tEntry >= currentMaxT) + continue; + + if (nodeIndex < leafNodesCount) { + currentMaxT = tEntry; + closestDistance = tEntry; + closestHit = leafDataPtr[node.DataIndex]; + found = true; + } else { + // Ordered child traversal: push children in descending tEntry order so closest pops first + int childBase = node.DataIndex; + float4 ts = new float4(float.MaxValue); + int4 idxs = new int4(-1); + for (int i = 0; i < BVHUtils.NbLeavesPerNode; i++) + { + int ci = childBase + i; + BVHNode child = nodesPtr[ci]; + if (!child.IsValid()) + continue; + if (child.AABB.IntersectsRay(rayOrigin, rayDirectionNormalized, currentMaxT, out float t)) + { + ts[i] = t; + idxs[i] = ci; + } + } + // Insertion sort, descending by t — smallest t ends up on top of stack + for (int a = 0; a < 4; a++) + { + for (int b = a + 1; b < 4; b++) + { + if (ts[b] > ts[a]) + { + (ts[a], ts[b]) = (ts[b], ts[a]); + (idxs[a], idxs[b]) = (idxs[b], idxs[a]); + } + } + } + for (int i = 0; i < 4; i++) + { + if (idxs[i] >= 0) { nodesStack.PushLast(nodesStackPtr, idxs[i]); } + } + } + } + + return found; + } + + // Returns true if any node was hit (useful for line-of-sight tests). + public unsafe bool QueryRayAny(float3 rayOrigin, float3 rayDirectionNormalized, float rayLength) + { + if (SortedNodes.Length < 1) + return false; + + Stack nodesStack = new Stack(256); + int* nodesStackPtr = stackalloc int[nodesStack.Capacity]; + BVHNode* nodesPtr = SortedNodes.GetUnsafeReadOnlyPtr(); + int leafNodesCount = LeafNodeDatas.Length; + nodesStack.PushLast(nodesStackPtr, SortedNodes.Length - 1); // start at root node; while (nodesStack.PopLast(nodesStackPtr, out int nodeIndex)) { @@ -397,7 +501,7 @@ public unsafe bool QueryRay(float3 rayOrigin, float3 rayDirectionNor if (nodeIndex < leafNodesCount) { - collector.AddNode(leafDataPtr[node.DataIndex]); + return true; } else { @@ -408,7 +512,15 @@ public unsafe bool QueryRay(float3 rayOrigin, float3 rayDirectionNor } } - return collector.HasFoundResults(); + return false; + } + + + // Perform a raycast and fetch only the closest entity (no collector required) + // Use this version if you don't care about the distance. + public bool QueryRayClosest(float3 rayOrigin, float3 rayDirectionNormalized, float rayLength, out TNodeData closestHit) + { + return QueryRayClosest(rayOrigin, rayDirectionNormalized, rayLength, out closestHit, out _); } public bool QueryNearestNeighbor(float3 position, ref NearestNeighborResultCollector collector,