summaryrefslogtreecommitdiff
path: root/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/RVO/RVOObstacleCache.cs
blob: 52618719ae78a2bcc7d4d04b0fe24694579d4fdd (plain)
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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
namespace Pathfinding.RVO {
	using Pathfinding;
	using UnityEngine;
	using Pathfinding.Util;
	using Unity.Mathematics;
	using Unity.Collections;
	using Unity.Jobs;
	using System.Collections.Generic;
	using Unity.Burst;
	using Unity.Profiling;
	using Pathfinding.Jobs;
#if MODULE_COLLECTIONS_2_1_0_OR_NEWER
	using NativeHashMapIntInt = Unity.Collections.NativeHashMap<int, int>;
#else
	using NativeHashMapIntInt = Unity.Collections.NativeParallelHashMap<int, int>;
#endif

	[BurstCompile]
	public static class RVOObstacleCache {
		public struct ObstacleSegment {
			public float3 vertex1;
			public float3 vertex2;
			public int vertex1LinkId;
			public int vertex2LinkId;
		}

		static ulong HashKey (GraphNode sourceNode, int traversableTags, SimpleMovementPlane movementPlane) {
			var hash = (ulong)sourceNode.NodeIndex;
			hash = hash * 786433 ^ (ulong)traversableTags;
			// The rotation is not particularly important for the obstacle. It's only used
			// to simplify the output a bit. So we allow similar rotations to share the same hash.
			const float RotationQuantization = 4;
			hash = hash * 786433 ^ (ulong)(movementPlane.rotation.x*RotationQuantization);
			hash = hash * 786433 ^ (ulong)(movementPlane.rotation.y*RotationQuantization);
			hash = hash * 786433 ^ (ulong)(movementPlane.rotation.z*RotationQuantization);
			hash = hash * 786433 ^ (ulong)(movementPlane.rotation.w*RotationQuantization);
			return hash;
		}

