Files
Rebirth/Assets/Datastructures/KDTree/KDTree.cs
2024-12-15 20:29:40 -06:00

459 lines
15 KiB
C#

/*MIT License
Copyright(c) 2018 Vili Volčini / viliwonka
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// change to !KDTREE_DUPLICATES
// if you know for sure you will not use duplicate coordinates (all unique)
#define KDTREE_DUPLICATES
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
namespace DataStructures.ViliWonka.KDTree {
public class KDTree {
public KDNode RootNode { get; private set; }
public Vector3[] Points { get { return points; } } // points on which kd-tree will build on. This array will stay unchanged when re/building kdtree!
private Vector3[] points;
public int[] Permutation { get { return permutation; } } // index aray, that will be permuted
private int[] permutation;
public int Count { get; private set; }
private int maxPointsPerLeafNode = 32;
private KDNode[] kdNodesStack;
private int kdNodesCount = 0;
public KDTree(int maxPointsPerLeafNode = 32) {
Count = 0;
points = new Vector3[0];
permutation = new int[0];
kdNodesStack = new KDNode[64];
this.maxPointsPerLeafNode = maxPointsPerLeafNode;
}
public KDTree(Vector3[] points, int maxPointsPerLeafNode = 32) {
this.points = points;
this.permutation = new int[points.Length];
Count = points.Length;
kdNodesStack = new KDNode[64];
this.maxPointsPerLeafNode = maxPointsPerLeafNode;
Rebuild();
}
public void Build(Vector3[] newPoints, int maxPointsPerLeafNode = -1) {
SetCount(newPoints.Length);
for(int i = 0; i < Count; i++) {
points[i] = newPoints[i];
}
Rebuild(maxPointsPerLeafNode);
}
public void Build(List<Vector3> newPoints, int maxPointsPerLeafNode = -1) {
SetCount(newPoints.Count);
for(int i = 0; i < Count; i++) {
points[i] = newPoints[i];
}
Rebuild(maxPointsPerLeafNode);
}
public void Rebuild(int maxPointsPerLeafNode = -1) {
for(int i = 0; i < Count; i++) {
permutation[i] = i;
}
if(maxPointsPerLeafNode > 0) {
this.maxPointsPerLeafNode = maxPointsPerLeafNode;
}
BuildTree();
}
public void SetCount(int newSize) {
Count = newSize;
// upsize internal arrays
if(Count > points.Length) {
Array.Resize(ref points, Count);
Array.Resize(ref permutation, Count);
}
}
void BuildTree() {
ResetKDNodeStack();
RootNode = GetKDNode();
RootNode.bounds = MakeBounds();
RootNode.start = 0;
RootNode.end = Count;
SplitNode(RootNode);
}
KDNode GetKDNode() {
KDNode node = null;
if(kdNodesCount < kdNodesStack.Length) {
if(kdNodesStack[kdNodesCount] == null) {
kdNodesStack[kdNodesCount] = node = new KDNode();
}
else {
node = kdNodesStack[kdNodesCount];
node.partitionAxis = -1;
}
}
else {
// automatic resize of KDNode pool array
Array.Resize(ref kdNodesStack, kdNodesStack.Length * 2);
node = kdNodesStack[kdNodesCount] = new KDNode();
}
kdNodesCount++;
return node;
}
void ResetKDNodeStack() {
kdNodesCount = 0;
}
/// <summary>
/// For calculating root node bounds
/// </summary>
/// <returns>Boundary of all Vector3 points</returns>
KDBounds MakeBounds() {
Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue);
Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
int even = Count & ~1; // calculate even Length
// min, max calculations
// 3n/2 calculations instead of 2n
for (int i0 = 0; i0 < even; i0 += 2) {
int i1 = i0 + 1;
// X Coords
if (points[i0].x > points[i1].x) {
// i0 is bigger, i1 is smaller
if (points[i1].x < min.x)
min.x = points[i1].x;
if (points[i0].x > max.x)
max.x = points[i0].x;
}
else {
// i1 is smaller, i0 is bigger
if (points[i0].x < min.x)
min.x = points[i0].x;
if (points[i1].x > max.x)
max.x = points[i1].x;
}
// Y Coords
if (points[i0].y > points[i1].y) {
// i0 is bigger, i1 is smaller
if (points[i1].y < min.y)
min.y = points[i1].y;
if (points[i0].y > max.y)
max.y = points[i0].y;
}
else {
// i1 is smaller, i0 is bigger
if (points[i0].y < min.y)
min.y = points[i0].y;
if (points[i1].y > max.y)
max.y = points[i1].y;
}
// Z Coords
if (points[i0].z > points[i1].z) {
// i0 is bigger, i1 is smaller
if (points[i1].z < min.z)
min.z = points[i1].z;
if (points[i0].z > max.z)
max.z = points[i0].z;
}
else {
// i1 is smaller, i0 is bigger
if (points[i0].z < min.z)
min.z = points[i0].z;
if (points[i1].z > max.z)
max.z = points[i1].z;
}
}
// if array was odd, calculate also min/max for the last element
if(even != Count) {
// X
if (min.x > points[even].x)
min.x = points[even].x;
if (max.x < points[even].x)
max.x = points[even].x;
// Y
if (min.y > points[even].y)
min.y = points[even].y;
if (max.y < points[even].y)
max.y = points[even].y;
// Z
if (min.z > points[even].z)
min.z = points[even].z;
if (max.z < points[even].z)
max.z = points[even].z;
}
KDBounds b = new KDBounds();
b.min = min;
b.max = max;
return b;
}
/// <summary>
/// Recursive splitting procedure
/// </summary>
/// <param name="parent">This is where root node goes</param>
/// <param name="depth"></param>
///
void SplitNode(KDNode parent) {
// center of bounding box
KDBounds parentBounds = parent.bounds;
Vector3 parentBoundsSize = parentBounds.size;
// Find axis where bounds are largest
int splitAxis = 0;
float axisSize = parentBoundsSize.x;
if (axisSize < parentBoundsSize.y) {
splitAxis = 1;
axisSize = parentBoundsSize.y;
}
if (axisSize < parentBoundsSize.z) {
splitAxis = 2;
}
// Our axis min-max bounds
float boundsStart = parentBounds.min[splitAxis];
float boundsEnd = parentBounds.max[splitAxis];
// Calculate the spliting coords
float splitPivot = CalculatePivot(parent.start, parent.end, boundsStart, boundsEnd, splitAxis);
parent.partitionAxis = splitAxis;
parent.partitionCoordinate = splitPivot;
// 'Spliting' array to two subarrays
int splittingIndex = Partition(parent.start, parent.end, splitPivot, splitAxis);
// Negative / Left node
Vector3 negMax = parentBounds.max;
negMax[splitAxis] = splitPivot;
KDNode negNode = GetKDNode();
negNode.bounds = parentBounds;
negNode.bounds.max = negMax;
negNode.start = parent.start;
negNode.end = splittingIndex;
parent.negativeChild = negNode;
// Positive / Right node
Vector3 posMin = parentBounds.min;
posMin[splitAxis] = splitPivot;
KDNode posNode = GetKDNode();
posNode.bounds = parentBounds;
posNode.bounds.min = posMin;
posNode.start = splittingIndex;
posNode.end = parent.end;
parent.positiveChild = posNode;
// check if we are actually splitting it anything
// this if check enables duplicate coordinates, but makes construction a bit slower
#if KDTREE_DUPLICATES
if(negNode.Count != 0 && posNode.Count != 0) {
#endif
// Constraint function deciding if split should be continued
if(ContinueSplit(negNode))
SplitNode(negNode);
if(ContinueSplit(posNode))
SplitNode(posNode);
#if KDTREE_DUPLICATES
}
#endif
}
/// <summary>
/// Sliding midpoint splitting pivot calculation
/// 1. First splits node to two equal parts (midPoint)
/// 2. Checks if elements are in both sides of splitted bounds
/// 3a. If they are, just return midPoint
/// 3b. If they are not, then points are only on left or right bound.
/// 4. Move the splitting pivot so that it shrinks part with points completely (calculate min or max dependent) and return.
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="boundsStart"></param>
/// <param name="boundsEnd"></param>
/// <param name="axis"></param>
/// <returns></returns>
float CalculatePivot(int start, int end, float boundsStart, float boundsEnd, int axis) {
//! sliding midpoint rule
float midPoint = (boundsStart + boundsEnd) / 2f;
bool negative = false;
bool positive = false;
float negMax = Single.MinValue;
float posMin = Single.MaxValue;
// this for loop section is used both for sorted and unsorted data
for (int i = start; i < end; i++) {
if (points[permutation[i]][axis] < midPoint)
negative = true;
else
positive = true;
if (negative == true && positive == true)
return midPoint;
}
if (negative) {
for (int i = start; i < end; i++)
if (negMax < points[permutation[i]][axis])
negMax = points[permutation[i]][axis];
return negMax;
}
else {
for (int i = start; i < end; i++)
if (posMin > points[permutation[i]][axis])
posMin = points[permutation[i]][axis];
return posMin;
}
}
/// <summary>
/// Similar to Hoare partitioning algorithm (used in Quick Sort)
/// Modification: pivot is not left-most element but is instead argument of function
/// Calculates splitting index and partially sorts elements (swaps them until they are on correct side - depending on pivot)
/// Complexity: O(n)
/// </summary>
/// <param name="start">Start index</param>
/// <param name="end">End index</param>
/// <param name="partitionPivot">Pivot that decides boundary between left and right</param>
/// <param name="axis">Axis of this pivoting</param>
/// <returns>
/// Returns splitting index that subdivides array into 2 smaller arrays
/// left = [start, pivot),
/// right = [pivot, end)
/// </returns>
int Partition(int start, int end, float partitionPivot, int axis) {
// note: increasing right pointer is actually decreasing!
int LP = start - 1; // left pointer (negative side)
int RP = end; // right pointer (positive side)
int temp; // temporary var for swapping permutation indexes
while (true) {
do {
// move from left to the right until "out of bounds" value is found
LP++;
}
while (LP < RP && points[permutation[LP]][axis] < partitionPivot);
do {
// move from right to the left until "out of bounds" value found
RP--;
}
while (LP < RP && points[permutation[RP]][axis] >= partitionPivot);
if (LP < RP) {
// swap
temp = permutation[LP];
permutation[LP] = permutation[RP];
permutation[RP] = temp;
}
else {
return LP;
}
}
}
/// <summary>
/// Constraint function. You can add custom constraints here - if you have some other data/classes binded to Vector3 points
/// Can hardcode it into
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
bool ContinueSplit(KDNode node) {
return (node.Count > maxPointsPerLeafNode);
}
}
}