-
Notifications
You must be signed in to change notification settings - Fork 5
/
NavMeshFromColliders.cs
356 lines (324 loc) · 13.5 KB
/
NavMeshFromColliders.cs
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
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
/// <summary>
/// This class represents a tool that helps to prepare the scene for NavMesh baking.
/// It will generate clone objects from colliders already in the scene.
/// These clone objects will contain the renderer generated from the collider shape.
/// Unity NavMesh baking works upon Renderers, so colliders are ignored.
/// This tool fulfil the need of using only colliders for NavMesh baking.
/// Read the instructions for more information.
/// </summary>
public class NavMeshFromCollidersWindow : EditorWindow
{
/// <summary>
/// Holds the renderers disabled when setting up the bake mode.
/// This will be used to go back to the normal mode after baking.
/// </summary>
private Stack<Renderer> disableds;
/// <summary>
/// Layer mask field, the tool will generate clone objects only within this layer mask.
/// </summary>
private LayerMask layerMask = 0;
/// <summary>
/// The object that will hold all the clones/fakes as childs.
/// Bake mode.
/// </summary>
private GameObject bakeModeRoot = null;
/// <summary>
/// Holds the diffuse material reference.
/// This is used to generate the MeshCollider's clone/fake.
/// </summary>
private Material diffuseDefaultMaterial = null;
/// <summary>
/// Mode to change the GUI draw. Bake Mode vs Normal Mode
/// </summary>
private bool bakeMode = false;
[MenuItem("Window/Open NavMeshFromCollidersWindow")]
public static void ShowWindow()
{
GetWindow<NavMeshFromCollidersWindow>();
}
private void Awake()
{
layerMask.value = 1;
}
private void OnGUI()
{
var oldValue = layerMask.value;
layerMask = CustomEditorExtensions.LayerMaskField("Layer Mask: ", layerMask);
if (layerMask.value != oldValue)
{
BackToNormalMode();
}
GUILayout.Box("", new GUILayoutOption[] { GUILayout.ExpandWidth(true), GUILayout.Height(1.0f) });
EditorGUILayout.LabelField("Action", EditorStyles.boldLabel);
if (!bakeMode)
{
// NORMAL MODE GUI
if (GUILayout.Button("Prepare for NavMesh Bake"))
{
bakeMode = true;
SetupNavMeshBakeMode();
}
}
else
{
// BAKE MODE GUI
if (GUILayout.Button("Back to Normal"))
{
BackToNormalMode();
}
}
// draw a horizontal line
GUILayout.Box("", new GUILayoutOption[] { GUILayout.ExpandWidth(true), GUILayout.Height(1) });
}
/// <summary>
/// Setup the Bake Mode.
/// Hide all the renderers in the scene, and generate all the clone/fake objects.
/// Keep the original object's static flags and navmesh area setup, and configure it on the clone/fake.
/// </summary>
private void SetupNavMeshBakeMode()
{
// get the real renderers before generate the new renderers(the fakes)
Renderer[] renderers = FindObjectsOfType<Renderer>();
// create the fake object's root
bakeModeRoot = new GameObject("Bake Mode Root");
bakeModeRoot.transform.parent = null;
// generate the fake objects based on the existent colliders
Collider[] colliders = FindObjectsOfType<Collider>();
for (int i = 0; i < colliders.Length; i++)
{
int theTrueMaskSupose = layerMask.value | 1 << colliders[i].gameObject.layer;
if (layerMask.value == theTrueMaskSupose && !colliders[i].isTrigger)
{
// generate and store the fake object
GameObject fakeObject = GenerateRendererObject(colliders[i]);
// in some cases, the fakeObject can be null
// for example: it has a MeshCollider but doesnt have a MeshRenderer
if (fakeObject != null)
{
// setup in the root(all fake objects must be child of this, to make it easy to setup and delete later)
fakeObject.transform.parent = bakeModeRoot.transform;
// setup layer
fakeObject.layer = colliders[i].gameObject.layer;
// setup flags
StaticEditorFlags flags = GameObjectUtility.GetStaticEditorFlags(colliders[i].gameObject);
GameObjectUtility.SetStaticEditorFlags(fakeObject, flags);
// setup nav mesh area
GameObjectUtility.SetNavMeshArea(fakeObject, GameObjectUtility.GetNavMeshArea(colliders[i].gameObject));
}
}
}
// disable renderers
disableds = new Stack<Renderer>();
for (int i = 0; i < renderers.Length; i++)
{
if (renderers[i].enabled)
{
renderers[i].enabled = false;
// keep the disableds reference in a stack, so we can enable it later
disableds.Push(renderers[i]);
}
}
}
/// <summary>
/// Go back to normal mode. (From BakeMode to NormalMode)
/// Enable all the renderers that were disabled by the BakeMode setup.
/// Destroy the temp/fake/clone objects.
/// Go back to the scene's normal setup.
/// </summary>
private void BackToNormalMode()
{
bakeMode = false;
// enable objects
while (disableds != null && disableds.Count > 0)
{
disableds.Pop().enabled = true;
}
// destroy the temp/fake objects
if (bakeModeRoot != null)
{
DestroyImmediate(bakeModeRoot);
bakeModeRoot = null;
}
}
/// <summary>
/// Generate an object with a renderer based on a collider.
/// The method accepts: BoxCollider, CapsuleCollider, SphereCollider and MeshCollider.
/// </summary>
private GameObject GenerateRendererObject(Collider theCollider)
{
GameObject fakeObject = null;
const string DEFAULT_FAKEOBJECT_NAME = "Object";
if (theCollider.GetType() == typeof(BoxCollider))
{
BoxCollider baseCollider = theCollider as BoxCollider;
fakeObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
DestroyImmediate(fakeObject.GetComponent<Collider>());
fakeObject.name = DEFAULT_FAKEOBJECT_NAME;
fakeObject.transform.rotation = theCollider.gameObject.transform.rotation;
fakeObject.transform.parent = theCollider.gameObject.transform;
fakeObject.transform.localPosition = baseCollider.center;
fakeObject.transform.parent = null;
fakeObject.transform.localScale = theCollider.gameObject.transform.lossyScale;
Vector3 tempScale = fakeObject.transform.localScale;
tempScale.x *= baseCollider.size.x;
tempScale.y *= baseCollider.size.y;
tempScale.z *= baseCollider.size.z;
fakeObject.transform.localScale = tempScale;
}
else if (theCollider.GetType() == typeof(CapsuleCollider))
{
CapsuleCollider baseCollider = theCollider as CapsuleCollider;
fakeObject = GameObject.CreatePrimitive(PrimitiveType.Capsule);
DestroyImmediate(fakeObject.GetComponent<Collider>());
fakeObject.name = DEFAULT_FAKEOBJECT_NAME;
fakeObject.transform.rotation = theCollider.gameObject.transform.rotation;
fakeObject.transform.parent = theCollider.gameObject.transform;
fakeObject.transform.localPosition = baseCollider.center;
fakeObject.transform.parent = null;
fakeObject.transform.localScale = theCollider.gameObject.transform.lossyScale;
const float DEFAULT_CAPSULE_RADIUS = 0.5f;
const float DEFAULT_CAPSULE_HEIGHT = 2.0f;
Vector3 tempScale = fakeObject.transform.localScale;
// max(x,z) code
if (Mathf.Abs(tempScale.x) > Mathf.Abs(tempScale.z))
{
tempScale.z = tempScale.x;
}
else
{
tempScale.x = tempScale.z;
}
tempScale.x *= baseCollider.radius / DEFAULT_CAPSULE_RADIUS;
tempScale.z *= baseCollider.radius / DEFAULT_CAPSULE_RADIUS;
tempScale.y *= baseCollider.height / DEFAULT_CAPSULE_HEIGHT;
fakeObject.transform.localScale = tempScale;
}
else if (theCollider.GetType() == typeof(SphereCollider))
{
SphereCollider baseCollider = theCollider as SphereCollider;
fakeObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
DestroyImmediate(fakeObject.GetComponent<Collider>());
fakeObject.name = DEFAULT_FAKEOBJECT_NAME;
fakeObject.transform.rotation = theCollider.gameObject.transform.rotation;
fakeObject.transform.parent = theCollider.gameObject.transform;
fakeObject.transform.localPosition = baseCollider.center;
fakeObject.transform.parent = null;
fakeObject.transform.localScale = theCollider.gameObject.transform.lossyScale;
const float DEFAULT_SPHERE_RADIUS = 0.5f;
Vector3 tempScale = fakeObject.transform.localScale;
// max(x,y,z) code
if (Mathf.Abs(tempScale.x) > Mathf.Abs(tempScale.y))
{
tempScale.y = tempScale.x;
}
else
{
tempScale.x = tempScale.y;
}
if (Mathf.Abs(tempScale.x) > Mathf.Abs(tempScale.z))
{
tempScale.z = tempScale.y = tempScale.x;
}
else
{
tempScale.x = tempScale.y = tempScale.z;
}
tempScale.x *= baseCollider.radius / DEFAULT_SPHERE_RADIUS;
tempScale.y *= baseCollider.radius / DEFAULT_SPHERE_RADIUS;
tempScale.z *= baseCollider.radius / DEFAULT_SPHERE_RADIUS;
fakeObject.transform.localScale = tempScale;
}
else if (theCollider.GetType() == typeof(MeshCollider))
{
MeshCollider baseCollider = theCollider as MeshCollider;
// to generate the MeshCollider object, the original MUST HAVE a MeshRenderer
if (baseCollider.GetComponent<MeshRenderer>() != null)
{
int materialsCount = baseCollider.GetComponent<MeshRenderer>().sharedMaterials.Length;
fakeObject = new GameObject(DEFAULT_FAKEOBJECT_NAME);
fakeObject.transform.SetPositionAndRotation(
theCollider.gameObject.transform.position,
theCollider.gameObject.transform.rotation);
fakeObject.AddComponent<MeshFilter>().sharedMesh = baseCollider.sharedMesh;
if (diffuseDefaultMaterial == null)
{
SetDefaultMaterialReference();
}
Material[] mats = new Material[materialsCount];
for (int i = 0; i < mats.Length; i++)
{
mats[i] = diffuseDefaultMaterial;
}
fakeObject.AddComponent<MeshRenderer>().materials = mats;
fakeObject.transform.localScale = theCollider.gameObject.transform.lossyScale;
}
}
return fakeObject;
}
/// <summary>
/// Called by the GenerateRendererObject method.
/// This is a hack to get the diffuse material and store it.
/// This material will be used to generate objects from a MeshCollider.
/// </summary>
private void SetDefaultMaterialReference()
{
GameObject tempPrimitive = GameObject.CreatePrimitive(PrimitiveType.Cube);
diffuseDefaultMaterial = tempPrimitive.GetComponent<MeshRenderer>().sharedMaterial;
DestroyImmediate(tempPrimitive);
}
private void OnDestroy()
{
BackToNormalMode();
}
}
/// <summary>
/// Class that holds Unity Editor custom methods and extension methods.
/// This class are intended to write facilitators and functionallity that the built in tools dont provide.
/// </summary>
public static class CustomEditorExtensions
{
/// <summary>
/// Creates a LayerMask field in an editor(EditorWindow, Editor).
/// Unity is missing it, so there is the need to implement this handmade.
/// Use example:
/// private LayerMask layerMask = 0; // this has global scope
///
/// layerMask = CustomEditorExtensions.LayerMaskField("Layer Mask: ", layerMask);
/// </summary>
public static LayerMask LayerMaskField(string label, LayerMask layerMask)
{
List<string> layers = new();
List<int> layerNumbers = new();
for (int i = 0; i < 32; i++)
{
string layerName = LayerMask.LayerToName(i);
if (layerName != "")
{
layers.Add(layerName);
layerNumbers.Add(i);
}
}
int maskWithoutEmpty = 0;
for (int i = 0; i < layerNumbers.Count; i++)
{
if (((1 << layerNumbers[i]) & layerMask.value) > 0)
{
maskWithoutEmpty |= (1 << i);
}
}
maskWithoutEmpty = EditorGUILayout.MaskField(label, maskWithoutEmpty, layers.ToArray());
int mask = 0;
for (int i = 0; i < layerNumbers.Count; i++)
{
if ((maskWithoutEmpty & (1 << i)) > 0)
{
mask |= (1 << layerNumbers[i]);
}
}
layerMask.value = mask;
return layerMask;
}
}