		/// <summary>
		/// Collects an unordered list of contour segments based on the given nodes.
		///
		/// Note: All nodes must be from the same graph.
		/// </summary>
		public static void CollectContours (List<GraphNode> nodes, NativeList<ObstacleSegment> obstacles) {
			if (nodes.Count == 0) return;
			if (nodes[0] is TriangleMeshNode) {
				for (int i = 0; i < nodes.Count; i++) {
					var tnode = nodes[i] as TriangleMeshNode;
					var used = 0;
					if (tnode.connections != null) {
						for (int j = 0; j < tnode.connections.Length; j++) {
							var conn = tnode.connections[j];
							if (conn.isEdgeShared) {
								used |= 1 << conn.shapeEdge;
							}
						}
					}

					tnode.GetVertices(out var v0, out var v1, out var v2);
					for (int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {
						if ((used & (1 << edgeIndex)) == 0) {
							// This edge is not shared, therefore it's a contour edge
							Int3 leftVertex, rightVertex;
							switch (edgeIndex) {
							case 0:
								leftVertex = v0;
								rightVertex = v1;
								break;
							case 1:
								leftVertex = v1;
								rightVertex = v2;
								break;
							case 2:
							default:
								leftVertex = v2;
								rightVertex = v0;
								break;
							}
							var leftVertexHash = leftVertex.GetHashCode();
							var rightVertexHash = rightVertex.GetHashCode();

							obstacles.Add(new ObstacleSegment {
								vertex1 = (Vector3)leftVertex,
								vertex2 = (Vector3)rightVertex,
								vertex1LinkId = leftVertexHash,
								vertex2LinkId = rightVertexHash,
							});
						}
					}
				}
			} else if (nodes[0] is GridNodeBase) {
				GridGraph graph;
				if (nodes[0] is LevelGridNode) graph = LevelGridNode.GetGridGraph(nodes[0].GraphIndex);
				else
					graph = GridNode.GetGridGraph(nodes[0].GraphIndex);
				unsafe {
					// Offsets from the center of the node to the corners of the node, in world space
					// Index dir is the offset to the left corner of the edge in direction dir
					// See GridNodeBase.GetNeighbourAlongDirection for the direction indices
					Vector3* offsets = stackalloc Vector3[4];
					for (int dir = 0; dir < 4; dir++) {
						var dl = (dir + 1) % 4;
						offsets[dir] = graph.transform.TransformVector(0.5f * new Vector3(GridGraph.neighbourXOffsets[dir] + GridGraph.neighbourXOffsets[dl], 0, GridGraph.neighbourZOffsets[dir] + GridGraph.neighbourZOffsets[dl]));
					}

					for (int i = 0; i < nodes.Count; i++) {
						var gnode = nodes[i] as GridNodeBase;
						if (gnode.HasConnectionsToAllAxisAlignedNeighbours) continue;

						for (int dir = 0; dir < 4; dir++) {
							if (!gnode.HasConnectionInDirection(dir)) {
								// ┌─────────┬─────────┐
								// │         │         │
								// │   nl1   │   nl2   │     ^
								// │         │         │     |
								// ├────────vL─────────┤     dl
								// │         │#########│
								// │   node  │#########│     dir->
								// │         │#########│
								// ├────────vR─────────┤     dr
								// │         │         │      |
								// │   nr1   │   nr2   │      v
								// │         │         │
								// └─────────┴─────────┘
								var dl = (dir + 1) % 4;
								var dr = (dir - 1 + 4) % 4;
								var nl1 = gnode.GetNeighbourAlongDirection(dl);
								var nr1 = gnode.GetNeighbourAlongDirection(dr);

								// All this hashing code looks slow, but really it's not compared to all the memory accesses to get the node data
								uint leftVertexHash;
								if (nl1 != null) {
									var nl2 = nl1.GetNeighbourAlongDirection(dir);
									if (nl2 != null) {
										// Outer corner. Uniquely determined by the 3 nodes that touch the corner.
										var a = gnode.NodeIndex;
										var b = nl1.NodeIndex;
										var c = nl2.NodeIndex;
										// Sort the values so that a <= b <= c
										if (a > b) Memory.Swap(ref a, ref b);
										if (b > c) Memory.Swap(ref b, ref c);
										if (a > b) Memory.Swap(ref a, ref b);
										leftVertexHash = math.hash(new uint3(a, b, c));
									} else {
										// Straight wall. Uniquely determined by the 2 nodes that touch the corner and the direction to the wall.
										var a = gnode.NodeIndex;
										var b = nl1.NodeIndex;
										// Sort the values so that a <= b
										if (a > b) Memory.Swap(ref a, ref b);
										leftVertexHash = math.hash(new uint3(a, b, (uint)dir));
									}
								} else {
									// Inner corner. Uniquely determined by the single node that touches the corner and the direction to it.
									var diagonalToCorner = dir + 4;
									leftVertexHash = math.hash(new uint2(gnode.NodeIndex, (uint)diagonalToCorner));
								}

								uint rightVertexHash;
								if (nr1 != null) {
									var nr2 = nr1.GetNeighbourAlongDirection(dir);
									if (nr2 != null) {
										// Outer corner. Uniquely determined by the 3 nodes that touch the corner.
										var a = gnode.NodeIndex;
										var b = nr1.NodeIndex;
										var c = nr2.NodeIndex;
										// Sort the values so that a <= b <= c
										if (a > b) Memory.Swap(ref a, ref b);
										if (b > c) Memory.Swap(ref b, ref c);
										if (a > b) Memory.Swap(ref a, ref b);
										rightVertexHash = math.hash(new uint3(a, b, c));
									} else {
										// Straight wall. Uniquely determined by the 2 nodes that touch the corner and the direction to the wall.
										var a = gnode.NodeIndex;
										var b = nr1.NodeIndex;
										// Sort the values so that a <= b
										if (a > b) Memory.Swap(ref a, ref b);
										rightVertexHash = math.hash(new uint3(a, b, (uint)dir));
									}
								} else {
									// Inner corner. Uniquely determined by the single node that touches the corner and the direction to it.
									// Note: It's not a typo that we use `dr+4` here and `dir+4` above. They are different directions.
									var diagonalToCorner = dr + 4;
									rightVertexHash = math.hash(new uint2(gnode.NodeIndex, (uint)diagonalToCorner));
								}

								var nodePos = (Vector3)gnode.position;
								obstacles.Add(new ObstacleSegment {
									vertex1 = nodePos + offsets[dir], // Left corner. Yes, it should be dir, not dl, as the offsets array already points to the left corners of each segment.
									vertex2 = nodePos + offsets[dr], // Right corner
									vertex1LinkId = (int)leftVertexHash,
									vertex2LinkId = (int)rightVertexHash,
								});
							}
						}
					}
				}
			}
		}

		private static readonly ProfilerMarker MarkerAllocate = new ProfilerMarker("Allocate");

		/// <summary>Trace contours generated by CollectContours.</summary>
		/// <param name="obstaclesSpan">Obstacle segments, typically the borders of the navmesh. In no particular order.
		///                  Each edge must be oriented the same way (e.g. all clockwise, or all counter-clockwise around the obstacles).</param>
		/// <param name="movementPlane">The movement plane used for simplification. The up direction will be treated as less important for purposes of simplification.</param>
		/// <param name="obstacleId">The ID of the obstacle to write into the outputObstacles array.</param>
		/// <param name="outputObstacles">Array to write the obstacle to.</param>
		/// <param name="verticesAllocator">Allocator to use for the vertices of the obstacle.</param>
		/// <param name="obstaclesAllocator">Allocator to use for the obstacle metadata.</param>
		/// <param name="spinLock">Lock to use when allocating from the allocators.</param>
		/// <param name="simplifyObstacles">If true, the obstacle will be simplified. This means that colinear vertices (when projected onto the movement plane) will be removed.</param>
		[BurstCompile]
		public static unsafe void TraceContours (ref UnsafeSpan<ObstacleSegment> obstaclesSpan, ref NativeMovementPlane movementPlane, int obstacleId, UnmanagedObstacle* outputObstacles, ref SlabAllocator<float3> verticesAllocator, ref SlabAllocator<ObstacleVertexGroup> obstaclesAllocator, ref SpinLock spinLock, bool simplifyObstacles) {
			var obstacles = obstaclesSpan;
			if (obstacles.Length == 0) {
				outputObstacles[obstacleId] = new UnmanagedObstacle {
					verticesAllocation = SlabAllocator<float3>.ZeroLengthArray,
					groupsAllocation = SlabAllocator<ObstacleVertexGroup>.ZeroLengthArray,
				};
				return;
			}

			MarkerAllocate.Begin();
			var traceLookup = new NativeHashMapIntInt(obstacles.Length, Unity.Collections.Allocator.Temp);
			// For each edge: Will be 0 if the segment should be ignored or if it has been visited, 1 if it has not been visited and has an ingoing edge, and 2 if it has not been visited and has no ingoing edge.
			var priority = new NativeArray<byte>(obstacles.Length, Unity.Collections.Allocator.Temp, Unity.Collections.NativeArrayOptions.UninitializedMemory);
			MarkerAllocate.End();
			for (int i = 0; i < obstacles.Length; i++) {
				// var obstacle = obstacles[i];
				// Add the edge to the lookup. But if it already exists, ignore it.
				// That it already exists is very much a special case that can happen if there is
				// overlapping geometry (typically added by a NavmeshAdd component).
				// In that case the "outer edge" that we want to trace is kinda undefined, but we will
				// do our best with it.
				if (traceLookup.TryAdd(obstacles[i].vertex1LinkId, i)) {
					priority[i] = 2;
				} else {
					priority[i] = 0;
				}
			}
			for (int i = 0; i < obstacles.Length; i++) {
				if (traceLookup.TryGetValue(obstacles[i].vertex2LinkId, out var other) && priority[other] > 0) {
					// The other segment has an ingoing edge. This means it cannot be the start of a contour.
					// Reduce the priority so that we only consider it when looking for cycles.
					priority[other] = 1;
				}
			}

			var outputMetadata = new NativeList<ObstacleVertexGroup>(16, Allocator.Temp);
			var outputVertices = new NativeList<float3>(16, Allocator.Temp);
			// We now have a hashmap of directed edges (vertex1 -> vertex2) such that these edges are directed the same (cw or ccw), and "outer edges".
			// We can now follow these directed edges to trace out the contours of the mesh.
			var toPlaneMatrix = movementPlane.AsWorldToPlaneMatrix();
			for (int allowLoops = 0; allowLoops <= 1; allowLoops++) {
				var minPriority = allowLoops == 1 ? 1 : 2;
				for (int i = 0; i < obstacles.Length; i++) {
					if (priority[i] >= minPriority) {
						int startVertices = outputVertices.Length;
						outputVertices.Add(obstacles[i].vertex1);

						var lastAdded = obstacles[i].vertex1;
						var candidateVertex = obstacles[i].vertex2;
						var index = i;
						var obstacleType = ObstacleType.Chain;
						var boundsMn = lastAdded;
						var boundsMx = lastAdded;
						while (true) {
							if (priority[index] == 0) {
								// This should not happen for a regular navmesh.
								// But it can happen if there are degenerate edges or overlapping triangles.
								// In that case we will just stop here
								break;
							}
							priority[index] = 0;

							float3 nextVertex;
							if (traceLookup.TryGetValue(obstacles[index].vertex2LinkId, out int nextIndex)) {
								nextVertex = 0.5f * (obstacles[index].vertex2 + obstacles[nextIndex].vertex1);
							} else {
								nextVertex = obstacles[index].vertex2;
								nextIndex = -1;
							}

							// Try to simplify and see if we even need to add the vertex C.
							var p1 = lastAdded;
							var p2 = nextVertex;
							var p3 = candidateVertex;
							var d1 = toPlaneMatrix.ToPlane(p2 - p1);
							var d2 = toPlaneMatrix.ToPlane(p3 - p1);
							var det = VectorMath.Determinant(d1, d2);
							if (math.abs(det) < 0.01f && simplifyObstacles) {
								// We don't need to add candidateVertex. It's just a straight line (p1,p2,p3 are colinear).
							} else {
								outputVertices.Add(candidateVertex);
								boundsMn = math.min(boundsMn, candidateVertex);
								boundsMx = math.max(boundsMx, candidateVertex);
								lastAdded = p3;
							}

							if (nextIndex == i) {
								// Loop
								outputVertices[startVertices] = nextVertex;
								obstacleType = ObstacleType.Loop;
								break;
							} else if (nextIndex == -1) {
								// End of chain
								outputVertices.Add(nextVertex);
								boundsMn = math.min(boundsMn, nextVertex);
								boundsMx = math.max(boundsMx, nextVertex);
								break;
							}

							index = nextIndex;
							candidateVertex = nextVertex;
						}

						outputMetadata.Add(new ObstacleVertexGroup {
							type = obstacleType,
							vertexCount = outputVertices.Length - startVertices,
							boundsMn = boundsMn,
							boundsMx = boundsMx,
						});
					}
				}
			}

			int obstacleSet, vertexSet;
			if (outputMetadata.Length > 0) {
				spinLock.Lock();
				obstacleSet = obstaclesAllocator.Allocate(outputMetadata);
				vertexSet = verticesAllocator.Allocate(outputVertices);
				spinLock.Unlock();
			} else {
				obstacleSet = SlabAllocator<ObstacleVertexGroup>.ZeroLengthArray;
				vertexSet = SlabAllocator<float3>.ZeroLengthArray;
			}
			outputObstacles[obstacleId] = new UnmanagedObstacle {
				verticesAllocation = vertexSet,
				groupsAllocation = obstacleSet,
			};
		}
	}
}