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 Pathfinding.Collections;
using UnityEngine.Tilemaps;
namespace Pathfinding.Graphs.Navmesh {
[BurstCompile]
struct CircleGeometryUtilities {
///
/// Cached values for CircleRadiusAdjustmentFactor.
///
/// We can calculate the area of a polygonized circle, and equate that with the area of a unit circle
///
/// x * cos(math.PI / steps) * sin(math.PI/steps) * steps = pi
///
/// Solving for the factor that makes them equal (x) gives the expression below.
///
/// Generated using the python code:
///
/// [math.sqrt(2 * math.pi / (i * math.sin(2 * math.pi / i))) for i in range(3, 23)]
///
///
/// It would be nice to generate this using a static constructor, but that is not supported by Unity's burst compiler.
///
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,
};
/// The number of steps required to get a circle with a maximum error of maxError
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;
}
///
/// 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.
///
#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]
internal static class ColliderMeshBuilder2D {
static int GetShapes (Collider2D coll, PhysicsShapeGroup2D group, HashSet handledRigidbodies) {
#if !UNITY_6000_0_OR_NEWER
var rigid = coll.attachedRigidbody;
if (rigid != null) {
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.
// Not quite sure which version of Unity stopped logging these errors, but they don't seem to be present in Unity 6 anymore.
return rigid.GetShapes(group);
} else {
return 0;
}
}
#endif
if (coll is TilemapCollider2D tilemapColl) {
// Ensure the tilemap is up to date
tilemapColl.ProcessTilemapChanges();
}
return coll.GetShapes(group);
}
public static int GenerateMeshesFromColliders (Collider2D[] colliders, int numColliders, float maxError, out NativeArray outputVertices, out NativeArray outputIndices, out NativeArray outputShapeMeshes) {
var group = new PhysicsShapeGroup2D();
var shapeList = new NativeList(numColliders, Allocator.Temp);
var verticesList = new NativeList(numColliders*4, Allocator.Temp);
var matricesList = new NativeList(numColliders, Allocator.Temp);
var colliderIndexList = new NativeList(numColliders, Allocator.Temp);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
var tempHandle = AtomicSafetyHandle.GetTempMemoryHandle();
#endif
#if UNITY_6000_0_OR_NEWER
HashSet handledRigidbodies = null;
#else
var handledRigidbodies = new HashSet();
#endif
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 shapeCount = GetShapes(coll, group, handledRigidbodies);
if (shapeCount == 0) 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(Allocator.Temp);
var indexBuffer = new NativeList(Allocator.Temp);
var shapeSpan = shapeList.AsUnsafeSpan();
var verticesSpan = verticesList.AsUnsafeSpan().Reinterpret();
var matricesSpan = matricesList.AsUnsafeSpan();
var indexSpan = colliderIndexList.AsUnsafeSpan();
outputShapeMeshes = new NativeArray(shapeList.Length, Allocator.Persistent);
var outputShapeMeshesSpan = outputShapeMeshes.AsUnsafeSpan();
int outputMeshCount;
unsafe {
outputMeshCount = GenerateMeshesFromShapes(
ref shapeSpan,
ref verticesSpan,
ref matricesSpan,
ref indexSpan,
ref UnsafeUtility.AsRef >(vertexBuffer.GetUnsafeList()),
ref UnsafeUtility.AsRef >(indexBuffer.GetUnsafeList()),
ref outputShapeMeshesSpan,
maxError
);
}
Profiler.EndSample();
Profiler.BeginSample("Copy");
outputVertices = vertexBuffer.ToArray(Allocator.Persistent);
outputIndices = new NativeArray(indexBuffer.AsArray().Reinterpret(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 outputVertices, ref UnsafeList 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 shapes,
ref UnsafeSpan vertices,
ref UnsafeSpan shapeMatrices,
ref UnsafeSpan groupIndices,
ref UnsafeList outputVertices,
ref UnsafeList outputIndices,
ref UnsafeSpan 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;
}
}
}