using System;
using System.Collections.Generic;
using UnityEngine;
using Pathfinding.ClipperLib;
using UnityEngine.Profiling;
namespace Pathfinding.Util {
using Pathfinding;
using Pathfinding.Poly2Tri;
///
/// Utility class for updating tiles of navmesh/recast graphs.
///
/// Most operations that this class does are asynchronous.
/// They will be added as work items to the AstarPath class
/// and executed when the pathfinding threads have finished
/// calculating their current paths.
///
/// See: navmeshcutting (view in online documentation for working links)
/// See:
///
public class TileHandler {
/// The underlaying graph which is handled by this instance
public readonly NavmeshBase graph;
/// Number of tiles along the x axis
readonly int tileXCount;
/// Number of tiles along the z axis
readonly int tileZCount;
/// Handles polygon clipping operations
readonly Clipper clipper = new Clipper();
/// Cached dictionary to avoid excessive allocations
readonly Dictionary cached_Int2_int_dict = new Dictionary();
///
/// Which tile type is active on each tile index.
/// This array will be tileXCount*tileZCount elements long.
///
readonly TileType[] activeTileTypes;
/// Rotations of the active tiles
readonly int[] activeTileRotations;
/// Offsets along the Y axis of the active tiles
readonly int[] activeTileOffsets;
/// A flag for each tile that is set to true if it has been reloaded while batching is in progress
readonly bool[] reloadedInBatch;
///
/// NavmeshCut and NavmeshAdd components registered to this tile handler.
/// This is updated by the class.
/// See:
///
public readonly GridLookup cuts;
///
/// Positive while batching tile updates.
/// Batching tile updates has a positive effect on performance
///
int batchDepth;
///
/// True while batching tile updates.
/// Batching tile updates has a positive effect on performance
///
bool isBatching { get { return batchDepth > 0; } }
///
/// Utility for clipping polygons to rectangles.
/// Implemented as a struct and not a bunch of static methods
/// because it needs some buffer arrays that are best cached
/// to avoid excessive allocations
///
readonly Pathfinding.Voxels.Int3PolygonClipper simpleClipper;
///
/// True if the tile handler still has the same number of tiles and tile layout as the graph.
/// If the graph is rescanned the tile handler will get out of sync and needs to be recreated.
///
public bool isValid {
get {
return graph != null && graph.exists && tileXCount == graph.tileXCount && tileZCount == graph.tileZCount;
}
}
public TileHandler (NavmeshBase graph) {
if (graph == null) throw new ArgumentNullException("graph");
if (graph.GetTiles() == null) Debug.LogWarning("Creating a TileHandler for a graph with no tiles. Please scan the graph before creating a TileHandler");
tileXCount = graph.tileXCount;
tileZCount = graph.tileZCount;
activeTileTypes = new TileType[tileXCount*tileZCount];
activeTileRotations = new int[activeTileTypes.Length];
activeTileOffsets = new int[activeTileTypes.Length];
reloadedInBatch = new bool[activeTileTypes.Length];
cuts = new GridLookup(new Int2(tileXCount, tileZCount));
this.graph = graph;
}
///
/// Call to update the specified tiles with new information based on the navmesh/recast graph.
/// This is usually called right after a navmesh/recast graph has recalculated some tiles
/// and thus some calculations need to be done to take navmesh cutting into account
/// as well.
///
/// Will reload all tiles in the list.
///
public void OnRecalculatedTiles (NavmeshTile[] recalculatedTiles) {
for (int i = 0; i < recalculatedTiles.Length; i++) {
UpdateTileType(recalculatedTiles[i]);
}
StartBatchLoad();
for (int i = 0; i < recalculatedTiles.Length; i++) {
ReloadTile(recalculatedTiles[i].x, recalculatedTiles[i].z);
}
EndBatchLoad();
}
///
/// Rotation of the specified tile relative to the original rotation of the tile type.
/// A result N means that the tile has been rotated N*90 degrees.
///
public int GetActiveRotation (Int2 p) {
return activeTileRotations[p.x + p.y*tileXCount];
}
/// A template for a single tile in a navmesh/recast graph
public class TileType {
Int3[] verts;
int[] tris;
Int3 offset;
int lastYOffset;
int lastRotation;
int width;
int depth;
public int Width {
get {
return width;
}
}
public int Depth {
get {
return depth;
}
}
///
/// Matrices for rotation.
/// Each group of 4 elements is a 2x2 matrix.
/// The XZ position is multiplied by this.
/// So
///
/// //A rotation by 90 degrees clockwise, second matrix in the array
/// (5,2) * ((0, 1), (-1, 0)) = (2,-5)
///
///
private static readonly int[] Rotations = {
1, 0, // Identity matrix
0, 1,
0, 1,
-1, 0,
-1, 0,
0, -1,
0, -1,
1, 0
};
public TileType (Int3[] sourceVerts, int[] sourceTris, Int3 tileSize, Int3 centerOffset, int width = 1, int depth = 1) {
if (sourceVerts == null) throw new ArgumentNullException("sourceVerts");
if (sourceTris == null) throw new ArgumentNullException("sourceTris");
tris = new int[sourceTris.Length];
for (int i = 0; i < tris.Length; i++) tris[i] = sourceTris[i];
verts = new Int3[sourceVerts.Length];
for (int i = 0; i < sourceVerts.Length; i++) {
verts[i] = sourceVerts[i] + centerOffset;
}
offset = tileSize/2;
offset.x *= width;
offset.z *= depth;
offset.y = 0;
for (int i = 0; i < sourceVerts.Length; i++) {
verts[i] = verts[i] + offset;
}
lastRotation = 0;
lastYOffset = 0;
this.width = width;
this.depth = depth;
}
///
/// Create a new TileType.
/// First all vertices of the source mesh are offseted by the centerOffset.
/// The source mesh is assumed to be centered (after offsetting). Corners of the tile should be at tileSize*0.5 along all axes.
/// When width or depth is not 1, the tileSize param should not change, but corners of the tile are assumed to lie further out.
///
/// The navmesh as a unity Mesh
/// The number of base tiles this tile type occupies on the x-axis
/// The number of base tiles this tile type occupies on the z-axis
/// Size of a single tile, the y-coordinate will be ignored.
/// This offset will be added to all vertices
public TileType (Mesh source, Int3 tileSize, Int3 centerOffset, int width = 1, int depth = 1) {
if (source == null) throw new ArgumentNullException("source");
Vector3[] vectorVerts = source.vertices;
tris = source.triangles;
verts = new Int3[vectorVerts.Length];
for (int i = 0; i < vectorVerts.Length; i++) {
verts[i] = (Int3)vectorVerts[i] + centerOffset;
}
offset = tileSize/2;
offset.x *= width;
offset.z *= depth;
offset.y = 0;
for (int i = 0; i < vectorVerts.Length; i++) {
verts[i] = verts[i] + offset;
}
lastRotation = 0;
lastYOffset = 0;
this.width = width;
this.depth = depth;
}
///
/// Load a tile, result given by the vert and tris array.
/// Warning: For performance and memory reasons, the returned arrays are internal arrays, so they must not be modified in any way or
/// subsequent calls to Load may give corrupt output. The contents of the verts array is only valid until the next call to Load since
/// different rotations and y offsets can be applied.
/// If you need persistent arrays, please copy the returned ones.
///
public void Load (out Int3[] verts, out int[] tris, int rotation, int yoffset) {
//Make sure it is a number 0 <= x < 4
rotation = ((rotation % 4) + 4) % 4;
//Figure out relative rotation (relative to previous rotation that is, since that is still applied to the verts array)
int tmp = rotation;
rotation = (rotation - (lastRotation % 4) + 4) % 4;
lastRotation = tmp;
verts = this.verts;
int relYOffset = yoffset - lastYOffset;
lastYOffset = yoffset;
if (rotation != 0 || relYOffset != 0) {
for (int i = 0; i < verts.Length; i++) {
Int3 op = verts[i] - offset;
Int3 p = op;
p.y += relYOffset;
p.x = op.x * Rotations[rotation*4 + 0] + op.z * Rotations[rotation*4 + 1];
p.z = op.x * Rotations[rotation*4 + 2] + op.z * Rotations[rotation*4 + 3];
verts[i] = p + offset;
}
}
tris = this.tris;
}
}
///
/// Register that a tile can be loaded from source.
///
/// Returns: Identifier for loading that tile type
///
/// Assumes that the mesh has its pivot point at the center of the tile.
/// If it has not, you can supply a non-zero centerOffset to offset all vertices.
/// width of the tile. In base tiles, not world units.
/// depth of the tile. In base tiles, not world units.
/// Source mesh, must be readable.
public TileType RegisterTileType (Mesh source, Int3 centerOffset, int width = 1, int depth = 1) {
return new TileType(source, (Int3) new Vector3(graph.TileWorldSizeX, 0, graph.TileWorldSizeZ), centerOffset, width, depth);
}
public void CreateTileTypesFromGraph () {
NavmeshTile[] tiles = graph.GetTiles();
if (tiles == null)
return;
if (!isValid) {
throw new InvalidOperationException("Graph tiles are invalid (number of tiles is not equal to width*depth of the graph). You need to create a new tile handler if you have changed the graph.");
}
for (int z = 0; z < tileZCount; z++) {
for (int x = 0; x < tileXCount; x++) {
NavmeshTile tile = tiles[x + z*tileXCount];
UpdateTileType(tile);
}
}
}
void UpdateTileType (NavmeshTile tile) {
int x = tile.x;
int z = tile.z;
Int3 size = (Int3) new Vector3(graph.TileWorldSizeX, 0, graph.TileWorldSizeZ);
Bounds b = graph.GetTileBoundsInGraphSpace(x, z);
var centerOffset = -((Int3)b.min + new Int3(size.x*tile.w/2, 0, size.z*tile.d/2));
var tileType = new TileType(tile.vertsInGraphSpace, tile.tris, size, centerOffset, tile.w, tile.d);
int index = x + z*tileXCount;
activeTileTypes[index] = tileType;
activeTileRotations[index] = 0;
activeTileOffsets[index] = 0;
}
///
/// Start batch loading.
/// Every call to this method must be matched by exactly one call to EndBatchLoad.
///
public void StartBatchLoad () {
batchDepth++;
if (batchDepth > 1) return;
AstarPath.active.AddWorkItem(new AstarWorkItem(force => {
graph.StartBatchTileUpdate();
return true;
}));
}
public void EndBatchLoad () {
if (batchDepth <= 0) throw new Exception("Ending batching when batching has not been started");
batchDepth--;
for (int i = 0; i < reloadedInBatch.Length; i++) reloadedInBatch[i] = false;
AstarPath.active.AddWorkItem(new AstarWorkItem((ctx, force) => {
Profiler.BeginSample("Apply Tile Modifications");
graph.EndBatchTileUpdate();
// Trigger post update event
// This can trigger for example recalculation of navmesh links
ctx.SetGraphDirty(graph);
Profiler.EndSample();
return true;
}));
}
[Flags]
public enum CutMode {
/// Cut holes in the navmesh
CutAll = 1,
/// Cut the navmesh but do not remove the interior of the cuts
CutDual = 2,
/// Also cut using the extra shape that was provided
CutExtra = 4
}
/// Internal class describing a single NavmeshCut
class Cut {
/// Bounds in XZ space
public IntRect bounds;
/// X is the lower bound on the y axis, Y is the upper bounds on the Y axis
public Int2 boundsY;
public bool isDual;
public bool cutsAddedGeom;
public List contour;
}
/// Internal class representing a mesh which is the result of the CutPoly method
struct CuttingResult {
public Int3[] verts;
public int[] tris;
}
///
/// Cuts a piece of navmesh using navmesh cuts.
///
/// Note: I am sorry for the really messy code in this method.
/// It really needs to be refactored.
///
/// See: NavmeshBase.transform
/// See: CutMode
///
/// Vertices that are going to be cut. Should be in graph space.
/// Triangles describing a mesh using the vertices.
/// If supplied the resulting mesh will be the intersection of the input mesh and this mesh.
/// Transform mapping graph space to world space.
/// Tiles in the recast graph which the mesh covers.
///
/// Move navmesh cuts around randomly a bit, the larger the value the more they are moved around.
/// Used to prevent edge cases that can cause the clipping to fail.
CuttingResult CutPoly (Int3[] verts, int[] tris, Int3[] extraShape, GraphTransform graphTransform, IntRect tiles, CutMode mode = CutMode.CutAll | CutMode.CutDual, int perturbate = -1) {
// Nothing to do here
if (verts.Length == 0 || tris.Length == 0) {
return new CuttingResult {
verts = ArrayPool.Claim(0),
tris = ArrayPool.Claim(0)
};
}
if (perturbate > 10) {
Debug.LogError("Too many perturbations aborting.\n" +
"This may cause a tile in the navmesh to become empty. " +
"Try to see see if any of your NavmeshCut or NavmeshAdd components use invalid custom meshes.");
return new CuttingResult {
verts = verts,
tris = tris
};
}
List extraClipShape = null;
// Do not cut with extra shape if there is no extra shape
if (extraShape == null && (mode & CutMode.CutExtra) != 0) {
throw new Exception("extraShape is null and the CutMode specifies that it should be used. Cannot use null shape.");
}
// Calculate tile bounds so that the correct cutting offset can be used
// The tile will be cut in local space (i.e it is at the world origin) so cuts need to be translated
// to that point from their world space coordinates
var graphSpaceBounds = graph.GetTileBoundsInGraphSpace(tiles);
var cutOffset = graphSpaceBounds.min;
var transform = graphTransform * Matrix4x4.TRS(cutOffset, Quaternion.identity, Vector3.one);
// cutRegionSize The cutting region is a rectangle with one corner at the origin and one at the coordinates of cutRegionSize
// NavmeshAdd components will be clipped against this rectangle. It is assumed that the input vertices do not extend outside the region.
// For navmesh tiles, cutRegionSize is set to the size of a single tile.
var cutRegionSize = new Vector2(graphSpaceBounds.size.x, graphSpaceBounds.size.z);
if ((mode & CutMode.CutExtra) != 0) {
extraClipShape = ListPool.Claim(extraShape.Length);
for (int i = 0; i < extraShape.Length; i++) {
var p = transform.InverseTransform(extraShape[i]);
extraClipShape.Add(new IntPoint(p.x, p.z));
}
}
var bounds = new IntRect(verts[0].x, verts[0].z, verts[0].x, verts[0].z);
// Expand bounds to contain all vertices
for (int i = 0; i < verts.Length; i++) {
bounds = bounds.ExpandToContain(verts[i].x, verts[i].z);
}
// Find all NavmeshCut components that could be inside these bounds
List navmeshCuts;
if (mode == CutMode.CutExtra) {
// Not needed when only cutting extra
navmeshCuts = ListPool.Claim();
} else {
navmeshCuts = cuts.QueryRect(tiles);
}
// Find all NavmeshAdd components that could be inside the bounds
List navmeshAdds = cuts.QueryRect(tiles);
var intersectingCuts = ListPool.Claim();
var cutInfos = PrepareNavmeshCutsForCutting(navmeshCuts, transform, bounds, perturbate, navmeshAdds.Count > 0);
var outverts = ListPool.Claim(verts.Length*2);
var outtris = ListPool.Claim(tris.Length);
if (navmeshCuts.Count == 0 && navmeshAdds.Count == 0 && (mode & ~(CutMode.CutAll | CutMode.CutDual)) == 0 && (mode & CutMode.CutAll) != 0) {
// Fast path for the common case, no cuts or adds to the navmesh, so we just copy the vertices
CopyMesh(verts, tris, outverts, outtris);
} else {
var poly = ListPool.Claim();
var point2Index = new Dictionary();
var polypoints = ListPool.Claim();
var clipResult = new Pathfinding.ClipperLib.PolyTree();
var intermediateClipResult = ListPool >.Claim();
var polyCache = StackPool.Claim();
// If we failed the previous iteration
// use a higher quality cutting
// this is heavier on the CPU, so only use it in special cases
clipper.StrictlySimple = perturbate > -1;
clipper.ReverseSolution = true;
Int3[] clipIn = null;
Int3[] clipOut = null;
Int2 clipSize = new Int2();
if (navmeshAdds.Count > 0) {
clipIn = new Int3[7];
clipOut = new Int3[7];
// TODO: What if the size is odd?
// Convert cutRegionSize to an Int2 (all the casting is used to scale it appropriately, Int2 does not have an explicit conversion)
clipSize = new Int2(((Int3)(Vector3)cutRegionSize).x, ((Int3)(Vector3)cutRegionSize).y);
}
// Iterate over all meshes that will make up the navmesh surface
Int3[] vertexBuffer = null;
for (int meshIndex = -1; meshIndex < navmeshAdds.Count; meshIndex++) {
// Current array of vertices and triangles that are being processed
Int3[] cverts;
int[] ctris;
if (meshIndex == -1) {
cverts = verts;
ctris = tris;
} else {
navmeshAdds[meshIndex].GetMesh(ref vertexBuffer, out ctris, transform);
cverts = vertexBuffer;
}
for (int tri = 0; tri < ctris.Length; tri += 3) {
Int3 tp1 = cverts[ctris[tri + 0]];
Int3 tp2 = cverts[ctris[tri + 1]];
Int3 tp3 = cverts[ctris[tri + 2]];
if (VectorMath.IsColinearXZ(tp1, tp2, tp3)) {
Debug.LogWarning("Skipping degenerate triangle.");
continue;
}
var triBounds = new IntRect(tp1.x, tp1.z, tp1.x, tp1.z);
triBounds = triBounds.ExpandToContain(tp2.x, tp2.z);
triBounds = triBounds.ExpandToContain(tp3.x, tp3.z);
// Upper and lower bound on the Y-axis, the above bounds do not have Y axis information
int tpYMin = Math.Min(tp1.y, Math.Min(tp2.y, tp3.y));
int tpYMax = Math.Max(tp1.y, Math.Max(tp2.y, tp3.y));
intersectingCuts.Clear();
bool hasDual = false;
for (int i = 0; i < cutInfos.Count; i++) {
int ymin = cutInfos[i].boundsY.x;
int ymax = cutInfos[i].boundsY.y;
if (IntRect.Intersects(triBounds, cutInfos[i].bounds) && !(ymax< tpYMin || ymin > tpYMax) && (cutInfos[i].cutsAddedGeom || meshIndex == -1)) {
Int3 p1 = tp1;
p1.y = ymin;
Int3 p2 = tp1;
p2.y = ymax;
intersectingCuts.Add(i);
hasDual |= cutInfos[i].isDual;
}
}
// Check if this is just a simple triangle which no navmesh cuts intersect and
// there are no other special things that should be done
if (intersectingCuts.Count == 0 && (mode & CutMode.CutExtra) == 0 && (mode & CutMode.CutAll) != 0 && meshIndex == -1) {
// Just add the triangle and be done with it
// Refers to vertices to be added a few lines below
outtris.Add(outverts.Count + 0);
outtris.Add(outverts.Count + 1);
outtris.Add(outverts.Count + 2);
outverts.Add(tp1);
outverts.Add(tp2);
outverts.Add(tp3);
continue;
}
// Add current triangle as subject polygon for cutting
poly.Clear();
if (meshIndex == -1) {
// Geometry from a tile mesh is assumed to be completely inside the tile
poly.Add(new IntPoint(tp1.x, tp1.z));
poly.Add(new IntPoint(tp2.x, tp2.z));
poly.Add(new IntPoint(tp3.x, tp3.z));
} else {
// Added geometry must be clipped against the tile bounds
clipIn[0] = tp1;
clipIn[1] = tp2;
clipIn[2] = tp3;
int ct = ClipAgainstRectangle(clipIn, clipOut, clipSize);
// Check if triangle was completely outside the tile
if (ct == 0) {
continue;
}
for (int q = 0; q < ct; q++)
poly.Add(new IntPoint(clipIn[q].x, clipIn[q].z));
}
point2Index.Clear();
// Loop through all possible modes (just 4 at the moment, so < 4 could be used actually)
for (int cmode = 0; cmode < 16; cmode++) {
// Ignore modes which are not active
if ((((int)mode >> cmode) & 0x1) == 0)
continue;
if (1 << cmode == (int)CutMode.CutAll) {
CutAll(poly, intersectingCuts, cutInfos, clipResult);
} else if (1 << cmode == (int)CutMode.CutDual) {
// No duals, don't bother processing this
if (!hasDual)
continue;
CutDual(poly, intersectingCuts, cutInfos, hasDual, intermediateClipResult, clipResult);
} else if (1 << cmode == (int)CutMode.CutExtra) {
CutExtra(poly, extraClipShape, clipResult);
}
for (int exp = 0; exp < clipResult.ChildCount; exp++) {
PolyNode node = clipResult.Childs[exp];
List outer = node.Contour;
List holes = node.Childs;
if (holes.Count == 0 && outer.Count == 3 && meshIndex == -1) {
for (int i = 0; i < 3; i++) {
var p = new Int3((int)outer[i].X, 0, (int)outer[i].Y);
p.y = Pathfinding.Polygon.SampleYCoordinateInTriangle(tp1, tp2, tp3, p);
outtris.Add(outverts.Count);
outverts.Add(p);
}
} else {
Poly2Tri.Polygon polygonToTriangulate = null;
// Loop over outer and all holes
int hole = -1;
List contour = outer;
while (contour != null) {
polypoints.Clear();
for (int i = 0; i < contour.Count; i++) {
// Create a new point
var pp = new PolygonPoint(contour[i].X, contour[i].Y);
// Add the point to the polygon
polypoints.Add(pp);
var p = new Int3((int)contour[i].X, 0, (int)contour[i].Y);
p.y = Pathfinding.Polygon.SampleYCoordinateInTriangle(tp1, tp2, tp3, p);
// Prepare a lookup table for pp -> vertex index
point2Index[pp] = outverts.Count;
// Add to resulting vertex list
outverts.Add(p);
}
Poly2Tri.Polygon contourPolygon = null;
if (polyCache.Count > 0) {
contourPolygon = polyCache.Pop();
contourPolygon.AddPoints(polypoints);
} else {
contourPolygon = new Poly2Tri.Polygon(polypoints);
}
// Since the outer contour is the first to be processed, polygonToTriangle will be null
// Holes are processed later, when polygonToTriangle is not null
if (hole == -1) {
polygonToTriangulate = contourPolygon;
} else {
polygonToTriangulate.AddHole(contourPolygon);
}
hole++;
contour = hole < holes.Count ? holes[hole].Contour : null;
}
// Triangulate the polygon with holes
try {
P2T.Triangulate(polygonToTriangulate);
} catch (Poly2Tri.PointOnEdgeException) {
Debug.LogWarning("PointOnEdgeException, perturbating vertices slightly.\nThis is usually fine. It happens sometimes because of rounding errors. Cutting will be retried a few more times.");
return CutPoly(verts, tris, extraShape, graphTransform, tiles, mode, perturbate + 1);
}
try {
for (int i = 0; i < polygonToTriangulate.Triangles.Count; i++) {
Poly2Tri.DelaunayTriangle t = polygonToTriangulate.Triangles[i];
// Add the triangle with the correct indices (using the previously built lookup table)
outtris.Add(point2Index[t.Points._0]);
outtris.Add(point2Index[t.Points._1]);
outtris.Add(point2Index[t.Points._2]);
}
} catch (System.Collections.Generic.KeyNotFoundException) {
Debug.LogWarning("KeyNotFoundException, perturbating vertices slightly.\nThis is usually fine. It happens sometimes because of rounding errors. Cutting will be retried a few more times.");
return CutPoly(verts, tris, extraShape, graphTransform, tiles, mode, perturbate + 1);
}
PoolPolygon(polygonToTriangulate, polyCache);
}
}
}
}
}
if (vertexBuffer != null) ArrayPool.Release(ref vertexBuffer);
StackPool.Release(polyCache);
ListPool >.Release(ref intermediateClipResult);
ListPool.Release(ref poly);
ListPool.Release(ref polypoints);
}
// This next step will remove all duplicate vertices in the data (of which there are quite a few)
// and output the final vertex and triangle arrays to the outVertsArr and outTrisArr variables
var result = new CuttingResult();
Pathfinding.Polygon.CompressMesh(outverts, outtris, out result.verts, out result.tris);
// Notify the navmesh cuts that they were used
for (int i = 0; i < navmeshCuts.Count; i++) {
navmeshCuts[i].UsedForCut();
}
// Release back to pools
ListPool.Release(ref outverts);
ListPool.Release(ref outtris);
ListPool.Release(ref intersectingCuts);
for (int i = 0; i < cutInfos.Count; i++) {
ListPool.Release(cutInfos[i].contour);
}
ListPool.Release(ref cutInfos);
ListPool.Release(ref navmeshCuts);
return result;
}
///
/// Generates a list of cuts from the navmesh cut components.
/// Each cut has a single contour (NavmeshCut components may contain multiple).
///
/// transform should transform a point from cut space to world space.
///
static List PrepareNavmeshCutsForCutting (List navmeshCuts, GraphTransform transform, IntRect cutSpaceBounds, int perturbate, bool anyNavmeshAdds) {
System.Random rnd = null;
if (perturbate > 0) {
rnd = new System.Random();
}
var contourBuffer = ListPool >.Claim();
var result = ListPool.Claim();
for (int i = 0; i < navmeshCuts.Count; i++) {
// Generate random perturbation for this obstacle if required
Int2 perturbation = new Int2(0, 0);
if (perturbate > 0) {
// Create a perturbation vector, choose a point with coordinates in the set [-3*perturbate,3*perturbate]
// makes sure none of the coordinates are zero
perturbation.x = (rnd.Next() % 6*perturbate) - 3*perturbate;
if (perturbation.x >= 0) perturbation.x++;
perturbation.y = (rnd.Next() % 6*perturbate) - 3*perturbate;
if (perturbation.y >= 0) perturbation.y++;
}
contourBuffer.Clear();
navmeshCuts[i].GetContour(contourBuffer);
var y0 = (int)(navmeshCuts[i].GetY(transform) * Int3.FloatPrecision);
for (int j = 0; j < contourBuffer.Count; j++) {
List worldContour = contourBuffer[j];
if (worldContour.Count == 0) {
Debug.LogError("A NavmeshCut component had a zero length contour. Ignoring that contour.");
continue;
}
// TODO: transform should include cutting offset
List contour = ListPool.Claim(worldContour.Count);
for (int q = 0; q < worldContour.Count; q++) {
var p = (Int3)transform.InverseTransform(worldContour[q]);
if (perturbate > 0) {
p.x += perturbation.x;
p.z += perturbation.y;
}
contour.Add(new IntPoint(p.x, p.z));
}
IntRect contourBounds = new IntRect((int)contour[0].X, (int)contour[0].Y, (int)contour[0].X, (int)contour[0].Y);
for (int q = 0; q < contour.Count; q++) {
IntPoint p = contour[q];
contourBounds = contourBounds.ExpandToContain((int)p.X, (int)p.Y);
}
Cut cut = new Cut();
// Calculate bounds on the y axis
int halfHeight = (int)(navmeshCuts[i].height * 0.5f * Int3.Precision);
cut.boundsY = new Int2(y0 - halfHeight, y0 + halfHeight);
cut.bounds = contourBounds;
cut.isDual = navmeshCuts[i].isDual;
cut.cutsAddedGeom = navmeshCuts[i].cutsAddedGeom;
cut.contour = contour;
result.Add(cut);
}
}
ListPool >.Release(ref contourBuffer);
return result;
}
static void PoolPolygon (Poly2Tri.Polygon polygon, Stack pool) {
if (polygon.Holes != null)
for (int i = 0; i < polygon.Holes.Count; i++) {
polygon.Holes[i].Points.Clear();
polygon.Holes[i].ClearTriangles();
if (polygon.Holes[i].Holes != null)
polygon.Holes[i].Holes.Clear();
pool.Push(polygon.Holes[i]);
}
polygon.ClearTriangles();
if (polygon.Holes != null)
polygon.Holes.Clear();
polygon.Points.Clear();
pool.Push(polygon);
}
void CutAll (List poly, List intersectingCutIndices, List cuts, Pathfinding.ClipperLib.PolyTree result) {
clipper.Clear();
clipper.AddPolygon(poly, PolyType.ptSubject);
// Add all holes (cuts) as clip polygons
// TODO: AddPolygon allocates quite a lot, modify ClipperLib to use object pooling
for (int i = 0; i < intersectingCutIndices.Count; i++) {
clipper.AddPolygon(cuts[intersectingCutIndices[i]].contour, PolyType.ptClip);
}
result.Clear();
clipper.Execute(ClipType.ctDifference, result, PolyFillType.pftNonZero, PolyFillType.pftNonZero);
}
void CutDual (List poly, List tmpIntersectingCuts, List cuts, bool hasDual, List > intermediateResult, Pathfinding.ClipperLib.PolyTree result) {
// First calculate
// a = original intersection dualCuts
// then
// b = a difference normalCuts
// then process b as normal
clipper.Clear();
clipper.AddPolygon(poly, PolyType.ptSubject);
// Add all holes (cuts) as clip polygons
for (int i = 0; i < tmpIntersectingCuts.Count; i++) {
if (cuts[tmpIntersectingCuts[i]].isDual) {
clipper.AddPolygon(cuts[tmpIntersectingCuts[i]].contour, PolyType.ptClip);
}
}
clipper.Execute(ClipType.ctIntersection, intermediateResult, PolyFillType.pftEvenOdd, PolyFillType.pftNonZero);
clipper.Clear();
if (intermediateResult != null) {
for (int i = 0; i < intermediateResult.Count; i++) {
clipper.AddPolygon(intermediateResult[i], Pathfinding.ClipperLib.Clipper.Orientation(intermediateResult[i]) ? PolyType.ptClip : PolyType.ptSubject);
}
}
for (int i = 0; i < tmpIntersectingCuts.Count; i++) {
if (!cuts[tmpIntersectingCuts[i]].isDual) {
clipper.AddPolygon(cuts[tmpIntersectingCuts[i]].contour, PolyType.ptClip);
}
}
result.Clear();
clipper.Execute(ClipType.ctDifference, result, PolyFillType.pftEvenOdd, PolyFillType.pftNonZero);
}
void CutExtra (List poly, List extraClipShape, Pathfinding.ClipperLib.PolyTree result) {
clipper.Clear();
clipper.AddPolygon(poly, PolyType.ptSubject);
clipper.AddPolygon(extraClipShape, PolyType.ptClip);
result.Clear();
clipper.Execute(ClipType.ctIntersection, result, PolyFillType.pftEvenOdd, PolyFillType.pftNonZero);
}
///
/// Clips the input polygon against a rectangle with one corner at the origin and one at size in XZ space.
///
/// Returns: Number of output vertices
///
/// Input vertices
/// Output vertices. This buffer must be large enough to contain all output vertices.
/// The clipping rectangle has one corner at the origin and one at this position in XZ space.
int ClipAgainstRectangle (Int3[] clipIn, Int3[] clipOut, Int2 size) {
int ct;
ct = simpleClipper.ClipPolygon(clipIn, 3, clipOut, 1, 0, 0);
if (ct == 0)
return ct;
ct = simpleClipper.ClipPolygon(clipOut, ct, clipIn, -1, size.x, 0);
if (ct == 0)
return ct;
ct = simpleClipper.ClipPolygon(clipIn, ct, clipOut, 1, 0, 2);
if (ct == 0)
return ct;
ct = simpleClipper.ClipPolygon(clipOut, ct, clipIn, -1, size.y, 2);
return ct;
}
/// Copy mesh from (vertices, triangles) to (outVertices, outTriangles)
static void CopyMesh (Int3[] vertices, int[] triangles, List outVertices, List outTriangles) {
outTriangles.Capacity = Math.Max(outTriangles.Capacity, triangles.Length);
outVertices.Capacity = Math.Max(outVertices.Capacity, vertices.Length);
for (int i = 0; i < vertices.Length; i++) {
outVertices.Add(vertices[i]);
}
for (int i = 0; i < triangles.Length; i++) {
outTriangles.Add(triangles[i]);
}
}
///
/// Refine a mesh using delaunay refinement.
/// Loops through all pairs of neighbouring triangles and check if it would be better to flip the diagonal joining them
/// using the delaunay criteria.
///
/// Does not require triangles to be clockwise, triangles will be checked for if they are clockwise and made clockwise if not.
/// The resulting mesh will have all triangles clockwise.
///
/// See: https://en.wikipedia.org/wiki/Delaunay_triangulation
///
void DelaunayRefinement (Int3[] verts, int[] tris, ref int tCount, bool delaunay, bool colinear) {
if (tCount % 3 != 0) throw new System.ArgumentException("Triangle array length must be a multiple of 3");
Dictionary lookup = cached_Int2_int_dict;
lookup.Clear();
for (int i = 0; i < tCount; i += 3) {
if (!VectorMath.IsClockwiseXZ(verts[tris[i]], verts[tris[i+1]], verts[tris[i+2]])) {
int tmp = tris[i];
tris[i] = tris[i+2];
tris[i+2] = tmp;
}
lookup[new Int2(tris[i+0], tris[i+1])] = i+2;
lookup[new Int2(tris[i+1], tris[i+2])] = i+0;
lookup[new Int2(tris[i+2], tris[i+0])] = i+1;
}
for (int i = 0; i < tCount; i += 3) {
for (int j = 0; j < 3; j++) {
int opp;
if (lookup.TryGetValue(new Int2(tris[i+((j+1)%3)], tris[i+((j+0)%3)]), out opp)) {
// The vertex which we are using as the viewpoint
Int3 po = verts[tris[i+((j+2)%3)]];
// Right vertex of the edge
Int3 pr = verts[tris[i+((j+1)%3)]];
// Left vertex of the edge
Int3 pl = verts[tris[i+((j+3)%3)]];
// Opposite vertex (in the other triangle)
Int3 popp = verts[tris[opp]];
po.y = 0;
pr.y = 0;
pl.y = 0;
popp.y = 0;
bool noDelaunay = false;
if (!VectorMath.RightOrColinearXZ(po, pl, popp) || VectorMath.RightXZ(po, pr, popp)) {
if (colinear) {
noDelaunay = true;
} else {
continue;
}
}
if (colinear) {
const int MaxError = 3 * 3;
// Check if op - right shared - opposite in other - is colinear
// and if the edge right-op is not shared and if the edge opposite in other - right shared is not shared
if (VectorMath.SqrDistancePointSegmentApproximate(po, popp, pr) < MaxError &&
!lookup.ContainsKey(new Int2(tris[i+((j+2)%3)], tris[i+((j+1)%3)])) &&
!lookup.ContainsKey(new Int2(tris[i+((j+1)%3)], tris[opp]))) {
tCount -= 3;
int root = (opp/3)*3;
// Move right vertex to the other triangle's opposite
tris[i+((j+1)%3)] = tris[opp];
// Move last triangle to delete
if (root != tCount) {
tris[root+0] = tris[tCount+0];
tris[root+1] = tris[tCount+1];
tris[root+2] = tris[tCount+2];
lookup[new Int2(tris[root+0], tris[root+1])] = root+2;
lookup[new Int2(tris[root+1], tris[root+2])] = root+0;
lookup[new Int2(tris[root+2], tris[root+0])] = root+1;
tris[tCount+0] = 0;
tris[tCount+1] = 0;
tris[tCount+2] = 0;
} else {
tCount += 3;
}
// Since the above mentioned edges are not shared, we don't need to bother updating them
// However some need to be updated
// left - new right (previously opp) should have opposite vertex po
//lookup[new Int2(tris[i+((j+3)%3)],tris[i+((j+1)%3)])] = i+((j+2)%3);
lookup[new Int2(tris[i+0], tris[i+1])] = i+2;
lookup[new Int2(tris[i+1], tris[i+2])] = i+0;
lookup[new Int2(tris[i+2], tris[i+0])] = i+1;
continue;
}
}
if (delaunay && !noDelaunay) {
float beta = Int3.Angle(pr-po, pl-po);
float alpha = Int3.Angle(pr-popp, pl-popp);
if (alpha > (2*Mathf.PI - 2*beta)) {
// Denaunay condition not holding, refine please
tris[i+((j+1)%3)] = tris[opp];
int root = (opp/3)*3;
int off = opp-root;
tris[root+((off-1+3) % 3)] = tris[i+((j+2)%3)];
lookup[new Int2(tris[i+0], tris[i+1])] = i+2;
lookup[new Int2(tris[i+1], tris[i+2])] = i+0;
lookup[new Int2(tris[i+2], tris[i+0])] = i+1;
lookup[new Int2(tris[root+0], tris[root+1])] = root+2;
lookup[new Int2(tris[root+1], tris[root+2])] = root+0;
lookup[new Int2(tris[root+2], tris[root+0])] = root+1;
}
}
}
}
}
}
/// Clear the tile at the specified tile coordinates
public void ClearTile (int x, int z) {
if (AstarPath.active == null) return;
if (x < 0 || z < 0 || x >= tileXCount || z >= tileZCount) return;
AstarPath.active.AddWorkItem(new AstarWorkItem((context, force) => {
//Replace the tile using the final vertices and triangles
graph.ReplaceTile(x, z, new Int3[0], new int[0]);
activeTileTypes[x + z*tileXCount] = null;
if (!isBatching) {
// Trigger post update event
// This can trigger for example recalculation of navmesh links
context.SetGraphDirty(graph);
}
return true;
}));
}
/// Reloads all tiles intersecting with the specified bounds
public void ReloadInBounds (Bounds bounds) {
ReloadInBounds(graph.GetTouchingTiles(bounds));
}
/// Reloads all tiles specified by the rectangle
public void ReloadInBounds (IntRect tiles) {
// Make sure the rect is inside graph bounds
tiles = IntRect.Intersection(tiles, new IntRect(0, 0, tileXCount-1, tileZCount-1));
if (!tiles.IsValid()) return;
for (int z = tiles.ymin; z <= tiles.ymax; z++) {
for (int x = tiles.xmin; x <= tiles.xmax; x++) {
ReloadTile(x, z);
}
}
}
///
/// Reload tile at tile coordinate.
/// The last tile loaded at that position will be reloaded (e.g to account for moved NavmeshCut components)
///
public void ReloadTile (int x, int z) {
if (x < 0 || z < 0 || x >= tileXCount || z >= tileZCount) return;
int index = x + z*tileXCount;
if (activeTileTypes[index] != null) LoadTile(activeTileTypes[index], x, z, activeTileRotations[index], activeTileOffsets[index]);
}
/// Load a tile at tile coordinate x, z.
/// Tile type to load
/// Tile x coordinate (first tile is at (0,0), second at (1,0) etc.. ).
/// Tile z coordinate.
/// Rotate tile by 90 degrees * value.
/// Offset Y coordinates by this amount. In Int3 space, so if you have a world space
/// offset, multiply by Int3.Precision and round to the nearest integer before calling this function.
public void LoadTile (TileType tile, int x, int z, int rotation, int yoffset) {
if (tile == null) throw new ArgumentNullException("tile");
if (AstarPath.active == null) return;
int index = x + z*tileXCount;
rotation = rotation % 4;
// If loaded during this batch with the same settings, skip it
if (isBatching && reloadedInBatch[index] && activeTileOffsets[index] == yoffset && activeTileRotations[index] == rotation && activeTileTypes[index] == tile) {
return;
}
reloadedInBatch[index] |= isBatching;
activeTileOffsets[index] = yoffset;
activeTileRotations[index] = rotation;
activeTileTypes[index] = tile;
// Add a work item
// This will pause pathfinding as soon as possible
// and call the delegate when it is safe to update graphs
AstarPath.active.AddWorkItem(new AstarWorkItem((context, force) => {
// If this was not the correct settings to load with, ignore
if (!(activeTileOffsets[index] == yoffset && activeTileRotations[index] == rotation && activeTileTypes[index] == tile)) return true;
GraphModifier.TriggerEvent(GraphModifier.EventType.PreUpdate);
Int3[] verts;
int[] tris;
tile.Load(out verts, out tris, rotation, yoffset);
Profiler.BeginSample("Cut Poly");
// Cut the polygon
var cuttingResult = CutPoly(verts, tris, null, graph.transform, new IntRect(x, z, x + tile.Width - 1, z + tile.Depth - 1));
Profiler.EndSample();
Profiler.BeginSample("Delaunay Refinement");
// Refine to tweak bad triangles
var tCount = cuttingResult.tris.Length;
DelaunayRefinement(cuttingResult.verts, cuttingResult.tris, ref tCount, true, false);
Profiler.EndSample();
if (tCount != cuttingResult.tris.Length) cuttingResult.tris = Memory.ShrinkArray(cuttingResult.tris, tCount);
// Rotate the mask correctly
// and update width and depth to match rotation
// (width and depth will swap if rotated 90 or 270 degrees )
int newWidth = rotation % 2 == 0 ? tile.Width : tile.Depth;
int newDepth = rotation % 2 == 0 ? tile.Depth : tile.Width;
if (newWidth != 1 || newDepth != 1) throw new System.Exception("Only tiles of width = depth = 1 are supported at this time");
Profiler.BeginSample("ReplaceTile");
// Replace the tile using the final vertices and triangles
// The vertices are still in local space
graph.ReplaceTile(x, z, cuttingResult.verts, cuttingResult.tris);
Profiler.EndSample();
if (!isBatching) {
// Trigger post update event
// This can trigger for example recalculation of navmesh links
// TODO: Does this need to be inside an if statement?
context.SetGraphDirty(graph);
}
return true;
}));
}
}
}