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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
|
using UnityEngine;
using UnityEditor;
using Pathfinding.Graphs.Navmesh;
using UnityEditorInternal;
namespace Pathfinding {
/// <summary>Editor for the RecastGraph.</summary>
[CustomGraphEditor(typeof(RecastGraph), "Recast Graph")]
public class RecastGraphEditor : GraphEditor {
public static bool tagMaskFoldout;
public static bool meshesUnreadableAtRuntimeFoldout;
ReorderableList tagMaskList;
ReorderableList perLayerModificationsList;
public enum UseTiles {
UseTiles = 0,
DontUseTiles = 1
}
static readonly GUIContent[] DimensionModeLabels = new [] {
new GUIContent("2D"),
new GUIContent("3D"),
};
static Rect SliceColumn (ref Rect rect, float width, float spacing = 0) {
return GUIUtilityx.SliceColumn(ref rect, width, spacing);
}
static void DrawIndentedList (ReorderableList list) {
GUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.IndentedRect(default).xMin);
list.DoLayoutList();
GUILayout.Space(3);
GUILayout.EndHorizontal();
}
static void DrawColliderDetail (RecastGraph.CollectionSettings settings) {
const float LowestApproximationError = 0.5f;
settings.colliderRasterizeDetail = EditorGUILayout.Slider(new GUIContent("Round Collider Detail", "Controls the detail of the generated sphere and capsule meshes. "+
"Higher values may increase navmesh quality slightly, and lower values improve graph scanning performance."), Mathf.Round(10*settings.colliderRasterizeDetail)*0.1f, 0, 1.0f / LowestApproximationError);
}
void DrawCollectionSettings (RecastGraph.CollectionSettings settings, RecastGraph.DimensionMode dimensionMode) {
settings.collectionMode = (RecastGraph.CollectionSettings.FilterMode)EditorGUILayout.EnumPopup("Filter objects by", settings.collectionMode);
if (settings.collectionMode == RecastGraph.CollectionSettings.FilterMode.Layers) {
settings.layerMask = EditorGUILayoutx.LayerMaskField("Layer Mask", settings.layerMask);
} else {
DrawIndentedList(tagMaskList);
}
if (dimensionMode == RecastGraph.DimensionMode.Dimension3D) {
settings.rasterizeTerrain = EditorGUILayout.Toggle(new GUIContent("Rasterize Terrains", "Should a rasterized terrain be included"), settings.rasterizeTerrain);
if (settings.rasterizeTerrain) {
EditorGUI.indentLevel++;
settings.rasterizeTrees = EditorGUILayout.Toggle(new GUIContent("Rasterize Trees", "Rasterize tree colliders on terrains. " +
"If the tree prefab has a collider, that collider will be rasterized. " +
"Otherwise a simple box collider will be used and the script will " +
"try to adjust it to the tree's scale, it might not do a very good job though so " +
"an attached collider is preferable."), settings.rasterizeTrees);
settings.terrainHeightmapDownsamplingFactor = EditorGUILayout.IntField(new GUIContent("Heightmap Downsampling", "How much to downsample the terrain's heightmap. A lower value is better, but slower to scan"), settings.terrainHeightmapDownsamplingFactor);
settings.terrainHeightmapDownsamplingFactor = Mathf.Max(1, settings.terrainHeightmapDownsamplingFactor);
EditorGUI.indentLevel--;
}
settings.rasterizeMeshes = EditorGUILayout.Toggle(new GUIContent("Rasterize Meshes", "Should meshes be rasterized and used for building the navmesh"), settings.rasterizeMeshes);
settings.rasterizeColliders = EditorGUILayout.Toggle(new GUIContent("Rasterize Colliders", "Should colliders be rasterized and used for building the navmesh"), settings.rasterizeColliders);
} else {
// Colliders are always rasterized in 2D mode
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.Toggle(new GUIContent("Rasterize Colliders", "Should colliders be rasterized and used for building the navmesh. In 2D mode, this is always enabled."), true);
EditorGUI.EndDisabledGroup();
}
if (settings.rasterizeMeshes && settings.rasterizeColliders && dimensionMode == RecastGraph.DimensionMode.Dimension3D) {
EditorGUILayout.HelpBox("You are rasterizing both meshes and colliders. This is likely just duplicate work if the colliders and meshes are similar in shape. You can use the RecastMeshObj component" +
" to always include some specific objects regardless of what the above settings are set to.", MessageType.Info);
}
}
public override void OnEnable () {
base.OnEnable();
var graph = target as RecastGraph;
tagMaskList = new ReorderableList(graph.collectionSettings.tagMask, typeof(string), true, true, true, true) {
drawElementCallback = (Rect rect, int index, bool active, bool isFocused) => {
graph.collectionSettings.tagMask[index] = EditorGUI.TagField(rect, graph.collectionSettings.tagMask[index]);
},
drawHeaderCallback = (Rect rect) => {
GUI.Label(rect, "Tag mask");
},
elementHeight = EditorGUIUtility.singleLineHeight,
onAddCallback = (ReorderableList list) => {
graph.collectionSettings.tagMask.Add("Untagged");
}
};
perLayerModificationsList = new ReorderableList(graph.perLayerModifications, typeof(RecastGraph.PerLayerModification), true, true, true, true) {
drawElementCallback = (Rect rect, int index, bool active, bool isFocused) => {
var element = graph.perLayerModifications[index];
var w = rect.width;
var spacing = EditorGUIUtility.standardVerticalSpacing;
element.layer = EditorGUI.LayerField(SliceColumn(ref rect, w * 0.3f, spacing), element.layer);
if (element.mode == RecastMeshObj.Mode.WalkableSurfaceWithTag) {
element.mode = (RecastMeshObj.Mode)EditorGUI.EnumPopup(SliceColumn(ref rect, w * 0.4f, spacing), element.mode);
element.surfaceID = Util.EditorGUILayoutHelper.TagField(rect, GUIContent.none, element.surfaceID, AstarPathEditor.EditTags);
element.surfaceID = Mathf.Clamp(element.surfaceID, 0, GraphNode.MaxTagIndex);
} else if (element.mode == RecastMeshObj.Mode.WalkableSurfaceWithSeam) {
element.mode = (RecastMeshObj.Mode)EditorGUI.EnumPopup(SliceColumn(ref rect, w * 0.4f, spacing), element.mode);
string helpTooltip = "All surfaces on this mesh will be walkable and a " +
"seam will be created between the surfaces on this mesh and the surfaces on other meshes (with a different surface id)";
GUI.Label(SliceColumn(ref rect, 70, spacing), new GUIContent("Surface ID", helpTooltip));
element.surfaceID = Mathf.Max(0, EditorGUI.IntField(rect, new GUIContent("", helpTooltip), element.surfaceID));
} else {
element.mode = (RecastMeshObj.Mode)EditorGUI.EnumPopup(rect, element.mode);
}
graph.perLayerModifications[index] = element;
},
drawHeaderCallback = (Rect rect) => {
GUI.Label(rect, "Per Layer Modifications");
},
elementHeight = EditorGUIUtility.singleLineHeight,
onAddCallback = (ReorderableList list) => {
// Find the first layer that is not already modified
var availableLayers = graph.collectionSettings.layerMask;
foreach (var mod in graph.perLayerModifications) {
availableLayers &= ~(1 << mod.layer);
}
var newMod = RecastGraph.PerLayerModification.Default;
for (int i = 0; i < 32; i++) {
if ((availableLayers & (1 << i)) != 0) {
newMod.layer = i;
break;
}
}
graph.perLayerModifications.Add(newMod);
}
};
}
public override void OnInspectorGUI (NavGraph target) {
var graph = target as RecastGraph;
Header("Shape");
graph.dimensionMode = (RecastGraph.DimensionMode)EditorGUILayout.Popup(new GUIContent("Dimensions", "Should the graph be for a 2D or 3D world?"), (int)graph.dimensionMode, DimensionModeLabels);
if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension2D && Mathf.Abs(Vector3.Dot(Quaternion.Euler(graph.rotation) * Vector3.up, Vector3.forward)) < 0.99999f) {
EditorGUI.indentLevel++;
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
GUILayout.Label(EditorGUIUtility.IconContent("console.warnicon"), GUILayout.ExpandWidth(false));
GUILayout.BeginVertical();
GUILayout.FlexibleSpace();
GUILayout.BeginHorizontal();
GUILayout.Label("Your graph is not in the XY plane");
if (GUILayout.Button("Align")) {
graph.rotation = new Vector3(-90, 0, 0);
graph.forcedBoundsCenter = new Vector3(graph.forcedBoundsCenter.x, graph.forcedBoundsCenter.y, -graph.forcedBoundsSize.y * 0.5f);
}
GUILayout.EndHorizontal();
GUILayout.FlexibleSpace();
GUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
EditorGUI.indentLevel--;
}
// In 3D mode, we use the graph's center as the pivot point, but in 2D mode, we use the center of the base plane of the graph as the pivot point.
// This makes sense because in 2D mode, you typically want to set the base plane's center to Z=0, and you don't care much about the height of the graph.
var pivot = graph.dimensionMode == RecastGraph.DimensionMode.Dimension2D ? new Vector3(0.0f, -0.5f, 0.0f) : Vector3.zero;
var centerOffset = Quaternion.Euler(graph.rotation) * Vector3.Scale(graph.forcedBoundsSize, pivot);
var newCenter = EditorGUILayout.Vector3Field("Center", graph.forcedBoundsCenter + centerOffset);
var newSize = EditorGUILayout.Vector3Field("Size", graph.forcedBoundsSize);
// Make sure the bounding box is not infinitely thin along any axis
newSize = Vector3.Max(newSize, Vector3.one * 0.001f);
// Recalculate the center offset with the new size, and then adjust the center so that the pivot point stays the same if the size changes
centerOffset = Quaternion.Euler(graph.rotation) * Vector3.Scale(newSize, pivot);
graph.forcedBoundsCenter = RoundVector3(newCenter) - centerOffset;
graph.forcedBoundsSize = RoundVector3(newSize);
graph.rotation = RoundVector3(EditorGUILayout.Vector3Field("Rotation", graph.rotation));
long estWidth = Mathf.RoundToInt(Mathf.Ceil(graph.forcedBoundsSize.x / graph.cellSize));
long estDepth = Mathf.RoundToInt(Mathf.Ceil(graph.forcedBoundsSize.z / graph.cellSize));
EditorGUI.BeginDisabledGroup(true);
var estTilesX = (estWidth + graph.editorTileSize - 1) / graph.editorTileSize;
var estTilesZ = (estDepth + graph.editorTileSize - 1) / graph.editorTileSize;
var label = estWidth.ToString() + " x " + estDepth.ToString();
if (graph.useTiles) {
label += " voxels, divided into " + (estTilesX*estTilesZ) + " tiles";
}
EditorGUILayout.LabelField(new GUIContent("Size", "Based on the voxel size and the bounding box"), new GUIContent(label));
EditorGUI.EndDisabledGroup();
// Show a warning if the number of voxels is too large
if (estWidth*estDepth >= 3000*3000) {
GUIStyle helpBox = GUI.skin.FindStyle("HelpBox") ?? GUI.skin.FindStyle("Box");
Color preColor = GUI.color;
if (estWidth*estDepth >= 8192*8192) {
GUI.color = Color.red;
} else {
GUI.color = Color.yellow;
}
GUILayout.Label("Warning: Might take some time to calculate", helpBox);
GUI.color = preColor;
}
if (!editor.isPrefab) {
if (GUILayout.Button(new GUIContent("Snap bounds to scene", "Will snap the bounds of the graph to exactly contain all meshes in the scene that matches the masks."))) {
graph.SnapForceBoundsToScene();
GUI.changed = true;
}
}
Separator();
Header("Input Filtering");
DrawCollectionSettings(graph.collectionSettings, graph.dimensionMode);
Separator();
Header("Rasterization");
graph.cellSize = EditorGUILayout.FloatField(new GUIContent("Voxel Size", "Size of one voxel in world units"), graph.cellSize);
if (graph.cellSize < 0.001F) graph.cellSize = 0.001F;
graph.useTiles = (UseTiles)EditorGUILayout.EnumPopup("Use Tiles", graph.useTiles ? UseTiles.UseTiles : UseTiles.DontUseTiles) == UseTiles.UseTiles;
if (graph.useTiles) {
EditorGUI.indentLevel++;
graph.editorTileSize = EditorGUILayout.IntField(new GUIContent("Tile Size (voxels)", "Size in voxels of a single tile.\n" +
"This is the width of the tile.\n" +
"\n" +
"A large tile size can be faster to initially scan (but beware of out of memory issues if you try with a too large tile size in a large world)\n" +
"smaller tile sizes are (much) faster to update.\n" +
"\n" +
"Different tile sizes can affect the quality of paths. It is often good to split up huge open areas into several tiles for\n" +
"better quality paths, but too small tiles can lead to effects looking like invisible obstacles.\n\n" +
"Typical values are between 64 and 256"), graph.editorTileSize);
graph.editorTileSize = Mathf.Max(10, graph.editorTileSize);
EditorGUI.indentLevel--;
}
if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension3D) {
graph.walkableHeight = EditorGUILayout.DelayedFloatField(new GUIContent("Character Height", "Minimum distance to the roof for an area to be walkable"), graph.walkableHeight);
graph.walkableHeight = Mathf.Max(graph.walkableHeight, 0);
graph.characterRadius = EditorGUILayout.FloatField(new GUIContent("Character Radius", "Radius of the character. It's good to add some margin.\nIn world units."), graph.characterRadius);
graph.characterRadius = Mathf.Max(graph.characterRadius, 0);
if (graph.characterRadius < graph.cellSize * 2) {
EditorGUILayout.HelpBox("For best navmesh quality, it is recommended to keep the character radius at least 2 times as large as the voxel size. Smaller voxels will give you higher quality navmeshes, but it will take more time to scan the graph.", MessageType.Warning);
}
graph.walkableClimb = EditorGUILayout.FloatField(new GUIContent("Max Step Height", "How high can the character step"), graph.walkableClimb);
// A walkableClimb higher than this can cause issues when generating the navmesh since then it can in some cases
// Both be valid for a character to walk under an obstacle and climb up on top of it (and that cannot be handled with a navmesh without links)
if (graph.walkableClimb >= graph.walkableHeight) {
graph.walkableClimb = graph.walkableHeight;
EditorGUILayout.HelpBox("Max Step Height should be less than Character Height. Clamping to " + graph.walkableHeight+".", MessageType.Warning);
} else if (graph.walkableClimb < 0) {
graph.walkableClimb = 0;
}
}
if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension3D) {
graph.maxSlope = EditorGUILayout.Slider(new GUIContent("Max Slope", "Approximate maximum slope"), graph.maxSlope, 0F, 90F);
}
graph.maxEdgeLength = EditorGUILayout.FloatField(new GUIContent("Max Border Edge Length", "Maximum length of one border edge in the completed navmesh before it is split. A lower value can often yield better quality graphs, but don't use so low values so that you get a lot of thin triangles."), graph.maxEdgeLength);
graph.maxEdgeLength = graph.maxEdgeLength < graph.cellSize ? graph.cellSize : graph.maxEdgeLength;
// This is actually a float, but to make things easier for the user, we only allow picking integers. Small changes don't matter that much anyway.
graph.contourMaxError = EditorGUILayout.IntSlider(new GUIContent("Edge Simplification", "Simplifies the edges of the navmesh such that it is no more than this number of voxels away from the true value.\nIn voxels."), Mathf.RoundToInt(graph.contourMaxError), 0, 5);
graph.minRegionSize = EditorGUILayout.FloatField(new GUIContent("Min Region Size", "Small regions will be removed. In voxels"), graph.minRegionSize);
if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension2D) {
graph.backgroundTraversability = (RecastGraph.BackgroundTraversability)EditorGUILayout.EnumPopup("Background traversability", graph.backgroundTraversability);
}
if (graph.collectionSettings.rasterizeColliders || (graph.dimensionMode == RecastGraph.DimensionMode.Dimension3D && graph.collectionSettings.rasterizeTerrain && graph.collectionSettings.rasterizeTrees)) {
DrawColliderDetail(graph.collectionSettings);
}
DrawIndentedList(perLayerModificationsList);
int seenLayers = 0;
for (int i = 0; i < graph.perLayerModifications.Count; i++) {
if ((seenLayers & 1 << graph.perLayerModifications[i].layer) != 0) {
EditorGUILayout.HelpBox("Duplicate layers. Each layer can only be modified by a single rule.", MessageType.Error);
break;
}
seenLayers |= 1 << graph.perLayerModifications[i].layer;
}
var countStillUnreadable = 0;
for (int i = 0; graph.meshesUnreadableAtRuntime != null && i < graph.meshesUnreadableAtRuntime.Count; i++) {
countStillUnreadable += graph.meshesUnreadableAtRuntime[i].Item2.isReadable ? 0 : 1;
}
if (countStillUnreadable > 0) {
GUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.IndentedRect(new Rect(0, 0, 0, 0)).xMin);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.BeginHorizontal();
GUILayout.BeginVertical();
GUILayout.FlexibleSpace();
meshesUnreadableAtRuntimeFoldout = GUILayout.Toggle(meshesUnreadableAtRuntimeFoldout, "", EditorStyles.foldout, GUILayout.Width(10));
GUILayout.FlexibleSpace();
GUILayout.EndVertical();
GUILayout.Label(EditorGUIUtility.IconContent("console.warnicon"), GUILayout.ExpandWidth(false));
GUILayout.Label(graph.meshesUnreadableAtRuntime.Count + " " + (graph.meshesUnreadableAtRuntime.Count > 1 ? "meshes" : "mesh") + " will be ignored if scanned in a standalone build, because they are marked as not readable." +
"If you plan to scan the graph in a standalone build, all included meshes must be marked as read/write in their import settings.", EditorStyles.wordWrappedMiniLabel);
// EditorGUI.DrawTextureTransparent() EditorGUIUtility.IconContent("console.warnicon")
GUILayout.EndHorizontal();
if (meshesUnreadableAtRuntimeFoldout) {
EditorGUILayout.Separator();
for (int i = 0; i < graph.meshesUnreadableAtRuntime.Count; i++) {
var(source, mesh) = graph.meshesUnreadableAtRuntime[i];
if (!mesh.isReadable) {
GUILayout.BeginHorizontal();
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.ObjectField(source, typeof(Mesh), true);
EditorGUILayout.ObjectField(mesh, typeof(Mesh), false);
EditorGUI.EndDisabledGroup();
if (GUILayout.Button("Make readable")) {
var importer = ModelImporter.GetAtPath(AssetDatabase.GetAssetPath(mesh)) as ModelImporter;
if (importer != null) {
importer.isReadable = true;
importer.SaveAndReimport();
}
}
GUILayout.EndHorizontal();
}
}
}
EditorGUILayout.EndVertical();
GUILayout.EndHorizontal();
}
Separator();
Header("Runtime Settings");
graph.enableNavmeshCutting = EditorGUILayout.Toggle(new GUIContent("Affected by Navmesh Cuts", "Makes this graph affected by NavmeshCut and NavmeshAdd components. See the documentation for more info."), graph.enableNavmeshCutting);
Separator();
Header("Debug");
GUILayout.BeginHorizontal();
GUILayout.Space(18);
graph.showMeshSurface = GUILayout.Toggle(graph.showMeshSurface, new GUIContent("Show surface", "Toggles gizmos for drawing the surface of the mesh"), EditorStyles.miniButtonLeft);
graph.showMeshOutline = GUILayout.Toggle(graph.showMeshOutline, new GUIContent("Show outline", "Toggles gizmos for drawing an outline of the nodes"), EditorStyles.miniButtonMid);
graph.showNodeConnections = GUILayout.Toggle(graph.showNodeConnections, new GUIContent("Show connections", "Toggles gizmos for drawing node connections"), EditorStyles.miniButtonRight);
GUILayout.EndHorizontal();
Separator();
Header("Advanced");
graph.relevantGraphSurfaceMode = (RecastGraph.RelevantGraphSurfaceMode)EditorGUILayout.EnumPopup(new GUIContent("Relevant Graph Surface Mode",
"Require every region to have a RelevantGraphSurface component inside it.\n" +
"A RelevantGraphSurface component placed in the scene specifies that\n" +
"the navmesh region it is inside should be included in the navmesh.\n\n" +
"If this is set to OnlyForCompletelyInsideTile\n" +
"a navmesh region is included in the navmesh if it\n" +
"has a RelevantGraphSurface inside it, or if it\n" +
"is adjacent to a tile border. This can leave some small regions\n" +
"which you didn't want to have included because they are adjacent\n" +
"to tile borders, but it removes the need to place a component\n" +
"in every single tile, which can be tedious (see below).\n\n" +
"If this is set to RequireForAll\n" +
"a navmesh region is included only if it has a RelevantGraphSurface\n" +
"inside it. Note that even though the navmesh\n" +
"looks continous between tiles, the tiles are computed individually\n" +
"and therefore you need a RelevantGraphSurface component for each\n" +
"region and for each tile."),
graph.relevantGraphSurfaceMode);
#pragma warning disable 618
if (graph.nearestSearchOnlyXZ) {
graph.nearestSearchOnlyXZ = EditorGUILayout.Toggle(new GUIContent("Nearest node queries in XZ space",
"Recomended for single-layered environments.\nFaster but can be inacurate esp. in multilayered contexts."), graph.nearestSearchOnlyXZ);
EditorGUILayout.HelpBox("The global toggle for node queries in XZ space has been deprecated. Use the NNConstraint settings instead.", MessageType.Warning);
}
#pragma warning restore 618
if (GUILayout.Button("Export to .obj file")) {
editor.RunTask(() => ExportToFile(graph));
}
}
static readonly Vector3[] handlePoints = new [] { new Vector3(-1, 0, 0), new Vector3(1, 0, 0), new Vector3(0, 0, -1), new Vector3(0, 0, 1), new Vector3(0, 1, 0), new Vector3(0, -1, 0) };
public override void OnSceneGUI (NavGraph target) {
var graph = target as RecastGraph;
Handles.matrix = Matrix4x4.identity;
Handles.color = AstarColor.BoundsHandles;
Handles.CapFunction cap = Handles.CylinderHandleCap;
var center = graph.forcedBoundsCenter;
Matrix4x4 matrix = Matrix4x4.TRS(center, Quaternion.Euler(graph.rotation), graph.forcedBoundsSize * 0.5f);
if (Tools.current == Tool.Scale) {
const float HandleScale = 0.1f;
Vector3 mn = Vector3.zero;
Vector3 mx = Vector3.zero;
EditorGUI.BeginChangeCheck();
for (int i = 0; i < handlePoints.Length; i++) {
var ps = matrix.MultiplyPoint3x4(handlePoints[i]);
Vector3 p = matrix.inverse.MultiplyPoint3x4(Handles.Slider(ps, ps - center, HandleScale*HandleUtility.GetHandleSize(ps), cap, 0));
if (i == 0) {
mn = mx = p;
} else {
mn = Vector3.Min(mn, p);
mx = Vector3.Max(mx, p);
}
}
if (EditorGUI.EndChangeCheck()) {
graph.forcedBoundsCenter = matrix.MultiplyPoint3x4((mn + mx) * 0.5f);
graph.forcedBoundsSize = Vector3.Scale(graph.forcedBoundsSize, (mx - mn) * 0.5f);
}
} else if (Tools.current == Tool.Move) {
EditorGUI.BeginChangeCheck();
center = Handles.PositionHandle(center, Tools.pivotRotation == PivotRotation.Global ? Quaternion.identity : Quaternion.Euler(graph.rotation));
if (EditorGUI.EndChangeCheck() && Tools.viewTool != ViewTool.Orbit) {
graph.forcedBoundsCenter = center;
}
} else if (Tools.current == Tool.Rotate) {
EditorGUI.BeginChangeCheck();
var rot = Handles.RotationHandle(Quaternion.Euler(graph.rotation), graph.forcedBoundsCenter);
if (EditorGUI.EndChangeCheck() && Tools.viewTool != ViewTool.Orbit) {
graph.rotation = rot.eulerAngles;
}
}
}
/// <summary>Exports the INavmesh graph to a .obj file</summary>
public static void ExportToFile (NavmeshBase target) {
if (target == null) return;
NavmeshTile[] tiles = target.GetTiles();
if (tiles == null) {
if (EditorUtility.DisplayDialog("Scan graph before exporting?", "The graph does not contain any mesh data. Do you want to scan it?", "Ok", "Cancel")) {
AstarPathEditor.MenuScan();
tiles = target.GetTiles();
if (tiles == null) return;
} else {
return;
}
}
string path = EditorUtility.SaveFilePanel("Export .obj", "", "navmesh.obj", "obj");
if (path == "") return;
//Generate .obj
var sb = new System.Text.StringBuilder();
string name = System.IO.Path.GetFileNameWithoutExtension(path);
sb.Append("g ").Append(name).AppendLine();
//Vertices start from 1
int vCount = 1;
//Define single texture coordinate to zero
sb.Append("vt 0 0\n");
for (int t = 0; t < tiles.Length; t++) {
NavmeshTile tile = tiles[t];
if (tile == null) continue;
var vertices = tile.verts;
//Write vertices
for (int i = 0; i < vertices.Length; i++) {
var v = (Vector3)vertices[i];
sb.Append(string.Format("v {0} {1} {2}\n", -v.x, v.y, v.z));
}
//Write triangles
TriangleMeshNode[] nodes = tile.nodes;
for (int i = 0; i < nodes.Length; i++) {
TriangleMeshNode node = nodes[i];
if (node == null) {
Debug.LogError("Node was null or no TriangleMeshNode. Critical error. Graph type " + target.GetType().Name);
return;
}
if (node.GetVertexArrayIndex(0) < 0 || node.GetVertexArrayIndex(0) >= vertices.Length) throw new System.Exception("ERR");
sb.Append(string.Format("f {0}/1 {1}/1 {2}/1\n", (node.GetVertexArrayIndex(0) + vCount), (node.GetVertexArrayIndex(1) + vCount), (node.GetVertexArrayIndex(2) + vCount)));
}
vCount += vertices.Length;
}
string obj = sb.ToString();
using (var sw = new System.IO.StreamWriter(path)) {
sw.Write(obj);
}
}
}
}
|