Navigation Mesh (NavMesh)

Generate navigation meshes from scene geometry for accurate AI pathfinding and navigation.

Overview#

Web Engine uses Recast/Detour for navigation mesh generation. NavMesh provides:

  • Automatic mesh generation from scene geometry
  • Configurable agent parameters (height, radius, max climb, slope)
  • Worker-based generation for non-blocking performance
  • A* pathfinding queries with path smoothing
  • Dynamic obstacle support
  • Debug visualization

Tag component that marks geometry as walkable for NavMesh generation:

navmesh-surface.ts
typescript
import { NavMeshSurface } from '@web-engine-dev/core/engine/ecs/components';
import { addComponent } from 'bitecs';
// Mark a mesh as walkable for NavMesh
addComponent(world, NavMeshSurface, groundEid);

Configuration#

NavMesh generation is controlled by these parameters:

ParameterDescriptionDefault
cellSizeVoxel grid cell size (smaller = more detail)0.3
cellHeightVoxel grid cell height0.2
agentHeightHeight of the agent2.0
agentRadiusRadius of the agent0.5
agentMaxClimbMaximum step height0.5
agentMaxSlopeMaximum slope angle (degrees)45

Generate NavMesh#

generate-navmesh.ts
typescript
import { NavMeshGenerator, NavMeshConfig } from '@web-engine-dev/core/engine/ai';
// Initialize the generator (only once)
await NavMeshGenerator.init();
// Configure NavMesh generation
const config: NavMeshConfig = {
cellSize: 0.3,
cellHeight: 0.2,
agentHeight: 2.0,
agentRadius: 0.5,
agentMaxClimb: 0.5,
agentMaxSlope: 45,
};
// Generate NavMesh from scene
const navMesh = NavMeshGenerator.generate(scene, world, config);
if (!navMesh) {
console.error('NavMesh generation failed');
}

Worker-Based Generation#

For large scenes, use the NavMesh worker bridge for non-blocking generation:

worker-generation.ts
typescript
import { navMeshBridge } from '@web-engine-dev/core/engine/ai';
// Generate NavMesh in a worker
const result = await navMeshBridge.generate(positions, indices, config);
if (result.success && result.navMesh) {
console.log('NavMesh generated successfully');
// Optionally get debug mesh for visualization
if (result.debugMesh) {
const { positions, indices } = result.debugMesh;
// Create THREE.Mesh for debug rendering
}
} else {
console.error('NavMesh generation failed:', result.error);
}

Path Queries#

Find Path#

find-path.ts
typescript
import { navMeshBridge } from '@web-engine-dev/core/engine/ai';
import * as THREE from 'three';
// Find a path between two points
const start = new THREE.Vector3(0, 0, 0);
const end = new THREE.Vector3(10, 0, 10);
const pathResult = await navMeshBridge.findPath(start, end);
if (pathResult.success && pathResult.path) {
// Path is array of {x, y, z} points
const waypoints = pathResult.path.map(p =>
new THREE.Vector3(p.x, p.y, p.z)
);
console.log(`Found path with ${waypoints.length} waypoints`);
} else {
console.error('Pathfinding failed:', pathResult.error);
}

Nearest Point Query#

nearest-point.ts
typescript
// Find nearest point on NavMesh
const nearestPoint = navMesh.getClosestPoint(position);
// Check if position is on NavMesh
const isOnNavMesh = navMesh.isPointOnNavMesh(position, maxDistance);

Manual Baking#

For runtime NavMesh updates, you can manually trigger baking:

manual-baking.ts
typescript
// Extract geometry from scene
const geometry = NavMeshGenerator.extractGeometry(scene, world);
if (geometry) {
const { positions, indices } = geometry;
// Generate NavMesh
const navMesh = NavMeshGenerator.generate(scene, world, config);
if (navMesh) {
console.log('NavMesh baked successfully');
}
}

Incremental Updates#

For dynamic environments, consider re-baking affected regions:

incremental-updates.ts
typescript
// Mark geometry as dirty when it changes
function onGeometryChanged(eid: number) {
// Queue for NavMesh rebuild
rebuildQueue.add(eid);
}
// Rebuild NavMesh periodically or on demand
function rebuildNavMesh() {
if (rebuildQueue.size > 0) {
// Re-extract and regenerate
const navMesh = NavMeshGenerator.generate(scene, world, config);
rebuildQueue.clear();
}
}

Dynamic Obstacles#

obstacles.ts
typescript
// Add dynamic obstacle (future API)
// Note: Dynamic obstacles require NavMesh tilecache
// Currently in development
import { NavMeshObstacle } from '@web-engine-dev/core/engine/ecs/components';
addComponent(world, NavMeshObstacle, obstacleEid);
NavMeshObstacle.radius[obstacleEid] = 1.0;
NavMeshObstacle.height[obstacleEid] = 2.0;

