using UnityEngine; using System.Collections.Generic; #if UNITY_5_5_OR_NEWER using UnityEngine.Profiling; #endif namespace Pathfinding.Util { /// /// Helper for drawing Gizmos in a performant way. /// This is a replacement for the Unity Gizmos class as that is not very performant /// when drawing very large amounts of geometry (for example a large grid graph). /// These gizmos can be persistent, so if the data does not change, the gizmos /// do not need to be updated. /// /// How to use /// - Create a Hasher object and hash whatever data you will be using to draw the gizmos /// Could be for example the positions of the vertices or something. Just as long as /// if the gizmos should change, then the hash changes as well. /// - Check if a cached mesh exists for that hash /// - If not, then create a Builder object and call the drawing methods until you are done /// and then call Finalize with a reference to a gizmos class and the hash you calculated before. /// - Call gizmos.Draw with the hash. /// - When you are done with drawing gizmos for this frame, call gizmos.FinalizeDraw /// /// /// var a = Vector3.zero; /// var b = Vector3.one; /// var color = Color.red; /// var hasher = new RetainedGizmos.Hasher(); /// hasher.AddHash(a.GetHashCode()); /// hasher.AddHash(b.GetHashCode()); /// hasher.AddHash(color.GetHashCode()); /// if (!gizmos.Draw(hasher)) { /// using (var helper = gizmos.GetGizmoHelper(active, hasher)) { /// builder.DrawLine(a, b, color); /// builder.Finalize(gizmos, hasher); /// } /// } /// /// public class RetainedGizmos { /// Combines hashes into a single hash value public struct Hasher { ulong hash; bool includePathSearchInfo; bool includeAreaInfo; PathHandler debugData; public Hasher (AstarPath active) { hash = 0; this.debugData = active.debugPathData; includePathSearchInfo = debugData != null && (active.debugMode == GraphDebugMode.F || active.debugMode == GraphDebugMode.G || active.debugMode == GraphDebugMode.H || active.showSearchTree); includeAreaInfo = active.debugMode == GraphDebugMode.Areas; AddHash((int)active.debugMode); AddHash(active.debugFloor.GetHashCode()); AddHash(active.debugRoof.GetHashCode()); AddHash(AstarColor.ColorHash()); } public void AddHash (int hash) { this.hash = (1572869UL * this.hash) ^ (ulong)hash; } public void HashNode (GraphNode node) { AddHash(node.GetGizmoHashCode()); if (includeAreaInfo) AddHash((int)node.Area); if (includePathSearchInfo) { var pathNode = debugData.GetPathNode(node.NodeIndex); AddHash((int)pathNode.pathID); AddHash(pathNode.pathID == debugData.PathID ? 1 : 0); AddHash((int) pathNode.F); } } public ulong Hash { get { return hash; } } } /// Helper for drawing gizmos public class Builder : IAstarPooledObject { List lines = new List(); List lineColors = new List(); List meshes = new List(); public void DrawMesh (RetainedGizmos gizmos, Vector3[] vertices, List triangles, Color[] colors) { var mesh = gizmos.GetMesh(); // Set all data on the mesh mesh.vertices = vertices; mesh.SetTriangles(triangles, 0); mesh.colors = colors; // Upload all data and mark the mesh as unreadable mesh.UploadMeshData(true); meshes.Add(mesh); } /// Draws a wire cube after being transformed the specified transformation public void DrawWireCube (GraphTransform tr, Bounds bounds, Color color) { var min = bounds.min; var max = bounds.max; DrawLine(tr.Transform(new Vector3(min.x, min.y, min.z)), tr.Transform(new Vector3(max.x, min.y, min.z)), color); DrawLine(tr.Transform(new Vector3(max.x, min.y, min.z)), tr.Transform(new Vector3(max.x, min.y, max.z)), color); DrawLine(tr.Transform(new Vector3(max.x, min.y, max.z)), tr.Transform(new Vector3(min.x, min.y, max.z)), color); DrawLine(tr.Transform(new Vector3(min.x, min.y, max.z)), tr.Transform(new Vector3(min.x, min.y, min.z)), color); DrawLine(tr.Transform(new Vector3(min.x, max.y, min.z)), tr.Transform(new Vector3(max.x, max.y, min.z)), color); DrawLine(tr.Transform(new Vector3(max.x, max.y, min.z)), tr.Transform(new Vector3(max.x, max.y, max.z)), color); DrawLine(tr.Transform(new Vector3(max.x, max.y, max.z)), tr.Transform(new Vector3(min.x, max.y, max.z)), color); DrawLine(tr.Transform(new Vector3(min.x, max.y, max.z)), tr.Transform(new Vector3(min.x, max.y, min.z)), color); DrawLine(tr.Transform(new Vector3(min.x, min.y, min.z)), tr.Transform(new Vector3(min.x, max.y, min.z)), color); DrawLine(tr.Transform(new Vector3(max.x, min.y, min.z)), tr.Transform(new Vector3(max.x, max.y, min.z)), color); DrawLine(tr.Transform(new Vector3(max.x, min.y, max.z)), tr.Transform(new Vector3(max.x, max.y, max.z)), color); DrawLine(tr.Transform(new Vector3(min.x, min.y, max.z)), tr.Transform(new Vector3(min.x, max.y, max.z)), color); } public void DrawLine (Vector3 start, Vector3 end, Color color) { lines.Add(start); lines.Add(end); var col32 = (Color32)color; lineColors.Add(col32); lineColors.Add(col32); } public void Submit (RetainedGizmos gizmos, Hasher hasher) { SubmitLines(gizmos, hasher.Hash); SubmitMeshes(gizmos, hasher.Hash); } void SubmitMeshes (RetainedGizmos gizmos, ulong hash) { for (int i = 0; i < meshes.Count; i++) { gizmos.meshes.Add(new MeshWithHash { hash = hash, mesh = meshes[i], lines = false }); gizmos.existingHashes.Add(hash); } } void SubmitLines (RetainedGizmos gizmos, ulong hash) { // Unity only supports 65535 vertices per mesh. 65532 used because MaxLineEndPointsPerBatch needs to be even. const int MaxLineEndPointsPerBatch = 65532/2; int batches = (lines.Count + MaxLineEndPointsPerBatch - 1)/MaxLineEndPointsPerBatch; for (int batch = 0; batch < batches; batch++) { int startIndex = MaxLineEndPointsPerBatch * batch; int endIndex = Mathf.Min(startIndex + MaxLineEndPointsPerBatch, lines.Count); int lineEndPointCount = endIndex - startIndex; UnityEngine.Assertions.Assert.IsTrue(lineEndPointCount % 2 == 0); // Use pooled lists to avoid excessive allocations var vertices = ListPool.Claim(lineEndPointCount*2); var colors = ListPool.Claim(lineEndPointCount*2); var normals = ListPool.Claim(lineEndPointCount*2); var uv = ListPool.Claim(lineEndPointCount*2); var tris = ListPool.Claim(lineEndPointCount*3); // Loop through each endpoint of the lines // and add 2 vertices for each for (int j = startIndex; j < endIndex; j++) { var vertex = (Vector3)lines[j]; vertices.Add(vertex); vertices.Add(vertex); var color = (Color32)lineColors[j]; colors.Add(color); colors.Add(color); uv.Add(new Vector2(0, 0)); uv.Add(new Vector2(1, 0)); } // Loop through each line and add // one normal for each vertex for (int j = startIndex; j < endIndex; j += 2) { var lineDir = (Vector3)(lines[j+1] - lines[j]); // Store the line direction in the normals. // A line consists of 4 vertices. The line direction will be used to // offset the vertices to create a line with a fixed pixel thickness normals.Add(lineDir); normals.Add(lineDir); normals.Add(lineDir); normals.Add(lineDir); } // Setup triangle indices // A triangle consists of 3 indices // A line (4 vertices) consists of 2 triangles, so 6 triangle indices for (int j = 0, v = 0; j < lineEndPointCount*3; j += 6, v += 4) { // First triangle tris.Add(v+0); tris.Add(v+1); tris.Add(v+2); // Second triangle tris.Add(v+1); tris.Add(v+3); tris.Add(v+2); } var mesh = gizmos.GetMesh(); // Set all data on the mesh mesh.SetVertices(vertices); mesh.SetTriangles(tris, 0); mesh.SetColors(colors); mesh.SetNormals(normals); mesh.SetUVs(0, uv); // Upload all data and mark the mesh as unreadable mesh.UploadMeshData(true); // Release the lists back to the pool ListPool.Release(ref vertices); ListPool.Release(ref colors); ListPool.Release(ref normals); ListPool.Release(ref uv); ListPool.Release(ref tris); gizmos.meshes.Add(new MeshWithHash { hash = hash, mesh = mesh, lines = true }); gizmos.existingHashes.Add(hash); } } void IAstarPooledObject.OnEnterPool () { lines.Clear(); lineColors.Clear(); meshes.Clear(); } } struct MeshWithHash { public ulong hash; public Mesh mesh; public bool lines; } List meshes = new List(); HashSet usedHashes = new HashSet(); HashSet existingHashes = new HashSet(); Stack cachedMeshes = new Stack(); public GraphGizmoHelper GetSingleFrameGizmoHelper (AstarPath active) { var uniqHash = new RetainedGizmos.Hasher(); uniqHash.AddHash(Time.realtimeSinceStartup.GetHashCode()); Draw(uniqHash); return GetGizmoHelper(active, uniqHash); } public GraphGizmoHelper GetGizmoHelper (AstarPath active, Hasher hasher) { var helper = ObjectPool.Claim(); helper.Init(active, hasher, this); return helper; } void PoolMesh (Mesh mesh) { mesh.Clear(); cachedMeshes.Push(mesh); } Mesh GetMesh () { if (cachedMeshes.Count > 0) { return cachedMeshes.Pop(); } else { return new Mesh { hideFlags = HideFlags.DontSave }; } } /// Material to use for the navmesh in the editor public Material surfaceMaterial; /// Material to use for the navmesh outline in the editor public Material lineMaterial; /// True if there already is a mesh with the specified hash public bool HasCachedMesh (Hasher hasher) { return existingHashes.Contains(hasher.Hash); } /// /// Schedules the meshes for the specified hash to be drawn. /// Returns: False if there is no cached mesh for this hash, you may want to /// submit one in that case. The draw command will be issued regardless of the return value. /// public bool Draw (Hasher hasher) { usedHashes.Add(hasher.Hash); return HasCachedMesh(hasher); } /// /// Schedules all meshes that were drawn the last frame (last time FinalizeDraw was called) to be drawn again. /// Also draws any new meshes that have been added since FinalizeDraw was last called. /// public void DrawExisting () { for (int i = 0; i < meshes.Count; i++) { usedHashes.Add(meshes[i].hash); } } /// Call after all commands for the frame have been done to draw everything public void FinalizeDraw () { RemoveUnusedMeshes(meshes); #if UNITY_EDITOR // Make sure the material references are correct if (surfaceMaterial == null) surfaceMaterial = UnityEditor.AssetDatabase.LoadAssetAtPath(EditorResourceHelper.editorAssets + "/Materials/Navmesh.mat", typeof(Material)) as Material; if (lineMaterial == null) lineMaterial = UnityEditor.AssetDatabase.LoadAssetAtPath(EditorResourceHelper.editorAssets + "/Materials/NavmeshOutline.mat", typeof(Material)) as Material; #endif var cam = Camera.current; var planes = GeometryUtility.CalculateFrustumPlanes(cam); // Silently do nothing if the materials are not set if (surfaceMaterial == null || lineMaterial == null) return; Profiler.BeginSample("Draw Retained Gizmos"); // First surfaces, then lines for (int matIndex = 0; matIndex <= 1; matIndex++) { var mat = matIndex == 0 ? surfaceMaterial : lineMaterial; for (int pass = 0; pass < mat.passCount; pass++) { mat.SetPass(pass); for (int i = 0; i < meshes.Count; i++) { if (meshes[i].lines == (mat == lineMaterial) && GeometryUtility.TestPlanesAABB(planes, meshes[i].mesh.bounds)) { Graphics.DrawMeshNow(meshes[i].mesh, Matrix4x4.identity); } } } } usedHashes.Clear(); Profiler.EndSample(); } /// /// Destroys all cached meshes. /// Used to make sure that no memory leaks happen in the Unity Editor. /// public void ClearCache () { usedHashes.Clear(); RemoveUnusedMeshes(meshes); while (cachedMeshes.Count > 0) { Mesh.DestroyImmediate(cachedMeshes.Pop()); } UnityEngine.Assertions.Assert.IsTrue(meshes.Count == 0); } void RemoveUnusedMeshes (List meshList) { // Walk the array with two pointers // i pointing to the entry that should be filled with something // and j pointing to the entry that is a potential candidate for // filling the entry at i. // When j reaches the end of the list it will be reduced in size for (int i = 0, j = 0; i < meshList.Count; ) { if (j == meshList.Count) { j--; meshList.RemoveAt(j); } else if (usedHashes.Contains(meshList[j].hash)) { meshList[i] = meshList[j]; i++; j++; } else { PoolMesh(meshList[j].mesh); existingHashes.Remove(meshList[j].hash); j++; } } } } }