// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.DataStructure
{
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using UnityEngine;
using Utils;
///
/// Immutable octree for efficient 3D spatial queries.
///
///
/// .Entry[] entries = points.Select(p => new OctTree3D.Entry(p, p)).ToArray();
/// OctTree3D tree = OctTree3D.Build(entries);
/// List hits = new List();
/// tree.GetElementsInBounds(searchBounds, hits);
/// ]]>
///
/// Element type contained in the tree.
///
/// Pros: Good all-around performance for 3D point queries, range queries, and approximate nearest neighbor searches.
/// Cons: Immutable structure by design; rebuild when positions change frequently.
/// Semantics: OctTree3D uses octant subdivision and inclusive half-open containment checks with internal
/// fast-paths when nodes are fully contained. These choices can lead to different results from KdTree3D for points
/// on boundaries or when numeric precision interacts with minimum node-size adjustments. See docs/features/spatial/spatial-tree-semantics.md.
///
[Serializable]
public sealed class OctTree3D : ISpatialTree3D
{
private const float MinimumNodeSize = 0.001f;
private const int NumChildren = 8;
[Serializable]
public readonly struct Entry
{
public readonly T value;
public readonly Vector3 position;
public Entry(T value, Vector3 position)
{
this.value = value;
this.position = position;
}
}
[Serializable]
public sealed class OctTreeNode
{
public readonly BoundingBox3D boundary;
internal readonly OctTreeNode[] _children;
internal readonly int _startIndex;
internal readonly int _count;
public readonly bool isTerminal;
public readonly Bounds unityBoundary;
private OctTreeNode(
BoundingBox3D boundary,
int startIndex,
int count,
bool isTerminal,
OctTreeNode[] children,
Bounds unityBoundary
)
{
this.boundary = boundary;
_startIndex = startIndex;
_count = count;
this.isTerminal = isTerminal;
_children = children ?? Array.Empty();
this.unityBoundary = unityBoundary;
}
internal static OctTreeNode CreateLeaf(
BoundingBox3D boundary,
int startIndex,
int count,
Bounds unityBoundary
)
{
return new OctTreeNode(
boundary,
startIndex,
count,
true,
Array.Empty(),
unityBoundary
);
}
internal static OctTreeNode CreateInternal(
BoundingBox3D boundary,
OctTreeNode[] children,
int startIndex,
int count,
Bounds unityBoundary
)
{
return new OctTreeNode(boundary, startIndex, count, false, children, unityBoundary);
}
}
private readonly struct NodeDistance
{
internal readonly OctTreeNode _node;
internal readonly float _distanceSquared;
internal NodeDistance(OctTreeNode node, float distanceSquared)
{
_node = node;
_distanceSquared = distanceSquared;
}
}
private readonly struct EntryDistance
{
internal readonly Entry entry;
internal readonly float distanceSquared;
internal EntryDistance(Entry entry, float distanceSquared)
{
this.entry = entry;
this.distanceSquared = distanceSquared;
}
}
private sealed class EntryDistanceComparer : IComparer
{
internal static readonly EntryDistanceComparer Instance = new();
public int Compare(EntryDistance x, EntryDistance y)
{
return x.distanceSquared.CompareTo(y.distanceSquared);
}
}
public const int DefaultBucketSize = 12;
public enum NodeVisitKind
{
FullyContainedInternal,
FullyContainedLeaf,
PartiallyContainedInternal,
PartiallyContainedLeaf,
}
public enum NodePruneReason
{
EmptyOrNullChild,
NoIntersection,
}
public readonly struct BoundsQueryNodeTrace
{
public BoundsQueryNodeTrace(
BoundingBox3D boundary,
Bounds unityBounds,
int count,
bool isTerminal,
bool nodeFullyContained
)
{
Boundary = boundary;
UnityBounds = unityBounds;
Count = count;
IsTerminal = isTerminal;
NodeFullyContained = nodeFullyContained;
VisitKind = DetermineVisitKind(isTerminal, nodeFullyContained);
}
public BoundingBox3D Boundary { get; }
public Bounds UnityBounds { get; }
public int Count { get; }
public bool IsTerminal { get; }
public bool NodeFullyContained { get; }
public NodeVisitKind VisitKind { get; }
private static NodeVisitKind DetermineVisitKind(
bool isTerminal,
bool nodeFullyContained
)
{
if (nodeFullyContained)
{
return isTerminal
? NodeVisitKind.FullyContainedLeaf
: NodeVisitKind.FullyContainedInternal;
}
return isTerminal
? NodeVisitKind.PartiallyContainedLeaf
: NodeVisitKind.PartiallyContainedInternal;
}
}
public interface IOctTreeBoundsQueryLogger
{
void OnQueryInitialized(
Bounds closedQuery,
BoundingBox3D halfOpenQuery,
BoundingBox3D treeBounds
);
void OnRootPruned();
void OnNodeVisited(in BoundsQueryNodeTrace trace);
void OnBulkAppend(
in BoundsQueryNodeTrace trace,
int appendedCount,
bool viaClosedContainment
);
void OnPointEvaluated(Vector3 position, bool included, in BoundsQueryNodeTrace trace);
void OnChildPruned(BoundingBox3D childBounds, NodePruneReason reason);
}
public readonly ImmutableArray elements;
public Bounds Boundary => _boundary;
private readonly BoundingBox3D _bounds;
private readonly Bounds _boundary;
private readonly Entry[] _entries;
private readonly int[] _indices;
private readonly OctTreeNode _head;
public OctTree3D(
IEnumerable points,
Func elementTransformer,
Bounds? boundary = null,
int bucketSize = DefaultBucketSize
)
{
if (elementTransformer is null)
{
throw new ArgumentNullException(nameof(elementTransformer));
}
elements =
points?.ToImmutableArray() ?? throw new ArgumentNullException(nameof(points));
int elementCount = elements.Length;
_entries = elementCount == 0 ? Array.Empty() : new Entry[elementCount];
_indices = elementCount == 0 ? Array.Empty() : new int[elementCount];
BoundingBox3D bounds = boundary.HasValue
? BoundingBox3D.FromClosedBounds(boundary.Value)
: BoundingBox3D.Empty;
bool anyPoints = boundary.HasValue;
float minX = float.PositiveInfinity;
float minY = float.PositiveInfinity;
float minZ = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float maxY = float.NegativeInfinity;
float maxZ = float.NegativeInfinity;
for (int i = 0; i < elementCount; ++i)
{
T element = elements[i];
Vector3 position = elementTransformer(element);
_entries[i] = new Entry(element, position);
if (position.x < minX)
{
minX = position.x;
}
if (position.y < minY)
{
minY = position.y;
}
if (position.z < minZ)
{
minZ = position.z;
}
if (position.x > maxX)
{
maxX = position.x;
}
if (position.y > maxY)
{
maxY = position.y;
}
if (position.z > maxZ)
{
maxZ = position.z;
}
if (anyPoints)
{
bounds = bounds.ExpandToInclude(position);
}
else
{
bounds = BoundingBox3D.FromPoint(position);
anyPoints = true;
}
_indices[i] = i;
}
if (anyPoints)
{
bounds = bounds.EnsureMinimumSize(0.001f);
}
_bounds = bounds;
if (elementCount == 0)
{
_boundary = new Bounds();
}
else
{
Vector3 closedMin = new(minX, minY, minZ);
Vector3 closedMax = new(maxX, maxY, maxZ);
Vector3 center = (closedMin + closedMax) * 0.5f;
Vector3 size = closedMax - closedMin;
_boundary = new Bounds(center, size);
}
if (elementCount == 0)
{
_head = OctTreeNode.CreateLeaf(_bounds, 0, 0, new Bounds());
return;
}
bucketSize = Math.Max(1, bucketSize);
int[] scratch = ArrayPool.Shared.Rent(elementCount);
try
{
_head = BuildNode(_bounds, 0, elementCount, bucketSize, scratch);
}
finally
{
ArrayPool.Shared.Return(scratch, clearArray: true);
}
}
private OctTreeNode BuildNode(
BoundingBox3D boundary,
int startIndex,
int count,
int bucketSize,
int[] scratch
)
{
if (count <= 0)
{
return OctTreeNode.CreateLeaf(boundary, startIndex, 0, new Bounds());
}
if (count <= bucketSize)
{
Bounds leafUnity = CalculateUnityBounds(startIndex, count);
return OctTreeNode.CreateLeaf(boundary, startIndex, count, leafUnity);
}
Span counts = stackalloc int[NumChildren];
Span starts = stackalloc int[NumChildren];
Span next = stackalloc int[NumChildren];
Span source = _indices.AsSpan(startIndex, count);
Span temp = scratch.AsSpan(0, count);
Vector3 boundaryMin = boundary.min;
Vector3 boundaryMax = boundary.max;
Vector3 boundaryCenter = boundary.Center;
float centerX = boundaryCenter.x;
float centerY = boundaryCenter.y;
float centerZ = boundaryCenter.z;
Entry[] entries = _entries;
for (int i = 0; i < count; ++i)
{
int entryIndex = source[i];
Vector3 position = entries[entryIndex].position;
bool east = position.x >= centerX;
bool north = position.y >= centerY;
bool up = position.z >= centerZ;
int octant = (up ? 4 : 0) | (east ? 2 : 0) | (north ? 1 : 0);
counts[octant]++;
}
int maxChildCount = 0;
int running = 0;
for (int q = 0; q < NumChildren; ++q)
{
starts[q] = running;
next[q] = running;
running += counts[q];
if (counts[q] > maxChildCount)
{
maxChildCount = counts[q];
}
}
if (maxChildCount == count)
{
Bounds degenerateUnity = CalculateUnityBounds(startIndex, count);
return OctTreeNode.CreateLeaf(boundary, startIndex, count, degenerateUnity);
}
for (int i = 0; i < count; ++i)
{
int entryIndex = source[i];
Vector3 position = entries[entryIndex].position;
bool east = position.x >= centerX;
bool north = position.y >= centerY;
bool up = position.z >= centerZ;
int octant = (up ? 4 : 0) | (east ? 2 : 0) | (north ? 1 : 0);
int destination = next[octant]++;
temp[destination] = entryIndex;
}
temp.CopyTo(source);
float minX = boundaryMin.x;
float minY = boundaryMin.y;
float minZ = boundaryMin.z;
float maxX = boundaryMax.x;
float maxY = boundaryMax.y;
float maxZ = boundaryMax.z;
Span octants = stackalloc BoundingBox3D[NumChildren];
// Bottom layer (z-)
octants[0] = new BoundingBox3D(
new Vector3(minX, minY, minZ),
new Vector3(centerX, centerY, centerZ)
);
octants[2] = new BoundingBox3D(
new Vector3(centerX, minY, minZ),
new Vector3(maxX, centerY, centerZ)
);
octants[1] = new BoundingBox3D(
new Vector3(minX, centerY, minZ),
new Vector3(centerX, maxY, centerZ)
);
octants[3] = new BoundingBox3D(
new Vector3(centerX, centerY, minZ),
new Vector3(maxX, maxY, centerZ)
);
// Top layer (z+)
octants[4] = new BoundingBox3D(
new Vector3(minX, minY, centerZ),
new Vector3(centerX, centerY, maxZ)
);
octants[6] = new BoundingBox3D(
new Vector3(centerX, minY, centerZ),
new Vector3(maxX, centerY, maxZ)
);
octants[5] = new BoundingBox3D(
new Vector3(minX, centerY, centerZ),
new Vector3(centerX, maxY, maxZ)
);
octants[7] = new BoundingBox3D(
new Vector3(centerX, centerY, centerZ),
new Vector3(maxX, maxY, maxZ)
);
OctTreeNode[] children = new OctTreeNode[NumChildren];
for (int q = 0; q < NumChildren; ++q)
{
int childCount = counts[q];
if (childCount <= 0)
{
continue;
}
int childStart = startIndex + starts[q];
children[q] = BuildNode(octants[q], childStart, childCount, bucketSize, scratch);
}
// Combine children Unity bounds to mirror KDTree behavior
Bounds nodeUnity = default;
bool initialized = false;
for (int q = 0; q < NumChildren; ++q)
{
OctTreeNode child = children[q];
if (child is null || child._count <= 0)
{
continue;
}
if (!initialized)
{
nodeUnity = child.unityBoundary;
initialized = true;
}
else
{
nodeUnity.Encapsulate(child.unityBoundary);
}
}
EnsureMinimumUnityBounds(ref nodeUnity);
return OctTreeNode.CreateInternal(boundary, children, startIndex, count, nodeUnity);
}
public List GetElementsInRange(
Vector3 position,
float range,
List elementsInRange,
float minimumRange = 0
)
{
elementsInRange.Clear();
if (range < 0f || _head._count <= 0)
{
return elementsInRange;
}
Sphere querySphere = new(position, range);
BoundingBox3D queryBounds = BoundingBox3D.FromCenterAndSize(
position,
new Vector3(range * 2f, range * 2f, range * 2f)
);
if (!queryBounds.Intersects(_bounds))
{
return elementsInRange;
}
using PooledResource> nodesToVisitResource =
Buffers.Stack.Get(out Stack nodesToVisit);
nodesToVisit.Push(_head);
Entry[] entries = _entries;
int[] indices = _indices;
float rangeSquared = range * range;
bool hasMinimumRange = 0f < minimumRange;
float minimumRangeSquared = minimumRange * minimumRange;
Sphere minimumSphere = hasMinimumRange ? new Sphere(position, minimumRange) : default;
while (nodesToVisit.TryPop(out OctTreeNode currentNode))
{
if (currentNode is null || currentNode._count <= 0)
{
continue;
}
if (!queryBounds.Intersects(currentNode.boundary))
{
continue;
}
// Use Sphere.Overlaps to check if the sphere fully contains the node's boundary
// This is more accurate than using a bounding box approximation
bool nodeFullyContained = querySphere.Overlaps(currentNode.boundary);
if (currentNode.isTerminal || nodeFullyContained)
{
int start = currentNode._startIndex;
int end = start + currentNode._count;
// If the node is fully contained, we can skip distance checks for points
// but still need to check minimum range
if (nodeFullyContained)
{
if (!hasMinimumRange)
{
for (int i = start; i < end; ++i)
{
elementsInRange.Add(entries[indices[i]].value);
}
}
else
{
// Node is fully in outer sphere, but need to check minimum range
// Check if node is fully outside minimum sphere
bool nodeFullyOutsideMinimum = !minimumSphere.Intersects(
currentNode.boundary
);
if (nodeFullyOutsideMinimum)
{
// Fast path: all points are in the annulus
for (int i = start; i < end; ++i)
{
elementsInRange.Add(entries[indices[i]].value);
}
}
else
{
// Need to check each point against minimum range
for (int i = start; i < end; ++i)
{
Entry entry = entries[indices[i]];
float squareDistance = (entry.position - position).sqrMagnitude;
if (squareDistance > minimumRangeSquared)
{
elementsInRange.Add(entry.value);
}
}
}
}
}
else
{
// Terminal node but not fully contained: check each point
for (int i = start; i < end; ++i)
{
Entry entry = entries[indices[i]];
float squareDistance = (entry.position - position).sqrMagnitude;
if (squareDistance > rangeSquared)
{
continue;
}
if (hasMinimumRange && squareDistance <= minimumRangeSquared)
{
continue;
}
elementsInRange.Add(entry.value);
}
}
continue;
}
OctTreeNode[] childNodes = currentNode._children;
for (int i = 0; i < childNodes.Length; ++i)
{
OctTreeNode child = childNodes[i];
if (child is null || child._count <= 0)
{
continue;
}
if (queryBounds.Intersects(child.boundary))
{
nodesToVisit.Push(child);
}
}
}
return elementsInRange;
}
public List GetElementsInBounds(Bounds queryBounds, List elementsInBounds)
{
return GetElementsInBoundsInternal(queryBounds, elementsInBounds, logger: null);
}
internal List GetElementsInBoundsWithDiagnostics(
Bounds queryBounds,
List elementsInBounds,
IOctTreeBoundsQueryLogger logger
)
{
if (logger is null)
{
throw new ArgumentNullException(nameof(logger));
}
return GetElementsInBoundsInternal(queryBounds, elementsInBounds, logger);
}
private List GetElementsInBoundsInternal(
Bounds queryBounds,
List elementsInBounds,
IOctTreeBoundsQueryLogger logger
)
{
elementsInBounds.Clear();
if (_head._count <= 0)
{
logger?.OnQueryInitialized(queryBounds, BoundingBox3D.Empty, _bounds);
logger?.OnRootPruned();
return elementsInBounds;
}
// Use inclusive-max conversion to align with KDTree semantics at max edges
BoundingBox3D queryHalfOpen = BoundingBox3D.FromClosedBoundsInclusiveMax(queryBounds);
logger?.OnQueryInitialized(queryBounds, queryHalfOpen, _bounds);
// Heavy closed-bounds asserts are optional to keep performance tests lightweight.
#if UNITY_ASSERTIONS && ENABLE_SPATIAL_DIAGNOSTICS
Bounds closedQuery = queryBounds;
#endif
bool rootIntersects = queryHalfOpen.Intersects(_bounds);
#if UNITY_ASSERTIONS && ENABLE_SPATIAL_DIAGNOSTICS
bool closedRootIntersects = ClosedIntersects(closedQuery, _bounds);
UnityEngine.Assertions.Assert.AreEqual(
closedRootIntersects,
rootIntersects,
"[OctTree3D] Root intersection semantics mismatch between half-open and closed bounds."
);
#endif
if (!rootIntersects)
{
logger?.OnRootPruned();
return elementsInBounds;
}
using PooledResource> stackResource = Buffers.Stack.Get(
out Stack nodesToVisit
);
nodesToVisit.Push(_head);
Entry[] entries = _entries;
int[] indices = _indices;
while (nodesToVisit.TryPop(out OctTreeNode currentNode))
{
if (currentNode is null || currentNode._count <= 0)
{
logger?.OnChildPruned(
currentNode?.boundary ?? BoundingBox3D.Empty,
NodePruneReason.EmptyOrNullChild
);
continue;
}
bool nodeFullyContained = queryHalfOpen.Contains(currentNode.boundary);
#if UNITY_ASSERTIONS && ENABLE_SPATIAL_DIAGNOSTICS
bool closedNodeContained = ClosedContains(closedQuery, currentNode.boundary);
UnityEngine.Assertions.Assert.AreEqual(
closedNodeContained,
nodeFullyContained,
"[OctTree3D] Node containment semantics mismatch between half-open and closed bounds."
);
#endif
BoundsQueryNodeTrace trace = new(
currentNode.boundary,
currentNode.unityBoundary,
currentNode._count,
currentNode.isTerminal,
nodeFullyContained
);
logger?.OnNodeVisited(trace);
if (nodeFullyContained)
{
// Conservative guard for leaves: ensure the leaf's closed Unity bounds
// are fully contained by the closed query before fast-adding.
// For internal nodes, inclusive half-open containment is sufficient.
if (currentNode.isTerminal)
{
Bounds u = currentNode.unityBoundary;
// If fully contained under closed semantics (via inclusive half-open), fast-add all entries.
if (queryHalfOpen.Contains(u.min) && queryHalfOpen.Contains(u.max))
{
int start = currentNode._startIndex;
int end = start + currentNode._count;
for (int i = start; i < end; ++i)
{
elementsInBounds.Add(entries[indices[i]].value);
}
logger?.OnBulkAppend(
trace,
currentNode._count,
viaClosedContainment: true
);
continue;
}
// Otherwise, fall through to the terminal per-point closed checks below.
}
else
{
int start = currentNode._startIndex;
int end = start + currentNode._count;
for (int i = start; i < end; ++i)
{
elementsInBounds.Add(entries[indices[i]].value);
}
logger?.OnBulkAppend(
trace,
currentNode._count,
viaClosedContainment: false
);
continue;
}
}
if (currentNode.isTerminal)
{
int start = currentNode._startIndex;
int end = start + currentNode._count;
for (int i = start; i < end; ++i)
{
Entry entry = entries[indices[i]];
// Per-point checks use inclusive half-open semantics for closed behavior
bool contains = queryHalfOpen.Contains(entry.position);
#if UNITY_ASSERTIONS && ENABLE_SPATIAL_DIAGNOSTICS
bool closedContains = queryBounds.Contains(entry.position);
UnityEngine.Assertions.Assert.AreEqual(
closedContains,
contains,
"[OctTree3D] Point inclusion mismatch between half-open and closed bounds."
);
#endif
logger?.OnPointEvaluated(entry.position, contains, trace);
if (contains)
{
elementsInBounds.Add(entry.value);
}
}
continue;
}
OctTreeNode[] childNodes = currentNode._children;
for (int i = 0; i < childNodes.Length; ++i)
{
OctTreeNode child = childNodes[i];
if (child is null || child._count <= 0)
{
logger?.OnChildPruned(
child?.boundary ?? BoundingBox3D.Empty,
NodePruneReason.EmptyOrNullChild
);
continue;
}
// Traverse children that intersect under half-open semantics
bool childIntersects = queryHalfOpen.Intersects(child.boundary);
#if UNITY_ASSERTIONS && ENABLE_SPATIAL_DIAGNOSTICS
bool closedChildIntersects = ClosedIntersects(closedQuery, child.boundary);
UnityEngine.Assertions.Assert.AreEqual(
closedChildIntersects,
childIntersects,
"[OctTree3D] Child traversal semantics mismatch between half-open and closed bounds."
);
#endif
if (childIntersects)
{
nodesToVisit.Push(child);
}
else
{
logger?.OnChildPruned(child.boundary, NodePruneReason.NoIntersection);
}
}
}
return elementsInBounds;
}
private Bounds CalculateUnityBounds(int startIndex, int count)
{
if (count <= 0)
{
return new Bounds();
}
Entry[] entries = _entries;
int[] indices = _indices;
float minX = float.PositiveInfinity;
float minY = float.PositiveInfinity;
float minZ = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float maxY = float.NegativeInfinity;
float maxZ = float.NegativeInfinity;
int end = startIndex + count;
for (int i = startIndex; i < end; ++i)
{
Vector3 p = entries[indices[i]].position;
if (p.x < minX)
{
minX = p.x;
}
if (p.y < minY)
{
minY = p.y;
}
if (p.z < minZ)
{
minZ = p.z;
}
if (p.x > maxX)
{
maxX = p.x;
}
if (p.y > maxY)
{
maxY = p.y;
}
if (p.z > maxZ)
{
maxZ = p.z;
}
}
Vector3 min = new(minX, minY, minZ);
Vector3 max = new(maxX, maxY, maxZ);
Vector3 center = (min + max) * 0.5f;
Vector3 size = max - min;
Bounds b = new(center, size);
EnsureMinimumUnityBounds(ref b);
return b;
}
private static void EnsureMinimumUnityBounds(ref Bounds bounds)
{
Vector3 size = bounds.size;
if (size.x < MinimumNodeSize)
{
size.x = MinimumNodeSize;
}
if (size.y < MinimumNodeSize)
{
size.y = MinimumNodeSize;
}
if (size.z < MinimumNodeSize)
{
size.z = MinimumNodeSize;
}
bounds.size = size;
}
#if UNITY_ASSERTIONS && ENABLE_SPATIAL_DIAGNOSTICS
private static Vector3 ComputeClosedMax(Vector3 min, Vector3 exclusiveMax)
{
float closedX = PrevFloat(exclusiveMax.x);
float closedY = PrevFloat(exclusiveMax.y);
float closedZ = PrevFloat(exclusiveMax.z);
if (closedX < min.x)
{
closedX = min.x;
}
if (closedY < min.y)
{
closedY = min.y;
}
if (closedZ < min.z)
{
closedZ = min.z;
}
return new Vector3(closedX, closedY, closedZ);
}
private static bool ClosedContains(Bounds container, BoundingBox3D contents)
{
Vector3 contentMin = contents.min;
Vector3 contentMax = ComputeClosedMax(contentMin, contents.max);
Vector3 containerMin = container.min;
Vector3 containerMax = container.max;
return containerMin.x <= contentMin.x
&& containerMin.y <= contentMin.y
&& containerMin.z <= contentMin.z
&& containerMax.x >= contentMax.x
&& containerMax.y >= contentMax.y
&& containerMax.z >= contentMax.z;
}
private static bool ClosedIntersects(Bounds closedQuery, BoundingBox3D nodeBoundary)
{
Vector3 nodeMin = nodeBoundary.min;
Vector3 nodeMax = ComputeClosedMax(nodeMin, nodeBoundary.max);
Vector3 queryMin = closedQuery.min;
Vector3 queryMax = closedQuery.max;
return nodeMax.x >= queryMin.x
&& nodeMin.x <= queryMax.x
&& nodeMax.y >= queryMin.y
&& nodeMin.y <= queryMax.y
&& nodeMax.z >= queryMin.z
&& nodeMin.z <= queryMax.z;
}
#endif
private static float PrevFloat(float value)
{
if (float.IsNaN(value) || float.IsInfinity(value))
{
return value;
}
if (value == float.MinValue)
{
return value;
}
if (value == 0f)
{
return -float.Epsilon;
}
int bits = BitConverter.SingleToInt32Bits(value);
bits = value > 0f ? bits - 1 : bits + 1;
return BitConverter.Int32BitsToSingle(bits);
}
// No additional helpers; use Unity Bounds methods to mirror KDTree behavior
public List GetApproximateNearestNeighbors(
Vector3 position,
int count,
List nearestNeighbors
)
{
nearestNeighbors.Clear();
if (count <= 0 || _head._count == 0)
{
return nearestNeighbors;
}
Entry[] entries = _entries;
int[] indices = _indices;
using PooledResource> nodeHeapResource =
Buffers.List.Get(out List nodeHeap);
PushNode(nodeHeap, _head, position);
using PooledResource> bestNeighborResource =
Buffers.List.Get(out List bestNeighbors);
using PooledResource> bestNeighborValuesResource = Buffers.HashSet.Get(
out HashSet bestNeighborValues
);
float currentWorstDistanceSquared = float.PositiveInfinity;
while (nodeHeap.Count > 0)
{
NodeDistance best = PopNode(nodeHeap);
if (
bestNeighbors.Count >= count
&& best._distanceSquared >= currentWorstDistanceSquared
)
{
break;
}
OctTreeNode currentNode = best._node;
if (!currentNode.isTerminal)
{
OctTreeNode[] childNodes = currentNode._children;
for (int i = 0; i < childNodes.Length; ++i)
{
OctTreeNode child = childNodes[i];
if (child is not null && child._count > 0)
{
PushNode(nodeHeap, child, position);
}
}
continue;
}
int startIndex = currentNode._startIndex;
int endIndex = startIndex + currentNode._count;
for (int i = startIndex; i < endIndex; ++i)
{
Entry entry = entries[indices[i]];
float distanceSquared = (entry.position - position).sqrMagnitude;
if (bestNeighbors.Count < count)
{
if (!bestNeighborValues.Add(entry.value))
{
continue;
}
bestNeighbors.Add(new EntryDistance(entry, distanceSquared));
if (bestNeighbors.Count == count)
{
currentWorstDistanceSquared = FindWorstDistanceSquared(bestNeighbors);
}
continue;
}
if (distanceSquared >= currentWorstDistanceSquared)
{
continue;
}
if (!bestNeighborValues.Add(entry.value))
{
continue;
}
EntryDistance replaced = ReplaceWorstNeighbor(
bestNeighbors,
entry,
distanceSquared
);
bestNeighborValues.Remove(replaced.entry.value);
currentWorstDistanceSquared = FindWorstDistanceSquared(bestNeighbors);
}
}
if (bestNeighbors.Count > 1)
{
bestNeighbors.Sort(EntryDistanceComparer.Instance);
}
nearestNeighbors.Clear();
for (int i = 0; i < bestNeighbors.Count && i < count; ++i)
{
nearestNeighbors.Add(bestNeighbors[i].entry.value);
}
return nearestNeighbors;
static void PushNode(List heap, OctTreeNode node, Vector3 point)
{
NodeDistance entry = new(node, node.boundary.DistanceSquaredTo(point));
heap.Add(entry);
int index = heap.Count - 1;
while (index > 0)
{
int parent = (index - 1) >> 1;
NodeDistance parentEntry = heap[parent];
if (parentEntry._distanceSquared <= entry._distanceSquared)
{
break;
}
heap[index] = parentEntry;
index = parent;
}
heap[index] = entry;
}
static NodeDistance PopNode(List heap)
{
int lastIndex = heap.Count - 1;
NodeDistance result = heap[0];
NodeDistance last = heap[lastIndex];
heap.RemoveAt(lastIndex);
int index = 0;
int count = heap.Count;
while (true)
{
int left = (index << 1) + 1;
if (left >= count)
{
break;
}
int right = left + 1;
int smallest =
right < count && heap[right]._distanceSquared < heap[left]._distanceSquared
? right
: left;
if (last._distanceSquared <= heap[smallest]._distanceSquared)
{
break;
}
heap[index] = heap[smallest];
index = smallest;
}
if (count > 0)
{
heap[index] = last;
}
return result;
}
static float FindWorstDistanceSquared(List candidates)
{
float worst = 0f;
for (int i = 0; i < candidates.Count; ++i)
{
float distance = candidates[i].distanceSquared;
if (distance > worst)
{
worst = distance;
}
}
return worst;
}
static EntryDistance ReplaceWorstNeighbor(
List candidates,
Entry entry,
float distanceSquared
)
{
int worstIndex = 0;
float worstDistance = candidates[0].distanceSquared;
for (int i = 1; i < candidates.Count; ++i)
{
float candidateDistance = candidates[i].distanceSquared;
if (candidateDistance > worstDistance)
{
worstDistance = candidateDistance;
worstIndex = i;
}
}
EntryDistance replaced = candidates[worstIndex];
candidates[worstIndex] = new EntryDistance(entry, distanceSquared);
return replaced;
}
}
}
}