Debug Visualization#

Render NavMesh#

debug-render.ts
typescript
import * as THREE from 'three';
// Get debug mesh from NavMesh
const debugMeshData = navMesh.getDebugNavMesh?.();
if (debugMeshData) {
const { positions, indices } = debugMeshData;
// Create geometry
const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
);
geometry.setIndex(
new THREE.BufferAttribute(indices, 1)
);
// Create mesh
const material = new THREE.MeshBasicMaterial({
color: 0x00ff00,
wireframe: true,
transparent: true,
opacity: 0.5,
});
const debugMesh = new THREE.Mesh(geometry, material);
scene.add(debugMesh);
}

Visualize Path#

visualize-path.ts
typescript
function visualizePath(waypoints: THREE.Vector3[]) {
const points = waypoints.map(p => new THREE.Vector3(p.x, p.y, p.z));
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color: 0xff0000,
linewidth: 2,
});
const line = new THREE.Line(geometry, material);
scene.add(line);
return line;
}

Performance Considerations#

Cell Size#

Smaller cell sizes produce more accurate NavMesh but increase generation time and memory:

  • 0.1-0.2 — Very high detail, slow generation, large memory
  • 0.3-0.5 — Good balance for most games (recommended)
  • 0.6-1.0 — Fast generation, lower detail, small memory

Agent Radius#

Agent radius erodes the NavMesh to ensure agents stay away from walls. Set it to match your agent's collision radius for accurate pathfinding.

Async Generation#

Always use worker-based generation for large scenes to avoid blocking the main thread:

async-generation.ts
typescript
// Use worker for non-blocking generation
const result = await navMeshBridge.generate(positions, indices, config);
// Show loading indicator during generation
setIsGenerating(true);
const result = await navMeshBridge.generate(positions, indices, config);
setIsGenerating(false);

Best Practices#

Geometry Preparation#

  • Only include walkable surfaces (floors, ramps, stairs)
  • Use NavMeshSurface component to mark geometry explicitly
  • Ensure geometry is indexed (Recast requires indexed geometry)
  • Apply world transforms before extraction
  • Use cellSize about 1/3 to 1/6 of agent radius
  • Set agentMaxClimb to match stairs/step height
  • Adjust agentMaxSlope based on terrain steepness
  • Test paths with different agent sizes

Memory Management#

cleanup.ts
typescript
import { disposeNavMeshBridge, resetNavMeshBridge } from '@web-engine-dev/core/engine/ai';
// Clean up when done
disposeNavMeshBridge();
// Reset for scene change
resetNavMeshBridge();

Common Issues#

NavMesh Generation Fails#

  • Check that geometry has NavMeshSurface component
  • Ensure geometry is indexed (has geometry.index)
  • Verify NavMeshGenerator.init() was called
  • Check browser console for Recast errors

Path Not Found#

  • Verify start and end positions are on NavMesh
  • Use getClosestPoint() to snap positions to NavMesh
  • Check if areas are connected (not separated by obstacles)
  • Increase search distance for edge cases

Performance Issues#

  • Increase cellSize to reduce detail
  • Use worker-based generation
  • Limit geometry to essential walkable surfaces
  • Cache NavMesh instead of regenerating frequently

Example: Complete NavMesh Setup#

complete-setup.ts
typescript
import {
NavMeshGenerator,
navMeshBridge,
NavMeshConfig,
} from '@web-engine-dev/core/engine/ai';
import { NavMeshSurface } from '@web-engine-dev/core/engine/ecs/components';
// 1. Initialize
await NavMeshGenerator.init();
// 2. Mark walkable geometry
addComponent(world, NavMeshSurface, groundEid);
addComponent(world, NavMeshSurface, rampEid);
// 3. Configure and generate
const config: NavMeshConfig = {
cellSize: 0.3,
cellHeight: 0.2,
agentHeight: 2.0,
agentRadius: 0.5,
agentMaxClimb: 0.5,
agentMaxSlope: 45,
};
const geometry = NavMeshGenerator.extractGeometry(scene, world);
if (!geometry) {
throw new Error('No walkable geometry found');
}
const result = await navMeshBridge.generate(
geometry.positions,
geometry.indices,
config
);
if (!result.success) {
throw new Error(`NavMesh generation failed: ${result.error}`);
}
// 4. Find paths
const pathResult = await navMeshBridge.findPath(start, end);
if (pathResult.success && pathResult.path) {
const waypoints = pathResult.path.map(p =>
new THREE.Vector3(p.x, p.y, p.z)
);
// Use waypoints for AI navigation
setBlackboardValue(blackboardId, 'path', waypoints);
}
// 5. Cleanup on scene unload
disposeNavMeshBridge();
Documentation | Web Engine