using UnityEngine; using System.Collections.Generic; namespace Pathfinding { using UnityEngine.Profiling; using Pathfinding.Util; using Pathfinding.Serialization; using Unity.Collections; using Unity.Jobs; using Pathfinding.Graphs.Navmesh.Jobs; using Pathfinding.Graphs.Navmesh; using Unity.Mathematics; /// /// Generates graphs based on navmeshes. /// [Open online documentation to see images] /// /// Navmeshes are meshes in which each triangle defines a walkable area. /// These are great because the AI can get so much more information on how it can walk. /// Polygons instead of points mean that the can produce really nice looking paths, and the graphs are also really fast to search /// and have a low memory footprint because fewer nodes are usually needed to describe the same area compared to grid graphs. /// /// The navmesh graph requires that you create a navmesh manually. The package also has support for generating navmeshes automatically using the . /// /// For a tutorial on how to configure a navmesh graph, take a look at getstarted2 (view in online documentation for working links). /// /// [Open online documentation to see images] /// /// \section navmeshgraph-inspector Inspector /// [Open online documentation to see images] /// /// \inspectorField{Source Mesh, sourceMesh} /// \inspectorField{Offset, offset} /// \inspectorField{Rotation, rotation} /// \inspectorField{Scale, scale} /// \inspectorField{Recalculate Normals, recalculateNormals} /// \inspectorField{Affected By Navmesh Cuts, enableNavmeshCutting} /// \inspectorField{Agent Radius, navmeshCuttingCharacterRadius} /// \inspectorField{Initial Penalty, initialPenalty} /// /// See: /// [JsonOptIn] [Pathfinding.Util.Preserve] public class NavMeshGraph : NavmeshBase, IUpdatableGraph { /// Mesh to construct navmesh from [JsonMember] public Mesh sourceMesh; /// Offset in world space [JsonMember] public Vector3 offset; /// Rotation in degrees [JsonMember] public Vector3 rotation; /// Scale of the graph [JsonMember] public float scale = 1; /// /// Determines how normals are calculated. /// Disable for spherical graphs or other complicated surfaces that allow the agents to e.g walk on walls or ceilings. /// /// By default the normals of the mesh will be flipped so that they point as much as possible in the upwards direction. /// The normals are important when connecting adjacent nodes. Two adjacent nodes will only be connected if they are oriented the same way. /// This is particularly important if you have a navmesh on the walls or even on the ceiling of a room. Or if you are trying to make a spherical navmesh. /// If you do one of those things then you should set disable this setting and make sure the normals in your source mesh are properly set. /// /// If you for example take a look at the image below. In the upper case then the nodes on the bottom half of the /// mesh haven't been connected with the nodes on the upper half because the normals on the lower half will have been /// modified to point inwards (as that is the direction that makes them face upwards the most) while the normals on /// the upper half point outwards. This causes the nodes to not connect properly along the seam. When this option /// is set to false instead the nodes are connected properly as in the original mesh all normals point outwards. /// [Open online documentation to see images] /// /// The default value of this field is true to reduce the risk for errors in the common case. If a mesh is supplied that /// has all normals pointing downwards and this option is false, then some methods like will not work correctly /// as they assume that the normals point upwards. For a more complicated surface like a spherical graph those methods make no sense anyway /// as there is no clear definition of what it means to be "inside" a triangle when there is no clear up direction. /// [JsonMember] public bool recalculateNormals = true; /// /// Cached bounding box minimum of . /// This is important when the graph has been saved to a file and is later loaded again, but the original mesh does not exist anymore (or has been moved). /// In that case we still need to be able to find the bounding box since the method uses it. /// [JsonMember] Vector3 cachedSourceMeshBoundsMin; /// /// Radius to use when expanding navmesh cuts. /// /// See: /// [JsonMember] public float navmeshCuttingCharacterRadius = 0.5f; public override float NavmeshCuttingCharacterRadius => navmeshCuttingCharacterRadius; public override bool RecalculateNormals => recalculateNormals; public override float TileWorldSizeX => forcedBoundsSize.x; public override float TileWorldSizeZ => forcedBoundsSize.z; // Tiles are not supported, so this is irrelevant public override float MaxTileConnectionEdgeDistance => 0f; /// /// True if the point is inside the bounding box of this graph. /// /// Warning: If your input mesh is entirely flat, the bounding box will also end up entirely flat (with a height of zero), this will make this function return false for almost all points, unless they are at exactly the right y-coordinate. /// /// Note: For an unscanned graph, this will always return false. /// public override bool IsInsideBounds (Vector3 point) { if (this.tiles == null || this.tiles.Length == 0 || sourceMesh == null) return false; var local = transform.InverseTransform(point); var size = sourceMesh.bounds.size*scale; // Allow a small margin const float EPS = 0.0001f; return local.x >= -EPS && local.y >= -EPS && local.z >= -EPS && local.x <= size.x + EPS && local.y <= size.y + EPS && local.z <= size.z + EPS; } /// /// World bounding box for the graph. /// /// This always contains the whole graph. /// /// Note: Since this is an axis-aligned bounding box, it may not be particularly tight if the graph is significantly rotated. /// /// If no mesh has been assigned, this will return a zero sized bounding box at the origin. /// /// [Open online documentation to see images] /// public override Bounds bounds { get { if (sourceMesh == null) return default; var m = (float4x4)CalculateTransform().matrix; var b = new ToWorldMatrix(new float3x3(m.c0.xyz, m.c1.xyz, m.c2.xyz)).ToWorld(new Bounds(Vector3.zero, sourceMesh.bounds.size * scale)); return b; } } public override GraphTransform CalculateTransform () { return new GraphTransform(Matrix4x4.TRS(offset, Quaternion.Euler(rotation), Vector3.one) * Matrix4x4.TRS(sourceMesh != null ? sourceMesh.bounds.min * scale : cachedSourceMeshBoundsMin * scale, Quaternion.identity, Vector3.one)); } class NavMeshGraphUpdatePromise : IGraphUpdatePromise { public NavMeshGraph graph; public List graphUpdates; public void Apply (IGraphUpdateContext ctx) { for (int i = 0; i < graphUpdates.Count; i++) { var graphUpdate = graphUpdates[i]; UpdateArea(graphUpdate, graph); // TODO: Not strictly accurate, since the update may affect node that have a surface that extends // outside of the bounds. ctx.DirtyBounds(graphUpdate.bounds); } } } IGraphUpdatePromise IUpdatableGraph.ScheduleGraphUpdates (List graphUpdates) => new NavMeshGraphUpdatePromise { graph = this, graphUpdates = graphUpdates }; public static void UpdateArea (GraphUpdateObject o, INavmeshHolder graph) { Bounds bounds = graph.transform.InverseTransform(o.bounds); // Bounding rectangle with integer coordinates var irect = new IntRect( Mathf.FloorToInt(bounds.min.x*Int3.Precision), Mathf.FloorToInt(bounds.min.z*Int3.Precision), Mathf.CeilToInt(bounds.max.x*Int3.Precision), Mathf.CeilToInt(bounds.max.z*Int3.Precision) ); // Corners of the bounding rectangle var a = new Int3(irect.xmin, 0, irect.ymin); var b = new Int3(irect.xmin, 0, irect.ymax); var c = new Int3(irect.xmax, 0, irect.ymin); var d = new Int3(irect.xmax, 0, irect.ymax); var ymin = ((Int3)bounds.min).y; var ymax = ((Int3)bounds.max).y; // Loop through all nodes and check if they intersect the bounding box graph.GetNodes(_node => { var node = _node as TriangleMeshNode; bool inside = false; int allLeft = 0; int allRight = 0; int allTop = 0; int allBottom = 0; // Check bounding box rect in XZ plane for (int v = 0; v < 3; v++) { Int3 p = node.GetVertexInGraphSpace(v); if (irect.Contains(p.x, p.z)) { inside = true; break; } if (p.x < irect.xmin) allLeft++; if (p.x > irect.xmax) allRight++; if (p.z < irect.ymin) allTop++; if (p.z > irect.ymax) allBottom++; } if (!inside && (allLeft == 3 || allRight == 3 || allTop == 3 || allBottom == 3)) { return; } // Check if the polygon edges intersect the bounding rect for (int v = 0; v < 3; v++) { int v2 = v > 1 ? 0 : v+1; Int3 vert1 = node.GetVertexInGraphSpace(v); Int3 vert2 = node.GetVertexInGraphSpace(v2); if (VectorMath.SegmentsIntersectXZ(a, b, vert1, vert2)) { inside = true; break; } if (VectorMath.SegmentsIntersectXZ(a, c, vert1, vert2)) { inside = true; break; } if (VectorMath.SegmentsIntersectXZ(c, d, vert1, vert2)) { inside = true; break; } if (VectorMath.SegmentsIntersectXZ(d, b, vert1, vert2)) { inside = true; break; } } // Check if the node contains any corner of the bounding rect if (inside || node.ContainsPointInGraphSpace(a) || node.ContainsPointInGraphSpace(b) || node.ContainsPointInGraphSpace(c) || node.ContainsPointInGraphSpace(d)) { inside = true; } if (!inside) { return; } int allAbove = 0; int allBelow = 0; // Check y coordinate for (int v = 0; v < 3; v++) { Int3 p = node.GetVertexInGraphSpace(v); if (p.y < ymin) allBelow++; if (p.y > ymax) allAbove++; } // Polygon is either completely above the bounding box or completely below it if (allBelow == 3 || allAbove == 3) return; // Triangle is inside the bounding box! // Update it! o.WillUpdateNode(node); o.Apply(node); }); } class NavMeshGraphScanPromise : IGraphUpdatePromise { public NavMeshGraph graph; bool emptyGraph; GraphTransform transform; NavmeshTile[] tiles; Vector3 forcedBoundsSize; IntRect tileRect; NavmeshUpdates.NavmeshUpdateSettings cutSettings; public IEnumerator Prepare () { var sourceMesh = graph.sourceMesh; graph.cachedSourceMeshBoundsMin = sourceMesh != null ? sourceMesh.bounds.min : Vector3.zero; transform = graph.CalculateTransform(); if (sourceMesh == null) { emptyGraph = true; yield break; } if (!sourceMesh.isReadable) { Debug.LogError("The source mesh " + sourceMesh.name + " is not readable. Enable Read/Write in the mesh's import settings.", sourceMesh); emptyGraph = true; yield break; } Profiler.BeginSample("GetMeshData"); var meshDatas = Mesh.AcquireReadOnlyMeshData(sourceMesh); MeshUtility.GetMeshData(meshDatas, 0, out var vertices, out var indices); meshDatas.Dispose(); Profiler.EndSample(); // Convert the vertices to graph space // so that the minimum of the bounding box of the mesh is at the origin // (the vertices will later be transformed to world space) var scale = graph.scale; var meshToGraphSpace = Matrix4x4.TRS(-sourceMesh.bounds.min * scale, Quaternion.identity, Vector3.one * scale); var promise = JobBuildTileMeshFromVertices.Schedule(vertices, indices, meshToGraphSpace, graph.RecalculateNormals); forcedBoundsSize = sourceMesh.bounds.size * scale; tileRect = new IntRect(0, 0, 0, 0); tiles = new NavmeshTile[tileRect.Area]; var tilesGCHandle = System.Runtime.InteropServices.GCHandle.Alloc(tiles); var tileLayout = new TileLayout(new Bounds(transform.Transform(forcedBoundsSize*0.5f), forcedBoundsSize), Quaternion.Euler(graph.rotation), 0.001f, 0, false); cutSettings = new NavmeshUpdates.NavmeshUpdateSettings(graph, tileLayout); var cutPromise = RecastBuilder.CutTiles(graph, cutSettings.clipperLookup, tileLayout).Schedule(promise); var tileNodeConnections = new NativeArray(tiles.Length, Allocator.Persistent); var postCutInput = cutPromise.GetValue(); var preCutInput = promise.GetValue(); NativeArray finalTileMeshes; if (postCutInput.tileMeshes.tileMeshes.IsCreated) { UnityEngine.Assertions.Assert.AreEqual(postCutInput.tileMeshes.tileMeshes.Length, tileRect.Area); finalTileMeshes = postCutInput.tileMeshes.tileMeshes; } else { finalTileMeshes = preCutInput.tileMeshes.tileMeshes; } var calculateConnectionsJob = new JobCalculateTriangleConnections { tileMeshes = finalTileMeshes, nodeConnections = tileNodeConnections, }.Schedule(cutPromise.handle); var createTilesJob = new JobCreateTiles { // If any cutting is done, we need to save the pre-cut data to be able to re-cut tiles later preCutTileMeshes = postCutInput.tileMeshes.tileMeshes.IsCreated ? preCutInput.tileMeshes.tileMeshes : default, tileMeshes = finalTileMeshes, tiles = tilesGCHandle, tileRect = tileRect, graphTileCount = new Vector2Int(tileRect.Width, tileRect.Height), graphIndex = graph.graphIndex, initialPenalty = graph.initialPenalty, recalculateNormals = graph.recalculateNormals, graphToWorldSpace = transform.matrix, tileWorldSize = tileLayout.TileWorldSize, }.Schedule(cutPromise.handle); var applyConnectionsJob = new JobWriteNodeConnections { tiles = tilesGCHandle, nodeConnections = tileNodeConnections, }.Schedule(JobHandle.CombineDependencies(createTilesJob, calculateConnectionsJob)); yield return applyConnectionsJob; // This has already been used in the createTilesJob promise.Complete().Dispose(); cutPromise.Complete().Dispose(); tileNodeConnections.Dispose(); vertices.Dispose(); indices.Dispose(); tilesGCHandle.Free(); } public void Apply (IGraphUpdateContext ctx) { if (emptyGraph) { graph.forcedBoundsSize = Vector3.zero; graph.transform = transform; graph.tileZCount = graph.tileXCount = 1; TriangleMeshNode.SetNavmeshHolder(AstarPath.active.data.GetGraphIndex(graph), graph); graph.FillWithEmptyTiles(); graph.navmeshUpdateData.Dispose(); return; } // Destroy all previous nodes (if any) graph.DestroyAllNodes(); // Initialize all nodes that were created in the jobs for (int j = 0; j < tiles.Length; j++) AstarPath.active.InitializeNodes(tiles[j].nodes); // Assign all data as one atomic operation (from the viewpoint of the main thread) graph.forcedBoundsSize = forcedBoundsSize; graph.transform = transform; graph.tileXCount = tileRect.Width; graph.tileZCount = tileRect.Height; graph.tiles = tiles; TriangleMeshNode.SetNavmeshHolder(graph.active.data.GetGraphIndex(graph), graph); cutSettings.AttachToGraph(); if (graph.OnRecalculatedTiles != null) graph.OnRecalculatedTiles(tiles.Clone() as NavmeshTile[]); } } protected override IGraphUpdatePromise ScanInternal (bool async) => new NavMeshGraphScanPromise { graph = this }; protected override void PostDeserialization (GraphSerializationContext ctx) { if (ctx.meta.version < AstarSerializer.V4_3_74) { this.navmeshCuttingCharacterRadius = 0; } base.PostDeserialization(ctx); } } }