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++;
}
}
}
}
}