diff options
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Navmesh/TileHandler.cs')
-rw-r--r-- | Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Navmesh/TileHandler.cs | 1258 |
1 files changed, 1258 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Navmesh/TileHandler.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Navmesh/TileHandler.cs new file mode 100644 index 0000000..c182b03 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Navmesh/TileHandler.cs @@ -0,0 +1,1258 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Pathfinding.ClipperLib; +using UnityEngine.Profiling; + +namespace Pathfinding.Graphs.Navmesh { + using Pathfinding; + using Pathfinding.Util; + using Pathfinding.Poly2Tri; + using Unity.Collections; + using Unity.Collections.LowLevel.Unsafe; + using Unity.Mathematics; + using Pathfinding.Graphs.Util; + + /// <summary> + /// 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: <see cref="NavmeshUpdates"/> + /// </summary> + public class TileHandler { + /// <summary>The underlaying graph which is handled by this instance</summary> + public readonly NavmeshBase graph; + + /// <summary>Number of tiles along the x axis</summary> + int tileXCount; + + /// <summary>Number of tiles along the z axis</summary> + int tileZCount; + + /// <summary>Handles polygon clipping operations</summary> + readonly Clipper clipper = new Clipper(); + + /// <summary>Cached dictionary to avoid excessive allocations</summary> + readonly Dictionary<Int2, int> cached_Int2_int_dict = new Dictionary<Int2, int>(); + + /// <summary> + /// Which tile type is active on each tile index. + /// This array will be tileXCount*tileZCount elements long. + /// </summary> + TileType[] activeTileTypes; + + /// <summary>Rotations of the active tiles</summary> + int[] activeTileRotations; + + /// <summary>Offsets along the Y axis of the active tiles</summary> + int[] activeTileOffsets; + + /// <summary>A flag for each tile that is set to true if it has been reloaded while batching is in progress</summary> + bool[] reloadedInBatch; + + /// <summary> + /// NavmeshCut and NavmeshAdd components registered to this tile handler. + /// This is updated by the <see cref="NavmeshUpdates"/> class. + /// See: <see cref="NavmeshUpdates"/> + /// </summary> + public readonly GridLookup<NavmeshClipper> cuts; + + /// <summary> + /// Positive while batching tile updates. + /// Batching tile updates has a positive effect on performance + /// </summary> + int batchDepth; + + /// <summary> + /// True while batching tile updates. + /// Batching tile updates has a positive effect on performance + /// </summary> + bool isBatching { get { return batchDepth > 0; } } + + /// <summary> + /// 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 + /// </summary> + // Note: Can technically be made readonly, but then C# will automatically copy the struct before every invocation + Voxelization.Int3PolygonClipper simpleClipper; + + /// <summary> + /// 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. + /// </summary> + 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<NavmeshClipper>(new Int2(tileXCount, tileZCount)); + this.graph = graph; + } + + /// <summary> + /// Resize the tile handler to a different tile count. + /// See: <see cref="RecastGraph.Resize"/> + /// </summary> + public void Resize (IntRect newTileBounds) { + UnityEngine.Assertions.Assert.IsFalse(this.isBatching); + var newActiveTileTypes = new TileType[newTileBounds.Area]; + var newActiveTileRotations = new int[newActiveTileTypes.Length]; + var newActiveTileOffsets = new int[newActiveTileTypes.Length]; + var newReloadedInBatch = new bool[newActiveTileTypes.Length]; + for (int z = 0; z < tileZCount; z++) { + for (int x = 0; x < tileXCount; x++) { + if (newTileBounds.Contains(x, z)) { + var oldIndex = x + z*tileXCount; + var newIndex = (x - newTileBounds.xmin) + (z - newTileBounds.ymin)*newTileBounds.Width; + newActiveTileTypes[newIndex] = activeTileTypes[oldIndex]; + newActiveTileRotations[newIndex] = activeTileRotations[oldIndex]; + newActiveTileOffsets[newIndex] = activeTileOffsets[oldIndex]; + } + } + } + + this.tileXCount = newTileBounds.Width; + this.tileZCount = newTileBounds.Height; + this.activeTileTypes = newActiveTileTypes; + this.activeTileRotations = newActiveTileRotations; + this.activeTileOffsets = newActiveTileOffsets; + this.reloadedInBatch = newReloadedInBatch; + + for (int z = 0; z < tileZCount; z++) { + for (int x = 0; x < tileXCount; x++) { + var tileIndex = x + z*tileXCount; + if (activeTileTypes[tileIndex] == null) { + UpdateTileType(graph.GetTile(x, z)); + } + } + } + + this.cuts.Resize(newTileBounds); + } + + /// <summary> + /// 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. + /// </summary> + 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(); + } + + /// <summary>A template for a single tile in a navmesh/recast graph</summary> + public class TileType { + Int3[] verts; + int[] tris; + uint[] tags; + Int3 offset; + int lastYOffset; + int lastRotation; + int width; + int depth; + + public int Width { + get { + return width; + } + } + + public int Depth { + get { + return depth; + } + } + + /// <summary> + /// Matrices for rotation. + /// Each group of 4 elements is a 2x2 matrix. + /// The XZ position is multiplied by this. + /// So + /// <code> + /// //A rotation by 90 degrees clockwise, second matrix in the array + /// (5,2) * ((0, 1), (-1, 0)) = (2,-5) + /// </code> + /// </summary> + 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 (UnsafeSpan<Int3> sourceVerts, UnsafeSpan<int> sourceTris, uint[] tags, Int3 tileSize, Int3 centerOffset, int width = 1, int depth = 1) { + tris = sourceTris.ToArray(); + this.tags = tags; + + verts = new Int3[sourceVerts.Length]; + + offset = tileSize/2; + offset.x *= width; + offset.z *= depth; + offset.y = 0; + offset += centerOffset; + + for (int i = 0; i < sourceVerts.Length; i++) { + verts[i] = sourceVerts[i] + offset; + } + + lastRotation = 0; + lastYOffset = 0; + + this.width = width; + this.depth = depth; + } + + /// <summary> + /// 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. + /// </summary> + /// <param name="source">The navmesh as a unity Mesh</param> + /// <param name="width">The number of base tiles this tile type occupies on the x-axis</param> + /// <param name="depth">The number of base tiles this tile type occupies on the z-axis</param> + /// <param name="tileSize">Size of a single tile, the y-coordinate will be ignored.</param> + /// <param name="centerOffset">This offset will be added to all vertices</param> + 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]; + this.tags = null; + + 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; + } + + /// <summary> + /// 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. + /// </summary> + public void Load (out Int3[] verts, out int[] tris, out uint[] tags, 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; + tags = this.tags; + } + } + + /// <summary> + /// Vertices and triangles used as input for the navmesh cutting. + /// + /// The vertices are in tile-space. So (0,0) is a corner of the tile. Distances are the same as in graph-space. + /// + /// 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 GetSourceTileData since + /// different rotations and y offsets can be applied. + /// If you need persistent arrays, please copy the returned ones. + /// </summary> + public void GetSourceTileData (int x, int z, out Int3[] verts, out int[] tris, out uint[] tags) { + var tileIndex = x + z*tileXCount; + this.activeTileTypes[tileIndex].Load(out verts, out tris, out tags, activeTileRotations[tileIndex], activeTileOffsets[tileIndex]); + } + + /// <summary> + /// Register that a tile can be loaded from source. + /// + /// Returns: Identifier for loading that tile type + /// </summary> + /// <param name="centerOffset">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.</param> + /// <param name="width">width of the tile. In base tiles, not world units.</param> + /// <param name="depth">depth of the tile. In base tiles, not world units.</param> + /// <param name="source">Source mesh, must be readable.</param> + 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 tags = new uint[tile.nodes.Length]; + for (int i = 0; i < tags.Length; i++) tags[i] = tile.nodes[i].Tag; + var tileType = new TileType(tile.vertsInGraphSpace, tile.tris, tags, size, centerOffset, tile.w, tile.d); + + int index = x + z*tileXCount; + + activeTileTypes[index] = tileType; + activeTileRotations[index] = 0; + activeTileOffsets[index] = 0; + } + + /// <summary> + /// Start batch loading. + /// Every call to this method must be matched by exactly one call to EndBatchLoad. + /// </summary> + 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(); + Profiler.EndSample(); + return true; + })); + } + + [Flags] + public enum CutMode { + /// <summary>Cut holes in the navmesh</summary> + CutAll = 1, + /// <summary>Cut the navmesh but do not remove the interior of the cuts</summary> + CutDual = 2, + /// <summary>Also cut using the extra shape that was provided</summary> + CutExtra = 4 + } + + /// <summary>Internal class describing a single NavmeshCut</summary> + class Cut { + /// <summary>Bounds in XZ space</summary> + public IntRect bounds; + + /// <summary>X is the lower bound on the y axis, Y is the upper bounds on the Y axis</summary> + public Int2 boundsY; + public bool isDual; + public bool cutsAddedGeom; + public List<IntPoint> contour; + } + + /// <summary>Internal class representing a mesh which is the result of the CutPoly method</summary> + struct CuttingResult { + public Int3[] verts; + public int[] tris; + public uint[] tags; + } + + /// <summary> + /// 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 + /// </summary> + /// <param name="verts">Vertices that are going to be cut. Should be in graph space.</param> + /// <param name="tris">Triangles describing a mesh using the vertices.</param> + /// <param name="tags">Tags for each triangle. Will be passed to the resulting mesh.</param> + /// <param name="extraShape">If supplied the resulting mesh will be the intersection of the input mesh and this mesh.</param> + /// <param name="graphTransform">Transform mapping graph space to world space.</param> + /// <param name="tiles">Tiles in the recast graph which the mesh covers.</param> + /// <param name="mode"></param> + /// <param name="perturbate">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.</param> + CuttingResult CutPoly (Int3[] verts, int[] tris, uint[] tags, Int3[] extraShape, GraphTransform graphTransform, IntRect tiles, CutMode mode = CutMode.CutAll | CutMode.CutDual, int perturbate = -1) { + // Find all NavmeshAdd components that could be inside the bounds + List<NavmeshAdd> navmeshAdds = cuts.QueryRect<NavmeshAdd>(tiles); + + // Nothing to do here + if ((verts.Length == 0 || tris.Length == 0) && navmeshAdds.Count == 0) { + return new CuttingResult { + verts = ArrayPool<Int3>.Claim(0), + tris = ArrayPool<int>.Claim(0), + tags = ArrayPool<uint>.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, + tags = tags, + }; + } + + List<IntPoint> 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); + var characterRadius = graph.NavmeshCuttingCharacterRadius; + + if ((mode & CutMode.CutExtra) != 0) { + extraClipShape = ListPool<IntPoint>.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)); + } + } + + // Find all NavmeshCut components that could be inside these bounds + List<NavmeshCut> navmeshCuts; + if (mode == CutMode.CutExtra) { + // Not needed when only cutting extra + navmeshCuts = ListPool<NavmeshCut>.Claim(); + } else { + navmeshCuts = cuts.QueryRect<NavmeshCut>(tiles); + } + + var intersectingCuts = ListPool<int>.Claim(); + + var cutInfos = PrepareNavmeshCutsForCutting(navmeshCuts, transform, perturbate, characterRadius); + + var outverts = ListPool<Int3>.Claim(verts.Length*2); + var outtris = ListPool<int>.Claim(tris.Length); + var outtags = ListPool<uint>.Claim(tags.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, tags, outverts, outtris, outtags); + } else { + var poly = ListPool<IntPoint>.Claim(); + var point2Index = new Dictionary<TriangulationPoint, int>(); + var polypoints = ListPool<Poly2Tri.PolygonPoint>.Claim(); + + var clipResult = new Pathfinding.ClipperLib.PolyTree(); + var intermediateClipResult = ListPool<List<IntPoint> >.Claim(); + var polyCache = StackPool<Poly2Tri.Polygon>.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; + uint[] ctags; + if (meshIndex == -1) { + cverts = verts; + ctris = tris; + ctags = tags; + } else { + navmeshAdds[meshIndex].GetMesh(ref vertexBuffer, out ctris, transform); + cverts = vertexBuffer; + ctags = null; + } + + 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]]; + var tag = ctags != null ? ctags[tri/3] : 0; + + 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); + + outtags.Add(tag); + 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 + for (int cmode = 0; cmode < 4; 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<IntPoint> outer = node.Contour; + List<PolyNode> 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); + } + outtags.Add(tag); + } else { + Poly2Tri.Polygon polygonToTriangulate = null; + // Loop over outer and all holes + int hole = -1; + List<IntPoint> 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, tags, 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]); + outtags.Add(tag); + } + } 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, tags, extraShape, graphTransform, tiles, mode, perturbate + 1); + } + + PoolPolygon(polygonToTriangulate, polyCache); + } + } + } + } + } + + if (vertexBuffer != null) ArrayPool<Int3>.Release(ref vertexBuffer); + StackPool<Poly2Tri.Polygon>.Release(polyCache); + ListPool<List<IntPoint> >.Release(ref intermediateClipResult); + ListPool<IntPoint>.Release(ref poly); + ListPool<Poly2Tri.PolygonPoint>.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, outtags, out result.verts, out result.tris, out result.tags); + + // Notify the navmesh cuts that they were used + for (int i = 0; i < navmeshCuts.Count; i++) { + navmeshCuts[i].UsedForCut(); + } + + // Release back to pools + ListPool<Int3>.Release(ref outverts); + ListPool<int>.Release(ref outtris); + ListPool<uint>.Release(ref outtags); + ListPool<int>.Release(ref intersectingCuts); + + for (int i = 0; i < cutInfos.Count; i++) { + ListPool<IntPoint>.Release(cutInfos[i].contour); + } + + ListPool<Cut>.Release(ref cutInfos); + ListPool<NavmeshCut>.Release(ref navmeshCuts); + return result; + } + + /// <summary> + /// 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. + /// </summary> + static List<Cut> PrepareNavmeshCutsForCutting (List<NavmeshCut> navmeshCuts, GraphTransform transform, int perturbate, float characterRadius) { + System.Random rnd = null; + if (perturbate > 0) { + rnd = new System.Random(); + } + + var contourVertices = new UnsafeList<float2>(0, Allocator.Temp); + var contours = new UnsafeList<NavmeshCut.ContourBurst>(0, Allocator.Temp); + var result = ListPool<Cut>.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++; + } + + unsafe { + navmeshCuts[i].GetContourBurst(&contourVertices, &contours, transform.inverseMatrix, characterRadius); + } + + for (int j = 0; j < contours.Length; j++) { + NavmeshCut.ContourBurst contour = contours[j]; + + if (contour.endIndex <= contour.startIndex) { + Debug.LogError("A NavmeshCut component had a zero length contour. Ignoring that contour."); + continue; + } + + // TODO: transform should include cutting offset + List<IntPoint> i3contour = ListPool<IntPoint>.Claim(contour.endIndex - contour.startIndex); + for (int q = contour.startIndex; q < contour.endIndex; q++) { + var p = contourVertices[q] * Int3.FloatPrecision; + var ip = new IntPoint((long)p.x, (long)p.y); + if (perturbate > 0) { + ip.X += perturbation.x; + ip.Y += perturbation.y; + } + + i3contour.Add(ip); + } + + IntRect contourBounds = new IntRect((int)i3contour[0].X, (int)i3contour[0].Y, (int)i3contour[0].X, (int)i3contour[0].Y); + + for (int q = 0; q < i3contour.Count; q++) { + IntPoint p = i3contour[q]; + contourBounds = contourBounds.ExpandToContain((int)p.X, (int)p.Y); + } + + Cut cut = new Cut(); + + // Calculate bounds on the y axis + cut.boundsY = new Int2((int)(contour.ymin * Int3.FloatPrecision), (int)(contour.ymax * Int3.FloatPrecision)); + cut.bounds = contourBounds; + cut.isDual = navmeshCuts[i].isDual; + cut.cutsAddedGeom = navmeshCuts[i].cutsAddedGeom; + cut.contour = i3contour; + result.Add(cut); + } + + contours.Clear(); + contourVertices.Clear(); + } + + contours.Dispose(); + contourVertices.Dispose(); + return result; + } + + static void PoolPolygon (Poly2Tri.Polygon polygon, Stack<Poly2Tri.Polygon> 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<IntPoint> poly, List<int> intersectingCutIndices, List<Cut> 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<IntPoint> poly, List<int> tmpIntersectingCuts, List<Cut> cuts, bool hasDual, List<List<IntPoint> > 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<IntPoint> poly, List<IntPoint> 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); + } + + /// <summary> + /// 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 + /// </summary> + /// <param name="clipIn">Input vertices</param> + /// <param name="clipOut">Output vertices. This buffer must be large enough to contain all output vertices.</param> + /// <param name="size">The clipping rectangle has one corner at the origin and one at this position in XZ space.</param> + 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; + } + + /// <summary>Copy mesh from (vertices, triangles) to (outVertices, outTriangles)</summary> + static void CopyMesh (Int3[] vertices, int[] triangles, uint[] tags, List<Int3> outVertices, List<int> outTriangles, List<uint> outTags) { + outTriangles.Capacity = Math.Max(outTriangles.Capacity, triangles.Length); + outVertices.Capacity = Math.Max(outVertices.Capacity, vertices.Length); + outTags.Capacity = Math.Max(outTags.Capacity, tags.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]); + } + + for (int i = 0; i < tags.Length; i++) { + outTags.Add(tags[i]); + } + } + + /// <summary> + /// 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 + /// </summary> + void DelaunayRefinement (Int3[] verts, int[] tris, uint[] tags, ref int tCount, bool delaunay, bool colinear) { + if (tCount % 3 != 0) throw new System.ArgumentException("Triangle array length must be a multiple of 3"); + if (tags != null && tags.Length != tCount / 3) throw new System.ArgumentException("There must be exactly 1 tag per 3 triangle indices"); + + Dictionary<Int2, int> 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) { + var tag = tags != null ? tags[i/3] : 0; + 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]]; + + var oppTag = tags != null ? tags[opp/3] : 0; + + // Only allow flipping if the two adjacent triangles share the same tag + if (tag != oppTag) continue; + + 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]; + + // Remove the opposite triangle by swapping it with the last triangle + if (root != tCount) { + tris[root+0] = tris[tCount+0]; + tris[root+1] = tris[tCount+1]; + tris[root+2] = tris[tCount+2]; + tags[root/3] = tags[tCount/3]; + 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; + } + + // 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; + } + } + } + } + } + } + + /// <summary>Clear the tile at the specified tile coordinates</summary> + 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; + })); + } + + /// <summary>Reloads all tiles intersecting with the specified bounds</summary> + public void ReloadInBounds (Bounds bounds) { + ReloadInBounds(graph.GetTouchingTiles(bounds)); + } + + /// <summary>Reloads all tiles specified by the rectangle</summary> + 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); + } + } + } + + /// <summary> + /// Reload tile at tile coordinate. + /// The last tile loaded at that position will be reloaded (e.g to account for moved NavmeshCut components) + /// </summary> + 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]); + } + + + /// <summary>Load a tile at tile coordinate x, z.</summary> + /// <param name="tile">Tile type to load</param> + /// <param name="x">Tile x coordinate (first tile is at (0,0), second at (1,0) etc.. ).</param> + /// <param name="z">Tile z coordinate.</param> + /// <param name="rotation">Rotate tile by 90 degrees * value.</param> + /// <param name="yoffset">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.</param> + 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; + var originalSize = new Int2(this.tileXCount, this.tileZCount); + + // 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; + // If the tile handler has been resized, ignore + if (originalSize != new Int2(this.tileXCount, this.tileZCount)) return true; + + context.PreUpdate(); + + tile.Load(out var verts, out var tris, out var tags, rotation, yoffset); + + Profiler.BeginSample("Cut Poly"); + // Cut the polygon + var tileBounds = new IntRect(x, z, x + tile.Width - 1, z + tile.Depth - 1); + var cuttingResult = CutPoly(verts, tris, tags, null, graph.transform, tileBounds); + Profiler.EndSample(); + + Profiler.BeginSample("Delaunay Refinement"); + // Refine to tweak bad triangles + var tCount = cuttingResult.tris.Length; + DelaunayRefinement(cuttingResult.verts, cuttingResult.tris, cuttingResult.tags, ref tCount, true, true); + Profiler.EndSample(); + + if (tCount != cuttingResult.tris.Length) { + cuttingResult.tris = Memory.ShrinkArray(cuttingResult.tris, tCount); + cuttingResult.tags = Memory.ShrinkArray(cuttingResult.tags, tCount/3); + } + + // 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, cuttingResult.tags); + Profiler.EndSample(); + return true; + })); + } + } +} |