Skip to content

Instantly share code, notes, and snippets.

@nilpunch
Created January 4, 2026 20:36
Show Gist options
  • Select an option

  • Save nilpunch/b59c6226205c1b612929ff9eaed4d440 to your computer and use it in GitHub Desktop.

Select an option

Save nilpunch/b59c6226205c1b612929ff9eaed4d440 to your computer and use it in GitHub Desktop.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Massive
{
/// <summary>
/// 12-byte.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Vec3
{
public float X;
public float Y;
public float Z;
public Vec3(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
}
/// <summary>
/// 24-byte.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct AABB
{
public Vec3 LowerBound;
public Vec3 UpperBound;
}
public class DynamicTree
{
public const int DefaultCategory = 1;
public const int NullIndex = -1;
public const ushort AllocatedNode = 1 << 0;
public const ushort EnlargedNode = 1 << 1;
public const ushort LeafNode = 1 << 2;
private TreeNode[] Nodes { get; set; } = Array.Empty<TreeNode>();
private int NodesCapacity { get; set; }
private int NodesCount { get; set; }
private int FreeList { set; get; } = NullIndex;
private int Root { get; set; } = NullIndex;
private TreeNode DefaultNode { get; } = new TreeNode()
{
AABB = new AABB(),
CategoryBits = DefaultCategory,
Children = new Children()
{
Child1 = NullIndex,
Child2 = NullIndex,
},
Parent = NullIndex,
Height = 0,
Flags = AllocatedNode,
};
private int proxyCount = 0;
private int[] leafIndices;
private int[] leafBoxes;
private int[] leafCenters;
private int[] binIndices;
private int rebuildCapacity = 0;
private int AllocateNode()
{
if (FreeList == NullIndex)
{
var newCapacity = MathUtils.RoundUpToPowerOfTwo(NodesCount + 1);
Nodes = Nodes.Resize(newCapacity);
for (var i = NodesCount; i < NodesCapacity - 1; ++i)
{
Nodes[i].NextFree = i + 1;
}
Nodes[NodesCapacity - 1].NextFree = NullIndex;
FreeList = NodesCount;
}
var nodeIndex = FreeList;
ref var node = ref Nodes[nodeIndex];
FreeList = node.NextFree;
node = DefaultNode;
NodesCount += 1;
return nodeIndex;
}
private void FreeNode(int nodeId)
{
Nodes[nodeId].NextFree = FreeList;
Nodes[nodeId].Flags = 0;
FreeList = nodeId;
NodesCount -= 1;
}
// Greedy algorithm for sibling selection using the SAH
// We have three nodes A-(B,C) and want to add a leaf D, there are three choices.
// 1: make a new parent for A and D : E-(A-(B,C), D)
// 2: associate D with B
// a: B is a leaf : A-(E-(B,D), C)
// b: B is an internal node: A-(B{D},C)
// 3: associate D with C
// a: C is a leaf : A-(B, E-(C,D))
// b: C is an internal node: A-(B, C{D})
// All of these have a clear cost except when B or C is an internal node. Hence we need to be greedy.
// The cost for cases 1, 2a, and 3a can be computed using the sibling cost formula.
// cost of sibling H = area(union(H, D)) + increased area of ancestors
// Suppose B (or C) is an internal node, then the lowest cost would be one of two cases:
// case1: D becomes a sibling of B
// case2: D becomes a descendant of B along with a new internal node of area(D).
private int FindBestSibling(AABB boxD)
{
var centerD = Center(boxD);
var areaD = Perimeter(boxD);
var nodes = Nodes;
var rootIndex = Root;
ref var root = ref nodes[rootIndex];
// Area of current node.
var areaBase = Perimeter(root.AABB);
// Area of inflated node.
var directCost = Perimeter(Union(root.AABB, boxD));
var inheritedCost = 0.0f;
var bestSibling = rootIndex;
var bestCost = directCost;
var index = rootIndex;
// Descend the tree from root, following a single greedy path.
while (nodes[index].Height > 0)
{
ref var node = ref nodes[index];
var child1 = node.Children.Child1;
var child2 = node.Children.Child2;
// Cost of creating a new parent for this node and the new leaf.
var cost = directCost + inheritedCost;
// Sometimes there are multiple identical costs within tolerance.
// This breaks the ties using the centroid distance.
if (cost < bestCost)
{
bestSibling = index;
bestCost = cost;
}
// Inheritance cost seen by children.
inheritedCost += directCost - areaBase;
ref var n1 = ref nodes[child1];
ref var n2 = ref nodes[child2];
var leaf1 = n1.Height == 0;
var leaf2 = n2.Height == 0;
// Cost of descending into child 1.
var lowerCost1 = float.MaxValue;
var directCost1 = Perimeter(Union(n1.AABB, boxD));
var area1 = 0f;
if (leaf1)
{
// Child 1 is a leaf.
// Cost of creating new node and increasing area of node P.
var cost1 = directCost1 + inheritedCost;
// Need this here due to while condition above.
if (cost1 < bestCost)
{
bestSibling = child1;
bestCost = cost1;
}
}
else
{
// Child 1 is an internal node.
area1 = Perimeter(n1.AABB);
// Lower bound cost of inserting under child 1. The minimum accounts for two possibilities:
// 1. Child1 could be the sibling with cost1 = inheritedCost + directCost1
// 2. A descendent of child1 could be the sibling with the lower bound cost of
// cost1 = inheritedCost + (directCost1 - area1) + areaD
// This minimum here leads to the minimum of these two costs.
lowerCost1 = inheritedCost + directCost1 + Min(areaD - area1, 0f);
}
// Cost of descending into child 2.
var lowerCost2 = float.MaxValue;
var directCost2 = Perimeter(Union(n2.AABB, boxD));
var area2 = 0f;
if (leaf2)
{
var cost2 = directCost2 + inheritedCost;
if (cost2 < bestCost)
{
bestSibling = child2;
bestCost = cost2;
}
}
else
{
area2 = Perimeter(n2.AABB);
lowerCost2 = inheritedCost + directCost2 + Min(areaD - area2, 0f);
}
if (leaf1 && leaf2)
{
break;
}
// Can the cost possibly be decreased?
if (bestCost <= lowerCost1 && bestCost <= lowerCost2)
{
break;
}
if (lowerCost1 == lowerCost2 && !leaf1)
{
// No clear choice based on lower bound surface area. This can happen when both
// children fully contain D. Fall back to node distance.
var d1 = Sub(Center(n1.AABB), centerD);
var d2 = Sub(Center(n2.AABB), centerD);
lowerCost1 = LengthSquared(d1);
lowerCost2 = LengthSquared(d2);
}
// Descend.
if (lowerCost1 < lowerCost2 && !leaf1)
{
index = child1;
areaBase = area1;
directCost = directCost1;
}
else
{
index = child2;
areaBase = area2;
directCost = directCost2;
}
}
return bestSibling;
}
private enum RotateType
{
None,
BF,
BG,
CD,
CE
}
// Perform a left or right rotation if node A is imbalanced.
// Returns the new root index.
private void RotateNodes(int iA)
{
if (iA == NullIndex)
{
return;
}
ref var A = ref Nodes[iA];
if (A.Height < 2)
{
return;
}
var iB = A.Children.Child1;
var iC = A.Children.Child2;
ref var B = ref Nodes[iB];
ref var C = ref Nodes[iC];
if (B.Height == 0)
{
// B is a leaf and C is internal.
var iF = C.Children.Child1;
var iG = C.Children.Child2;
ref var F = ref Nodes[iF];
ref var G = ref Nodes[iG];
// Base cost.
var costBase = Perimeter(C.AABB);
// Cost of swapping B and F.
var aabbBG = Union(B.AABB, G.AABB);
var costBF = Perimeter(aabbBG);
// Cost of swapping B and G.
var aabbBF = Union(B.AABB, F.AABB);
var costBG = Perimeter(aabbBF);
if (costBase < costBF && costBase < costBG)
{
// Rotation does not improve cost.
return;
}
if (costBF < costBG)
{
// Swap B and F.
A.Children.Child1 = iF;
C.Children.Child1 = iB;
B.Parent = iC;
F.Parent = iA;
C.AABB = aabbBG;
C.Height = (ushort)(1 + Max(B.Height, G.Height));
A.Height = (ushort)(1 + Max(C.Height, F.Height));
C.CategoryBits = B.CategoryBits | G.CategoryBits;
A.CategoryBits = C.CategoryBits | F.CategoryBits;
C.Flags |= (ushort)((B.Flags | G.Flags) & EnlargedNode);
A.Flags |= (ushort)((C.Flags | F.Flags) & EnlargedNode);
}
else
{
// Swap B and G.
A.Children.Child1 = iG;
C.Children.Child2 = iB;
B.Parent = iC;
G.Parent = iA;
C.AABB = aabbBF;
C.Height = (ushort)(1 + Max(B.Height, F.Height));
A.Height = (ushort)(1 + Max(C.Height, G.Height));
C.CategoryBits = B.CategoryBits | F.CategoryBits;
A.CategoryBits = C.CategoryBits | G.CategoryBits;
C.Flags |= (ushort)((B.Flags | F.Flags) & EnlargedNode);
A.Flags |= (ushort)((C.Flags | G.Flags) & EnlargedNode);
}
}
else if (C.Height == 0)
{
// C is a leaf and B is internal.
var iD = B.Children.Child1;
var iE = B.Children.Child2;
ref var D = ref Nodes[iD];
ref var E = ref Nodes[iE];
// Base cost.
var costBase = Perimeter(B.AABB);
// Cost of swapping C and D.
var aabbCE = Union(C.AABB, E.AABB);
var costCD = Perimeter(aabbCE);
// Cost of swapping C and E.
var aabbCD = Union(C.AABB, D.AABB);
var costCE = Perimeter(aabbCD);
if (costBase < costCD && costBase < costCE)
{
// Rotation does not improve cost.
return;
}
if (costCD < costCE)
{
// Swap C and D.
A.Children.Child2 = iD;
B.Children.Child1 = iC;
C.Parent = iB;
D.Parent = iA;
B.AABB = aabbCE;
B.Height = (ushort)(1 + Max(C.Height, E.Height));
A.Height = (ushort)(1 + Max(B.Height, D.Height));
B.CategoryBits = C.CategoryBits | E.CategoryBits;
A.CategoryBits = B.CategoryBits | D.CategoryBits;
B.Flags |= (ushort)((C.Flags | E.Flags) & EnlargedNode);
A.Flags |= (ushort)((B.Flags | D.Flags) & EnlargedNode);
}
else
{
// Swap C and E.
A.Children.Child2 = iE;
B.Children.Child2 = iC;
C.Parent = iB;
E.Parent = iA;
B.AABB = aabbCD;
B.Height = (ushort)(1 + Max(C.Height, D.Height));
A.Height = (ushort)(1 + Max(B.Height, E.Height));
B.CategoryBits = C.CategoryBits | D.CategoryBits;
A.CategoryBits = B.CategoryBits | E.CategoryBits;
B.Flags |= (ushort)((C.Flags | D.Flags) & EnlargedNode);
A.Flags |= (ushort)((B.Flags | E.Flags) & EnlargedNode);
}
}
else
{
var iD = B.Children.Child1;
var iE = B.Children.Child2;
var iF = C.Children.Child1;
var iG = C.Children.Child2;
ref var D = ref Nodes[iD];
ref var E = ref Nodes[iE];
ref var F = ref Nodes[iF];
ref var G = ref Nodes[iG];
// Base cost.
var areaB = Perimeter(B.AABB);
var areaC = Perimeter(C.AABB);
var costBase = areaB + areaC;
var bestCost = costBase;
var bestRotation = RotateType.None;
// Cost of swapping B and F.
var aabbBG = Union(B.AABB, G.AABB);
var costBF = areaB + Perimeter(aabbBG);
if (costBF < bestCost)
{
bestCost = costBF;
bestRotation = RotateType.BF;
}
// Cost of swapping B and G.
var aabbBF = Union(B.AABB, F.AABB);
var costBG = areaB + Perimeter(aabbBF);
if (costBG < bestCost)
{
bestCost = costBG;
bestRotation = RotateType.BG;
}
// Cost of swapping C and D.
var aabbCE = Union(C.AABB, E.AABB);
var costCD = areaC + Perimeter(aabbCE);
if (costCD < bestCost)
{
bestCost = costCD;
bestRotation = RotateType.CD;
}
// Cost of swapping C and E.
var aabbCD = Union(C.AABB, D.AABB);
var costCE = areaC + Perimeter(aabbCD);
if (costCE < bestCost)
{
bestRotation = RotateType.CE;
// bestCost = costCE;
}
switch (bestRotation)
{
case RotateType.None:
break;
case RotateType.BF:
A.Children.Child1 = iF;
C.Children.Child1 = iB;
B.Parent = iC;
F.Parent = iA;
C.AABB = aabbBG;
C.Height = (ushort)(1 + Max(B.Height, G.Height));
A.Height = (ushort)(1 + Max(C.Height, F.Height));
C.CategoryBits = B.CategoryBits | G.CategoryBits;
A.CategoryBits = C.CategoryBits | F.CategoryBits;
C.Flags |= (ushort)((B.Flags | G.Flags) & EnlargedNode);
A.Flags |= (ushort)((C.Flags | F.Flags) & EnlargedNode);
break;
case RotateType.BG:
A.Children.Child1 = iG;
C.Children.Child2 = iB;
B.Parent = iC;
G.Parent = iA;
C.AABB = aabbBF;
C.Height = (ushort)(1 + Max(B.Height, F.Height));
A.Height = (ushort)(1 + Max(C.Height, G.Height));
C.CategoryBits = B.CategoryBits | F.CategoryBits;
A.CategoryBits = C.CategoryBits | G.CategoryBits;
C.Flags |= (ushort)((B.Flags | F.Flags) & EnlargedNode);
A.Flags |= (ushort)((C.Flags | G.Flags) & EnlargedNode);
break;
case RotateType.CD:
A.Children.Child2 = iD;
B.Children.Child1 = iC;
C.Parent = iB;
D.Parent = iA;
B.AABB = aabbCE;
B.Height = (ushort)(1 + Max(C.Height, E.Height));
A.Height = (ushort)(1 + Max(B.Height, D.Height));
B.CategoryBits = C.CategoryBits | E.CategoryBits;
A.CategoryBits = B.CategoryBits | D.CategoryBits;
B.Flags |= (ushort)((C.Flags | E.Flags) & EnlargedNode);
A.Flags |= (ushort)((B.Flags | D.Flags) & EnlargedNode);
break;
case RotateType.CE:
A.Children.Child2 = iE;
B.Children.Child2 = iC;
C.Parent = iB;
E.Parent = iA;
B.AABB = aabbCD;
B.Height = (ushort)(1 + Max(C.Height, D.Height));
A.Height = (ushort)(1 + Max(B.Height, E.Height));
B.CategoryBits = C.CategoryBits | D.CategoryBits;
A.CategoryBits = B.CategoryBits | E.CategoryBits;
B.Flags |= (ushort)((C.Flags | D.Flags) & EnlargedNode);
A.Flags |= (ushort)((B.Flags | E.Flags) & EnlargedNode);
break;
}
}
}
private void InsertLeaf(int leaf, bool shouldRotate)
{
if (Root == NullIndex)
{
Root = leaf;
if (NodesCapacity == 0)
{
AllocateNode();
}
Nodes[Root].Parent = NullIndex;
return;
}
// Stage 1: find the best sibling for this node.
var leafAABB = Nodes[leaf].AABB;
var sibling = FindBestSibling(leafAABB);
// Stage 2: create a new parent for the leaf and sibling.
var oldParent = Nodes[sibling].Parent;
var newParent = AllocateNode();
// Warning: node ref can change after allocation.
ref var newNode = ref Nodes[newParent];
newNode.Parent = oldParent;
newNode.UserData = ulong.MaxValue;
newNode.AABB = Union(leafAABB, Nodes[sibling].AABB);
newNode.CategoryBits = Nodes[leaf].CategoryBits | Nodes[sibling].CategoryBits;
newNode.Height = (ushort)(Nodes[sibling].Height + 1);
if (oldParent != NullIndex)
{
// The sibling was not the root.
if (Nodes[oldParent].Children.Child1 == sibling)
{
Nodes[oldParent].Children.Child1 = newParent;
}
else
{
Nodes[oldParent].Children.Child2 = newParent;
}
}
else
{
// The sibling was the root.
Root = newParent;
}
newNode.Children.Child1 = sibling;
newNode.Children.Child2 = leaf;
Nodes[sibling].Parent = newParent;
Nodes[leaf].Parent = newParent;
// Stage 3: walk back up the tree fixing heights and AABBs.
var index = Nodes[leaf].Parent;
while (index != NullIndex)
{
ref var node = ref Nodes[index];
var c1 = node.Children.Child1;
var c2 = node.Children.Child2;
node.AABB = Union(Nodes[c1].AABB, Nodes[c2].AABB);
node.CategoryBits = Nodes[c1].CategoryBits | Nodes[c2].CategoryBits;
node.Height = (ushort)(1 + Max(Nodes[c1].Height, Nodes[c2].Height));
node.Flags |= (ushort)((Nodes[c1].Flags | Nodes[c2].Flags) & EnlargedNode);
if (shouldRotate)
{
RotateNodes(index);
}
index = node.Parent;
}
}
private void RemoveLeaf(int leaf)
{
if (leaf == Root)
{
Root = NullIndex;
return;
}
ref var leafNode = ref Nodes[leaf];
var parent = leafNode.Parent;
var grandParent = Nodes[parent].Parent;
var sibling = (Nodes[parent].Children.Child1 == leaf)
? Nodes[parent].Children.Child2
: Nodes[parent].Children.Child1;
if (grandParent != NullIndex)
{
// Destroy parent and connect sibling to grandParent.
if (Nodes[grandParent].Children.Child1 == parent)
{
Nodes[grandParent].Children.Child1 = sibling;
}
else
{
Nodes[grandParent].Children.Child2 = sibling;
}
Nodes[sibling].Parent = grandParent;
FreeNode(parent);
// Adjust ancestor bounds.
var index = grandParent;
while (index != NullIndex)
{
ref var node = ref Nodes[index];
ref var c1 = ref Nodes[node.Children.Child1];
ref var c2 = ref Nodes[node.Children.Child2];
node.AABB = Union(c1.AABB, c2.AABB);
node.CategoryBits = c1.CategoryBits | c2.CategoryBits;
node.Height = (ushort)(1 + Max(c1.Height, c2.Height));
index = node.Parent;
}
}
else
{
Root = sibling;
Nodes[sibling].Parent = NullIndex;
FreeNode(parent);
}
}
public int CreateProxy(AABB aabb, ulong categoryBits, ulong userData)
{
// Create a proxy in the tree as a leaf node.
// We return the index of the node instead of a pointer so that we can grow the node pool.
var proxyId = AllocateNode();
ref var node = ref Nodes[proxyId];
node.AABB = aabb;
node.UserData = userData;
node.CategoryBits = categoryBits;
node.Height = 0;
node.Flags = AllocatedNode | LeafNode;
InsertLeaf(proxyId, shouldRotate: true);
proxyCount++;
return proxyId;
}
public void DestroyProxy(int proxyId)
{
// Remove and free the node.
RemoveLeaf(proxyId);
FreeNode(proxyId);
proxyCount--;
}
public int GetProxyCount()
{
return proxyCount;
}
public void MoveProxy(int proxyId, AABB aabb)
{
// Update the position of a proxy in the tree.
RemoveLeaf(proxyId);
Nodes[proxyId].AABB = aabb;
InsertLeaf(proxyId, shouldRotate: false);
}
public void EnlargeProxy(int proxyId, AABB aabb)
{
// Update and flag nodes as enlarged
ref var node = ref Nodes[proxyId];
node.AABB = aabb;
var parentIndex = node.Parent;
while (parentIndex != NullIndex)
{
ref var parent = ref Nodes[parentIndex];
var changed = EnlargeAABB(ref parent.AABB, aabb);
parent.Flags |= EnlargedNode;
if (!changed)
{
break;
}
parentIndex = parent.Parent;
}
while (parentIndex != NullIndex)
{
ref var parent = ref Nodes[parentIndex];
if ((parent.Flags & EnlargedNode) != 0)
{
break;
}
parent.Flags |= EnlargedNode;
parentIndex = parent.Parent;
}
}
public TreeStats Query(AABB aabb, ulong maskBits, TreeQueryCallback callback, object context = null)
{
TreeStats result = default;
if (NodesCount == 0)
{
return result;
}
// Fixed-size stack.
const int StackSize = 1024;
Span<int> stack = stackalloc int[StackSize];
var stackCount = 0;
stack[stackCount++] = Root;
while (stackCount > 0)
{
var nodeId = stack[--stackCount];
ref var node = ref Nodes[nodeId];
result.NodeVisits++;
if (Overlaps(node.AABB, aabb) && (node.CategoryBits & maskBits) != 0)
{
if (node.IsLeaf())
{
// Callback to user code with proxy id.
var proceed = callback(nodeId, node.UserData, context);
result.LeafVisits++;
if (!proceed)
{
return result;
}
}
else
{
if (stackCount < StackSize - 1)
{
stack[stackCount++] = node.Children.Child1;
stack[stackCount++] = node.Children.Child2;
}
}
}
}
return result;
}
public TreeStats RayCast(in RayCastInput input, ulong maskBits, TreeRayCastCallback callback, object context = null)
{
TreeStats result = default;
if (NodesCount == 0)
{
return result;
}
var p1 = input.Origin;
var d = input.Translation;
var r = Normalize(d);
// v is perpendicular to the segment (arbitrary axis)
var v = Cross(new Vec3(1, 0, 0), r); // you could also use (0,1,0) or (0,0,1)
var absV = Abs(v);
var maxFraction = input.MaxFraction;
var p2 = Add(p1, Mul(d, maxFraction));
var segmentAABB = new AABB
{
LowerBound = Min(p1, p2),
UpperBound = Max(p1, p2)
};
const int StackSize = 1024;
Span<int> stack = stackalloc int[StackSize];
var stackCount = 0;
stack[stackCount++] = Root;
var subInput = input;
while (stackCount > 0)
{
var nodeId = stack[--stackCount];
ref var node = ref Nodes[nodeId];
result.NodeVisits++;
if ((node.CategoryBits & maskBits) == 0 || !Overlaps(node.AABB, segmentAABB))
{
continue;
}
var c = Center(node.AABB);
var h = Extents(node.AABB);
var term1 = Abs(Dot(v, Sub(p1, c)));
var term2 = Dot(absV, h);
if (term2 < term1)
{
continue;
}
if (node.IsLeaf())
{
subInput.MaxFraction = maxFraction;
var value = callback(ref subInput, nodeId, node.UserData, context);
result.LeafVisits++;
if (value == 0.0f)
{
return result;
}
if (value > 0.0f && value <= maxFraction)
{
maxFraction = value;
p2 = Add(p1, Mul(d, maxFraction));
segmentAABB.LowerBound = Min(p1, p2);
segmentAABB.UpperBound = Max(p1, p2);
}
}
else
{
ref var c1 = ref Nodes[node.Children.Child1];
ref var c2 = ref Nodes[node.Children.Child2];
var d1 = DistanceSquared(Center(c1.AABB), p1);
var d2 = DistanceSquared(Center(c2.AABB), p1);
if (d1 < d2)
{
stack[stackCount++] = node.Children.Child2;
stack[stackCount++] = node.Children.Child1;
}
else
{
stack[stackCount++] = node.Children.Child1;
stack[stackCount++] = node.Children.Child2;
}
}
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool EnlargeAABB(ref AABB a, AABB b)
{
var changed = false;
if (b.LowerBound.X < a.LowerBound.X)
{
a.LowerBound.X = b.LowerBound.X;
changed = true;
}
if (b.LowerBound.Y < a.LowerBound.Y)
{
a.LowerBound.Y = b.LowerBound.Y;
changed = true;
}
if (b.LowerBound.Z < a.LowerBound.Z)
{
a.LowerBound.Z = b.LowerBound.Z;
changed = true;
}
if (a.UpperBound.X < b.UpperBound.X)
{
a.UpperBound.X = b.UpperBound.X;
changed = true;
}
if (a.UpperBound.Y < b.UpperBound.Y)
{
a.UpperBound.Y = b.UpperBound.Y;
changed = true;
}
if (a.UpperBound.Z < b.UpperBound.Z)
{
a.UpperBound.Z = b.UpperBound.Z;
changed = true;
}
return changed;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float Perimeter(AABB box)
{
var wx = box.UpperBound.X - box.LowerBound.X;
var wy = box.UpperBound.Y - box.LowerBound.Y;
var wz = box.UpperBound.Z - box.LowerBound.Z;
return 2f * (wx + wy + wz);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static AABB Union(AABB a, AABB b)
{
return new AABB
{
LowerBound = new Vec3(
Min(a.LowerBound.X, b.LowerBound.X),
Min(a.LowerBound.Y, b.LowerBound.Y),
Min(a.LowerBound.Z, b.LowerBound.Z)),
UpperBound = new Vec3(
Max(a.UpperBound.X, b.UpperBound.X),
Max(a.UpperBound.Y, b.UpperBound.Y),
Max(a.UpperBound.Z, b.UpperBound.Z))
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool Overlaps(AABB a, AABB b)
{
return a.LowerBound.X <= b.UpperBound.X && a.UpperBound.X >= b.LowerBound.X &&
a.LowerBound.Y <= b.UpperBound.Y && a.UpperBound.Y >= b.LowerBound.Y &&
a.LowerBound.Z <= b.UpperBound.Z && a.UpperBound.Z >= b.LowerBound.Z;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vec3 Center(AABB aabb) => new Vec3(
(aabb.LowerBound.X + aabb.UpperBound.X) * 0.5f,
(aabb.LowerBound.Y + aabb.UpperBound.Y) * 0.5f,
(aabb.LowerBound.Z + aabb.UpperBound.Z) * 0.5f);
private static Vec3 Add(Vec3 a, Vec3 b) => new Vec3(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
private static Vec3 Sub(Vec3 a, Vec3 b) => new Vec3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
private static Vec3 Mul(Vec3 a, float scalar) => new Vec3(a.X * scalar, a.Y * scalar, a.Z * scalar);
private static float Dot(Vec3 a, Vec3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;
private static float DistanceSquared(Vec3 a, Vec3 b) => Dot(Sub(a, b), Sub(a, b));
private static float Abs(float f) => MathF.Abs(f);
private static Vec3 Abs(Vec3 v) => new Vec3(Abs(v.X), Abs(v.Y), Abs(v.Z));
private static Vec3 Min(Vec3 a, Vec3 b) => new Vec3(MathF.Min(a.X, b.X), MathF.Min(a.Y, b.Y), MathF.Min(a.Z, b.Z));
private static Vec3 Max(Vec3 a, Vec3 b) => new Vec3(MathF.Max(a.X, b.X), MathF.Max(a.Y, b.Y), MathF.Max(a.Z, b.Z));
private static Vec3 Cross(Vec3 a, Vec3 b) =>
new Vec3(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);
private static float Length(Vec3 v) => MathF.Sqrt(Dot(v, v));
private static Vec3 Normalize(Vec3 v)
{
var len = Length(v);
return len > 0f ? Mul(v, 1f / len) : new Vec3(0, 0, 0);
}
private static Vec3 Extents(AABB aabb) =>
new Vec3((aabb.UpperBound.X - aabb.LowerBound.X) * 0.5f,
(aabb.UpperBound.Y - aabb.LowerBound.Y) * 0.5f,
(aabb.UpperBound.Z - aabb.LowerBound.Z) * 0.5f);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float LengthSquared(Vec3 v) => v.X * v.X + v.Y * v.Y + v.Z * v.Z;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float Max(float a, float b)
{
return a > b ? a : b;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float Min(float a, float b)
{
return a < b ? a : b;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort Max(ushort a, ushort b)
{
return a > b ? a : b;
}
/// <summary>
/// 48-byte.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct TreeNode
{
[FieldOffset(0)] public AABB AABB;
[FieldOffset(24)] public ulong CategoryBits;
/// <summary>
/// Children (internal node).
/// </summary>
[FieldOffset(32)] public Children Children;
/// <summary>
/// User data (leaf node).
/// </summary>
[FieldOffset(32)] public ulong UserData;
/// <summary>
/// The node parent index (allocated node).
/// </summary>
[FieldOffset(40)] public int Parent;
/// <summary>
/// The node freelist next index (free node).
/// </summary>
[FieldOffset(40)] public int NextFree;
[FieldOffset(44)] public ushort Height;
[FieldOffset(46)] public ushort Flags;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsLeaf()
{
return (Flags & LeafNode) == 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsAllocated()
{
return (Flags & AllocatedNode) == 1;
}
}
/// <summary>
/// 8-byte.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Children
{
public int Child1;
public int Child2;
}
public struct TreeStats
{
public int NodeVisits;
public int LeafVisits;
}
public delegate bool TreeQueryCallback(int nodeId, ulong userData, object context);
public delegate float TreeRayCastCallback(ref RayCastInput input, int nodeId, ulong userData, object context);
public struct RayCastInput
{
public Vec3 Origin;
public Vec3 Translation;
public float MaxFraction;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment