1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
|
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;
public interface INavmesh {
void GetNodes(System.Action<GraphNode> del);
}
/// <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]
/// [Open online documentation to see images]
///
/// See: Pathfinding.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;
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 meshToGraphSpace = Matrix4x4.TRS(-sourceMesh.bounds.min * graph.scale, Quaternion.identity, Vector3.one * graph.scale);
var promise = JobBuildTileMeshFromVertices.Schedule(vertices, indices, meshToGraphSpace, graph.RecalculateNormals);
forcedBoundsSize = sourceMesh.bounds.size * graph.scale;
tileRect = new IntRect(0, 0, 0, 0);
tiles = new NavmeshTile[tileRect.Area];
var tilesGCHandle = System.Runtime.InteropServices.GCHandle.Alloc(tiles);
var tileWorldSize = new Vector2(forcedBoundsSize.x, forcedBoundsSize.z);
var tileNodeConnections = new NativeArray<JobCalculateTriangleConnections.TileNodeConnectionsUnsafe>(tiles.Length, Allocator.Persistent);
var calculateConnectionsJob = new JobCalculateTriangleConnections {
tileMeshes = promise.GetValue().tiles,
nodeConnections = tileNodeConnections,
}.Schedule(promise.handle);
var createTilesJob = new JobCreateTiles {
tileMeshes = promise.GetValue().tiles,
tiles = tilesGCHandle,
tileRect = tileRect,
graphTileCount = new Int2(tileRect.Width, tileRect.Height),
graphIndex = graph.graphIndex,
initialPenalty = graph.initialPenalty,
recalculateNormals = graph.recalculateNormals,
graphToWorldSpace = transform.matrix,
tileWorldSize = tileWorldSize,
}.Schedule(promise.handle);
var applyConnectionsJob = new JobWriteNodeConnections {
tiles = tilesGCHandle,
nodeConnections = tileNodeConnections,
}.Schedule(JobHandle.CombineDependencies(createTilesJob, calculateConnectionsJob));
yield return applyConnectionsJob;
var navmeshOutput = promise.Complete();
// This has already been used in the createTilesJob
navmeshOutput.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();
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);
// Signal that tiles have been recalculated to the navmesh cutting system.
graph.navmeshUpdateData.OnRecalculatedTiles(tiles);
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);
}
}
}
|