Files
sbox-public/engine/Sandbox.Engine/Game/Navigation/Generation/NavMeshGenerator.cs
Lorenz Junglas 65524a3f6a Navmesh Polymesh Generation Optimizations (#3964)
Navmesh generation is split into two steps:
1. Heightfield/Voxelfield Generation
2. Polygon Mesh Generation

This PR focuses on Step 2.

Step 2 still needs to run even when loading a navmesh from disk, since during baking we write the compressed Heightfield rather than the final polygon mesh. Step 2 is the cheaper of the two steps and already quite fast, but while profiling I noticed some additional optimization potential.

This PR makes Step 2 roughly 20–30% faster, which will directly translate into faster navmesh loads from baked data. Looking at the whole pipeline (Step 1 + Step 2), I expect we'll see roughly a 10% improvement in our navmesh_gen benchmark.

Optimizations primarily include:
- Smarter caching of resources between tile generation runs
- Reducing algorithmic complexity (O(n²) → O(n)) in some hot paths (at the cost of a small amount of memory)
- Unrolling some loops
- Loads of micro-optimizations
2026-02-02 15:14:51 +00:00

99 lines
2.8 KiB
C#

namespace Sandbox.Navigation.Generation;
[SkipHotload]
class NavMeshGenerator : IDisposable
{
// Created in init disposed of after generate
private CompactHeightfield chfWorkingCopy;
private Config cfg;
public void Init( Config config, CompactHeightfield inputChf )
{
cfg = config;
if ( chfWorkingCopy == null )
{
chfWorkingCopy = inputChf.Copy();
}
else
{
// Reuse memory
inputChf.CopyTo( chfWorkingCopy );
}
}
public void Dispose()
{
if ( chfWorkingCopy != null )
{
chfWorkingCopy.Dispose();
chfWorkingCopy = null;
}
}
public void MarkArea( NavMeshAreaData area, int areaId )
{
var navTransform = NavMesh.ToNav( area.Transform );
var navBounds = NavMesh.ToNav( area.LocalBounds ).Transform( navTransform );
if ( area.Volume.Type == Volumes.SceneVolume.VolumeTypes.Box )
{
var navBox = NavMesh.ToNav( area.Volume.Box );
chfWorkingCopy.MarkBoxArea( navBox, navTransform, navBounds, areaId );
}
else if ( area.Volume.Type == Volumes.SceneVolume.VolumeTypes.Capsule )
{
var navCapsule = NavMesh.ToNav( area.Volume.Capsule );
chfWorkingCopy.MarkCapsuleArea( navCapsule, navTransform, navBounds, areaId );
}
else if ( area.Volume.Type == Volumes.SceneVolume.VolumeTypes.Sphere )
{
var navSpehere = NavMesh.ToNav( area.Volume.Sphere );
chfWorkingCopy.MarkSphereArea( navSpehere, navTransform, navBounds, areaId );
}
else if ( area.Volume.Type == Volumes.SceneVolume.VolumeTypes.Infinite )
{
var infiniteBounds = new BBox( new Vector3( float.MinValue ), new Vector3( float.MaxValue ) );
chfWorkingCopy.MarkBoxArea( infiniteBounds, Transform.Zero, infiniteBounds, areaId );
}
}
private List<int> prevCache = new( 512 );
// Cached pools for reuse across Generate() calls
private PolyMeshBuilder.PolyMeshBuilderContext polyMeshBuilderContext = new();
private RegionBuilder.RegionBuilderContext regionBuilderContext = new();
private ContourBuilder.ContourBuilderContext contourBuilderContext = new();
public PolyMesh Generate()
{
// According to recast docs good for tiles
if ( !RegionBuilder.BuildLayerRegions( chfWorkingCopy, cfg.BorderSize, cfg.MinRegionArea, prevCache, regionBuilderContext ) )
{
//Log.Warning( "buildNavigation: Could not build layer regions.\n" );
return null;
}
//
// Step 5. Trace and simplify region contours.
//
// Create contours.
ContourBuilder.BuildContours( chfWorkingCopy, cfg.MaxSimplificationError, cfg.MaxEdgeLen, contourBuilderContext );
if ( contourBuilderContext.ContourSet.Contours.Count == 0 )
{
//Log.Warning( "buildNavigation: No contours could be build for regions.\n" );
return null;
}
//
// Step 6. Build polygons mesh from contours.
//
// Build polygon navmesh from the contours.
return PolyMeshBuilder.BuildPolyMesh( contourBuilderContext.ContourSet, cfg.MaxVertsPerPoly, polyMeshBuilderContext );
}
}