navcat

repository·main·Indexed 42 days ago

https://github.com/isaac-mason/navcat

An engine-agnostic JavaScript library for constructing and querying 3D floor-based navigation meshes. It provides tools for navmesh generation, pathfinding (including smooth and node-based paths), spatial queries like raycasting and snapping, and a high-level crowd API for agent simulation. Designed for use with engines such as Three.js, Babylon.js, and PlayCanvas, navcat v0.4.1 supports both single and multi-tile meshes with full JSON serialization.

Tokens
27.8K
Snippets
67
Records
89
Agent score
54%

What's inside navcat

  1. Overview of navcat features

    main

    navcat is a javascript navigation mesh construction and querying library for 3D floor-based navigation. It is designed for use in games, simulations, and creative websites.

    Key capabilities include:

    • Navigation mesh generation: Create meshes from 3D geometry.
    • Navigation mesh querying: Perform pathfinding and spatial queries.
    • Mesh support: Supports both single and multi-tile navigation meshes.
    • Serialization: All data structures are fully JSON serializable.
    • Engine Agnostic: Works with any javascript engine or library, including Babylon.js, PlayCanvas, Three.js, or custom engines.
  2. What is a Navigation Mesh?

    main
    A navigation mesh (or navmesh) is a simplified representation of a 3D environment used for pathfinding and AI navigation. It consists of interconnected polygons that define walkable areas. These polygons are connected by edges and off-mesh connections, allowing agents to move from one polygon to another.
  3. Implement custom pathfinding algorithms

    main

    When built-in high-level APIs do not meet your requirements, navcat provides low-level access to the navigation mesh structure. This allows developers to implement custom pathfinding algorithms in userland.

    Common patterns for custom pathfinding demonstrated in navcat examples include:

    • Flow Field Pathfinding
    • Finding Shortest Paths
    • Finding Diverse Paths

    To implement these, you should use the Advanced Navigation Mesh APIs to interact directly with the mesh structure.

  4. Optimize agent movement using `findNodePath` and `findStraightPath`

    main

    For dynamic agent movement, instead of calling the heavy findPath repeatedly, you can split the process into two steps to improve efficiency:

    1. findNodePath: Finds a sequence of polygon and offmesh connection node references. You can cache this node path and only recalculate it if the destination changes.
    2. findStraightPath: Performs 'string pulling' on the cached node path to convert it into actual waypoint positions. This can be called frequently (e.g., every frame) as the agent's start position changes.

    When to use: Use this pattern for high-performance agent steering where the destination is relatively stable but the agent is constantly moving.

    // 1. Find the node path (can be cached)
    const nodePath = Nav.findNodePath(
        navMesh,
        startNode.nodeRef,
        endNode.nodeRef,
        startNode.position,
        endNode.position,
        Nav.DEFAULT_QUERY_FILTER
    );
    
    // 2. Find the actual waypoints (call frequently as agent moves)
    if (nodePath.success) {
        const straightPathResult = Nav.findStraightPath(
            navMesh, 
            currentPosition, 
            destinationPosition, 
            nodePath.path
        );
    }
  5. Understand Navigation Mesh structures: Single-Tile vs Tiled

    main

    A navigation mesh is a graph of nodes (polygons) and links (connections) used for pathfinding. It is organized into one or more tiles.

    Single-Tile Navmesh

    • Best for: Small, static scenes.
    • Pros: Simpler to implement and manage.

    Tiled Navmesh

    • Best for: Large worlds, dynamic environments, or memory-constrained systems.
    • Pros:
      • Dynamic updates: Rebuild only specific tiles when geometry changes.
      • Memory management: Stream tiles in/out based on player location.
      • Parallel generation: Generate tiles independently.
      • Large worlds: Better results over vast areas.

    Warning: If you remove and re-add tiles at specific coordinates at runtime, existing node references for polygons will become invalid. Always use isValidNodeRef to verify a node reference before using it in custom pathfinding logic.

  6. How navcat compares to recast-navigation-js

    main

    When choosing between navcat and recast-navigation-js, consider the following trade-offs:

    Advantages of navcat

    • Bundle Size & Tree-shaking: navcat is a pure JavaScript library that is highly tree-shakeable, whereas recast-navigation-js has a large bundle size due to its WASM binary and Emscripten glue code.
    • Off-Mesh Connections: navcat allows adding/removing off-mesh connections dynamically without regenerating tiles. Connections can be created between any tiles (not just neighbors). It also provides a crowd API with animation hooks.
    • Query Filters: Filters in navcat are regular JavaScript functions, offering more flexibility than the area cost tables or include/exclude flags used in Recast.
    • External NavMeshes: navcat supports pathfinding with externally provided navigation mesh polys.
    • Extensibility & Debugging: navcat uses transparent JavaScript data structures, making it easy to write custom traversal algorithms or debug using standard JS tools. recast-navigation-js uses WASM-wrapped C++ objects which are harder to inspect.
    • Memory Management: navcat uses regular JS objects and requires no manual memory management (unlike recast-navigation-js which requires calling .destroy() on certain objects).

    Advantages of recast-navigation-js

    • Performance: recast-navigation-js typically offers better performance for runtime navmesh generation and querying. However, navcat is highly performant for pathfinding and suitable for many dynamic generation use cases.
  7. Convert build-time meshes to runtime navmesh tile format

    main

    After generating a PolyMesh and PolyMeshDetail at build-time, you must convert them into a runtime-optimized format. This involves computing polygon adjacency and mapping the data to the NavMeshTile format.

    Use Nav.polyMeshToTilePolys to convert the polygon mesh, and Nav.polyMeshDetailToTileDetailMesh to convert the detail mesh using the newly converted polygons.

    // convert the poly mesh to a navmesh tile polys
    const tilePolys = Nav.polyMeshToTilePolys(polyMesh);
    
    // convert the poly mesh detail to a navmesh tile detail mesh
    const tileDetailMesh = Nav.polyMeshDetailToTileDetailMesh(tilePolys.polys, polyMeshDetail);
  8. Quick Start: Generate and query a NavMesh

    main

    To get started, use the generateSoloNavMesh function from navcat/blocks to create a navigation mesh from geometry, and then use findPath from navcat to perform pathfinding.

    Workflow:

    1. Prepare positions (Float32Array) and indices (Uint32Array) from your level geometry.
    2. Define SoloNavMeshInput containing your geometry.
    3. Configure SoloNavMeshOptions (see parameter table below).
    4. Call generateSoloNavMesh(input, options) to get the navMesh.
    5. Use findPath(navMesh, start, end, halfExtents, filter) to find a path.
    import { DEFAULT_QUERY_FILTER, findPath, type Vec3 } from 'navcat';
    import { generateSoloNavMesh, type SoloNavMeshInput, type SoloNavMeshOptions } from 'navcat/blocks';
    
    /* generation input */
    const positions = new Float32Array([/* ... */]);
    const indices = new Uint32Array([/* ... */]);
    
    const input: SoloNavMeshInput = {
        positions,
        indices,
    };
    
    /* generation options */
    const options: SoloNavMeshOptions = {
        cellSize: 0.15,
        cellHeight: 0.25,
        walkableRadiusWorld: 0.3,
        walkableRadiusVoxels: Math.ceil(0.3 / 0.15),
        walkableClimbWorld: 0.5,
        walkableClimbVoxels: Math.ceil(0.5 / 0.25),
        walkableHeightWorld: 2.0,
        walkableHeightVoxels: Math.ceil(2.0 / 0.25),
        walkableSlopeAngleDegrees: 45,
        borderSize: 0,
        minRegionArea: 8,
        mergeRegionArea: 20,
        maxSimplificationError: 1.3,
        maxEdgeLength: 12,
        maxVerticesPerPoly: 5,
        detailSampleDistance: 0.15 * 6,
        detailSampleMaxError: 0.25 * 1,
    };
    
    /* generate the navmesh */
    const result = generateSoloNavMesh(input, options);
    const navMesh = result.navMesh;
    
    /* find a path */
    const start: Vec3 = [-4, 0, -4];
    const end: Vec3 = [4, 0, 4];
    const halfExtents: Vec3 = [0.5, 0.5, 0.5];
    
    const path = findPath(navMesh, start, end, halfExtents, DEFAULT_QUERY_FILTER);
    
    console.log('path:', path.path.map((p) => p.position));
  9. Build compact heightfield regions

    main

    After building the CompactHeightfield, you must partition the walkable surface into distinct regions.

    Steps:

    1. Calculate Distance Field: Call buildDistanceField(compactHeightfield) to prepare for partitioning.
    2. Partition Regions: Use one of the following algorithms:
      • buildRegions: A watershed-based algorithm. Requires borderSize, minRegionArea, and mergeRegionArea.
      • buildRegionsMonotone: A sweep-line algorithm that creates regions by sweeping the heightfield. It does not generate overlapping regions and is recommended for multi-agent erosion workflows.
      • buildLayerRegions: A sweep-line algorithm that creates layers to handle overlapping walkable areas.
    // 1. Prepare distance field
    Nav.buildDistanceField(compactHeightfield);
    
    // 2. Partition regions
    const borderSize = 0;
    const minRegionArea = 8;
    const mergeRegionArea = 20;
    
    Nav.buildRegions(ctx, compactHeightfield, borderSize, minRegionArea, mergeRegionArea);
  10. Use externally created navigation meshes

    main

    If you author your own navigation mesh or generate it with external tools, you can import it into navcat using these utilities:

    1. Convert Polygons: Use polygonsToNavMeshTilePolys to convert external polygon data into the navcat runtime tile format.
    2. Generate Detail Mesh: Use polysToTileDetailMesh to generate a detail mesh for your polygons, or provide your own detail triangle mesh if you have height data.
  11. Use Generation Presets for quick setup

    main

    The navcat/blocks entrypoint provides high-level presets that bundle the common steps of navigation mesh generation. These are ideal for getting started quickly without manually managing the voxelization pipeline.

    • generateSoloNavMesh: For single-tile meshes.
    • generateTiledNavMesh: For tiled meshes.

    As your requirements become more complex, you can 'eject' from these presets by copying the logic into your own project and modifying it.

  12. Prepare input geometry for NavMesh generation

    main

    Navigation mesh generation requires 3D triangles representing collision surfaces.

    Requirements:

    • Coordinate System: Must follow OpenGL conventions (right-handed coordinate system, counter-clockwise winding order).
    • Geometry: Use only collidable surfaces; exclude non-collidable decorative geometry.
    • Data Format: Vertices should be a flat array of positions [x1, y1, z1, x2, y2, z2, ...] and indices should be a flat array of triangle vertex indices.

    To capture diagnostic messages, warnings, and errors during the process, use a BuildContext.

    import * as Nav from 'navcat';
    
    // flat array of vertex positions [x1, y1, z1, x2, y2, z2, ...]
    const positions: number[] = [];
    
    // flat array of triangle vertex indices
    const indices: number[] = [];
    
    // build context to capture diagnostic messages, warnings, and errors
    const ctx = Nav.BuildContext.create();