using UnityEngine; using System.Collections.Generic; using Unity.Mathematics; using Unity.Collections; using Unity.Burst; namespace Pathfinding.Graphs.Navmesh { using System; using Pathfinding; using Voxelization.Burst; using Pathfinding.Util; using Pathfinding.Jobs; using Pathfinding.Collections; using Pathfinding.Pooling; using UnityEngine.Profiling; using Unity.Collections.LowLevel.Unsafe; [BurstCompile] public class RecastMeshGatherer { readonly int terrainDownsamplingFactor; public readonly LayerMask mask; public readonly List tagMask; readonly float maxColliderApproximationError; public readonly Bounds bounds; public readonly UnityEngine.SceneManagement.Scene scene; Dictionary cachedMeshes = new Dictionary(); readonly Dictionary cachedTreePrefabs = new Dictionary(); readonly List > vertexBuffers; readonly List > triangleBuffers; readonly List meshData; readonly RecastGraph.PerLayerModification[] modificationsByLayer; readonly RecastGraph.PerLayerModification[] modificationsByLayer2D; #if UNITY_EDITOR readonly List<(UnityEngine.Object, Mesh)> meshesUnreadableAtRuntime = new List<(UnityEngine.Object, Mesh)>(); #else bool anyNonReadableMesh = false; #endif List meshes; List dummyMaterials = new List(); public RecastMeshGatherer (UnityEngine.SceneManagement.Scene scene, Bounds bounds, int terrainDownsamplingFactor, LayerMask mask, List tagMask, List perLayerModifications, float maxColliderApproximationError) { // Clamp to at least 1 since that's the resolution of the heightmap terrainDownsamplingFactor = Math.Max(terrainDownsamplingFactor, 1); this.bounds = bounds; this.terrainDownsamplingFactor = terrainDownsamplingFactor; this.mask = mask; this.tagMask = tagMask ?? new List(); this.maxColliderApproximationError = maxColliderApproximationError; this.scene = scene; meshes = ListPool.Claim(); vertexBuffers = ListPool >.Claim(); triangleBuffers = ListPool >.Claim(); cachedMeshes = ObjectPoolSimple >.Claim(); meshData = ListPool.Claim(); modificationsByLayer = RecastGraph.PerLayerModification.ToLayerLookup(perLayerModifications, RecastGraph.PerLayerModification.Default); // 2D colliders default to being unwalkable var default2D = RecastGraph.PerLayerModification.Default; default2D.mode = RecastNavmeshModifier.Mode.UnwalkableSurface; modificationsByLayer2D = RecastGraph.PerLayerModification.ToLayerLookup(perLayerModifications, default2D); } struct TreeInfo { public List submeshes; public Vector3 localScale; public bool supportsRotation; } public struct MeshCollection : IArenaDisposable { List > vertexBuffers; List > triangleBuffers; public NativeArray meshes; #if UNITY_EDITOR public List<(UnityEngine.Object, Mesh)> meshesUnreadableAtRuntime; #endif public MeshCollection (List > vertexBuffers, List > triangleBuffers, NativeArray meshes #if UNITY_EDITOR , List<(UnityEngine.Object, Mesh)> meshesUnreadableAtRuntime #endif ) { this.vertexBuffers = vertexBuffers; this.triangleBuffers = triangleBuffers; this.meshes = meshes; #if UNITY_EDITOR this.meshesUnreadableAtRuntime = meshesUnreadableAtRuntime; #endif } void IArenaDisposable.DisposeWith (DisposeArena arena) { for (int i = 0; i < vertexBuffers.Count; i++) { arena.Add(vertexBuffers[i]); arena.Add(triangleBuffers[i]); } arena.Add(meshes); } } [BurstCompile] static void CalculateBounds (ref UnsafeSpan vertices, ref float4x4 localToWorldMatrix, out Bounds bounds) { if (vertices.Length == 0) { bounds = new Bounds(); } else { float3 max = float.NegativeInfinity; float3 min = float.PositiveInfinity; for (uint i = 0; i < vertices.Length; i++) { var v = math.transform(localToWorldMatrix, vertices[i]); max = math.max(max, v); min = math.min(min, v); } bounds = new Bounds((min+max)*0.5f, max-min); } } public MeshCollection Finalize () { #if UNITY_EDITOR // This skips the Mesh.isReadable check Mesh.MeshDataArray data = UnityEditor.MeshUtility.AcquireReadOnlyMeshData(meshData); #else Mesh.MeshDataArray data = Mesh.AcquireReadOnlyMeshData(meshData); #endif var meshes = new NativeArray(this.meshes.Count, Allocator.Persistent); int meshBufferOffset = vertexBuffers.Count; Profiler.BeginSample("Copying vertices"); // TODO: We should be able to hold the `data` for the whole scan and not have to copy all vertices/triangles for (int i = 0; i < data.Length; i++) { MeshUtility.GetMeshData(data, i, out var verts, out var tris); vertexBuffers.Add(verts); triangleBuffers.Add(tris); } Profiler.EndSample(); Profiler.BeginSample("Creating RasterizationMeshes"); for (int i = 0; i < meshes.Length; i++) { var gatheredMesh = this.meshes[i]; int bufferIndex; if (gatheredMesh.meshDataIndex >= 0) { bufferIndex = meshBufferOffset + gatheredMesh.meshDataIndex; } else { bufferIndex = -(gatheredMesh.meshDataIndex+1); } var bounds = gatheredMesh.bounds; var vertexSpan = vertexBuffers[bufferIndex].Reinterpret().AsUnsafeReadOnlySpan(); if (bounds == new Bounds()) { // Recalculate bounding box float4x4 m = gatheredMesh.matrix; CalculateBounds(ref vertexSpan, ref m, out bounds); } var triangles = triangleBuffers[bufferIndex]; meshes[i] = new RasterizationMesh { vertices = vertexSpan, triangles = triangles.AsUnsafeSpan().Slice(gatheredMesh.indexStart, (gatheredMesh.indexEnd != -1 ? gatheredMesh.indexEnd : triangles.Length) - gatheredMesh.indexStart), area = gatheredMesh.area, areaIsTag = gatheredMesh.areaIsTag, bounds = bounds, matrix = gatheredMesh.matrix, solid = gatheredMesh.solid, doubleSided = gatheredMesh.doubleSided, flatten = gatheredMesh.flatten, }; } Profiler.EndSample(); cachedMeshes.Clear(); ObjectPoolSimple >.Release(ref cachedMeshes); ListPool.Release(ref this.meshes); data.Dispose(); return new MeshCollection( vertexBuffers, triangleBuffers, meshes #if UNITY_EDITOR , this.meshesUnreadableAtRuntime #endif ); } /// /// Add vertex and triangle buffers that can later be used to create a . /// /// The returned index can be used in the field of the struct. /// /// You can use the returned index multiple times with different matrices, to create instances of the same object in multiple locations. /// public int AddMeshBuffers (Vector3[] vertices, int[] triangles) { return AddMeshBuffers(new NativeArray(vertices, Allocator.Persistent), new NativeArray(triangles, Allocator.Persistent)); } /// /// Add vertex and triangle buffers that can later be used to create a . /// /// The returned index can be used in the field of the struct. /// /// You can use the returned index multiple times with different matrices, to create instances of the same object in multiple locations. /// public int AddMeshBuffers (NativeArray vertices, NativeArray triangles) { var meshDataIndex = -vertexBuffers.Count-1; vertexBuffers.Add(vertices); triangleBuffers.Add(triangles); return meshDataIndex; } /// Add a mesh to the list of meshes to rasterize public void AddMesh (Renderer renderer, Mesh gatheredMesh) { if (ConvertMeshToGatheredMesh(renderer, gatheredMesh, out var gm)) { meshes.Add(gm); } } /// Add a mesh to the list of meshes to rasterize public void AddMesh (GatheredMesh gatheredMesh) { meshes.Add(gatheredMesh); } /// Holds info about a mesh to be rasterized public struct GatheredMesh { /// /// Index in the meshData array. /// Can be retrieved from the method. /// public int meshDataIndex; /// /// Area ID of the mesh. 0 means walkable, and -1 indicates that the mesh should be treated as unwalkable. /// Other positive values indicate a custom area ID which will create a seam in the navmesh. /// public int area; /// Start index in the triangle array public int indexStart; /// End index in the triangle array. -1 indicates the end of the array. public int indexEnd; /// World bounds of the mesh. Assumed to already be multiplied with the . public Bounds bounds; /// Matrix to transform the vertices by public Matrix4x4 matrix; /// /// If true then the mesh will be treated as solid and its interior will be unwalkable. /// The unwalkable region will be the minimum to maximum y coordinate in each cell. /// public bool solid; /// See public bool doubleSided; /// See public bool flatten; /// See public bool areaIsTag; /// /// Recalculate the from the vertices. /// /// The bounds will not be recalculated immediately. /// public void RecalculateBounds () { // This will cause the bounds to be recalculated later bounds = new Bounds(); } public void ApplyNavmeshModifier (RecastNavmeshModifier navmeshModifier) { area = AreaFromSurfaceMode(navmeshModifier.mode, navmeshModifier.surfaceID); areaIsTag = navmeshModifier.mode == RecastNavmeshModifier.Mode.WalkableSurfaceWithTag; solid |= navmeshModifier.solid; } public void ApplyLayerModification (RecastGraph.PerLayerModification modification) { area = AreaFromSurfaceMode(modification.mode, modification.surfaceID); areaIsTag = modification.mode == RecastNavmeshModifier.Mode.WalkableSurfaceWithTag; } } enum MeshType { Mesh, Box, Capsule, } struct MeshCacheItem : IEquatable { public MeshType type; public Mesh mesh; public int rows; public int quantizedHeight; public MeshCacheItem (Mesh mesh) { type = MeshType.Mesh; this.mesh = mesh; rows = 0; quantizedHeight = 0; } public static readonly MeshCacheItem Box = new MeshCacheItem { type = MeshType.Box, mesh = null, rows = 0, quantizedHeight = 0, }; public bool Equals (MeshCacheItem other) { return type == other.type && mesh == other.mesh && rows == other.rows && quantizedHeight == other.quantizedHeight; } public override int GetHashCode () { return (((int)type * 31 ^ (mesh != null ? mesh.GetHashCode() : -1)) * 31 ^ rows) * 31 ^ quantizedHeight; } } bool MeshFilterShouldBeIncluded (MeshFilter filter) { if (filter.TryGetComponent(out var rend)) { if (filter.sharedMesh != null && rend.enabled && (((1 << filter.gameObject.layer) & mask) != 0 || (tagMask.Count > 0 && tagMask.Contains(filter.tag)))) { if (!(filter.TryGetComponent(out var rmo) && rmo.enabled)) { return true; } } } return false; } bool ConvertMeshToGatheredMesh (Renderer renderer, Mesh mesh, out GatheredMesh gatheredMesh) { // Ignore meshes that do not have a Position vertex attribute. // This can happen for meshes that are empty, i.e. have no vertices at all. if (!mesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.Position)) { gatheredMesh = default; return false; } #if !UNITY_EDITOR if (!mesh.isReadable) { // Cannot scan this if (!anyNonReadableMesh) { Debug.LogError("Some meshes could not be included when scanning the graph because they are marked as not readable. This includes the mesh '" + mesh.name + "'. You need to mark the mesh with read/write enabled in the mesh importer. Alternatively you can only rasterize colliders and not meshes. Mesh Collider meshes still need to be readable.", mesh); } anyNonReadableMesh = true; gatheredMesh = default; return false; } #endif renderer.GetSharedMaterials(dummyMaterials); var submeshStart = renderer is MeshRenderer mrend ? mrend.subMeshStartIndex : 0; var submeshCount = dummyMaterials.Count; int indexStart = 0; int indexEnd = -1; if (submeshStart > 0 || submeshCount < mesh.subMeshCount) { var a = mesh.GetSubMesh(submeshStart); var b = mesh.GetSubMesh(submeshStart + submeshCount - 1); indexStart = a.indexStart; indexEnd = b.indexStart + b.indexCount; } // Check the cache to avoid allocating // a new array unless necessary if (!cachedMeshes.TryGetValue(new MeshCacheItem(mesh), out int meshBufferIndex)) { #if UNITY_EDITOR if (!mesh.isReadable) meshesUnreadableAtRuntime.Add((renderer, mesh)); #endif meshBufferIndex = meshData.Count; meshData.Add(mesh); cachedMeshes[new MeshCacheItem(mesh)] = meshBufferIndex; } gatheredMesh = new GatheredMesh { meshDataIndex = meshBufferIndex, bounds = renderer.bounds, indexStart = indexStart, indexEnd = indexEnd, matrix = renderer.localToWorldMatrix, doubleSided = false, flatten = false, }; return true; } GatheredMesh? GetColliderMesh (MeshCollider collider, Matrix4x4 localToWorldMatrix) { if (collider.sharedMesh != null) { Mesh mesh = collider.sharedMesh; // Ignore meshes that do not have a Position vertex attribute. // This can happen for meshes that are empty, i.e. have no vertices at all. if (!mesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.Position)) { return null; } #if !UNITY_EDITOR if (!mesh.isReadable) { // Cannot scan this if (!anyNonReadableMesh) { Debug.LogError("Some mesh collider meshes could not be included when scanning the graph because they are marked as not readable. This includes the mesh '" + mesh.name + "'. You need to mark the mesh with read/write enabled in the mesh importer.", mesh); } anyNonReadableMesh = true; return null; } #endif // Check the cache to avoid allocating // a new array unless necessary if (!cachedMeshes.TryGetValue(new MeshCacheItem(mesh), out int meshDataIndex)) { #if UNITY_EDITOR if (!mesh.isReadable) meshesUnreadableAtRuntime.Add((collider, mesh)); #endif meshDataIndex = meshData.Count; meshData.Add(mesh); cachedMeshes[new MeshCacheItem(mesh)] = meshDataIndex; } return new GatheredMesh { meshDataIndex = meshDataIndex, bounds = collider.bounds, areaIsTag = false, area = 0, indexStart = 0, indexEnd = -1, // Treat the collider as solid iff the collider is convex solid = collider.convex, matrix = localToWorldMatrix, doubleSided = false, flatten = false, }; } return null; } public void CollectSceneMeshes () { if (tagMask.Count > 0 || mask != 0) { // This is unfortunately the fastest way to find all mesh filters.. and it is not particularly fast. // Note: We have to sort these because the recast graph is not completely deterministic in terms of ordering of meshes. // Different ordering can in rare cases lead to different spans being merged which can lead to different navmeshes. var meshFilters = UnityCompatibility.FindObjectsByTypeSorted(); bool containedStatic = false; for (int i = 0; i < meshFilters.Length; i++) { MeshFilter filter = meshFilters[i]; if (!MeshFilterShouldBeIncluded(filter)) continue; // Note, guaranteed to have a renderer as MeshFilterShouldBeIncluded checks for it. // but it can be either a MeshRenderer or a SkinnedMeshRenderer filter.TryGetComponent(out var rend); if (rend.isPartOfStaticBatch) { // Statically batched meshes cannot be used due to Unity limitations // log a warning about this containedStatic = true; } else { // Only include it if it intersects with the graph if (rend.bounds.Intersects(bounds)) { if (ConvertMeshToGatheredMesh(rend, filter.sharedMesh, out var gatheredMesh)) { gatheredMesh.ApplyLayerModification(modificationsByLayer[filter.gameObject.layer]); meshes.Add(gatheredMesh); } } } } if (containedStatic) { Debug.LogWarning("Some meshes were statically batched. These meshes can not be used for navmesh calculation" + " due to technical constraints.\nDuring runtime scripts cannot access the data of meshes which have been statically batched.\n" + "One way to solve this problem is to use cached startup (Save & Load tab in the inspector) to only calculate the graph when the game is not playing."); } } } static int AreaFromSurfaceMode (RecastNavmeshModifier.Mode mode, int surfaceID) { switch (mode) { default: case RecastNavmeshModifier.Mode.UnwalkableSurface: return -1; case RecastNavmeshModifier.Mode.WalkableSurface: return 0; case RecastNavmeshModifier.Mode.WalkableSurfaceWithSeam: case RecastNavmeshModifier.Mode.WalkableSurfaceWithTag: return surfaceID; } } /// Find all relevant RecastNavmeshModifier components and create ExtraMeshes for them public void CollectRecastNavmeshModifiers () { var buffer = ListPool.Claim(); // Get all recast navmesh modifiers inside the bounds RecastNavmeshModifier.GetAllInBounds(buffer, bounds); // Create an RasterizationMesh object // for each RecastNavmeshModifier for (int i = 0; i < buffer.Count; i++) { AddNavmeshModifier(buffer[i]); } ListPool.Release(ref buffer); } void AddNavmeshModifier (RecastNavmeshModifier navmeshModifier) { if (navmeshModifier.includeInScan == RecastNavmeshModifier.ScanInclusion.AlwaysExclude) return; if (navmeshModifier.includeInScan == RecastNavmeshModifier.ScanInclusion.Auto && (((mask >> navmeshModifier.gameObject.layer) & 1) == 0 && !tagMask.Contains(navmeshModifier.tag))) return; navmeshModifier.ResolveMeshSource(out var filter, out var collider, out var collider2D); if (filter != null) { // Add based on mesh filter Mesh mesh = filter.sharedMesh; if (filter.TryGetComponent(out var rend) && mesh != null) { if (ConvertMeshToGatheredMesh(rend, filter.sharedMesh, out var gatheredMesh)) { gatheredMesh.ApplyNavmeshModifier(navmeshModifier); meshes.Add(gatheredMesh); } } } else if (collider != null) { // Add based on collider if (ConvertColliderToGatheredMesh(collider) is GatheredMesh rmesh) { rmesh.ApplyNavmeshModifier(navmeshModifier); meshes.Add(rmesh); } } else if (collider2D != null) { // 2D colliders are handled separately } else { if (navmeshModifier.geometrySource == RecastNavmeshModifier.GeometrySource.Auto) { Debug.LogError("Couldn't get geometry source for RecastNavmeshModifier ("+navmeshModifier.gameObject.name +"). It didn't have a collider or MeshFilter+Renderer attached", navmeshModifier.gameObject); } else { Debug.LogError("Couldn't get geometry source for RecastNavmeshModifier ("+navmeshModifier.gameObject.name +"). It didn't have a " + navmeshModifier.geometrySource + " attached", navmeshModifier.gameObject); } } } public void CollectTerrainMeshes (bool rasterizeTrees, float desiredChunkSize) { // Find all terrains in the scene var terrains = Terrain.activeTerrains; if (terrains.Length > 0) { // Loop through all terrains in the scene for (int j = 0; j < terrains.Length; j++) { if (terrains[j].terrainData == null) continue; Profiler.BeginSample("Generate terrain chunks"); bool anyTerrainChunks = GenerateTerrainChunks(terrains[j], bounds, desiredChunkSize); Profiler.EndSample(); // Don't rasterize trees if the terrain did not intersect the graph bounds if (rasterizeTrees && anyTerrainChunks) { Profiler.BeginSample("Find tree meshes"); // Rasterize all tree colliders on this terrain object CollectTreeMeshes(terrains[j]); Profiler.EndSample(); } } } } static int NonNegativeModulus (int x, int m) { int r = x%m; return r < 0 ? r+m : r; } /// Returns ceil(lhs/rhs), i.e lhs/rhs rounded up static int CeilDivision (int lhs, int rhs) { return (lhs + rhs - 1)/rhs; } bool GenerateTerrainChunks (Terrain terrain, Bounds bounds, float desiredChunkSize) { var terrainData = terrain.terrainData; if (terrainData == null) throw new ArgumentException("Terrain contains no terrain data"); Vector3 offset = terrain.GetPosition(); var terrainSize = terrainData.size; Vector3 center = offset + terrainSize * 0.5F; // Figure out the bounds of the terrain in world space var terrainBounds = new Bounds(center, terrainSize); // Only include terrains which intersects the graph if (!terrainBounds.Intersects(bounds)) return false; // Original heightmap size int heightmapWidth = terrainData.heightmapResolution; int heightmapDepth = terrainData.heightmapResolution; // Size of a single sample Vector3 sampleSize = terrainData.heightmapScale; sampleSize.y = terrainSize.y; // Make chunks at least 12 quads wide // since too small chunks just decreases performance due // to the overhead of checking for bounds and similar things const int MinChunkSize = 12; // Find the number of samples along each edge that corresponds to a world size of desiredChunkSize // Then round up to the nearest multiple of terrainSampleSize var chunkSizeAlongX = Mathf.CeilToInt(Mathf.Max(desiredChunkSize / (sampleSize.x * terrainDownsamplingFactor), MinChunkSize)) * terrainDownsamplingFactor; var chunkSizeAlongZ = Mathf.CeilToInt(Mathf.Max(desiredChunkSize / (sampleSize.z * terrainDownsamplingFactor), MinChunkSize)) * terrainDownsamplingFactor; chunkSizeAlongX = Mathf.Min(chunkSizeAlongX, heightmapWidth); chunkSizeAlongZ = Mathf.Min(chunkSizeAlongZ, heightmapDepth); Vector2Int startSample, chunks; if (float.IsFinite(bounds.size.x)) { startSample = new Vector2Int( Mathf.FloorToInt((bounds.min.x - offset.x) / sampleSize.x), Mathf.FloorToInt((bounds.min.z - offset.z) / sampleSize.z) ); // Ensure we always start at a multiple of the terrainDownsamplingFactor. // Otherwise, the generated meshes may not look the same, depending on the original bounds. // Not rounding would not technically be wrong, but this makes it more predictable. startSample.x -= NonNegativeModulus(startSample.x, terrainDownsamplingFactor); startSample.y -= NonNegativeModulus(startSample.y, terrainDownsamplingFactor); // Figure out which chunks might intersect the bounding box var worldChunkSizeAlongX = chunkSizeAlongX * sampleSize.x; var worldChunkSizeAlongZ = chunkSizeAlongZ * sampleSize.z; chunks = new Vector2Int( Mathf.CeilToInt((bounds.max.x - offset.x - startSample.x * sampleSize.x) / worldChunkSizeAlongX), Mathf.CeilToInt((bounds.max.z - offset.z - startSample.y * sampleSize.z) / worldChunkSizeAlongZ) ); } else { startSample = new Vector2Int(0, 0); chunks = new Vector2Int(CeilDivision(heightmapWidth, chunkSizeAlongX), CeilDivision(heightmapDepth, chunkSizeAlongZ)); } // Figure out which samples we need from the terrain heightmap var sampleRect = new IntRect(0, 0, chunks.x * chunkSizeAlongX - 1, chunks.y * chunkSizeAlongZ - 1).Offset(startSample); var allSamples = new IntRect(0, 0, heightmapWidth - 1, heightmapDepth - 1); // Clamp the samples to the heightmap bounds sampleRect = IntRect.Intersection(sampleRect, allSamples); if (!sampleRect.IsValid()) return false; chunks = new Vector2Int(CeilDivision(sampleRect.Width, chunkSizeAlongX), CeilDivision(sampleRect.Height, chunkSizeAlongZ)); Profiler.BeginSample("Get heightmap data"); float[, ] heights = terrainData.GetHeights( sampleRect.xmin, sampleRect.ymin, sampleRect.Width, sampleRect.Height ); bool[, ] holes = terrainData.GetHoles( sampleRect.xmin, sampleRect.ymin, sampleRect.Width - 1, sampleRect.Height - 1 ); Profiler.EndSample(); unsafe { var heightsSpan = new UnsafeSpan(heights, out var gcHandle1); var holesSpan = new UnsafeSpan(holes, out var gcHandle2); var chunksOffset = offset + new Vector3(sampleRect.xmin * sampleSize.x, 0, sampleRect.ymin * sampleSize.z); var chunksMatrix = Matrix4x4.TRS(chunksOffset, Quaternion.identity, sampleSize); for (int z = 0; z < chunks.y; z++) { for (int x = 0; x < chunks.x; x++) { Profiler.BeginSample("Generate chunk"); GenerateHeightmapChunk( ref heightsSpan, ref holesSpan, sampleRect.Width, sampleRect.Height, x * chunkSizeAlongX, z * chunkSizeAlongZ, chunkSizeAlongX, chunkSizeAlongZ, terrainDownsamplingFactor, out var vertsSpan, out var trisSpan ); Profiler.EndSample(); var verts = vertsSpan.MoveToNativeArray(Allocator.Persistent); var tris = trisSpan.MoveToNativeArray(Allocator.Persistent); var meshDataIndex = AddMeshBuffers(verts, tris); var chunk = new GatheredMesh { meshDataIndex = meshDataIndex, // An empty bounding box indicates that it should be calculated from the vertices later. bounds = new Bounds(), indexStart = 0, indexEnd = -1, areaIsTag = false, area = 0, solid = false, matrix = chunksMatrix, doubleSided = false, flatten = false, }; chunk.ApplyLayerModification(modificationsByLayer[terrain.gameObject.layer]); meshes.Add(chunk); } } UnsafeUtility.ReleaseGCObject(gcHandle1); UnsafeUtility.ReleaseGCObject(gcHandle2); } return true; } /// Generates a terrain chunk mesh [BurstCompile] public static void GenerateHeightmapChunk (ref UnsafeSpan heights, ref UnsafeSpan holes, int heightmapWidth, int heightmapDepth, int x0, int z0, int width, int depth, int stride, out UnsafeSpan verts, out UnsafeSpan tris) { // Downsample to a smaller mesh (full resolution will take a long time to rasterize) // Round up the width to the nearest multiple of terrainSampleSize and then add 1 // (off by one because there are vertices at the edge of the mesh) int resultWidth = CeilDivision(Mathf.Min(width, heightmapWidth - x0), stride) + 1; int resultDepth = CeilDivision(Mathf.Min(depth, heightmapDepth - z0), stride) + 1; // Create a mesh from the heightmap var numVerts = resultWidth * resultDepth; var numTris = (resultWidth-1)*(resultDepth-1)*2*3; verts = new UnsafeSpan(Allocator.Persistent, numVerts); tris = new UnsafeSpan(Allocator.Persistent, numTris); // Create lots of vertices for (int z = 0; z < resultDepth; z++) { int sampleZ = Math.Min(z0 + z*stride, heightmapDepth-1); for (int x = 0; x < resultWidth; x++) { int sampleX = Math.Min(x0 + x*stride, heightmapWidth-1); verts[z*resultWidth + x] = new Vector3(sampleX, heights[sampleZ*heightmapWidth + sampleX], sampleZ); } } // Create the mesh by creating triangles in a grid like pattern int triangleIndex = 0; for (int z = 0; z < resultDepth-1; z++) { for (int x = 0; x < resultWidth-1; x++) { // Try to check if the center of the cell is a hole or not. // Note that the holes array has a size which is 1 less than the heightmap size int sampleX = Math.Min(x0 + stride/2 + x*stride, heightmapWidth-2); int sampleZ = Math.Min(z0 + stride/2 + z*stride, heightmapDepth-2); if (holes[sampleZ*(heightmapWidth-1) + sampleX]) { // Not a hole, generate a mesh here tris[triangleIndex] = z*resultWidth + x; tris[triangleIndex+1] = (z+1)*resultWidth + x+1; tris[triangleIndex+2] = z*resultWidth + x+1; triangleIndex += 3; tris[triangleIndex] = z*resultWidth + x; tris[triangleIndex+1] = (z+1)*resultWidth + x; tris[triangleIndex+2] = (z+1)*resultWidth + x+1; triangleIndex += 3; } } } tris = tris.Slice(0, triangleIndex); } void CollectTreeMeshes (Terrain terrain) { Profiler.BeginSample("Get tree data from terrain"); TerrainData data = terrain.terrainData; var treeInstances = data.treeInstances; var treePrototypes = data.treePrototypes; var terrainPos = terrain.transform.position; var terrainSize = data.size; Profiler.EndSample(); Profiler.BeginSample("Process tree prototypes"); var treeInfos = new TreeInfo[treePrototypes.Length]; for (int i = 0; i < treePrototypes.Length; i++) { TreePrototype prot = treePrototypes[i]; // Make sure that the tree prefab exists if (prot.prefab == null) { continue; } if (!cachedTreePrefabs.TryGetValue(prot.prefab, out TreeInfo treeInfo)) { treeInfo.submeshes = new List(); // The unity terrain system only supports rotation for trees with a LODGroup on the root object. // Unity still sets the instance.rotation field to values even they are not used, so we need to explicitly check for this. treeInfo.supportsRotation = prot.prefab.TryGetComponent(out var dummy); treeInfo.localScale = prot.prefab.transform.localScale; var colliders = ListPool.Claim(); var rootMatrixInv = prot.prefab.transform.localToWorldMatrix.inverse; prot.prefab.GetComponentsInChildren(false, colliders); for (int j = 0; j < colliders.Count; j++) { // The prefab has a collider, use that instead var collider = colliders[j]; // Generate a mesh from the collider if (ConvertColliderToGatheredMesh(collider, rootMatrixInv * collider.transform.localToWorldMatrix) is GatheredMesh mesh) { // For trees, we only suppport generating a mesh from a collider. So we ignore the navmeshModifier.geometrySource field. if (collider.gameObject.TryGetComponent(out var navmeshModifier) && navmeshModifier.enabled) { if (navmeshModifier.includeInScan == RecastNavmeshModifier.ScanInclusion.AlwaysExclude) continue; mesh.ApplyNavmeshModifier(navmeshModifier); } else { mesh.ApplyLayerModification(modificationsByLayer[collider.gameObject.layer]); } // The bounds are incorrectly based on collider.bounds, and may not even be initialized by the physics system. // It is incorrect because the collider is on the prefab, not on the tree instance // so we need to recalculate the bounds based on the actual vertex positions mesh.RecalculateBounds(); treeInfo.submeshes.Add(mesh); } } ListPool.Release(ref colliders); cachedTreePrefabs[prot.prefab] = treeInfo; } treeInfos[i] = treeInfo; } Profiler.EndSample(); Profiler.BeginSample("Convert trees to meshes"); for (int i = 0; i < treeInstances.Length; i++) { TreeInstance instance = treeInstances[i]; var treeInfo = treeInfos[instance.prototypeIndex]; if (treeInfo.submeshes == null || treeInfo.submeshes.Count == 0) continue; var treePosition = terrainPos + Vector3.Scale(instance.position, terrainSize); var instanceSize = new Vector3(instance.widthScale, instance.heightScale, instance.widthScale); var prefabScale = Vector3.Scale(instanceSize, treeInfo.localScale); var rotation = treeInfo.supportsRotation ? Quaternion.AngleAxis(instance.rotation * Mathf.Rad2Deg, Vector3.up) : Quaternion.identity; var matrix = Matrix4x4.TRS(treePosition, rotation, prefabScale); for (int j = 0; j < treeInfo.submeshes.Count; j++) { var item = treeInfo.submeshes[j]; item.matrix = matrix * item.matrix; meshes.Add(item); } } Profiler.EndSample(); } bool ShouldIncludeCollider (Collider collider) { if (!collider.enabled || collider.isTrigger || !collider.bounds.Intersects(bounds) || (collider.TryGetComponent(out var rmo) && rmo.enabled)) return false; var go = collider.gameObject; if (((mask >> go.layer) & 1) != 0) return true; // Iterate over the tag mask and use CompareTag instead of tagMask.Includes(collider.tag), as this will not allocate. for (int i = 0; i < tagMask.Count; i++) { if (go.CompareTag(tagMask[i])) return true; } return false; } public void CollectColliderMeshes () { if (tagMask.Count == 0 && mask == 0) return; var physicsScene = scene.GetPhysicsScene(); // Find all colliders that could possibly be inside the bounds // TODO: Benchmark? // Repeatedly do a OverlapBox check and make the buffer larger if it's too small. int numColliders = 256; Collider[] colliderBuffer = null; bool finiteBounds = math.all(math.isfinite(bounds.extents)); if (!finiteBounds) { colliderBuffer = UnityCompatibility.FindObjectsByTypeSorted(); numColliders = colliderBuffer.Length; } else { do { if (colliderBuffer != null) ArrayPool.Release(ref colliderBuffer); colliderBuffer = ArrayPool.Claim(numColliders * 4); numColliders = physicsScene.OverlapBox(bounds.center, bounds.extents, colliderBuffer, Quaternion.identity, ~0, QueryTriggerInteraction.Ignore); } while (numColliders == colliderBuffer.Length); } for (int i = 0; i < numColliders; i++) { Collider collider = colliderBuffer[i]; if (ShouldIncludeCollider(collider)) { if (ConvertColliderToGatheredMesh(collider) is GatheredMesh mesh) { mesh.ApplyLayerModification(modificationsByLayer[collider.gameObject.layer]); meshes.Add(mesh); } } } if (finiteBounds) ArrayPool.Release(ref colliderBuffer); } /// /// Box Collider triangle indices can be reused for multiple instances. /// Warning: This array should never be changed /// private readonly static int[] BoxColliderTris = { 0, 1, 2, 0, 2, 3, 6, 5, 4, 7, 6, 4, 0, 5, 1, 0, 4, 5, 1, 6, 2, 1, 5, 6, 2, 7, 3, 2, 6, 7, 3, 4, 0, 3, 7, 4 }; /// /// Box Collider vertices can be reused for multiple instances. /// Warning: This array should never be changed /// private readonly static Vector3[] BoxColliderVerts = { new Vector3(-1, -1, -1), new Vector3(1, -1, -1), new Vector3(1, -1, 1), new Vector3(-1, -1, 1), new Vector3(-1, 1, -1), new Vector3(1, 1, -1), new Vector3(1, 1, 1), new Vector3(-1, 1, 1), }; /// /// Rasterizes a collider to a mesh. /// This will pass the col.transform.localToWorldMatrix to the other overload of this function. /// GatheredMesh? ConvertColliderToGatheredMesh (Collider col) { return ConvertColliderToGatheredMesh(col, col.transform.localToWorldMatrix); } /// /// Rasterizes a collider to a mesh assuming it's vertices should be multiplied with the matrix. /// Note that the bounds of the returned RasterizationMesh is based on collider.bounds. So you might want to /// call myExtraMesh.RecalculateBounds on the returned mesh to recalculate it if the collider.bounds would /// not give the correct value. /// public GatheredMesh? ConvertColliderToGatheredMesh (Collider col, Matrix4x4 localToWorldMatrix) { if (col is BoxCollider box) { return RasterizeBoxCollider(box, localToWorldMatrix); } else if (col is SphereCollider || col is CapsuleCollider) { var scollider = col as SphereCollider; var ccollider = col as CapsuleCollider; float radius = scollider != null ? scollider.radius : ccollider.radius; float height = scollider != null ? 0 : (ccollider.height*0.5f/radius) - 1; Quaternion rot = Quaternion.identity; // Capsule colliders can be aligned along the X, Y or Z axis if (ccollider != null) rot = Quaternion.Euler(ccollider.direction == 2 ? 90 : 0, 0, ccollider.direction == 0 ? 90 : 0); Matrix4x4 matrix = Matrix4x4.TRS(scollider != null ? scollider.center : ccollider.center, rot, Vector3.one*radius); matrix = localToWorldMatrix * matrix; return RasterizeCapsuleCollider(radius, height, col.bounds, matrix); } else if (col is MeshCollider collider) { return GetColliderMesh(collider, localToWorldMatrix); } return null; } GatheredMesh RasterizeBoxCollider (BoxCollider collider, Matrix4x4 localToWorldMatrix) { Matrix4x4 matrix = Matrix4x4.TRS(collider.center, Quaternion.identity, collider.size*0.5f); matrix = localToWorldMatrix * matrix; if (!cachedMeshes.TryGetValue(MeshCacheItem.Box, out int meshDataIndex)) { meshDataIndex = AddMeshBuffers(BoxColliderVerts, BoxColliderTris); cachedMeshes[MeshCacheItem.Box] = meshDataIndex; } return new GatheredMesh { meshDataIndex = meshDataIndex, bounds = collider.bounds, indexStart = 0, indexEnd = -1, areaIsTag = false, area = 0, solid = true, matrix = matrix, doubleSided = false, flatten = false, }; } static int CircleSteps (Matrix4x4 matrix, float radius, float maxError) { // Take the maximum scale factor among the 3 axes. // If the current matrix has a uniform scale then they are all the same. var maxScaleFactor = math.sqrt(math.max(math.max(math.lengthsq((Vector3)matrix.GetColumn(0)), math.lengthsq((Vector3)matrix.GetColumn(1))), math.lengthsq((Vector3)matrix.GetColumn(2)))); var realWorldRadius = radius * maxScaleFactor; var cosAngle = 1 - maxError / realWorldRadius; int steps = cosAngle < 0 ? 3 : (int)math.ceil(math.PI / math.acos(cosAngle)); return steps; } /// /// If a circle is approximated by fewer segments, it will be slightly smaller than the original circle. /// This factor is used to adjust the radius of the circle so that the resulting circle will have roughly the same area as the original circle. /// static float CircleRadiusAdjustmentFactor (int steps) { return 0.5f * (1 - math.cos(2 * math.PI / steps)); } GatheredMesh RasterizeCapsuleCollider (float radius, float height, Bounds bounds, Matrix4x4 localToWorldMatrix) { // Calculate the number of rows to use int rows = CircleSteps(localToWorldMatrix, radius, maxColliderApproximationError); int cols = rows; var cacheItem = new MeshCacheItem { type = MeshType.Capsule, mesh = null, rows = rows, // Capsules that differ by a very small amount in height will be rasterized in the same way quantizedHeight = Mathf.RoundToInt(height/maxColliderApproximationError), }; if (!cachedMeshes.TryGetValue(cacheItem, out var meshDataIndex)) { // Generate a sphere/capsule mesh var verts = new NativeArray(rows*cols + 2, Allocator.Persistent); var tris = new NativeArray(rows*cols*2*3, Allocator.Persistent); for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { verts[c + r*cols] = new Vector3(Mathf.Cos(c*Mathf.PI*2/cols)*Mathf.Sin((r*Mathf.PI/(rows-1))), Mathf.Cos((r*Mathf.PI/(rows-1))) + (r < rows/2 ? height : -height), Mathf.Sin(c*Mathf.PI*2/cols)*Mathf.Sin((r*Mathf.PI/(rows-1)))); } } verts[verts.Length-1] = Vector3.up; verts[verts.Length-2] = Vector3.down; int triIndex = 0; for (int i = 0, j = cols-1; i < cols; j = i++) { tris[triIndex + 0] = (verts.Length-1); tris[triIndex + 1] = (0*cols + j); tris[triIndex + 2] = (0*cols + i); triIndex += 3; } for (int r = 1; r < rows; r++) { for (int i = 0, j = cols-1; i < cols; j = i++) { tris[triIndex + 0] = (r*cols + i); tris[triIndex + 1] = (r*cols + j); tris[triIndex + 2] = ((r-1)*cols + i); triIndex += 3; tris[triIndex + 0] = ((r-1)*cols + j); tris[triIndex + 1] = ((r-1)*cols + i); tris[triIndex + 2] = (r*cols + j); triIndex += 3; } } for (int i = 0, j = cols-1; i < cols; j = i++) { tris[triIndex + 0] = (verts.Length-2); tris[triIndex + 1] = ((rows-1)*cols + j); tris[triIndex + 2] = ((rows-1)*cols + i); triIndex += 3; } UnityEngine.Assertions.Assert.AreEqual(triIndex, tris.Length); // TOOD: Avoid allocating original C# array // Store custom vertex buffers as negative indices meshDataIndex = AddMeshBuffers(verts, tris); cachedMeshes[cacheItem] = meshDataIndex; } return new GatheredMesh { meshDataIndex = meshDataIndex, bounds = bounds, areaIsTag = false, area = 0, indexStart = 0, indexEnd = -1, solid = true, matrix = localToWorldMatrix, doubleSided = false, flatten = false, }; } bool ShouldIncludeCollider2D (Collider2D collider) { // Note: Some things are already checked, namely that: // - collider.enabled is true // - that the bounds intersect (at least approxmately) // - that the collider is not a trigger // This is not completely analogous to ShouldIncludeCollider, as this one will // always include the collider if it has an attached RecastNavmeshModifier, while // 3D colliders handle RecastNavmeshModifier components separately. if (((mask >> collider.gameObject.layer) & 1) != 0) return true; if ((collider.attachedRigidbody as Component ?? collider).TryGetComponent(out var rmo) && rmo.enabled && rmo.includeInScan == RecastNavmeshModifier.ScanInclusion.AlwaysInclude) return true; for (int i = 0; i < tagMask.Count; i++) { if (collider.CompareTag(tagMask[i])) return true; } return false; } public void Collect2DColliderMeshes () { if (tagMask.Count == 0 && mask == 0) return; var physicsScene = scene.GetPhysicsScene2D(); // Find all colliders that could possibly be inside the bounds // TODO: Benchmark? int numColliders = 256; Collider2D[] colliderBuffer = null; bool finiteBounds = math.isfinite(bounds.extents.x) && math.isfinite(bounds.extents.y); if (!finiteBounds) { colliderBuffer = UnityCompatibility.FindObjectsByTypeSorted(); numColliders = colliderBuffer.Length; } else { // Repeatedly do a OverlapArea check and make the buffer larger if it's too small. var min2D = (Vector2)bounds.min; var max2D = (Vector2)bounds.max; var filter = new ContactFilter2D().NoFilter(); // It would be nice to add the layer mask filter here as well, // but we cannot since a collider may have a RecastNavmeshModifier component // attached, and in that case we want to include it even if it is on an excluded layer. // The user may also want to include objects based on tags. // But we can at least exclude all triggers. filter.useTriggers = false; do { if (colliderBuffer != null) ArrayPool.Release(ref colliderBuffer); colliderBuffer = ArrayPool.Claim(numColliders * 4); numColliders = physicsScene.OverlapArea(min2D, max2D, filter, colliderBuffer); } while (numColliders == colliderBuffer.Length); } // Filter out colliders that should not be included for (int i = 0; i < numColliders; i++) { if (!ShouldIncludeCollider2D(colliderBuffer[i])) colliderBuffer[i] = null; } int shapeMeshCount = ColliderMeshBuilder2D.GenerateMeshesFromColliders(colliderBuffer, numColliders, maxColliderApproximationError, out var vertices, out var indices, out var shapeMeshes); var bufferIndex = AddMeshBuffers(vertices.Reinterpret(), indices); for (int i = 0; i < shapeMeshCount; i++) { var shape = shapeMeshes[i]; // Skip if the shape is not inside the bounds. // This is a more granular check than the one done by the OverlapArea call above, // since each collider may generate multiple shapes with different bounds. // This is particularly important for TilemapColliders which may generate a lot of shapes. if (!bounds.Intersects(shape.bounds)) continue; var coll = colliderBuffer[shape.tag]; (coll.attachedRigidbody as Component ?? coll).TryGetComponent(out var navmeshModifier); var rmesh = new GatheredMesh { meshDataIndex = bufferIndex, bounds = shape.bounds, indexStart = shape.startIndex, indexEnd = shape.endIndex, areaIsTag = false, // Colliders default to being unwalkable area = -1, solid = false, matrix = shape.matrix, doubleSided = true, flatten = true, }; if (navmeshModifier != null) { if (navmeshModifier.includeInScan == RecastNavmeshModifier.ScanInclusion.AlwaysExclude) continue; rmesh.ApplyNavmeshModifier(navmeshModifier); } else { rmesh.ApplyLayerModification(modificationsByLayer2D[coll.gameObject.layer]); } // 2D colliders are never solid rmesh.solid = false; meshes.Add(rmesh); } if (finiteBounds) ArrayPool.Release(ref colliderBuffer); shapeMeshes.Dispose(); } } }