399 lines
16 KiB
C#

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;
/// <summary>
/// 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 <see cref="FunnelModifier"/> 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 <see cref="RecastGraph"/>.
///
/// 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: <see cref="RecastGraph"/>
/// </summary>
[JsonOptIn]
[Pathfinding.Util.Preserve]
public class NavMeshGraph : NavmeshBase, IUpdatableGraph {
/// <summary>Mesh to construct navmesh from</summary>
[JsonMember]
public Mesh sourceMesh;
/// <summary>Offset in world space</summary>
[JsonMember]
public Vector3 offset;
/// <summary>Rotation in degrees</summary>
[JsonMember]
public Vector3 rotation;
/// <summary>Scale of the graph</summary>
[JsonMember]
public float scale = 1;
/// <summary>
/// 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 <see cref="PointOnNavmesh"/> 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.
/// </summary>
[JsonMember]
public bool recalculateNormals = true;
/// <summary>
/// Cached bounding box minimum of <see cref="sourceMesh"/>.
/// 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 <see cref="CalculateTransform"/> method uses it.
/// </summary>
[JsonMember]
Vector3 cachedSourceMeshBoundsMin;
/// <summary>
/// Radius to use when expanding navmesh cuts.
///
/// See: <see cref="NavmeshCut.radiusExpansionMode"/>
/// </summary>
[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;
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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]
/// </summary>
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<GraphUpdateObject> 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<GraphUpdateObject> 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<JobHandle> 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<JobCalculateTriangleConnections.TileNodeConnectionsUnsafe>(tiles.Length, Allocator.Persistent);
var postCutInput = cutPromise.GetValue();
var preCutInput = promise.GetValue();
NativeArray<TileMesh.TileMeshUnsafe> 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);
}
}
}