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
NavMesh Components#
NavMeshSurface#
Tag component that marks geometry as walkable for NavMesh generation:
import { NavMeshSurface } from '@web-engine-dev/core/engine/ecs/components';import { addComponent } from 'bitecs'; // Mark a mesh as walkable for NavMeshaddComponent(world, NavMeshSurface, groundEid);NavMesh Generation#
Configuration#
NavMesh generation is controlled by these parameters:
| Parameter | Description | Default |
|---|---|---|
| cellSize | Voxel grid cell size (smaller = more detail) | 0.3 |
| cellHeight | Voxel grid cell height | 0.2 |
| agentHeight | Height of the agent | 2.0 |
| agentRadius | Radius of the agent | 0.5 |
| agentMaxClimb | Maximum step height | 0.5 |
| agentMaxSlope | Maximum slope angle (degrees) | 45 |
Generate NavMesh#
import { NavMeshGenerator, NavMeshConfig } from '@web-engine-dev/core/engine/ai'; // Initialize the generator (only once)await NavMeshGenerator.init(); // Configure NavMesh generationconst config: NavMeshConfig = { cellSize: 0.3, cellHeight: 0.2, agentHeight: 2.0, agentRadius: 0.5, agentMaxClimb: 0.5, agentMaxSlope: 45,}; // Generate NavMesh from sceneconst 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:
import { navMeshBridge } from '@web-engine-dev/core/engine/ai'; // Generate NavMesh in a workerconst 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#
import { navMeshBridge } from '@web-engine-dev/core/engine/ai';import * as THREE from 'three'; // Find a path between two pointsconst 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#
// Find nearest point on NavMeshconst nearestPoint = navMesh.getClosestPoint(position); // Check if position is on NavMeshconst isOnNavMesh = navMesh.isPointOnNavMesh(position, maxDistance);NavMesh Baking#
Manual Baking#
For runtime NavMesh updates, you can manually trigger baking:
// Extract geometry from sceneconst 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:
// Mark geometry as dirty when it changesfunction onGeometryChanged(eid: number) { // Queue for NavMesh rebuild rebuildQueue.add(eid);} // Rebuild NavMesh periodically or on demandfunction rebuildNavMesh() { if (rebuildQueue.size > 0) { // Re-extract and regenerate const navMesh = NavMeshGenerator.generate(scene, world, config); rebuildQueue.clear(); }}Dynamic Obstacles#
NavMesh Obstacle Component#
// 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#
import * as THREE from 'three'; // Get debug mesh from NavMeshconst 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#
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:
// Use worker for non-blocking generationconst result = await navMeshBridge.generate(positions, indices, config); // Show loading indicator during generationsetIsGenerating(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
NavMesh Quality#
- 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#
import { disposeNavMeshBridge, resetNavMeshBridge } from '@web-engine-dev/core/engine/ai'; // Clean up when donedisposeNavMeshBridge(); // Reset for scene changeresetNavMeshBridge();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#
import { NavMeshGenerator, navMeshBridge, NavMeshConfig,} from '@web-engine-dev/core/engine/ai';import { NavMeshSurface } from '@web-engine-dev/core/engine/ecs/components'; // 1. Initializeawait NavMeshGenerator.init(); // 2. Mark walkable geometryaddComponent(world, NavMeshSurface, groundEid);addComponent(world, NavMeshSurface, rampEid); // 3. Configure and generateconst 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 pathsconst 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 unloaddisposeNavMeshBridge();