using NativeEngine; using System.Buffers; using System.Runtime.InteropServices; namespace Sandbox.Navigation.Generation; [SkipHotload] class HeightFieldGenerator : IDisposable { internal Vector3[] inputGeoVertices = null; internal int[] inputGeoIndices = null; internal int inputGeoVerticesCount = 0; internal int inputGeoIndicesCount = 0; float inputGeometryMaxZ; float inputGeometryMinZ; Config cfg; Heightfield cachedHeightField; CUtlVectorVector triangulationVertArrCache = CUtlVectorVector.Create( 0, 512 ); CUtlVectorUInt32 triangulationIndexArrCache = CUtlVectorUInt32.Create( 0, 1024 ); public void Dispose() { cachedHeightField?.Dispose(); cachedHeightField = null; triangulationVertArrCache.DeleteThis(); triangulationIndexArrCache.DeleteThis(); if ( inputGeoVertices != null ) { ArrayPool.Shared.Return( inputGeoVertices ); inputGeoVertices = null; } if ( inputGeoIndices != null ) { ArrayPool.Shared.Return( inputGeoIndices ); inputGeoIndices = null; } } public void Init( Config config ) { inputGeoVertices ??= ArrayPool.Shared.Rent( 2048 ); inputGeoIndices ??= ArrayPool.Shared.Rent( 4096 ); inputGeometryMaxZ = float.MinValue; inputGeometryMinZ = float.MaxValue; cfg = config; } public void CollectGeometry( NavMesh navMesh, PhysicsWorld world, BBox tileBoundsWorld ) { ThreadSafe.AssertIsMainThread(); if ( !world.IsValid() ) return; var results = CQueryResult.Create(); world.native.Query( results, tileBoundsWorld, 0x07 ); // clear arrays inputGeoVerticesCount = 0; inputGeoIndicesCount = 0; for ( int i = 0; i < results.Count(); i++ ) { var shape = results.Element( i ); if ( !shape.IsValid() ) continue; var body = shape.Body; if ( !body.IsValid() ) continue; if ( !navMesh.IsBodyRelevantForNavmesh( body ) ) continue; AddGeometryFromPhysicsShape( shape ); } results.DeleteThis(); } internal void AddGeometryFromPhysicsShape( PhysicsShape shape ) { triangulationVertArrCache.SetCount( 0 ); triangulationIndexArrCache.SetCount( 0 ); shape.native.GetTriangulationForNavmesh( triangulationVertArrCache, triangulationIndexArrCache, cfg.Bounds ); if ( inputGeoVerticesCount + triangulationVertArrCache.Count() > inputGeoVertices.Length ) { var newVerts = ArrayPool.Shared.Rent( (inputGeoVerticesCount + triangulationVertArrCache.Count()) * 2 ); Array.Copy( inputGeoVertices, newVerts, inputGeoVerticesCount ); ArrayPool.Shared.Return( inputGeoVertices ); inputGeoVertices = newVerts; } if ( inputGeoIndicesCount + triangulationIndexArrCache.Count() > inputGeoIndices.Length ) { var newIndices = ArrayPool.Shared.Rent( (inputGeoIndicesCount + triangulationIndexArrCache.Count()) * 2 ); Array.Copy( inputGeoIndices, newIndices, inputGeoIndicesCount ); ArrayPool.Shared.Return( inputGeoIndices ); inputGeoIndices = newIndices; } int startV = inputGeoVerticesCount; for ( int i = 0; i < triangulationIndexArrCache.Count(); i += 3 ) { // invert winding and increment by startV var idx0 = triangulationIndexArrCache.Element( i + 0 ); var idx1 = triangulationIndexArrCache.Element( i + 1 ); var idx2 = triangulationIndexArrCache.Element( i + 2 ); inputGeoIndices[inputGeoIndicesCount++] = (int)idx0 + startV; inputGeoIndices[inputGeoIndicesCount++] = (int)idx2 + startV; inputGeoIndices[inputGeoIndicesCount++] = (int)idx1 + startV; } // Transform positions added to the vertex data var bodyTransform = shape.Body.Transform; for ( int v = 0; v < triangulationVertArrCache.Count(); ++v ) { var vertexWorldPos = bodyTransform.PointToWorld( triangulationVertArrCache.Element( v ) ); if ( vertexWorldPos.z > inputGeometryMaxZ ) inputGeometryMaxZ = vertexWorldPos.z; if ( vertexWorldPos.z < inputGeometryMinZ ) inputGeometryMinZ = vertexWorldPos.z; inputGeoVertices[inputGeoVerticesCount++] = NavMesh.ToNav( vertexWorldPos ); } } public CompactHeightfield Generate() { var nverts = inputGeoVerticesCount; var ntris = inputGeoIndicesCount / 3; // Nothing todo bail if ( nverts < 3 || ntris == 0 ) { return null; } // Ideally we should also never arrive here if ( cfg.TileSizeXY == 0 ) { Log.Warning( "buildNavigation: Empty tile provided.\n" ); // Bail nothing more todo return null; } // The geometry may not fully cover the vertical space of the tile. // By shrinking the bounds we can avoid precissions issues due to large tile height. // Make sure we are at least couple voxels high though cfg.Bounds.Maxs.z = inputGeometryMaxZ + cfg.CellHeight * 2; // add alittle bit extra to avoid precission issues cfg.Bounds.Mins.z = inputGeometryMinZ - cfg.CellHeight * 2; // add alittle bit extra to avoid precission issues // Set the area where the navigation will be build. var minBoundsNavSpace = NavMesh.ToNav( cfg.Bounds.Mins ); var maxBoundsNavSpace = NavMesh.ToNav( cfg.Bounds.Maxs ); // // Step 2. Rasterize input polygon soup. // // Allocate voxel heightfield where we rasterize our input data to. if ( cachedHeightField == null ) { cachedHeightField = new( cfg.TileSizeXY, cfg.TileSizeXY, minBoundsNavSpace, maxBoundsNavSpace, cfg.CellSize, cfg.CellHeight ); } else { // Reuse memory cachedHeightField.Init( cfg.TileSizeXY, cfg.TileSizeXY, minBoundsNavSpace, maxBoundsNavSpace, cfg.CellSize, cfg.CellHeight ); } using var pooledTriAreas = new PooledSpan( ntris ); var triAreas = pooledTriAreas.Span; InputFilter.MarkWalkableTriangles( cfg.WalkableSlopeAngle, inputGeoVertices.AsSpan( 0, inputGeoVerticesCount ), inputGeoIndices.AsSpan( 0, inputGeoIndicesCount ), triAreas ); Rasterization.RasterizeTriangles( inputGeoVertices.AsSpan( 0, inputGeoVerticesCount ), inputGeoIndices.AsSpan( 0, inputGeoIndicesCount ), triAreas, cachedHeightField, cfg.WalkableClimb ); cachedHeightField.EnsureCompressed(); if ( cachedHeightField.TotalSpanCount == 0 ) { return null; } // // Step 3. Filter walkables surfaces. // // Once all geometry is rasterized, we do initial pass of filtering to // remove unwanted overhangs caused by the conservative rasterization // as well as filter spans where the character cannot possibly stand. SpanFilter.Filter( cfg.WalkableHeight, cfg.WalkableClimb, cachedHeightField ); // // Step 4. Partition walkable surface to simple regions. // // Compact the heightfield so that it is faster to handle from now on. // This will result more cache coherent data as well as the neighbours // between walkable cells will be calculated. var chf = cachedHeightField.BuildCompactHeightfield( cfg.WalkableHeight, cfg.WalkableClimb ); // Erode the walkable area by agent radius. AreaFilter.ErodeWalkableArea( cfg.WalkableRadius, chf ); return chf; } }