2024-06-03 18:26:03 +00:00
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 ;
2024-11-28 23:07:50 +00:00
using Pathfinding.Collections ;
using Pathfinding.Pooling ;
2024-06-03 18:26:03 +00:00
using UnityEngine.Profiling ;
2024-11-28 23:07:50 +00:00
using Unity.Collections.LowLevel.Unsafe ;
2024-06-03 18:26:03 +00:00
[BurstCompile]
public class RecastMeshGatherer {
readonly int terrainDownsamplingFactor ;
public readonly LayerMask mask ;
public readonly List < string > tagMask ;
readonly float maxColliderApproximationError ;
public readonly Bounds bounds ;
public readonly UnityEngine . SceneManagement . Scene scene ;
Dictionary < MeshCacheItem , int > cachedMeshes = new Dictionary < MeshCacheItem , int > ( ) ;
readonly Dictionary < GameObject , TreeInfo > cachedTreePrefabs = new Dictionary < GameObject , TreeInfo > ( ) ;
readonly List < NativeArray < Vector3 > > vertexBuffers ;
readonly List < NativeArray < int > > triangleBuffers ;
readonly List < Mesh > 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 < GatheredMesh > meshes ;
List < Material > dummyMaterials = new List < Material > ( ) ;
public RecastMeshGatherer ( UnityEngine . SceneManagement . Scene scene , Bounds bounds , int terrainDownsamplingFactor , LayerMask mask , List < string > tagMask , List < RecastGraph . PerLayerModification > 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 < string > ( ) ;
this . maxColliderApproximationError = maxColliderApproximationError ;
this . scene = scene ;
meshes = ListPool < GatheredMesh > . Claim ( ) ;
vertexBuffers = ListPool < NativeArray < Vector3 > > . Claim ( ) ;
triangleBuffers = ListPool < NativeArray < int > > . Claim ( ) ;
cachedMeshes = ObjectPoolSimple < Dictionary < MeshCacheItem , int > > . Claim ( ) ;
meshData = ListPool < Mesh > . Claim ( ) ;
modificationsByLayer = RecastGraph . PerLayerModification . ToLayerLookup ( perLayerModifications , RecastGraph . PerLayerModification . Default ) ;
// 2D colliders default to being unwalkable
var default2D = RecastGraph . PerLayerModification . Default ;
2024-11-28 23:07:50 +00:00
default2D . mode = RecastNavmeshModifier . Mode . UnwalkableSurface ;
2024-06-03 18:26:03 +00:00
modificationsByLayer2D = RecastGraph . PerLayerModification . ToLayerLookup ( perLayerModifications , default2D ) ;
}
struct TreeInfo {
public List < GatheredMesh > submeshes ;
2024-11-28 23:07:50 +00:00
public Vector3 localScale ;
2024-06-03 18:26:03 +00:00
public bool supportsRotation ;
}
public struct MeshCollection : IArenaDisposable {
List < NativeArray < Vector3 > > vertexBuffers ;
List < NativeArray < int > > triangleBuffers ;
public NativeArray < RasterizationMesh > meshes ;
#if UNITY_EDITOR
public List < ( UnityEngine . Object , Mesh ) > meshesUnreadableAtRuntime ;
#endif
public MeshCollection ( List < NativeArray < Vector3 > > vertexBuffers , List < NativeArray < int > > triangleBuffers , NativeArray < RasterizationMesh > 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 < float3 > 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 < RasterizationMesh > ( this . meshes . Count , Allocator . Persistent ) ;
int meshBufferOffset = vertexBuffers . Count ;
2024-11-28 23:07:50 +00:00
Profiler . BeginSample ( "Copying vertices" ) ;
2024-06-03 18:26:03 +00:00
// 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 ) ;
}
2024-11-28 23:07:50 +00:00
Profiler . EndSample ( ) ;
2024-06-03 18:26:03 +00:00
2024-11-28 23:07:50 +00:00
Profiler . BeginSample ( "Creating RasterizationMeshes" ) ;
2024-06-03 18:26:03 +00:00
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 < float3 > ( ) . 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 ,
} ;
}
2024-11-28 23:07:50 +00:00
Profiler . EndSample ( ) ;
2024-06-03 18:26:03 +00:00
cachedMeshes . Clear ( ) ;
ObjectPoolSimple < Dictionary < MeshCacheItem , int > > . Release ( ref cachedMeshes ) ;
ListPool < GatheredMesh > . Release ( ref this . meshes ) ;
data . Dispose ( ) ;
return new MeshCollection (
vertexBuffers ,
triangleBuffers ,
meshes
#if UNITY_EDITOR
, this . meshesUnreadableAtRuntime
#endif
) ;
}
/// <summary>
/// Add vertex and triangle buffers that can later be used to create a <see cref="GatheredMesh"/>.
///
/// The returned index can be used in the <see cref="GatheredMesh.meshDataIndex"/> field of the <see cref="GatheredMesh"/> struct.
2024-11-28 23:07:50 +00:00
///
/// You can use the returned index multiple times with different matrices, to create instances of the same object in multiple locations.
2024-06-03 18:26:03 +00:00
/// </summary>
public int AddMeshBuffers ( Vector3 [ ] vertices , int [ ] triangles ) {
return AddMeshBuffers ( new NativeArray < Vector3 > ( vertices , Allocator . Persistent ) , new NativeArray < int > ( triangles , Allocator . Persistent ) ) ;
}
/// <summary>
/// Add vertex and triangle buffers that can later be used to create a <see cref="GatheredMesh"/>.
///
/// The returned index can be used in the <see cref="GatheredMesh.meshDataIndex"/> field of the <see cref="GatheredMesh"/> struct.
2024-11-28 23:07:50 +00:00
///
/// You can use the returned index multiple times with different matrices, to create instances of the same object in multiple locations.
2024-06-03 18:26:03 +00:00
/// </summary>
public int AddMeshBuffers ( NativeArray < Vector3 > vertices , NativeArray < int > triangles ) {
var meshDataIndex = - vertexBuffers . Count - 1 ;
vertexBuffers . Add ( vertices ) ;
triangleBuffers . Add ( triangles ) ;
return meshDataIndex ;
}
/// <summary>Add a mesh to the list of meshes to rasterize</summary>
public void AddMesh ( Renderer renderer , Mesh gatheredMesh ) {
if ( ConvertMeshToGatheredMesh ( renderer , gatheredMesh , out var gm ) ) {
meshes . Add ( gm ) ;
}
}
/// <summary>Add a mesh to the list of meshes to rasterize</summary>
public void AddMesh ( GatheredMesh gatheredMesh ) {
meshes . Add ( gatheredMesh ) ;
}
/// <summary>Holds info about a mesh to be rasterized</summary>
public struct GatheredMesh {
/// <summary>
/// Index in the meshData array.
/// Can be retrieved from the <see cref="RecastMeshGatherer.AddMeshBuffers"/> method.
/// </summary>
public int meshDataIndex ;
/// <summary>
/// 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.
/// </summary>
public int area ;
/// <summary>Start index in the triangle array</summary>
public int indexStart ;
/// <summary>End index in the triangle array. -1 indicates the end of the array.</summary>
public int indexEnd ;
/// <summary>World bounds of the mesh. Assumed to already be multiplied with the <see cref="matrix"/>.</summary>
public Bounds bounds ;
/// <summary>Matrix to transform the vertices by</summary>
public Matrix4x4 matrix ;
/// <summary>
/// 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.
/// </summary>
public bool solid ;
/// <summary>See <see cref="RasterizationMesh.doubleSided"/></summary>
public bool doubleSided ;
/// <summary>See <see cref="RasterizationMesh.flatten"/></summary>
public bool flatten ;
/// <summary>See <see cref="RasterizationMesh.areaIsTag"/></summary>
public bool areaIsTag ;
/// <summary>
/// Recalculate the <see cref="bounds"/> from the vertices.
///
/// The bounds will not be recalculated immediately.
/// </summary>
public void RecalculateBounds ( ) {
// This will cause the bounds to be recalculated later
bounds = new Bounds ( ) ;
}
2024-11-28 23:07:50 +00:00
public void ApplyNavmeshModifier ( RecastNavmeshModifier navmeshModifier ) {
area = AreaFromSurfaceMode ( navmeshModifier . mode , navmeshModifier . surfaceID ) ;
areaIsTag = navmeshModifier . mode = = RecastNavmeshModifier . Mode . WalkableSurfaceWithTag ;
solid | = navmeshModifier . solid ;
2024-06-03 18:26:03 +00:00
}
public void ApplyLayerModification ( RecastGraph . PerLayerModification modification ) {
area = AreaFromSurfaceMode ( modification . mode , modification . surfaceID ) ;
2024-11-28 23:07:50 +00:00
areaIsTag = modification . mode = = RecastNavmeshModifier . Mode . WalkableSurfaceWithTag ;
2024-06-03 18:26:03 +00:00
}
}
enum MeshType {
Mesh ,
Box ,
Capsule ,
}
struct MeshCacheItem : IEquatable < MeshCacheItem > {
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 < Renderer > ( out var rend ) ) {
if ( filter . sharedMesh ! = null & & rend . enabled & & ( ( ( 1 < < filter . gameObject . layer ) & mask ) ! = 0 | | ( tagMask . Count > 0 & & tagMask . Contains ( filter . tag ) ) ) ) {
2024-11-28 23:07:50 +00:00
if ( ! ( filter . TryGetComponent < RecastNavmeshModifier > ( out var rmo ) & & rmo . enabled ) ) {
2024-06-03 18:26:03 +00:00
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 < MeshFilter > ( ) ;
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 < Renderer > ( 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." ) ;
}
}
}
2024-11-28 23:07:50 +00:00
static int AreaFromSurfaceMode ( RecastNavmeshModifier . Mode mode , int surfaceID ) {
2024-06-03 18:26:03 +00:00
switch ( mode ) {
default :
2024-11-28 23:07:50 +00:00
case RecastNavmeshModifier . Mode . UnwalkableSurface :
2024-06-03 18:26:03 +00:00
return - 1 ;
2024-11-28 23:07:50 +00:00
case RecastNavmeshModifier . Mode . WalkableSurface :
2024-06-03 18:26:03 +00:00
return 0 ;
2024-11-28 23:07:50 +00:00
case RecastNavmeshModifier . Mode . WalkableSurfaceWithSeam :
case RecastNavmeshModifier . Mode . WalkableSurfaceWithTag :
2024-06-03 18:26:03 +00:00
return surfaceID ;
}
}
2024-11-28 23:07:50 +00:00
/// <summary>Find all relevant RecastNavmeshModifier components and create ExtraMeshes for them</summary>
public void CollectRecastNavmeshModifiers ( ) {
var buffer = ListPool < RecastNavmeshModifier > . Claim ( ) ;
2024-06-03 18:26:03 +00:00
2024-11-28 23:07:50 +00:00
// Get all recast navmesh modifiers inside the bounds
RecastNavmeshModifier . GetAllInBounds ( buffer , bounds ) ;
2024-06-03 18:26:03 +00:00
// Create an RasterizationMesh object
2024-11-28 23:07:50 +00:00
// for each RecastNavmeshModifier
2024-06-03 18:26:03 +00:00
for ( int i = 0 ; i < buffer . Count ; i + + ) {
2024-11-28 23:07:50 +00:00
AddNavmeshModifier ( buffer [ i ] ) ;
2024-06-03 18:26:03 +00:00
}
2024-11-28 23:07:50 +00:00
ListPool < RecastNavmeshModifier > . Release ( ref buffer ) ;
2024-06-03 18:26:03 +00:00
}
2024-11-28 23:07:50 +00:00
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 ;
2024-06-03 18:26:03 +00:00
2024-11-28 23:07:50 +00:00
navmeshModifier . ResolveMeshSource ( out var filter , out var collider , out var collider2D ) ;
2024-06-03 18:26:03 +00:00
if ( filter ! = null ) {
// Add based on mesh filter
Mesh mesh = filter . sharedMesh ;
if ( filter . TryGetComponent < MeshRenderer > ( out var rend ) & & mesh ! = null ) {
if ( ConvertMeshToGatheredMesh ( rend , filter . sharedMesh , out var gatheredMesh ) ) {
2024-11-28 23:07:50 +00:00
gatheredMesh . ApplyNavmeshModifier ( navmeshModifier ) ;
2024-06-03 18:26:03 +00:00
meshes . Add ( gatheredMesh ) ;
}
}
} else if ( collider ! = null ) {
// Add based on collider
if ( ConvertColliderToGatheredMesh ( collider ) is GatheredMesh rmesh ) {
2024-11-28 23:07:50 +00:00
rmesh . ApplyNavmeshModifier ( navmeshModifier ) ;
2024-06-03 18:26:03 +00:00
meshes . Add ( rmesh ) ;
}
} else if ( collider2D ! = null ) {
// 2D colliders are handled separately
} else {
2024-11-28 23:07:50 +00:00
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 ) ;
2024-06-03 18:26:03 +00:00
} else {
2024-11-28 23:07:50 +00:00
Debug . LogError ( "Couldn't get geometry source for RecastNavmeshModifier (" + navmeshModifier . gameObject . name + "). It didn't have a " + navmeshModifier . geometrySource + " attached" , navmeshModifier . gameObject ) ;
2024-06-03 18:26:03 +00:00
}
}
}
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" ) ;
2024-11-28 23:07:50 +00:00
bool anyTerrainChunks = GenerateTerrainChunks ( terrains [ j ] , bounds , desiredChunkSize ) ;
2024-06-03 18:26:03 +00:00
Profiler . EndSample ( ) ;
2024-11-28 23:07:50 +00:00
// Don't rasterize trees if the terrain did not intersect the graph bounds
if ( rasterizeTrees & & anyTerrainChunks ) {
2024-06-03 18:26:03 +00:00
Profiler . BeginSample ( "Find tree meshes" ) ;
// Rasterize all tree colliders on this terrain object
CollectTreeMeshes ( terrains [ j ] ) ;
Profiler . EndSample ( ) ;
}
}
}
}
2024-11-28 23:07:50 +00:00
static int NonNegativeModulus ( int x , int m ) {
int r = x % m ;
return r < 0 ? r + m : r ;
}
/// <summary>Returns ceil(lhs/rhs), i.e lhs/rhs rounded up</summary>
static int CeilDivision ( int lhs , int rhs ) {
return ( lhs + rhs - 1 ) / rhs ;
}
bool GenerateTerrainChunks ( Terrain terrain , Bounds bounds , float desiredChunkSize ) {
2024-06-03 18:26:03 +00:00
var terrainData = terrain . terrainData ;
if ( terrainData = = null )
throw new ArgumentException ( "Terrain contains no terrain data" ) ;
Vector3 offset = terrain . GetPosition ( ) ;
2024-11-28 23:07:50 +00:00
var terrainSize = terrainData . size ;
Vector3 center = offset + terrainSize * 0.5F ;
2024-06-03 18:26:03 +00:00
// Figure out the bounds of the terrain in world space
2024-11-28 23:07:50 +00:00
var terrainBounds = new Bounds ( center , terrainSize ) ;
2024-06-03 18:26:03 +00:00
// Only include terrains which intersects the graph
if ( ! terrainBounds . Intersects ( bounds ) )
2024-11-28 23:07:50 +00:00
return false ;
2024-06-03 18:26:03 +00:00
// Original heightmap size
int heightmapWidth = terrainData . heightmapResolution ;
int heightmapDepth = terrainData . heightmapResolution ;
// Size of a single sample
Vector3 sampleSize = terrainData . heightmapScale ;
2024-11-28 23:07:50 +00:00
sampleSize . y = terrainSize . y ;
2024-06-03 18:26:03 +00:00
// 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 ) ;
2024-11-28 23:07:50 +00:00
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" ) ;
2024-06-03 18:26:03 +00:00
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
) ;
2024-11-28 23:07:50 +00:00
Profiler . EndSample ( ) ;
unsafe {
var heightsSpan = new UnsafeSpan < float > ( heights , out var gcHandle1 ) ;
var holesSpan = new UnsafeSpan < bool > ( 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 ( ) ;
2024-06-03 18:26:03 +00:00
2024-11-28 23:07:50 +00:00
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 ) ;
}
2024-06-03 18:26:03 +00:00
}
2024-11-28 23:07:50 +00:00
UnsafeUtility . ReleaseGCObject ( gcHandle1 ) ;
UnsafeUtility . ReleaseGCObject ( gcHandle2 ) ;
}
return true ;
2024-06-03 18:26:03 +00:00
}
/// <summary>Generates a terrain chunk mesh</summary>
2024-11-28 23:07:50 +00:00
[BurstCompile]
public static void GenerateHeightmapChunk ( ref UnsafeSpan < float > heights , ref UnsafeSpan < bool > holes , int heightmapWidth , int heightmapDepth , int x0 , int z0 , int width , int depth , int stride , out UnsafeSpan < Vector3 > verts , out UnsafeSpan < int > tris ) {
2024-06-03 18:26:03 +00:00
// 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 ;
2024-11-28 23:07:50 +00:00
var numTris = ( resultWidth - 1 ) * ( resultDepth - 1 ) * 2 * 3 ;
verts = new UnsafeSpan < Vector3 > ( Allocator . Persistent , numVerts ) ;
tris = new UnsafeSpan < int > ( Allocator . Persistent , numTris ) ;
2024-06-03 18:26:03 +00:00
// 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 ) ;
2024-11-28 23:07:50 +00:00
verts [ z * resultWidth + x ] = new Vector3 ( sampleX , heights [ sampleZ * heightmapWidth + sampleX ] , sampleZ ) ;
2024-06-03 18:26:03 +00:00
}
}
// 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 ) ;
2024-11-28 23:07:50 +00:00
if ( holes [ sampleZ * ( heightmapWidth - 1 ) + sampleX ] ) {
2024-06-03 18:26:03 +00:00
// Not a hole, generate a mesh here
2024-11-28 23:07:50 +00:00
tris [ triangleIndex ] = z * resultWidth + x ;
tris [ triangleIndex + 1 ] = ( z + 1 ) * resultWidth + x + 1 ;
tris [ triangleIndex + 2 ] = z * resultWidth + x + 1 ;
2024-06-03 18:26:03 +00:00
triangleIndex + = 3 ;
2024-11-28 23:07:50 +00:00
tris [ triangleIndex ] = z * resultWidth + x ;
tris [ triangleIndex + 1 ] = ( z + 1 ) * resultWidth + x ;
tris [ triangleIndex + 2 ] = ( z + 1 ) * resultWidth + x + 1 ;
2024-06-03 18:26:03 +00:00
triangleIndex + = 3 ;
}
}
}
2024-11-28 23:07:50 +00:00
tris = tris . Slice ( 0 , triangleIndex ) ;
2024-06-03 18:26:03 +00:00
}
void CollectTreeMeshes ( Terrain terrain ) {
2024-11-28 23:07:50 +00:00
Profiler . BeginSample ( "Get tree data from terrain" ) ;
2024-06-03 18:26:03 +00:00
TerrainData data = terrain . terrainData ;
var treeInstances = data . treeInstances ;
var treePrototypes = data . treePrototypes ;
2024-11-28 23:07:50 +00:00
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 ] ;
2024-06-03 18:26:03 +00:00
// Make sure that the tree prefab exists
if ( prot . prefab = = null ) {
continue ;
}
if ( ! cachedTreePrefabs . TryGetValue ( prot . prefab , out TreeInfo treeInfo ) ) {
treeInfo . submeshes = new List < GatheredMesh > ( ) ;
// 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 < LODGroup > ( out var dummy ) ;
2024-11-28 23:07:50 +00:00
treeInfo . localScale = prot . prefab . transform . localScale ;
2024-06-03 18:26:03 +00:00
var colliders = ListPool < Collider > . 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 ) {
2024-11-28 23:07:50 +00:00
// For trees, we only suppport generating a mesh from a collider. So we ignore the navmeshModifier.geometrySource field.
if ( collider . gameObject . TryGetComponent < RecastNavmeshModifier > ( out var navmeshModifier ) & & navmeshModifier . enabled ) {
if ( navmeshModifier . includeInScan = = RecastNavmeshModifier . ScanInclusion . AlwaysExclude ) continue ;
2024-06-03 18:26:03 +00:00
2024-11-28 23:07:50 +00:00
mesh . ApplyNavmeshModifier ( navmeshModifier ) ;
2024-06-03 18:26:03 +00:00
} else {
mesh . ApplyLayerModification ( modificationsByLayer [ collider . gameObject . layer ] ) ;
}
2024-11-28 23:07:50 +00:00
// The bounds are incorrectly based on collider.bounds, and may not even be initialized by the physics system.
2024-06-03 18:26:03 +00:00
// 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 ( ) ;
2024-11-28 23:07:50 +00:00
2024-06-03 18:26:03 +00:00
treeInfo . submeshes . Add ( mesh ) ;
}
}
ListPool < Collider > . Release ( ref colliders ) ;
cachedTreePrefabs [ prot . prefab ] = treeInfo ;
}
2024-11-28 23:07:50 +00:00
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 ;
2024-06-03 18:26:03 +00:00
2024-11-28 23:07:50 +00:00
var treePosition = terrainPos + Vector3 . Scale ( instance . position , terrainSize ) ;
2024-06-03 18:26:03 +00:00
var instanceSize = new Vector3 ( instance . widthScale , instance . heightScale , instance . widthScale ) ;
2024-11-28 23:07:50 +00:00
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 ) ;
2024-06-03 18:26:03 +00:00
for ( int j = 0 ; j < treeInfo . submeshes . Count ; j + + ) {
var item = treeInfo . submeshes [ j ] ;
item . matrix = matrix * item . matrix ;
meshes . Add ( item ) ;
}
}
2024-11-28 23:07:50 +00:00
Profiler . EndSample ( ) ;
2024-06-03 18:26:03 +00:00
}
bool ShouldIncludeCollider ( Collider collider ) {
2024-11-28 23:07:50 +00:00
if ( ! collider . enabled | | collider . isTrigger | | ! collider . bounds . Intersects ( bounds ) | | ( collider . TryGetComponent < RecastNavmeshModifier > ( out var rmo ) & & rmo . enabled ) ) return false ;
2024-06-03 18:26:03 +00:00
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 < Collider > ( ) ;
numColliders = colliderBuffer . Length ;
} else {
do {
if ( colliderBuffer ! = null ) ArrayPool < Collider > . Release ( ref colliderBuffer ) ;
colliderBuffer = ArrayPool < Collider > . 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 < Collider > . Release ( ref colliderBuffer ) ;
}
/// <summary>
/// Box Collider triangle indices can be reused for multiple instances.
/// Warning: This array should never be changed
/// </summary>
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
} ;
/// <summary>
/// Box Collider vertices can be reused for multiple instances.
/// Warning: This array should never be changed
/// </summary>
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 ) ,
} ;
/// <summary>
/// Rasterizes a collider to a mesh.
/// This will pass the col.transform.localToWorldMatrix to the other overload of this function.
/// </summary>
GatheredMesh ? ConvertColliderToGatheredMesh ( Collider col ) {
return ConvertColliderToGatheredMesh ( col , col . transform . localToWorldMatrix ) ;
}
/// <summary>
/// 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.
/// </summary>
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 ;
}
/// <summary>
/// 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.
/// </summary>
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 < Vector3 > ( rows * cols + 2 , Allocator . Persistent ) ;
var tris = new NativeArray < int > ( 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
2024-11-28 23:07:50 +00:00
// always include the collider if it has an attached RecastNavmeshModifier, while
// 3D colliders handle RecastNavmeshModifier components separately.
2024-06-03 18:26:03 +00:00
if ( ( ( mask > > collider . gameObject . layer ) & 1 ) ! = 0 ) return true ;
2024-11-28 23:07:50 +00:00
if ( ( collider . attachedRigidbody as Component ? ? collider ) . TryGetComponent < RecastNavmeshModifier > ( out var rmo ) & & rmo . enabled & & rmo . includeInScan = = RecastNavmeshModifier . ScanInclusion . AlwaysInclude ) return true ;
2024-06-03 18:26:03 +00:00
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 < Collider2D > ( ) ;
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,
2024-11-28 23:07:50 +00:00
// but we cannot since a collider may have a RecastNavmeshModifier component
2024-06-03 18:26:03 +00:00
// 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 < Collider2D > . Release ( ref colliderBuffer ) ;
colliderBuffer = ArrayPool < Collider2D > . 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 < Vector3 > ( ) , 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 ] ;
2024-11-28 23:07:50 +00:00
( coll . attachedRigidbody as Component ? ? coll ) . TryGetComponent < RecastNavmeshModifier > ( out var navmeshModifier ) ;
2024-06-03 18:26:03 +00:00
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 ,
} ;
2024-11-28 23:07:50 +00:00
if ( navmeshModifier ! = null ) {
if ( navmeshModifier . includeInScan = = RecastNavmeshModifier . ScanInclusion . AlwaysExclude ) continue ;
rmesh . ApplyNavmeshModifier ( navmeshModifier ) ;
2024-06-03 18:26:03 +00:00
} else {
rmesh . ApplyLayerModification ( modificationsByLayer2D [ coll . gameObject . layer ] ) ;
}
// 2D colliders are never solid
rmesh . solid = false ;
meshes . Add ( rmesh ) ;
}
if ( finiteBounds ) ArrayPool < Collider2D > . Release ( ref colliderBuffer ) ;
shapeMeshes . Dispose ( ) ;
}
}
}