1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
|
using System.Collections.Generic;
namespace Pathfinding {
using Pathfinding.Util;
/// <summary>
/// Represents a collection of GraphNodes.
/// It allows for fast lookups of the closest node to a point.
///
/// See: https://en.wikipedia.org/wiki/K-d_tree
/// </summary>
public class PointKDTree {
public const int LeafSize = 10;
public const int LeafArraySize = LeafSize*2 + 1;
Node[] tree = new Node[16];
int numNodes = 0;
readonly List<GraphNode> largeList = new List<GraphNode>();
readonly Stack<GraphNode[]> arrayCache = new Stack<GraphNode[]>();
static readonly IComparer<GraphNode>[] comparers = new IComparer<GraphNode>[] { new CompareX(), new CompareY(), new CompareZ() };
struct Node {
/// <summary>Nodes in this leaf node (null if not a leaf node)</summary>
public GraphNode[] data;
/// <summary>Split point along the <see cref="splitAxis"/> if not a leaf node</summary>
public int split;
/// <summary>Number of non-null entries in <see cref="data"/></summary>
public ushort count;
/// <summary>Axis to split along if not a leaf node (x=0, y=1, z=2)</summary>
public byte splitAxis;
}
// Pretty ugly with one class for each axis, but it has been verified to make the tree around 5% faster
class CompareX : IComparer<GraphNode> {
public int Compare (GraphNode lhs, GraphNode rhs) { return lhs.position.x.CompareTo(rhs.position.x); }
}
class CompareY : IComparer<GraphNode> {
public int Compare (GraphNode lhs, GraphNode rhs) { return lhs.position.y.CompareTo(rhs.position.y); }
}
class CompareZ : IComparer<GraphNode> {
public int Compare (GraphNode lhs, GraphNode rhs) { return lhs.position.z.CompareTo(rhs.position.z); }
}
public PointKDTree() {
tree[1] = new Node { data = GetOrCreateList() };
}
/// <summary>Add the node to the tree</summary>
public void Add (GraphNode node) {
numNodes++;
Add(node, 1);
}
/// <summary>Rebuild the tree starting with all nodes in the array between index start (inclusive) and end (exclusive)</summary>
public void Rebuild (GraphNode[] nodes, int start, int end) {
if (start < 0 || end < start || end > nodes.Length)
throw new System.ArgumentException();
for (int i = 0; i < tree.Length; i++) {
var data = tree[i].data;
if (data != null) {
for (int j = 0; j < LeafArraySize; j++) data[j] = null;
arrayCache.Push(data);
tree[i].data = null;
}
}
numNodes = end - start;
Build(1, new List<GraphNode>(nodes), start, end);
}
GraphNode[] GetOrCreateList () {
// Note, the lists will never become larger than this initial capacity, so possibly they should be replaced by arrays
return arrayCache.Count > 0 ? arrayCache.Pop() : new GraphNode[LeafArraySize];
}
int Size (int index) {
return tree[index].data != null ? tree[index].count : Size(2 * index) + Size(2 * index + 1);
}
void CollectAndClear (int index, List<GraphNode> buffer) {
var nodes = tree[index].data;
var count = tree[index].count;
if (nodes != null) {
tree[index] = new Node();
for (int i = 0; i < count; i++) {
buffer.Add(nodes[i]);
nodes[i] = null;
}
arrayCache.Push(nodes);
} else {
CollectAndClear(index*2, buffer);
CollectAndClear(index*2 + 1, buffer);
}
}
static int MaxAllowedSize (int numNodes, int depth) {
// Allow a node to be 2.5 times as full as it should ideally be
// but do not allow it to contain more than 3/4ths of the total number of nodes
// (important to make sure nodes near the top of the tree also get rebalanced).
// A node should ideally contain numNodes/(2^depth) nodes below it (^ is exponentiation, not xor)
return System.Math.Min(((5 * numNodes) / 2) >> depth, (3 * numNodes) / 4);
}
void Rebalance (int index) {
CollectAndClear(index, largeList);
Build(index, largeList, 0, largeList.Count);
largeList.ClearFast();
}
void EnsureSize (int index) {
if (index >= tree.Length) {
var newLeaves = new Node[System.Math.Max(index + 1, tree.Length*2)];
tree.CopyTo(newLeaves, 0);
tree = newLeaves;
}
}
void Build (int index, List<GraphNode> nodes, int start, int end) {
EnsureSize(index);
if (end - start <= LeafSize) {
var leafData = tree[index].data = GetOrCreateList();
tree[index].count = (ushort)(end - start);
for (int i = start; i < end; i++)
leafData[i - start] = nodes[i];
} else {
Int3 mn, mx;
mn = mx = nodes[start].position;
for (int i = start; i < end; i++) {
var p = nodes[i].position;
mn = new Int3(System.Math.Min(mn.x, p.x), System.Math.Min(mn.y, p.y), System.Math.Min(mn.z, p.z));
mx = new Int3(System.Math.Max(mx.x, p.x), System.Math.Max(mx.y, p.y), System.Math.Max(mx.z, p.z));
}
Int3 diff = mx - mn;
var axis = diff.x > diff.y ? (diff.x > diff.z ? 0 : 2) : (diff.y > diff.z ? 1 : 2);
nodes.Sort(start, end - start, comparers[axis]);
int mid = (start+end)/2;
tree[index].split = (nodes[mid-1].position[axis] + nodes[mid].position[axis] + 1)/2;
tree[index].splitAxis = (byte)axis;
Build(index*2 + 0, nodes, start, mid);
Build(index*2 + 1, nodes, mid, end);
}
}
void Add (GraphNode point, int index, int depth = 0) {
// Move down in the tree until the leaf node is found that this point is inside of
while (tree[index].data == null) {
index = 2 * index + (point.position[tree[index].splitAxis] < tree[index].split ? 0 : 1);
depth++;
}
// Add the point to the leaf node
tree[index].data[tree[index].count++] = point;
// Check if the leaf node is large enough that we need to do some rebalancing
if (tree[index].count >= LeafArraySize) {
int levelsUp = 0;
// Search upwards for nodes that are too large and should be rebalanced
// Rebalance the node above the node that had a too large size so that it can
// move children over to the sibling
while (depth - levelsUp > 0 && Size(index >> levelsUp) > MaxAllowedSize(numNodes, depth-levelsUp)) {
levelsUp++;
}
Rebalance(index >> levelsUp);
}
}
/// <summary>Closest node to the point which satisfies the constraint and is at most at the given distance</summary>
public GraphNode GetNearest (Int3 point, NNConstraint constraint, ref float distanceSqr) {
GraphNode best = null;
long bestSqrDist = distanceSqr < float.PositiveInfinity ? (long)(Int3.FloatPrecision * Int3.FloatPrecision * distanceSqr) : long.MaxValue;
GetNearestInternal(1, point, constraint, ref best, ref bestSqrDist);
distanceSqr = best != null ? Int3.PrecisionFactor*Int3.PrecisionFactor * bestSqrDist : float.PositiveInfinity;
return best;
}
void GetNearestInternal (int index, Int3 point, NNConstraint constraint, ref GraphNode best, ref long bestSqrDist) {
var data = tree[index].data;
if (data != null) {
for (int i = tree[index].count - 1; i >= 0; i--) {
var dist = (data[i].position - point).sqrMagnitudeLong;
if (dist < bestSqrDist && (constraint == null || constraint.Suitable(data[i]))) {
bestSqrDist = dist;
best = data[i];
}
}
} else {
var dist = (long)(point[tree[index].splitAxis] - tree[index].split);
var childIndex = 2 * index + (dist < 0 ? 0 : 1);
GetNearestInternal(childIndex, point, constraint, ref best, ref bestSqrDist);
// Try the other one if it is possible to find a valid node on the other side
if (dist*dist < bestSqrDist) {
// childIndex ^ 1 will flip the last bit, so if childIndex is odd, then childIndex ^ 1 will be even
GetNearestInternal(childIndex ^ 0x1, point, constraint, ref best, ref bestSqrDist);
}
}
}
/// <summary>Closest node to the point which satisfies the constraint</summary>
public GraphNode GetNearestConnection (Int3 point, NNConstraint constraint, long maximumSqrConnectionLength) {
GraphNode best = null;
long bestSqrDist = long.MaxValue;
// Given a found point at a distance of r world units
// then any node that has a connection on which a closer point lies must have a squared distance lower than
// d^2 < (maximumConnectionLength/2)^2 + r^2
// Note: (x/2)^2 = (x^2)/4
// Note: (x+3)/4 to round up
long offset = (maximumSqrConnectionLength+3)/4;
GetNearestConnectionInternal(1, point, constraint, ref best, ref bestSqrDist, offset);
return best;
}
void GetNearestConnectionInternal (int index, Int3 point, NNConstraint constraint, ref GraphNode best, ref long bestSqrDist, long distanceThresholdOffset) {
var data = tree[index].data;
if (data != null) {
var pointv3 = (UnityEngine.Vector3)point;
for (int i = tree[index].count - 1; i >= 0; i--) {
var dist = (data[i].position - point).sqrMagnitudeLong;
// Note: the subtraction is important. If we used an addition on the RHS instead the result might overflow as bestSqrDist starts as long.MaxValue
if (dist - distanceThresholdOffset < bestSqrDist && (constraint == null || constraint.Suitable(data[i]))) {
// This node may contains the closest connection
// Check all connections
var conns = (data[i] as PointNode).connections;
if (conns != null) {
var nodePos = (UnityEngine.Vector3)data[i].position;
for (int j = 0; j < conns.Length; j++) {
// Find the closest point on the connection, but only on this node's side of the connection
// This ensures that we will find the closest node with the closest connection.
var connectionMidpoint = ((UnityEngine.Vector3)conns[j].node.position + nodePos) * 0.5f;
float sqrConnectionDistance = VectorMath.SqrDistancePointSegment(nodePos, connectionMidpoint, pointv3);
// Convert to Int3 space
long sqrConnectionDistanceInt = (long)(sqrConnectionDistance*Int3.FloatPrecision*Int3.FloatPrecision);
if (sqrConnectionDistanceInt < bestSqrDist) {
bestSqrDist = sqrConnectionDistanceInt;
best = data[i];
}
}
}
// Also check if the node itself is close enough.
// This is important if the node has no connections at all.
if (dist < bestSqrDist) {
bestSqrDist = dist;
best = data[i];
}
}
}
} else {
var dist = (long)(point[tree[index].splitAxis] - tree[index].split);
var childIndex = 2 * index + (dist < 0 ? 0 : 1);
GetNearestConnectionInternal(childIndex, point, constraint, ref best, ref bestSqrDist, distanceThresholdOffset);
// Try the other one if it is possible to find a valid node on the other side
// Note: the subtraction is important. If we used an addition on the RHS instead the result might overflow as bestSqrDist starts as long.MaxValue
if (dist*dist - distanceThresholdOffset < bestSqrDist) {
// childIndex ^ 1 will flip the last bit, so if childIndex is odd, then childIndex ^ 1 will be even
GetNearestConnectionInternal(childIndex ^ 0x1, point, constraint, ref best, ref bestSqrDist, distanceThresholdOffset);
}
}
}
/// <summary>Add all nodes within a squared distance of the point to the buffer.</summary>
/// <param name="point">Nodes around this point will be added to the buffer.</param>
/// <param name="sqrRadius">squared maximum distance in Int3 space. If you are converting from world space you will need to multiply by Int3.Precision:
/// <code> var sqrRadius = (worldSpaceRadius * Int3.Precision) * (worldSpaceRadius * Int3.Precision); </code></param>
/// <param name="buffer">All nodes will be added to this list.</param>
public void GetInRange (Int3 point, long sqrRadius, List<GraphNode> buffer) {
GetInRangeInternal(1, point, sqrRadius, buffer);
}
void GetInRangeInternal (int index, Int3 point, long sqrRadius, List<GraphNode> buffer) {
var data = tree[index].data;
if (data != null) {
for (int i = tree[index].count - 1; i >= 0; i--) {
var dist = (data[i].position - point).sqrMagnitudeLong;
if (dist < sqrRadius) {
buffer.Add(data[i]);
}
}
} else {
var dist = (long)(point[tree[index].splitAxis] - tree[index].split);
// Pick the first child to enter based on which side of the splitting line the point is
var childIndex = 2 * index + (dist < 0 ? 0 : 1);
GetInRangeInternal(childIndex, point, sqrRadius, buffer);
// Try the other one if it is possible to find a valid node on the other side
if (dist*dist < sqrRadius) {
// childIndex ^ 1 will flip the last bit, so if childIndex is odd, then childIndex ^ 1 will be even
GetInRangeInternal(childIndex ^ 0x1, point, sqrRadius, buffer);
}
}
}
}
}
|