summaryrefslogtreecommitdiff
path: root/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Navmesh/ColliderMeshBuilder2D.cs
blob: 98f433ffb328232e2500a2f2381db8c7514da50a (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
using UnityEngine;
using System.Collections.Generic;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Burst;
using UnityEngine.Assertions;
using UnityEngine.Profiling;
using Pathfinding.Util;
using UnityEngine.Tilemaps;


namespace Pathfinding.Graphs.Navmesh {
	[BurstCompile]
	public struct CircleGeometryUtilities {
		/// <summary>
		/// Cached values for CircleRadiusAdjustmentFactor.
		///
		/// We can calculate the area of a polygonized circle, and equate that with the area of a unit circle
		/// <code>
		/// x * cos(math.PI / steps) * sin(math.PI/steps) * steps = pi
		/// </code>
		/// Solving for the factor that makes them equal (x) gives the expression below.
		///
		/// Generated using the python code:
		/// <code>
		/// [math.sqrt(2 * math.pi / (i * math.sin(2 * math.pi / i))) for i in range(3, 23)]
		/// </code>
		///
		/// It would be nice to generate this using a static constructor, but that is not supported by Unity's burst compiler.
		/// </summary>
		static readonly float[] circleRadiusAdjustmentFactors = new float[] {
			1.56f, 1.25f, 1.15f, 1.1f, 1.07f, 1.05f, 1.04f, 1.03f, 1.03f, 1.02f, 1.02f, 1.02f, 1.01f, 1.01f, 1.01f, 1.01f, 1.01f, 1.01f, 1.01f, 1.01f,
		};

		/// <summary>The number of steps required to get a circle with a maximum error of maxError</summary>
		public static int CircleSteps (Matrix4x4 matrix, float radius, float maxError) {
			// Take the maximum scale factor among the 3 axes.
			// If the current matrix has a uniform scale then they are all the same.
			var maxScaleFactor = math.sqrt(math.max(math.max(math.lengthsq((Vector3)matrix.GetColumn(0)), math.lengthsq((Vector3)matrix.GetColumn(1))), math.lengthsq((Vector3)matrix.GetColumn(2))));
			var realWorldRadius = radius * maxScaleFactor;

			// This expression is the first taylor expansion term of the formula below.
			// It is almost identical to the formula below, but it avoids expensive trigonometric functions.
			// return math.max(3, (int)math.ceil(math.PI * math.sqrt(realWorldRadius / (2*maxError))));
			var cosAngle = 1 - maxError / realWorldRadius;
			int steps = math.max(3, (int)math.ceil(math.PI / math.acos(cosAngle)));
			return steps;
		}

		/// <summary>
		/// Radius factor to adjust for circle approximation errors.
		/// If a circle is approximated by fewer segments, it will be slightly smaller than the original circle.
		/// This factor is used to adjust the radius of the circle so that the resulting circle will have roughly the same area as the original circle.
		/// </summary>
#if MODULE_COLLECTIONS_2_0_0_OR_NEWER && UNITY_2022_2_OR_NEWER
		[GenerateTestsForBurstCompatibility]
#endif
		public static float CircleRadiusAdjustmentFactor (int steps) {
			var index = steps - 3;
			if (index < circleRadiusAdjustmentFactors.Length) {
				if (index < 0) throw new System.ArgumentOutOfRangeException("Steps must be at least 3");
				return circleRadiusAdjustmentFactors[index];
			} else {
				// Larger steps are approximately one
				return 1;
			}
		}
	}

	[BurstCompile]
	public static class ColliderMeshBuilder2D {
		public static int GenerateMeshesFromColliders (Collider2D[] colliders, int numColliders, float maxError, out NativeArray<float3> outputVertices, out NativeArray<int> outputIndices, out NativeArray<ShapeMesh> outputShapeMeshes) {
			var group = new PhysicsShapeGroup2D();
			var shapeList = new NativeList<PhysicsShape2D>(numColliders, Allocator.Temp);
			var verticesList = new NativeList<Vector2>(numColliders*4, Allocator.Temp);
			var matricesList = new NativeList<Matrix4x4>(numColliders, Allocator.Temp);
			var colliderIndexList = new NativeList<int>(numColliders, Allocator.Temp);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
			var tempHandle = AtomicSafetyHandle.GetTempMemoryHandle();
#endif
			var handledRigidbodies = new HashSet<Rigidbody2D>();
			Profiler.BeginSample("GetShapes");

			// Get the low level physics shapes from all colliders
			var indexOffset = 0;
			for (int i = 0; i < numColliders; i++) {
				var coll = colliders[i];
				// Prevent errors from being logged when calling GetShapes on a collider that has no shapes
				if (coll == null || coll.shapeCount == 0) continue;

				var rigid = coll.attachedRigidbody;
				int shapeCount;
				if (rigid == null) {
					if (coll is TilemapCollider2D tilemapColl) {
						// Ensure the tilemap is up to date
						tilemapColl.ProcessTilemapChanges();
					}
					shapeCount = coll.GetShapes(group);
				} else if (handledRigidbodies.Add(rigid)) {
					// Trying to get the shapes from a collider that is attached to a rigidbody will log annoying errors (this seems like a Unity bug tbh),
					// so we must call GetShapes on the rigidbody instead.
					shapeCount = rigid.GetShapes(group);
				} else {
					continue;
				}
				shapeList.Length += shapeCount;
				verticesList.Length += group.vertexCount;
				var subShapes = shapeList.AsArray().GetSubArray(shapeList.Length - shapeCount, shapeCount);
				var subVertices = verticesList.AsArray().GetSubArray(verticesList.Length - group.vertexCount, group.vertexCount);
				// Using AsArray and then GetSubArray will create an invalid safety handle due to unity limitations.
				// We work around this by setting the safety handle to a temporary handle.
#if ENABLE_UNITY_COLLECTIONS_CHECKS
				NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref subShapes, tempHandle);
				NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref subVertices, tempHandle);
#endif
				group.GetShapeData(subShapes, subVertices);
				for (int j = 0; j < shapeCount; j++) {
					var shape = subShapes[j];
					shape.vertexStartIndex += indexOffset;
					subShapes[j] = shape;
				}
				indexOffset += subVertices.Length;
				matricesList.AddReplicate(group.localToWorldMatrix, shapeCount);
				colliderIndexList.AddReplicate(i, shapeCount);
			}
			Profiler.EndSample();
			Assert.AreEqual(shapeList.Length, matricesList.Length);

			Profiler.BeginSample("GenerateMeshes");
			var vertexBuffer = new NativeList<float3>(Allocator.Temp);
			var indexBuffer = new NativeList<int3>(Allocator.Temp);
			var shapeSpan = shapeList.AsUnsafeSpan();
			var verticesSpan = verticesList.AsUnsafeSpan().Reinterpret<float2>();
			var matricesSpan = matricesList.AsUnsafeSpan();
			var indexSpan = colliderIndexList.AsUnsafeSpan();
			outputShapeMeshes = new NativeArray<ShapeMesh>(shapeList.Length, Allocator.Persistent);
			var outputShapeMeshesSpan = outputShapeMeshes.AsUnsafeSpan();
			int outputMeshCount;
			unsafe {
				outputMeshCount = GenerateMeshesFromShapes(
					ref shapeSpan,
					ref verticesSpan,
					ref matricesSpan,
					ref indexSpan,
					ref UnsafeUtility.AsRef<UnsafeList<float3> >(vertexBuffer.GetUnsafeList()),
					ref UnsafeUtility.AsRef<UnsafeList<int3> >(indexBuffer.GetUnsafeList()),
					ref outputShapeMeshesSpan,
					maxError
					);
			}

			Profiler.EndSample();
			Profiler.BeginSample("Copy");
			outputVertices = vertexBuffer.ToArray(Allocator.Persistent);
			outputIndices = new NativeArray<int>(indexBuffer.AsArray().Reinterpret<int>(12), Allocator.Persistent);
			Profiler.EndSample();
			return outputMeshCount;
		}

		public struct ShapeMesh {
			public Matrix4x4 matrix;
			public Bounds bounds;
			public int startIndex;
			public int endIndex;
			public int tag;
		}

		static void AddCapsuleMesh (float2 c1, float2 c2, ref Matrix4x4 shapeMatrix, float radius, float maxError, ref UnsafeList<float3> outputVertices, ref UnsafeList<int3> outputIndices, ref float3 mn, ref float3 mx) {
			var steps = math.max(4, CircleGeometryUtilities.CircleSteps(shapeMatrix, radius, maxError));
			// We are only generating a semicircle at a time, so reduce the number of steps
			steps = (steps / 2) + 1;
			radius = radius * CircleGeometryUtilities.CircleRadiusAdjustmentFactor(2*(steps-1));

			var center1 = new Vector3(c1.x, c1.y, 0);
			var center2 = new Vector3(c2.x, c2.y, 0);
			var axis = math.normalizesafe(c2 - c1);
			var crossAxis = new float2(-axis.y, axis.x);
			var dx = radius * new Vector3(crossAxis.x, crossAxis.y, 0);
			var dy = radius * new Vector3(axis.x, axis.y, 0);
			var angle = math.PI / (steps-1);

			var startVertex = outputVertices.Length;
			var startVertex2 = startVertex + steps;
			outputVertices.Length += steps*2;
			for (int j = 0; j < steps; j++) {
				math.sincos(angle * j, out var sin, out var cos);

				// Generate first semi-circle
				var p = center1 + cos * dx - sin * dy;
				mn = math.min(mn, p);
				mx = math.max(mx, p);
				outputVertices[startVertex + j] = p;

				// Generate second semi-circle
				p = center2 - cos * dx + sin * dy;
				mn = math.min(mn, p);
				mx = math.max(mx, p);
				outputVertices[startVertex2 + j] = p;
			}
			var startIndex = outputIndices.Length;
			var startIndex2 = startIndex + steps-2;
			outputIndices.Length += (steps-2)*2;
			for (int j = 1; j < steps - 1; j++) {
				// Triangle for first semi-circle
				outputIndices[startIndex + j - 1]  = new int3(startVertex, startVertex + j, startVertex + j + 1);
				// Triangle for second semi-circle
				outputIndices[startIndex2 + j - 1] = new int3(startVertex2, startVertex2 + j, startVertex2 + j + 1);
			}

			// Generate the connection between the two semi-circles
			outputIndices.Add(new int3(startVertex, startVertex + steps - 1, startVertex2));
			outputIndices.Add(new int3(startVertex, startVertex2, startVertex2 + steps - 1));
		}

		[BurstCompile]
		public static int GenerateMeshesFromShapes (
			ref UnsafeSpan<PhysicsShape2D> shapes,
			ref UnsafeSpan<float2> vertices,
			ref UnsafeSpan<Matrix4x4> shapeMatrices,
			ref UnsafeSpan<int> groupIndices,
			ref UnsafeList<float3> outputVertices,
			ref UnsafeList<int3> outputIndices,
			ref UnsafeSpan<ShapeMesh> outputShapeMeshes,
			float maxError
			) {
			var groupStartIndex = 0;
			var mn = new float3(float.MaxValue, float.MaxValue, float.MaxValue);
			var mx = new float3(float.MinValue, float.MinValue, float.MinValue);
			int outputMeshIndex = 0;
			for (int i = 0; i < shapes.Length; i++) {
				var shape = shapes[i];
				var shapeVertices = vertices.Slice(shape.vertexStartIndex, shape.vertexCount);
				var shapeMatrix = shapeMatrices[i];
				switch (shape.shapeType) {
				case PhysicsShapeType2D.Circle: {
					var steps = CircleGeometryUtilities.CircleSteps(shapeMatrix, shape.radius, maxError);
					var radius = shape.radius * CircleGeometryUtilities.CircleRadiusAdjustmentFactor(steps);
					var center = new Vector3(shapeVertices[0].x, shapeVertices[0].y, 0);
					var d1 = new Vector3(radius, 0, 0);
					var d2 = new Vector3(0, radius, 0);
					var angle = 2 * math.PI / steps;
					var startVertex = outputVertices.Length;
					for (int j = 0; j < steps; j++) {
						math.sincos(angle * j, out var sin, out var cos);
						var p = center + cos * d1 + sin * d2;
						mn = math.min(mn, p);
						mx = math.max(mx, p);
						outputVertices.Add(p);
					}
					for (int j = 1; j < steps; j++) {
						outputIndices.Add(new int3(startVertex, startVertex + j, startVertex + (j + 1) % steps));
					}
					break;
				}
				case PhysicsShapeType2D.Capsule: {
					var c1 = shapeVertices[0];
					var c2 = shapeVertices[1];
					AddCapsuleMesh(c1, c2, ref shapeMatrix, shape.radius, maxError, ref outputVertices, ref outputIndices, ref mn, ref mx);
					break;
				}
				case PhysicsShapeType2D.Polygon: {
					var startVertex = outputVertices.Length;
					outputVertices.Resize(startVertex + shape.vertexCount, NativeArrayOptions.UninitializedMemory);
					for (int j = 0; j < shape.vertexCount; j++) {
						var p = new Vector3(shapeVertices[j].x, shapeVertices[j].y, 0);
						mn = math.min(mn, p);
						mx = math.max(mx, p);
						outputVertices[startVertex + j] = p;
					}
					outputIndices.SetCapacity(math.ceilpow2(outputIndices.Length + (shape.vertexCount - 2)));
					for (int j = 1; j < shape.vertexCount - 1; j++) {
						outputIndices.AddNoResize(new int3(startVertex, startVertex + j, startVertex + j + 1));
					}
					break;
				}
				case PhysicsShapeType2D.Edges: {
					if (shape.radius > maxError) {
						for (int j = 0; j < shape.vertexCount - 1; j++) {
							AddCapsuleMesh(shapeVertices[j], shapeVertices[j+1], ref shapeMatrix, shape.radius, maxError, ref outputVertices, ref outputIndices, ref mn, ref mx);
						}
					} else {
						var startVertex = outputVertices.Length;
						outputVertices.Resize(startVertex + shape.vertexCount, NativeArrayOptions.UninitializedMemory);
						for (int j = 0; j < shape.vertexCount; j++) {
							var p = new Vector3(shapeVertices[j].x, shapeVertices[j].y, 0);
							mn = math.min(mn, p);
							mx = math.max(mx, p);
							outputVertices[startVertex + j] = p;
						}
						outputIndices.SetCapacity(math.ceilpow2(outputIndices.Length + (shape.vertexCount - 1)));
						for (int j = 0; j < shape.vertexCount - 1; j++) {
							// An edge is represented by a degenerate triangle
							outputIndices.AddNoResize(new int3(startVertex + j, startVertex + j + 1, startVertex + j + 1));
						}
					}
					break;
				}
				default:
					throw new System.Exception("Unexpected PhysicsShapeType2D");
				}

				// Merge shapes which are in the same group into a single ShapeMesh struct.
				// This is done to reduce the per-shape overhead a bit.
				// Don't do it too much, though, since that can cause filtering to not work too well.
				// For example if a recast graph recalculates a single tile in a 2D scene, we don't want to include the whole collider for the
				// TilemapCollider2D in the scene when doing rasterization, only the shapes around the tile that is recalculated.
				// We will still process the whole TilemapCollider2D (no way around that), but we want to be able to exclude shapes shapes as quickly as possible
				// based on their bounding boxes.
				const int DesiredTrianglesPerGroup = 100;
				if (i == shapes.Length - 1 || groupIndices[i] != groupIndices[i+1] || outputIndices.Length - groupStartIndex > DesiredTrianglesPerGroup) {
					// Transform the bounding box to world space
					// This is not the tightest bounding box, but it is good enough
					var m = new ToWorldMatrix(new float3x3((float4x4)shapeMatrix));
					var bounds = new Bounds((mn + mx)*0.5f, mx - mn);
					bounds = m.ToWorld(bounds);
					bounds.center += (Vector3)shapeMatrix.GetColumn(3);

					outputShapeMeshes[outputMeshIndex++] = new ShapeMesh {
						bounds = bounds,
						matrix = shapeMatrix,
						startIndex = groupStartIndex * 3,
						endIndex = outputIndices.Length * 3,
						tag = groupIndices[i]
					};

					mn = new float3(float.MaxValue, float.MaxValue, float.MaxValue);
					mx = new float3(float.MinValue, float.MinValue, float.MinValue);
					groupStartIndex = outputIndices.Length;
				}
			}

			return outputMeshIndex;
		}
	}
}