mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-13 16:59:38 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
220 lines
6.8 KiB
C#
220 lines
6.8 KiB
C#
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<Vector3>.Shared.Return( inputGeoVertices );
|
|
inputGeoVertices = null;
|
|
}
|
|
|
|
if ( inputGeoIndices != null )
|
|
{
|
|
ArrayPool<int>.Shared.Return( inputGeoIndices );
|
|
inputGeoIndices = null;
|
|
}
|
|
}
|
|
|
|
public void Init( Config config )
|
|
{
|
|
inputGeoVertices ??= ArrayPool<Vector3>.Shared.Rent( 2048 );
|
|
inputGeoIndices ??= ArrayPool<int>.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<Vector3>.Shared.Rent( (inputGeoVerticesCount + triangulationVertArrCache.Count()) * 2 );
|
|
Array.Copy( inputGeoVertices, newVerts, inputGeoVerticesCount );
|
|
ArrayPool<Vector3>.Shared.Return( inputGeoVertices );
|
|
inputGeoVertices = newVerts;
|
|
}
|
|
if ( inputGeoIndicesCount + triangulationIndexArrCache.Count() > inputGeoIndices.Length )
|
|
{
|
|
var newIndices = ArrayPool<int>.Shared.Rent( (inputGeoIndicesCount + triangulationIndexArrCache.Count()) * 2 );
|
|
Array.Copy( inputGeoIndices, newIndices, inputGeoIndicesCount );
|
|
ArrayPool<int>.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<int>( 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;
|
|
}
|
|
}
|