NavMesh Generation - MeFisto94/jme3-recast4j-demo GitHub Wiki

The jme3-recast4j implementation for jMonkeyEngine is very robust and offers you a plethora of ways to build your navigation mesh. This wiki is not a definitive guide for doing so. If you posess a strong knowledge of the jMonkeyEngine and java, you can use the tests found under Additional Resources to customize the process explained here.

The basic steps involved in generating a NavMesh are as follows.

  • Decide if you want a solo or tiled NavMesh.

  • Create an JmeInputGeomProvider object and gather your geometry with GeometryProviderBuilder. Assets can be any of the following types.

    • Node.

    • Geometry

    • Mesh - scaled or un-scaled.

  • Create the Recast configuration that will be passed to the RecastConfigBuilder.

  • Build the parameters with the configuration object and set any flags using RecastBuilder.

    • Tiled

    • Solo

  • Build the MeshData from the parameters object.

  • Build the NavMesh from the MeshData.

  • Save the NavMesh or MeshData.

Solo or Tiled NavMesh

You can build a solo NavMesh or break it up into tiles for tile cache loading. Solo NavMeshes are actually just one large tile vs many smaller tiles. Solo NavMeshes are obsolete and provide no benefits over a tiled NavMesh. You can build any tile on the fly, where a smaller tile size makes sense, but small tile sizes may lead to longer build times for the entire NavMesh. Both types are covered in this wiki.

There are some limitations which may help you decide which to use. Polygons have their vertex indices stored as int in Recast4j, however, Recast used unsigned short, which meant that Recast could hold up to 65536 vertices per each navigation mesh tile where Recast4j would allow two billion. The detail meshes index polygons used unsigned short also, so in practice that is the max number of NavMesh polygons per tile Recast could use, where once again, Recast4j is two billion. The NavMeshQuery object for path finding has these same limitations. Taking all this into consideration, limit your vertices/polygons per tile to 32768, i.e. short value. If you need to have more polygons or verts, then you should split your world into tiles.

If you have a really big world, be aware of floating-point accuracy. Detour works well when coordinates are in the range of -4000..4000, although you may get some inaccuracies at those extremes.

Solo NavMesh

Box boxMesh = new Box(20f,.1f,20f);
Geometry boxGeo = new Geometry("Colored Box", boxMesh);
Material boxMat = new Material(getApplication().getAssetManager(), "Common/MatDefs/Light/Lighting.j3md");
boxMat.setBoolean("UseMaterialColors", true);
boxMat.setColor("Ambient", ColorRGBA.LightGray);
boxMat.setColor("Diffuse", ColorRGBA.LightGray);
boxGeo.setMaterial(boxMat);
((SimpleApplication) getApplication()).getRootNode().attachChild(boxGeo);

//Step 1. Gather our geometry.
JmeInputGeomProvider geomProvider = new GeometryProviderBuilder(boxGeo).build();
//Step 2. Create a Recast configuration object. Start by creating the builder.
RecastConfigBuilder builder = new RecastConfigBuilder();
//Instantiate the configuration parameters.
RecastConfig cfg = builder
        .withAgentRadius(0.4f)              // r
        .withAgentHeight(2.0f)              // h
        //cs and ch should be .1 at min.
        .withCellSize(0.2f)                 // cs=r/2
        .withCellHeight(0.1f)               // ch=cs/2 but not < .1f
        .withAgentMaxClimb(0.3f)            // > 2*ch
        .withAgentMaxSlope(45f)
        .withEdgeMaxLen(3.2f)               // r*8
        .withEdgeMaxError(1.3f)             // 1.1 - 1.5
        .withDetailSampleDistance(6.0f)     // increase if exception
        .withDetailSampleMaxError(5.0f)     // increase if exception
        .withVertsPerPoly(3).build();
//Create a RecastBuilderConfig builder with world bounds of our geometry.
RecastBuilderConfigBuilder rcb = new RecastBuilderConfigBuilder(boxGeo);
//Build the configuration object using our cfg.
RecastBuilderConfig bcfg = rcb.withDetailMesh(true).build(cfg);
//Step 3. Build our Navmesh data using our gathered geometry and configuration.
//This is where we decide if this is a solo NavMesh build or tiled.
//Tiled will be covered later.
RecastBuilder rb = new RecastBuilder();
RecastBuilderResult rbr = rb.build(geomProvider, bcfg);
//Set the parameters needed to build our MeshData using the RecastBuilder results.
NavMeshDataCreateParamsBuilder paramBuilder = new NavMeshDataCreateParamsBuilder(rbr);
//Update poly flags from areas. Set any flags here.
PolyMesh pmesh = paramBuilder.getPolyMesh();
for (int i = 0; i < pmesh.npolys; ++i) {
    if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GROUND
      || pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GRASS
      || pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD) {
        paramBuilder.withPolyFlag(i, SampleAreaModifications.SAMPLE_POLYFLAGS_WALK);
    } else if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER) {
        paramBuilder.withPolyFlag(i, SampleAreaModifications.SAMPLE_POLYFLAGS_SWIM);
    } else if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_DOOR) {
        paramBuilder.withPolyFlags(i, SampleAreaModifications.SAMPLE_POLYFLAGS_WALK
        | SampleAreaModifications.SAMPLE_POLYFLAGS_DOOR);
    }
}
//Build the parameter object.
NavMeshDataCreateParams params = paramBuilder.build(bcfg);
//Step 4. Generate MeshData using our parameters object.
MeshData meshData = NavMeshBuilder.createNavMeshData(params);
//Step 5. Build the NavMesh.
NavMesh navMesh = new NavMesh(meshData, bcfg.cfg.maxVertsPerPoly, 0);

try {
    //Step 6. Save our work. Using compressed format.
    MeshDataWriter mdw = new MeshDataWriter();
    mdw.write(new FileOutputStream(new File("myMeshData.md")),  meshData, ByteOrder.BIG_ENDIAN, false);
    //Or the native format using tiles.
    MeshSetWriter msw = new MeshSetWriter();
    msw.write(new FileOutputStream(new File("myNavMesh.nm")), navMesh, ByteOrder.BIG_ENDIAN, false);

    //Read in saved MeshData and build new NavMesh.
    MeshDataReader mdr = new MeshDataReader();
    MeshData savedMeshData = mdr.read(new FileInputStream("myMeshData.md"), 3);
    NavMesh navMeshFromData = new NavMesh(savedMeshData, 3, 0);
    //Or read in saved NavMesh.
    MeshSetReader msr = new MeshSetReader();
    NavMesh navMeshFromSaved = msr.read(new FileInputStream("myNavMesh.nm"), 3);
  } catch (IOException ex) {
  LOG.info("{} {}", CrowdBuilderState.class.getName(), ex);
}

Gathering Geometries

//Step 1. Gather our geometry.
JmeInputGeomProvider geomProvider = new GeometryProviderBuilder(boxGeo).build();

How you get your assets for the build process is entirely up to your personal preference. Typically, you will load your scaled, translated, rotated game assets as usual with the assetManager. You then use GeometryProviderBuilder to gather geometries together and merge them into one mesh using one of the constructors for the class or supply the final mesh yourself. You use the returned JmeInputGeomProvider to get your min and max bounds from the merged mesh to pass to RecastBuilder. The object also contains a list of the geometry.

NavMesh building is not going to be included in your release build. Once the NavMeshes are generated you will save them, as is described in the Saving and Loading topic, and remove any code related to the build process.

There is no static pipeline for doing any of this. It all depends on your asset type and how you wish to do things.

The various constructors are explained below.

Mesh
public GeometryProviderBuilder(Mesh m)

With this constructor, you provide the mesh. There will be no merging done. Mesh does not contain translation, rotation, or scale information so you should only use this on objects that do not require it. It’s recommended that you use one of other constructors if you are doing any of these.

Geometry
public GeometryProviderBuilder(Geometry g)

This constructor will gather any geometries under geometry (g) into a list and merge them for you. This means if the geometry is broken up into smaller geometries by material, they will all be merged into one for NavMesh purposes. Keep in mind, none of this affects the original geometries.

Node
public GeometryProviderBuilder(Node n, Predicate<Spatial> filter)

Provides this Node to the Builder and performs a search through the SceneGraph to gather all Geometries under the Node. You supply a Filter (analogue to the Java 8 Stream API) which defines when a Spatial should be gathered.

Node
public GeometryProviderBuilder(Node n)

Provides this Node to the Builder and performs a search through the SceneGraph to gather all Geometries under the node. This uses the default filter: If userData "no_collission" is set, ignore this spatial.

Initialize Configuration

RecastConfig Constructor
public RecastConfig(PartitionType partitionType, float cellSize, float cellHeight, float agentHeight,
        float agentRadius, float agentMaxClimb, float agentMaxSlope, int regionMinSize, int regionMergeSize,
        float edgeMaxLen, float edgeMaxError, int vertsPerPoly, float detailSampleDist, float detailSampleMaxError,
        int tileSize, AreaModification walkableAreaMod)

After gathering your geometries, you next will create the NavMesh configuration. The constructor for RecastConfig is extensive so this guide will explain each parameter and the methods used to set them.

The basic decision-making process for setting these parameters goes as follows.

  1. Decide the size of your character “capsule”. For example if you are using meters as units in your game world, a good size of human sized characters might be radius (r) r=0.4, height (h) h=2.0.

  2. The voxelization cell size (cs) will be derived from step 1. Usually good values for cs are r/2 or r/3. In outdoor environments, r/2 might be enough, indoors you sometimes want the extra precision and you might choose to use r/3 or smaller.

  3. The voxelization cell height (ch) is defined separately in order to allow greater precision in height tests. Good starting point for ch is cs/2. If you get small holes where there are discontinuities in the height (steps), you may want to decrease cell height.

  4. Set the agentMaxClimb. This should be greater than two times cellHeight. This is what determines how the navmesh will climb steps or curbs.

  5. Set the parameter agentMaxSlope. This is used before voxelization to check if the slope of a triangle is too high and whether those polygons will be given the unwalkable flag. You may tweak the triangle flags yourself too, for example if you wish to make certain objects or materials unwalkable. The parameter is in radians.

  6. Set edgeMaxLen. In certain cases really long outer edges may decrease the triangulation results. Sometimes this can be remedied by just tesselating the long edges. The parameter edgeMaxLen defines the max edge length in voxel coordinates. A good value for edgeMaxLen is something like r*8. A good way to tweak this value is to first set it really high and see if your data creates long edges. If so, then try to find as big value as possible which happens to create those few extra vertices which makes the tessellation better.

  7. Set edgeMaxError. When the rasterized areas are converted back to vectorized representation the edgeMaxError describes how loosely the simplification is done (the simplification is Douglas-Peucker, so this value describes the max deviation in voxels). Good values are between 1.1-1.5 (1.3 usually yield good results). If the value is less, some stair-casing starts to appear at the edges and if it is more than that, the simplification starts to cut some corners.

The jme3-recast4j wrapper has an optional build method that will create some settings for you based on the recommended settings above.

public RecastConfig build(boolean deriveValues)
/**
 * CellSize, CellHeight, AgentMaxClimb and EdgeMaxLength can be derived from
 * some formulas. Calling this method will do that for you (and thus also
 * overwrite the values manually set!)
 *
 * @return this
 */
public RecastConfigBuilder deriveValues() {
    cellSize = agentRadius / 2f; // r / 2
    cellHeight = cellSize / 2f; // cs / 2
    agentMaxClimb = 2f * cellHeight; // > 2 * ch
    edgeMaxLen = 8f * agentRadius; // r*8
    return this;
}

Typically, you will need to manually set these instead so you can fine tune your NavMesh to get the desired results you’re after.

Using our suggested settings, the RecastConfig build looks like this.

//Step 2. Create a Recast configuration object. Start by creating the builder.
RecastConfigBuilder builder = new RecastConfigBuilder();
//Instantiate the configuration parameters.
RecastConfig cfg = builder
        .withAgentRadius(0.4f)              // r
        .withAgentHeight(2.0f)              // h
        //cs and ch should be .1 at min.
        .withCellSize(0.2f)                 // cs=r/2
        .withCellHeight(0.1f)               // ch=cs/2 but not < .1f
        .withAgentMaxClimb(0.3f)            // > 2*ch
        .withAgentMaxSlope(45f)
        .withEdgeMaxLen(3.2f)               // r*8
        .withEdgeMaxError(1.3f)             // 1.1 - 1.5
        .withDetailSampleDistance(6.0f)     // increase if exception
        .withDetailSampleMaxError(5.0f)     // increase if exception
        .withVertsPerPoly(3).build();

Here is a breakdown of the parameters and methods to set them with their defaults.

Method(default) Description

withPartitionType(WATERSHED)

Partition the heightfield so that we can use a simple algorithm later to triangulate the walkable areas.

There are 3 partitioning methods, each with some pros and cons:

  1. Watershed partitioning.

    • The classic Recast partitioning.

    • Creates the nicest tessellation.

    • Usually slowest.

    • Partitions the heightfield into nice regions without holes or overlaps.

    • There are some corner cases where this method produces holes and overlaps.

      • Holes may appear when a small obstacle is close to large open area. (triangulation can handle this)

      • Overlaps may occur if you have narrow spiral corridors (i.e. stairs), this can make triangulation fail.

    • Generally the best choice if you precompute the navMesh. Use this if you have large open areas.

  2. Monotone partitioning.

    • Fastest.

    • Partitions the heightfield into regions without holes and overlaps. (guaranteed)

    • Creates long thin polygons, which sometimes causes paths with detours.

      • Use this if you want fast navmesh generation.

  3. Layer partitioning.

    • Quite fast.

    • Partitions the heightField into non-overlapping regions.

    • Relies on the triangulation code to cope with holes. (thus slower than monotone partitioning)

    • Produces better triangles than monotone partitioning.

    • Does not have the corner cases of watershed partitioning.

    • Can be slow and create a bit ugly tessellation (still better than monotone) if you have large open areas with small obstacles. (not a problem if you use tiles)

    • Good choice to use for tiled navMesh with medium and small sized tiles.

withCellSize(0.3f)

[Limit: > 0] [Units: wu]

Rasterized cell size. The width and depth of the cell columns that make up voxel fields. Cells are laid out on the width/depth plane of voxel fields.

  • Width is associated with the x-axis of the source geometry.

  • Depth is associated with the z-axis.

  • A lower value allows for the generated mesh to more closely match the source geometry, but at a higher processing and memory cost.

  • Small cell sizes are needed to allow the mesh to travel up stairs. Adjust cellSize and cellHeight for contour simplification exceptions.

Typical settings:

  • outdoors = agentRadius/2

  • indoors = agentRadius/3

  • very small cells = agentRadius/> 3

withCellHeight(0.2f)

[Limit: > 0] [Units: wu]

Rasterized cell height. Height is associated with the y-axis of the source geometry. A smaller value allows for the final mesh to more closely match the source geometry at a potentially higher processing cost. Unlike cellSize, using a lower value for cellHeight does not significantly increase memory use.

  • This is a core configuration value that impacts almost all other parameters.

  • The PolyMeshDetail mesh will be offset in the “Y” direction by this amount.

  • agentHeight, agentMaxClimb, and detailSampleMaxError will need to be greater than this value in order to function correctly.

  • agentMaxClimb is especially susceptible to impact from the value of cellHeight.

Typical setting: cellSize/2

withAgentHeight(2.0f)

[Limit: >= 3] [Units: vx]

Represents the minimum floor to ceiling height that will still allow the floor area to be considered traversable. It permits detection of overhangs in the geometry that make the geometry below become unwalkable.

  • This value should be at least two times the value of cellHeight in order to get good results.

withAgentRadius(0.6f)

[Limit: >=0] [Units:vx]

Sets the radius of the typical agent. Represents the closest any part of a mesh can get to an obstruction in the source geometry. Usually this value is set to the maximum bounding radius of agents utilizing the meshes for navigation decisions. Extreme outliers should have their own NavMesh.

  • If this value is greater than zero, the NavMesh (PolyMesh) will be shrunken by the agent radius, this includes height. The shrinking is done in voxel representation, so some precision is lost there. This step allows simpler checks at run time. If you want to have tight fit NavMesh, use zero radius.

    • This means it affects and is used for culling in heightFields as well.

  • This value must be greater than cellSize to have an effect.

withAgentMaxClimb(0.9f)

[Limit: >=0] [Units: vx]

Represents the maximum ledge height that is considered to still be traversable. Prevents minor deviations in height from improperly showing as obstructions. Permits detection of stair-like structures, curbs, etc.

  • agentMaxClimb should be greater than two times cellHeight.

    (agentMaxClimb > cellHeight * 2)

    Otherwise the resolution of the voxel field may not be high enough to accurately detect traversable ledges. Ledges may merge, effectively doubling their step height. This is especially an issue for stairways.

withAgentMaxSlope(45f)

[Limits: 0 ⇐ value < 90] [Units: Degrees]

The maximum slope that is considered traversable.

withRegionMinSize(8)

[Limit: >=0] [Units: vx]

The minimum region size for unconnected (island) regions. (smaller regions will be deleted)

withRegionMergeSize(20)

[Limit: >=0] [Units: vx]

Any regions smaller than this size will, if possible, be merged with larger regions.

withEdgeMaxLen(12f)

[Limit: >=0] [Units: vx]

Maximum contour edge length. The maximum length of polygon edges that represent the border of meshes.

  • Adjust to decrease dangling errors.

Typical setting: agentRadius * 8

withEdgeMaxError(1.3f)

[Limit: >=0] [Units: vx]

Maximum distance error from contour to cells. The maximum distance the edges of meshes may deviate from the source geometry. A lower value will result in mesh edges following the xz-plane geometry contour more accurately at the expense of an increased triangle count.

Typical setting: 1.1 to 1.5 for best results. 1.1 takes 2x as long to generate mesh as 1.5.

withVertsPerPoly(6)

[Limit: >= 3]

The maximum number of vertices per polygon for polygons generated during the voxel to polygon conversion process.

withDetailSampleDistance(6f)

[Limits: 0 or >= 0.9] [Units: wu]

Sets the sampling distance to use when matching the detail mesh to the surface of the original geometry. Higher values result in a detail mesh that conforms more closely to the original geometries surface at the cost of a higher final triangle count and higher processing cost.

The difference between this parameter and edgeMaxError is that this parameter operates on the height rather than the xz-plane. It also matches the entire detail mesh surface to the contour of the original geometry. edgeMaxError only matches edges of meshes to the contour of the original geometry.

  • Increase to reduce dangling errors at the cost of accuracy.

withDetailSampleMaxError(1f)

[Limit: >=0] [Units: wu]

The maximum distance the surface of the detail mesh may deviate from the surface of the original geometry.

  • Increase to reduce dangling errors at the cost of accuracy.

withTileSize(0)

[Limit: >= 0] [Units: vx]

The width/height size of tile’s on the xz-plane.

withWalkableAreaMod( SAMPLE_AREAMOD_GROUND )

These are flag types you set for the polygons of an area. You use these flags for filtering during pathfinding and can also poll the area to apply actions to your agents based on the type found.

See: Area Modifications for more information.

Area Modifications

public class SampleAreaModifications {
    public static int SAMPLE_POLYAREA_TYPE_MASK = 0x07;
    public static int SAMPLE_POLYAREA_TYPE_GROUND = 0x1;
    public static int SAMPLE_POLYAREA_TYPE_WATER = 0x2;
    public static int SAMPLE_POLYAREA_TYPE_ROAD = 0x3;
    public static int SAMPLE_POLYAREA_TYPE_DOOR = 0x4;
    public static int SAMPLE_POLYAREA_TYPE_GRASS = 0x5;
    public static int SAMPLE_POLYAREA_TYPE_JUMP = 0x6;

    public static AreaModification SAMPLE_AREAMOD_GROUND = new AreaModification(SAMPLE_POLYAREA_TYPE_GROUND,
            SAMPLE_POLYAREA_TYPE_MASK);
    public static AreaModification SAMPLE_AREAMOD_WATER = new AreaModification(SAMPLE_POLYAREA_TYPE_WATER,
            SAMPLE_POLYAREA_TYPE_MASK);
    public static AreaModification SAMPLE_AREAMOD_ROAD = new AreaModification(SAMPLE_POLYAREA_TYPE_ROAD,
            SAMPLE_POLYAREA_TYPE_MASK);
    public static AreaModification SAMPLE_AREAMOD_GRASS = new AreaModification(SAMPLE_POLYAREA_TYPE_GRASS,
            SAMPLE_POLYAREA_TYPE_MASK);
    public static AreaModification SAMPLE_AREAMOD_DOOR = new AreaModification(SAMPLE_POLYAREA_TYPE_DOOR,
            SAMPLE_POLYAREA_TYPE_DOOR);
    public static AreaModification SAMPLE_AREAMOD_JUMP = new AreaModification(SAMPLE_POLYAREA_TYPE_JUMP,
            SAMPLE_POLYAREA_TYPE_JUMP);

    public static final int SAMPLE_POLYFLAGS_WALK = 0x01;	// Ability to walk (ground, grass, road)
    public static final int SAMPLE_POLYFLAGS_SWIM = 0x02;   // Ability to swim (water).
    public static final int SAMPLE_POLYFLAGS_DOOR = 0x04;   // Ability to move through doors.
    public static final int SAMPLE_POLYFLAGS_JUMP = 0x08;   // Ability to jump.
    public static final int SAMPLE_POLYFLAGS_DISABLED = 0x10; // Disabled polygon
    public static final int SAMPLE_POLYFLAGS_ALL = 0xffff; // All abilities.
}

Area modifications are broken down into Area Types and Ability Flags. The Area Type specifies the cost to travel across the polygon and Ability Flag specifies if the agent can travel through the area at all. There are 64 Area Types and 16 Ability Flags. Types consist of an int value and int mask while flags are binary power of two. If there are two overlapping Area Types, the one with the lowest cost will prevail when path finding.

Jme3-recast4j includes a basic implementation of these flags to demonstrate how to use them. The actual abilities and types are specified by you and the game you design.

If your NavMesh will only have one Area Type, you set that with the 'withWalkableAreaMod' in your RecastConfig file. If your NavMesh will consist of more than one Area Type, you need a way to mark these areas separately.

This can be be done by using some tricks inside any 3d modeling program paired with a jme3-recast4j Modification object. You need a way to identify the triangles that are to be marked when loading the model and the easiest way to do that is with a material and a naming convention. You could also just manually set the Modification parameters if you know what they are.

For this example, I will use a door.

door.png

First set your models materials as normal but you also create a new material that will represent the “Area Type”. You then apply that material to the area you wish to mark. Jme will then split the geometry up based off materials when you create the .j3o file.

Here, I applied a material using the same colors as the other floor material and applied it to these 4 triangles. For my naming convention I named it “door_green”. I will use the underscore as a delimiter in my search of the scene and create a new jme3-recast4j Modification object based off the geometries triangle length and the AreaModification I wish to set for that area.

//Step 1. Gather our geometry.
JmeInputGeomProvider geom = new GeometryProviderBuilder2(worldMap).build();

worldMap.depthFirstTraversal(new SceneGraphVisitorAdapter() {
    @Override
    public void visit(Geometry spat) {
        int geomLength = spat.getMesh().getTriangleCount() *3;

        String[] name = spat.getMaterial().getName().split("_");

        switch (name[0]) {

            case "water":
                geom.addMod(new Modification(geomLength, AREAMOD_WATER));
                break;
            case "road":
                geom.addMod(new Modification(geomLength, AREAMOD_ROAD));
                break;
            case "grass":
                geom.addMod(new Modification(geomLength, AREAMOD_GRASS));
                break;
            case "door":
                geom.addMod(new Modification(geomLength, AREAMOD_DOOR));
                break;
            default:
                geom.addMod(new Modification(geomLength, AREAMOD_GROUND));
        }
    }
});

I create the Modification object and add it the JmeInputGeomProvider. Anything not water, road, grass, or door in this case will be set to ground. Thats all there is to it. Now you can set the Ability Flag when you call the NavMeshDataCreateParams method. You set the "Area Cost" in the individual filter used for the character(s) path finding.

Note
This will only work with a JmeInputGeomProvider. Any other provider will set the Area Type based off the supplied RecastConfig object.

See the NavMeshDataCreateParams topic for information on setting Ability Flags.

Filtering for path finding is covered in more detail under Path Filters.

Crowd filtering is found under Crowd Filters

RecastBuilderConfigBuilder

//Create a RecastBuilderConfig builder with world bounds of our geometry.
RecastBuilderConfigBuilder rcb = new RecastBuilderConfigBuilder(boxGeo);

The RecastBuilderConfigBuilder constructor will accept spatials, nodes, geometry, or meshes. The main purpose of these constructors is determining the min and max bounds for the object you are using for your NavMesh. The builder will use this and the RecastConfig to build the RecastBuilderConfig.

Known Bounds Example
public RecastBuilderConfigBuilder(Vector3f minBounds, Vector3f maxBounds)

With this constructor, you have already gathered the information for the min and max bounds. You can get this information from the JmeInputGeomProvider object as is done in the following example.

JmeInputGeomProvider geomProvider = new GeometryProviderBuilder(boxGeo).build();
Vector3f minBounds = DetourUtils.createVector3f(geomProvider.getMeshBoundsMin());
Vector3f maxBounds = DetourUtils.createVector3f(geomProvider.getMeshBoundsMax());
Note
Recast4j uses arrays for everything so it becomes tedious and repetitive constantly converting Vector3f to array and vice versa. Jme3-recast4j includes a small set of utility methods that greatly simplifies this. See DetourUtils.java.
Mesh
public RecastBuilderConfigBuilder(Mesh m)

This constructor doesn’t support scaling, so ensure your geometry has a scale of one OR call the appropriate constructor.

Scaled Mesh
public RecastBuilderConfigBuilder(Mesh m, Vector3f worldScale)

This constructor will use the supplied world scale to determine the minBounds and maxBounds.

Geometry
public RecastBuilderConfigBuilder(Geometry g)

This constructor will derive the world scale from the geometry itself and determine the minBounds and maxBounds.

Node
public RecastBuilderConfigBuilder(Node n)

This constructor will use the nodes BoundingVolume to derive the minBounds and maxBounds. This BoundingVolume must be of type.AABB.

Spatial
public RecastBuilderConfigBuilder(Spatial s)

This constructor will use the appropriate instance type (geometry or node) to determine the minBounds and maxBounds.

RecastBuilderConfig

//Build the configuration object using our cfg.
RecastBuilderConfig bcfg = rcb.withDetailMesh(true).build(cfg);

Now that we have our RecastBuilderConfigBuilder configured with the world bounds, we use it to build the RecastBuilderConfig that will be passed to our RecastBuilder. This is the Recast specific object that our builder will use to generate the parameters for our MeshData.

There are two types of meshes used for pathfinding.

  • PolyMesh - This represents the NavMesh which has potentially overlapping convex polygons and exists within the context of an axis-aligned bounding box (AABB) with vertices laid out in an evenly spaced grid, based on the values of cell size and cell height.

  • PolyMeshDetail - This is made up of triangle sub-meshes that provide extra height detail for each polygon in its assoicated PolyMesh. The sub-meshes are stored in the same order as the polygons from the PolyMesh they represent. E.g. PolyMeshDetail sub-mesh 5 is associated with PolyMesh polygon 5.

    An explaination of the PolyMeshDetail from the author of CAINav.

    The NavMesh class supports polygons with up to six sides. The polygons can get very large when generating meshes for this type of navigation mesh. This is great for pathfinding search efficiency. But it can result in loss of height data.

    Consider a large, unobstructed meadow. It may be only a single huge polygon. But if the meadow contains a hillock at its center, the hillock’s height will be lost. This doesn’t matter from the perspective of the pathfinding search. But some navigation use cases require the navigation mesh to follow reasonably close to the surface of the source geometry. This is what the detail mesh is meant to support.

    The detail mesh can be used to add additional vertices to the edges and surface of the navigation mesh polygons. These vertices are ignored for pathfinding. But various NavmeshQuery methods will return heights that are corrected using the detail mesh.

    — Stephen Pratt
    CAINav

RecastBuilder

//Step 3. Build our Navmesh data using our gathered geometry and configuration.
//This is where we decide if this is a solo NavMesh build or tiled.
//Tiled will be covered later.
RecastBuilder rb = new RecastBuilder();
RecastBuilderResult rbr = rb.build(geomProvider, bcfg);

Up to this point we have gathered the geometry for the NavMesh and used it to initialize a config object. Now we start the process of building the Data for the NavMesh. We create the RecastBuilder and pass our geometry (JmeInputGeomProvider object) and configuration to it. We will use the results to build our parameters object that will be used to build the MeshData. This is where you decide if you are building a Solo or Tiled NavMesh as well.

NavMeshDataCreateParams

//Set the parameters needed to build our MeshData using the RecastBuilder results.
NavMeshDataCreateParamsBuilder paramBuilder = new NavMeshDataCreateParamsBuilder(rbr);
//Update poly flags from areas. Set any flags here.
PolyMesh pmesh = paramBuilder.getPolyMesh();
for (int i = 0; i < pmesh.npolys; ++i) {
    if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GROUND
      || pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GRASS
      || pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD) {
        paramBuilder.withPolyFlag(i, SampleAreaModifications.SAMPLE_POLYFLAGS_WALK);
    } else if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER) {
        paramBuilder.withPolyFlag(i, SampleAreaModifications.SAMPLE_POLYFLAGS_SWIM);
    } else if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_DOOR) {
        paramBuilder.withPolyFlags(i, SampleAreaModifications.SAMPLE_POLYFLAGS_WALK
        | SampleAreaModifications.SAMPLE_POLYFLAGS_DOOR);
    }
}

The NavMeshDataCreateParamsBuilder is where you set the Ability Flags discussed in the Area Modifications topic. The way polygon flag filtering works for path finding is that you specify certain flags, which must be on (included), and certain flags which must not be on (excluded) for a polygon to be valid.

The flag setting methods are as follows:

    public NavMeshDataCreateParamsBuilder withPolyFlag(int id, int flag)

With this method, you set a single flag or “append” a flag to existing flags. In the example above, we are saying this polygon has the Ability to walk (ground, grass, road).

public NavMeshDataCreateParamsBuilder withPolyFlags(int id, int flags)

With this method, you set a single flag or multiple flags on a polygon. In our example we used two flags, SAMPLE_POLYFLAGS_WALK | SAMPLE_POLYFLAGS_DOOR for a door. With these flags we are saying we have the ability to walk (ground, grass, road) and this is a door. Any subsequent flag setting on this poly with this same method would overwrite any existing flags.

public NavMeshDataCreateParamsBuilder withPolyFlagsAll(int flags)

With this method, you don’t use any loop to set flags individually. We are setting the flags attribute for all polygons. Using the door example, this means every polygon in this polyMesh would have SAMPLE_POLYFLAGS_WALK | SAMPLE_POLYFLAGS_DOOR set. Any subsequent flag setting on this poly with this same method would overwrite any existing flags.

public NavMeshDataCreateParamsBuilder withPolyFlagAll(int flag)

Once again, this will set a single flag or “append” a flag to any existing flags, for all polygons in this polyMesh.

After you have set the flags, you will build your parameter object, passing your builder config object to it.

//Build the parameter object.
NavMeshDataCreateParams params = paramBuilder.build(bcfg);

MeshData

//Step 4. Generate MeshData using our parameters object.
MeshData meshData = NavMeshBuilder.createNavMeshData(params);

All the complicated work has been completed by this point. Now that you have the parameters object built, you build the MeshData for the NavMesh.

NavMesh

//Step 5. Build the NavMesh.
NavMesh navMesh = new NavMesh(meshData, bcfg.cfg.maxVertsPerPoly, 0);

Finally, you use the MeshData to build the NavMesh.

Saving and Loading

//Step 6. Save our work. Using compressed format.
MeshDataWriter mdw = new MeshDataWriter();
mdw.write(new FileOutputStream(new File("myMeshData.md")),  meshData, ByteOrder.BIG_ENDIAN, false);
//Or the native format using tiles.
MeshSetWriter msw = new MeshSetWriter();
msw.write(new FileOutputStream(new File("myNavMesh.nm")), navMesh, ByteOrder.BIG_ENDIAN, false);

You will normally be pre-building the NavMesh and either saving it’s MeshData or the entire NavMesh for later use. You save your built navMesh using either MeshDataWriter or MeshSetWriter.

MeshDataWriter saves the MeshData only, which is a more compressed file format than MeshSetWriter produces. MeshSetWriter files use MeshDataWriter to write the Meshdata but also saves the NavMesh as tiles. Both formats are compatible with other code like C++/C.

Note

Whether you build a solo NavMesh or tiled NavMesh is irrelevant to MeshSetWriter because a solo built NavMesh is really just one big tile in reality.

With MeshDataWriter, you rebuild the NavMesh from the MeshData point of creation.

    //Read in saved MeshData and build new NavMesh.
    MeshDataReader mdr = new MeshDataReader();
    MeshData read = mdr.read(new FileInputStream("test.md"), 3);
    NavMesh navMeshFromData = new NavMesh(read, 3, 0);

With MeshSetReader, you have your complete NavMesh upon read.

    //Or read in saved NavMesh.
    MeshSetReader msr = new MeshSetReader();
    NavMesh navMeshFromSaved = msr.read(new FileInputStream("myNavMesh.nm"), 3);

Tiled NavMesh

//We need to know these variables for the tile NavMeshDataCreateParams
//object.
float agentHeight = 2.0f;
float agentRadius = 0.4f;
float agentMaxClimb = 0.3f;

Box boxMesh = new Box(20f,.1f,20f);
Geometry boxGeo = new Geometry("Colored Box", boxMesh);
Material boxMat = new Material(getApplication().getAssetManager(), "Common/MatDefs/Light/Lighting.j3md");
boxMat.setBoolean("UseMaterialColors", true);
boxMat.setColor("Ambient", ColorRGBA.LightGray);
boxMat.setColor("Diffuse", ColorRGBA.LightGray);
boxGeo.setMaterial(boxMat);
((SimpleApplication) getApplication()).getRootNode().attachChild(boxGeo);

//Step 1. Gather our geometry.
JmeInputGeomProvider geomProvider = new GeometryProviderBuilder(boxGeo).build();
//Step 2. Create a Recast configuration object.
RecastConfigBuilder builder = new RecastConfigBuilder();
//Instantiate the configuration parameters.
RecastConfig cfg = builder
        .withAgentRadius(agentRadius)       // r
        .withAgentHeight(agentHeight)       // h
        //cs and ch should be .1 at min.
        .withCellSize(0.2f)                 // cs=r/2
        .withCellHeight(0.1f)               // ch=cs/2 but not < .1f
        .withAgentMaxClimb(agentMaxClimb)   // > 2*ch
        .withAgentMaxSlope(45f)
        .withEdgeMaxLen(3.2f)               // r*8
        .withEdgeMaxError(1.3f)             // 1.1 - 1.5
        .withDetailSampleDistance(6.0f)     // increase if exception
        .withDetailSampleMaxError(5.0f)     // increase if exception
        .withVertsPerPoly(3)
        .withTileSize(32).build();          // set tile size

//Build all tiles
RecastBuilder rb = new RecastBuilder();
RecastBuilderResult[][] rcResult = rb.buildTiles(geomProvider, cfg, 1);

//Set the parameters needed to build our MeshData using the RecastBuilder results.
int tw = rcResult.length;
int th = rcResult[0].length;

//Create empty nav mesh.
NavMeshParams navMeshParams = new NavMeshParams();
copy(navMeshParams.orig, geomProvider.getMeshBoundsMin());
navMeshParams.tileWidth = cfg.tileSize * cfg.cs;
navMeshParams.tileHeight = cfg.tileSize * cfg.cs;
navMeshParams.maxTiles = tw * th;
navMeshParams.maxPolys = 32768;
NavMesh navMesh = new NavMesh(navMeshParams, 3);
//Add tiles to nav mesh
for (int y = 0; y < th; y++) {
    for (int x = 0; x < tw; x++) {
        PolyMesh pmesh = rcResult[x][y].getMesh();
        if (pmesh.npolys == 0) {
                continue;
        }

        //Update poly flags from areas.
        for (int i = 0; i < pmesh.npolys; ++i) {
            if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GROUND
                    || pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GRASS
                    || pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD) {
                pmesh.flags[i] = SampleAreaModifications.SAMPLE_POLYFLAGS_WALK;
            } else if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER) {
                pmesh.flags[i] = SampleAreaModifications.SAMPLE_POLYFLAGS_SWIM;
            } else if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_DOOR) {
                pmesh.flags[i] = SampleAreaModifications.SAMPLE_POLYFLAGS_WALK
                        | SampleAreaModifications.SAMPLE_POLYFLAGS_DOOR;
            }
        }
        //Create empty parameters object to set params.
        NavMeshDataCreateParams params = new NavMeshDataCreateParams();
        params.verts = pmesh.verts;
        params.vertCount = pmesh.nverts;
        params.polys = pmesh.polys;
        params.polyAreas = pmesh.areas;
        params.polyFlags = pmesh.flags;
        params.polyCount = pmesh.npolys;
        params.nvp = pmesh.nvp;
        //Save detail mesh data.
        PolyMeshDetail dmesh = rcResult[x][y].getMeshDetail();
        params.detailMeshes = dmesh.meshes;
        params.detailVerts = dmesh.verts;
        params.detailVertsCount = dmesh.nverts;
        params.detailTris = dmesh.tris;
        params.detailTriCount = dmesh.ntris;
        params.walkableHeight = agentHeight;
        params.walkableRadius = agentRadius;
        params.walkableClimb = agentMaxClimb;
        params.bmin = pmesh.bmin;
        params.bmax = pmesh.bmax;
        params.cs = cfg.cs;
        params.ch = cfg.ch;
        params.tileX = x;
        params.tileY = y;
        params.buildBvTree = true;
        //add tile to navMesh.
        navMesh.addTile(NavMeshBuilder.createNavMeshData(params), 0, 0);
    }
}

try {
    //Native format using tiles.
    MeshSetWriter msw = new MeshSetWriter();
    msw.write(new FileOutputStream(new File("myNavMesh.nm")), navMesh, ByteOrder.BIG_ENDIAN, false);
    //Or read in saved NavMesh.
    MeshSetReader msr = new MeshSetReader();
    NavMesh navMeshFromSaved = msr.read(new FileInputStream("myNavMesh.nm"), 3);
}  catch (IOException ex) {
    LOG.info("{} {}", CrowdBuilderState.class.getName(), ex);
}

With a tiled NavMesh build, you are breaking up the NavMesh into individual tiles. The first two steps of the tile build are therefore similar to the Solo NavMesh build.

You gather all the geometry needed and merge it into one, then set the configuration for the NavMesh based off that geometry. You do not use a RecastBuilderConfig because this will be created for you during the buildTile process.

Afterwards, you create an empty NavMesh, split the geometry into individual Polymeshes, set each PolyMeshes parameters individually, build the tile and add it to your empty NavMesh.

RecastConfig

Our RecastConfig build has only one minor change over the Solo NavMesh build. We set the tile size for the NavMesh.

        .withTileSize(32).build();          // set tile size

The thing to note is tile size is in Voxels. You should select a tile size which nicely balances the tile calculation time and the granularity needed for example to load or unload tiles. The complexity of the geometry and number of triangles can greatly increase build times. An 2048 x 2048 containing 3500 plus tiles at half a second per tile takes about thirty minutes to build for example. If you want to use the tiles just to cover large terrain, it’s suggested to use something between 128-512 for tile size. Test out a couple of different sizes and see which works the best (fastest to build, cleanest navmesh, etc). If you’re planning to rebuild tiles during runtime, then a good range is 16-64.

RecastBuilder

//Build all tiles
RecastBuilder rb = new RecastBuilder();
RecastBuilderResult[][] rcResult = rb.buildTiles(geomProvider, cfg, 1);

The RecastBuilder is where you break up the geometry into tiles. Pass your gathered geometry and configuration to the buildTiles() method to build the tiles. The returned RecastBuilderResult holds each individual PolyMesh and PolyMeshDetail that were cut from the geometry based on the configuration.

A feature of the buildTiles() method is the ability to break up the workload among threads. For most use, one thread is fine, as we use here with the last parameter for the method.

The RecastBuilder accepts an optional RecastBuilderProgressListener that will report the tile number and total tiles for each build. You can set timers on this and determine, with pretty good accuracy, total build times for larger builds.

Example RecastBuilderProgressListener
private class ProgressListen implements RecastBuilderProgressListener {

    private long time = System.nanoTime();
    private long elapsedTime;
    private long avBuildTime;
    private long estTotalTime;
    private long estTimeRemain;
    private long buildTimeNano;
    private long elapsedTimeHr;
    private long elapsedTimeMin;
    private long elapsedTimeSec;
    private long totalTimeHr;
    private long totalTimeMin;
    private long totalTimeSec;
    private long timeRemainHr;
    private long timeRemainMin;
    private long timeRemainSec;

    @Override
    public void onProgress(int completed, int total) {
        elapsedTime += System.nanoTime() - time;
        avBuildTime = elapsedTime/(long)completed;
        estTotalTime = avBuildTime * (long)total;
        estTimeRemain = estTotalTime - elapsedTime;

        buildTimeNano = TimeUnit.MILLISECONDS.convert(avBuildTime, TimeUnit.NANOSECONDS);
        System.out.printf("Completed %d[%d] Average [%dms] ", completed, total, buildTimeNano);

        elapsedTimeHr = TimeUnit.HOURS.convert(elapsedTime, TimeUnit.NANOSECONDS) % 24;
        elapsedTimeMin = TimeUnit.MINUTES.convert(elapsedTime, TimeUnit.NANOSECONDS) % 60;
        elapsedTimeSec = TimeUnit.SECONDS.convert(elapsedTime, TimeUnit.NANOSECONDS) % 60;
        System.out.printf("Elapsed Time [%02d:%02d:%02d] ", elapsedTimeHr, elapsedTimeMin, elapsedTimeSec);

        totalTimeHr = TimeUnit.HOURS.convert(estTotalTime, TimeUnit.NANOSECONDS) % 24;
        totalTimeMin = TimeUnit.MINUTES.convert(estTotalTime, TimeUnit.NANOSECONDS) % 60;
        totalTimeSec = TimeUnit.SECONDS.convert(estTotalTime, TimeUnit.NANOSECONDS) % 60;
        System.out.printf("Estimated Total [%02d:%02d:%02d] ", totalTimeHr, totalTimeMin, totalTimeSec);

        timeRemainHr = TimeUnit.HOURS.convert(estTimeRemain, TimeUnit.NANOSECONDS) % 24;
        timeRemainMin = TimeUnit.MINUTES.convert(estTimeRemain, TimeUnit.NANOSECONDS) % 60;
        timeRemainSec = TimeUnit.SECONDS.convert(estTimeRemain, TimeUnit.NANOSECONDS) % 60;
        System.out.printf("Remaining Time [%02d:%02d:%02d]%n", timeRemainHr, timeRemainMin, timeRemainSec);

        //reset time
        time = System.nanoTime();
    }

}

To use this, just create the object and pass it to the RecastBuilder constructor.

// Build all tiles
RecastBuilder rb = new RecastBuilder(new ProgressListen());

Once you have your RecastBuilderResult, you can begin the process of setting the parameters and adding the tiles to the NavMesh. Before you move onto that, you set your grid width and height based on the returned results.

//Set the parameters needed to build our MeshData using the RecastBuilder results.
int tw = rcResult.length;
int th = rcResult[0].length;

NavMesh

//Create empty nav mesh.
NavMeshParams navMeshParams = new NavMeshParams();
copy(navMeshParams.orig, geomProvider.getMeshBoundsMin());
navMeshParams.tileWidth = cfg.tileSize * cfg.cs;
navMeshParams.tileHeight = cfg.tileSize * cfg.cs;
navMeshParams.maxTiles = tw * th;
navMeshParams.maxPolys = 32768;
NavMesh navMesh = new NavMesh(navMeshParams, 3);

As was stated before, our Geometry has been chopped up into individual PolyMeshes and each is now referenced in our RecastBuilderResult. We now need to create an empty NavMesh and set its parameters before we can add the tiles to it using these PolyMeshes. Use your configuration and geometry objects to gather the data.

A note on navMeshParams.maxTiles. You must allot the number of tiles you expect to build for the Navmesh. You can do this by setting any arbitrary number but if you do not allot enough tiles, an exception will be thrown. The easiest way to assure there will be no exception is to multiply the width times height of the grid. This does not guarantee an exact match of tiles built since some PolyMeshes may be unwalkable.

Adding Tiles

//Add tiles to nav mesh
for (int y = 0; y < th; y++) {
    for (int x = 0; x < tw; x++) {
        PolyMesh pmesh = rcResult[x][y].getMesh();
        if (pmesh.npolys == 0) {
                continue;
        }

        //Update poly flags from areas.
        for (int i = 0; i < pmesh.npolys; ++i) {
            if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GROUND
                    || pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GRASS
                    || pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD) {
                pmesh.flags[i] = SampleAreaModifications.SAMPLE_POLYFLAGS_WALK;
            } else if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER) {
                pmesh.flags[i] = SampleAreaModifications.SAMPLE_POLYFLAGS_SWIM;
            } else if (pmesh.areas[i] == SampleAreaModifications.SAMPLE_POLYAREA_TYPE_DOOR) {
                pmesh.flags[i] = SampleAreaModifications.SAMPLE_POLYFLAGS_WALK
                        | SampleAreaModifications.SAMPLE_POLYFLAGS_DOOR;
            }
        }
        //Create empty parameters object to set params.
        NavMeshDataCreateParams params = new NavMeshDataCreateParams();
        params.verts = pmesh.verts;
        params.vertCount = pmesh.nverts;
        params.polys = pmesh.polys;
        params.polyAreas = pmesh.areas;
        params.polyFlags = pmesh.flags;
        params.polyCount = pmesh.npolys;
        params.nvp = pmesh.nvp;
        //Save detail mesh data.
        PolyMeshDetail dmesh = rcResult[x][y].getMeshDetail();
        params.detailMeshes = dmesh.meshes;
        params.detailVerts = dmesh.verts;
        params.detailVertsCount = dmesh.nverts;
        params.detailTris = dmesh.tris;
        params.detailTriCount = dmesh.ntris;
        params.walkableHeight = agentHeight;
        params.walkableRadius = agentRadius;
        params.walkableClimb = agentMaxClimb;
        params.bmin = pmesh.bmin;
        params.bmax = pmesh.bmax;
        params.cs = cfg.cs;
        params.ch = cfg.ch;
        params.tileX = x;
        params.tileY = y;
        params.buildBvTree = true;
        //add tile to navMesh.
        navMesh.addTile(NavMeshBuilder.createNavMeshData(params), 0, 0);
    }
}

The RecastBuilderResult contains the PolyMeshes you need to build each tile. The build process is one of cycling through the polygons, skipping any that are not walkable, then setting parameters for each and adding each to the NavMesh. The RecastBuilderResult can be looked at in essence as a grid where y = column, x = row.

Notice that you have to check the PolyMesh (NavMesh) for the existance of polys.

PolyMesh pmesh = rcResult[x][y].getMesh();
if (pmesh.npolys == 0) {
        continue;
}

This is because some PolyMeshes may return as unwalkable so you need to skip creating the tile. This is also why navMeshParams.maxTiles is just a generic setting.

The rest of the loop is normal parameter setting and the variables show you where to pull the data from.

//add tile to navMesh.
navMesh.addTile(NavMeshBuilder.createNavMeshData(params), 0, 0);

Lastly, as is done with Solo NavMesh, you create the MeshData object and pass it to the addTile() method. The data is designed so that you can save it directly to disk, and read back and add the tile. You can also just save the NavMesh when done.

Debugging

When building the NavMesh, you want to end up with very few long triangles and a clean, squared NavMesh. If you run into problems with exceptions being thrown, you will need to tweak the parameters. Each mesh is different so it’s best to start with defaults and tweak one or two parameters at a time until you get as good a NavMesh as possible for your situation.

You can use a debug mesh to display your NavMesh when using jMonkeyEngine. This is done using standard jMonkeyEngine materials and geometry. Jme3-recast4j has the RecastUtils.java utility class that will convert the MeshData into a jMonkeyEngine compatible mesh. Feel free to create your own versions.

Example Debug Mesh
private void showDebugMeshes(MeshData meshData, boolean wireframe) {
    Material matRed = new Material(getApplication().getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
    matRed.setColor("Color", ColorRGBA.Red);

    if (wireframe) {
        matRed.getAdditionalRenderState().setWireframe(true);
    }

    Material matGreen = new Material(getApplication().getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
    matGreen.setColor("Color", ColorRGBA.Green);

    // navMesh.getTile(0).data == meshData (in this particular case)
    Geometry gDetailed = new Geometry("DebugMeshDetailed", RecastUtils.getDebugMesh(meshData.detailMeshes, meshData.detailVerts, meshData.detailTris));
    Geometry g = new Geometry("DebugMeshSimple", RecastUtils.getDebugMesh(meshData));

    g.setMaterial(matRed);
    gDetailed.setMaterial(matGreen);
    g.move(0f, 0.125f, 0f);
    gDetailed.move(0f, 0.25f, 0f);

    ((SimpleApplication) getApplication()).getRootNode().attachChild(g);
    ((SimpleApplication) getApplication()).getRootNode().attachChild(gDetailed);
}
Tip
Blender includes a NavMesh generation process when using the Blender Game engine. You can find it under Properties  Scene  Navigation Mesh. This can be a fast way to narrow down the parameters needed so you can then fine tune your NavMesh using the procedural methods used here.

OffMesh Connections

//Step 1. Gather our geometry.
JmeInputGeomProvider geom = new GeometryProviderBuilder2(worldMap).build();

//Set offmesh connections.
offMeshCon.depthFirstTraversal(new SceneGraphVisitorAdapter() {
    //Id will be used for OffMeshConnections.
    int id = 0;

    @Override
    public void visit(Node spat) {
        /**
         * offMeshCon has no skeleton and is instance of node so the
         * search will include its children. This will return with a
         * child SkeletonControl because of this. Add check to skip
         * offMeshCon.
         */
        if (!spat.getName().equals(offMeshCon.getName())) {

            SkeletonControl skelCont = getState(UtilState.class).findControl(spat, SkeletonControl.class);

            if (skelCont != null) {
                /**
                * Offmesh connections require two connections, a
                * start/end vector3f and must connect to a surrounding
                * tile. To complete a connection, start and end must be
                * the same for each. You can supply the Vector3f manually
                * or for example, use bones from an armature. When using
                * bones, they should be paired and use a naming convention.
                *
                * In our case, we used bones and this naming convention:
                *
                * arg[0](delimiter)arg[1](delimiter)arg[2]
                *
                * We set each bone origin to any vertices, in any mesh,
                * as long as the same string for arg[0] and arg[1] are
                * identical and they do not use the same vertices. We
                * duplicate two polygons (triangles) in the mesh or
                * separate meshes and add an armature(s) using a naming
                * convention.
                *
                * Naming convention for two bones:
                *
                * Bone 1 naming: offmesh.anything.a
                * Bone 2 naming: offmesh.anything.b
                *
                * arg[0]: offmesh   = same value all bones
                * arg[1]: anything  = same value paired bones
                * arg[2]: a or b    = one paired bone
                *
                * The value of arg[0] applies to ALL bones and
                * dictates these are link bones.
                *
                * The value of arg[1] dictates these pair of bones
                * belong together.
                *
                * The value of arg[2] distinguishes the paired bones
                * from each other.
                *
                * Examples:
                *
                * offmesh.pond.a
                * offmesh.pond.b
                * offmesh.1.a
                * offmesh.1.b
                */
                Bone[] roots = skelCont.getSkeleton().getRoots();
                for (Bone b: roots) {
                    /**
                     * Split the name up using delimiter.
                     */
                    String[] arg = b.getName().split("\\.");

                    if (arg[0].equals("offmesh")) {

                        //New connection.
                        org.recast4j.detour.OffMeshConnection link1 = new org.recast4j.detour.OffMeshConnection();

                        /**
                         * The bones worldTranslation will be the start
                         * or end Vector3f of each OffMeshConnection
                         * object.
                         */
                        float[] linkPos = DetourUtils.toFloatArray(spat.localToWorld(b.getModelSpacePosition(), null));

                        /**
                         * Prepare new position array. The endpoints of
                         * the connection.
                         *
                         *  startPos    endPos
                         * [ax, ay, az, bx, by, bz]
                         */
                        float[] pos = new float[6];

                        /**
                         * Copy link1 current position to pos array. If
                         * link1 is bone (a), it becomes the link start.
                         * If (b), the link end.
                         */
                        System.arraycopy(linkPos, 0, pos, arg[2].equals("a") ? 0:3, 3);

                        //Set link1 to new array.
                        link1.pos = pos;

                        //Player (r)adius. Links fire at (r) * 2.25.
                        link1.rad = radius;

                        /**
                         * Move through link1 both directions. Only
                         * works if both links have identical in start/end.
                         * Set to 0 for one direction link.
                         */
                        link1.flags = NavMesh.DT_OFFMESH_CON_BIDIR;

                        /**
                         * We need to look for the bones mate. Based off
                         * our naming convention, this will be
                         * offmesh.anything."a" or "b" so we set the
                         * search to whatever link1 arg[2] isn't.
                         */
                        String link2 = String.join(".", arg[0], arg[1], arg[2].equals("a") ? "b": "a");

                        /**
                         * If the paired bone has already been added to
                         * map, set start or end determined by link1 arg[2].
                         */
                        if (mapOffMeshCon.containsKey(link2)) {
                            /**
                             * Copy link1 pos to link2 pos. If link1 is
                             * start(a) of link, copy link1 start to
                             * link2 start. If link1 is the end(b) of
                             * link, copy link1 end to link2 end.
                             */
                            System.arraycopy(link1.pos, arg[2].equals("a") ? 0:3, mapOffMeshCon.get(link2).pos, arg[2].equals("a") ? 0:3, 3);

                            /**
                             * Copy link2 pos to link1 pos. If link1 is
                             * the start(a) of link, copy link2 end to
                             * link1 end. If link1 is end(b) of link,
                             * copy link2 start to link1 start.
                             */
                            System.arraycopy(mapOffMeshCon.get(link2).pos, arg[2].equals("a") ? 3:0, link1.pos, arg[2].equals("a") ? 3:0, 3);

                            /**
                             * OffMeshconnections with id of 0 don't get
                             * processed later if not set here.
                             */
                            if (arg[2].equals("a")) {
                                link1.userId = ++id;
                                LOG.info("OffMeshConnection [{}] id [{}]", b.getName(), link1.userId);
                                mapOffMeshCon.get(link2).userId = ++id;
                                LOG.info("OffMeshConnection [{}] id [{}]", link2, mapOffMeshCon.get(link2).userId);
                            } else {
                                mapOffMeshCon.get(link2).userId = ++id;
                                LOG.info("OffMeshConnection [{}] id [{}]", link2, mapOffMeshCon.get(link2).userId);
                                link1.userId = ++id;
                                LOG.info("OffMeshConnection [{}] id [{}]", b.getName(), link1.userId);
                            }

                        }
                        //Add this bone to map.
                        mapOffMeshCon.put(b.getName(), link1);
                    }
                }
            }
        }
    }
});

An offmesh connection is a link from one tile to another tile, that is not part of either tile, that allows for connecting a path between those tiles. Common use would be jumping, for example, from one object to another. These links can be one direction or bi-directional. They are not meant for teleporting between areas. If the link distance is to great, the connection will fail. Teleporting is best done with some other form of code and is not covered by this tutorial since it does not apply to recast.

Note
When you build a tile, the connection will not complete until the second tile that holds the matching conncetion is added to the NavMesh.
/**
 * Defines an navigation mesh off-mesh connection within a dtMeshTile object. An off-mesh connection is a user defined
 * traversable connection made up to two vertices.
 */
public class OffMeshConnection {
	/** The endpoints of the connection. [(ax, ay, az, bx, by, bz)] */
	public float[] pos = new float[6];
	/** The radius of the endpoints. [Limit: >= 0] */
	public float rad;
	/** The polygon reference of the connection within the tile. */
	public int poly;
	/**
	 * Link flags.
	 *
	 * @note These are not the connection's user defined flags. Those are assigned via the connection's Poly definition.
	 *       These are link flags used for internal purposes.
	 */
	public int flags;
	/** End point side. */
	public int side;
	/** The id of the offmesh connection. (User assigned when the navigation mesh is built.) */
	public int userId;
}

For any link to work, the above data must be set. In the example used for this tutorial, I use my 3d modeling program, armature bones and a naming convention. The naming convention is explained in the code example. The steps taken in the modeling program are as follows.

  1. Determine what vertices you want for start/end points. These do not have to be in the same mesh or even the same file. All that matters is the bones set at the vertices use the same naming convention so we know these two points are related.

  2. After choosing the points, duplicate one triangle that contains each point.

  3. Add an armature with a root bone set at the origin of the model its targeting. In other words, treat this offmesh connection object no different than any other model so you can move it around like the object you are targeting for the connection.

  4. Add a bone to the desired vertice or vertices.

  5. Rename the bones with a convention.

You can then just export the one or two triangle object as a normal, load it into your game as any other “.j3o” and traverse the scene/node looking for the object. The idea is to find the objects bone world translation and use it for either the start or end point as is shown in the code example.

/** The endpoints of the connection. [(ax, ay, az, bx, by, bz)] */
public float[] pos = new float[6];

Each connection has a startpoint and endpoint and requires two connections to complete the circut. One for the start tile (a) and one for the end tile (b). Links only work with neighboring tiles. The start(a)/end(b) must be identical for each link. You can fill this array manually if you know the points or use some other method to determine the points. You are not required to build two links. You can create a single link, store it in a central location, and use the userId to set the connection for the two tiles.

//Player (r)adius. Links fire at (r) * 2.25.
link1.rad = radius;

/**
 * Move through link1 both directions. Only
 * works if both links have identical in start/end.
 * Set to 0 for one direction link.
 */
link1.flags = NavMesh.DT_OFFMESH_CON_BIDIR;

Set as much information for the link in advance as possible. Use the expected radius of the agent for the radius. Flags in this instance do not mean Area Type or Ability Flag. These are link specific flags.

/**
 * OffMeshconnections with id of 0 don't get
 * processed later if not set here.
 */
if (arg[2].equals("a")) {
    link1.userId = ++id;
    LOG.info("OffMeshConnection [{}] id [{}]", b.getName(), link1.userId);
    mapOffMeshCon.get(link2).userId = ++id;
    LOG.info("OffMeshConnection [{}] id [{}]", link2, mapOffMeshCon.get(link2).userId);
} else {
    mapOffMeshCon.get(link2).userId = ++id;
    LOG.info("OffMeshConnection [{}] id [{}]", link2, mapOffMeshCon.get(link2).userId);
    link1.userId = ++id;
    LOG.info("OffMeshConnection [{}] id [{}]", b.getName(), link1.userId);
}

The key design point of this code is that the “userId” will only be set if there is a start and end link. Later in the setting process, we will exclude setting of any connection that does not have a paired connection which means it has an userId of zero.

//Clean up offMesh connections.
offMeshCon.detachAllChildren();

Once you extract the data from the object, it is no longer needed and can be removed.

Process OffMesh Connections

/**
 * Process OffMeshConnections.
 * Basic flow:
 * Check each mapOffMeshConnection for an index > 0.
 * findNearestPoly() for the start/end positions of the link.
 * getTileAndPolyByRef() using the returned poly reference.
 * If both start and end are good values, set the connection properties.
 */
Iterator<Map.Entry<String, org.recast4j.detour.OffMeshConnection>> itOffMesh = mapOffMeshCon.entrySet().iterator();
while (itOffMesh.hasNext()) {
    Map.Entry<String, org.recast4j.detour.OffMeshConnection> next = itOffMesh.next();

    /**
     * If the OffMeshConnection id is 0, there is no paired bone for the
     * link so skip.
     */
    if (next.getValue().userId > 0) {
        //Create a new filter for findNearestPoly
        DefaultQueryFilter filter = new DefaultQueryFilter();

        //In our case, we only need swim or walk flags.
        int include = POLYFLAGS_WALK | POLYFLAGS_SWIM;
        filter.setIncludeFlags(include);

        //No excludes.
        int exclude = 0;
        filter.setExcludeFlags(exclude);

        //Get the start position for the link.
        float[] startPos = new float[3];
        System.arraycopy(next.getValue().pos, 0, startPos, 0, 3);
        //Get the end position for the link.
        float[] endPos = new float[3];
        System.arraycopy(next.getValue().pos, 3, endPos, 0, 3);

        //Find the nearest polys to start/end.
        Result<FindNearestPolyResult> startPoly = query.findNearestPoly(startPos, new float[] {radius,radius,radius}, filter);
        Result<FindNearestPolyResult> endPoly = query.findNearestPoly(endPos, new float[] {radius,radius,radius}, filter);

        /**
         * Note: not isFailure() here, because isSuccess guarantees us,
         * that the result isn't "RUNNING", which it could be if we only
         * check it's not failure.
         */
        if (!startPoly.status.isSuccess()
        ||  !endPoly.status.isSuccess()
        ||   startPoly.result.getNearestRef() == 0
        ||   endPoly.result.getNearestRef() == 0) {
            LOG.error("offmeshCon findNearestPoly unsuccessful or getNearestRef is not > 0.");
            LOG.error("Link [{}] pos {} id [{}]", next.getKey(), Arrays.toString(next.getValue().pos), next.getValue().userId);
            LOG.error("findNearestPoly startPoly [{}] getNearestRef [{}]", startPoly.status.isSuccess(), startPoly.result.getNearestRef());
            LOG.error("findNearestPoly endPoly [{}] getNearestRef [{}].", endPoly.status.isSuccess(), endPoly.result.getNearestRef());
        } else {
            //Get the tile and poly from reference.
            Result<Tupple2<MeshTile, Poly>> startTileByRef = navMesh.getTileAndPolyByRef(startPoly.result.getNearestRef());
            Result<Tupple2<MeshTile, Poly>> endTileByRef = navMesh.getTileAndPolyByRef(endPoly.result.getNearestRef());

            //Mesh data for the start/end tile.
            MeshData startTile = startTileByRef.result.first.data;
            MeshData endTile = endTileByRef.result.first.data;

            //Both start and end poly must be vailid.
            if (startTileByRef.result.second != null && endTileByRef.result.second != null) {
                //We will add a new poly that will become our "link"
                //between start and end points so make room for it.
                startTile.polys = Arrays.copyOf(startTile.polys, startTile.polys.length + 1);
                //We shifted everything but haven't incremented polyCount
                //yet so this will become our new poly's index.
                int poly = startTile.header.polyCount;
                /**
                 * Off-mesh connections are stored in the navigation
                 * mesh as special 2-vertex polygons with a single edge.
                 * At least one of the vertices is expected to be inside
                 * a normal polygon. So an off-mesh connection is
                 * "entered" from a normal polygon at one of its
                 * endpoints. Jme requires 3 vertices per poly to
                 * build a debug mesh so we have to create a
                 * 3-vertex polygon here if using debug. The extra
                 * vertex position will be connected automatically
                 * when we add the tile back to the navmesh. For
                 * games, this would be a two vert poly.
                 *
                 * See: https://github.com/ppiastucki/recast4j/blob/3c532068d79fe0306fedf035e50216008c306cdf/detour/src/main/java/org/recast4j/detour/NavMesh.java#L406
                 */
                startTile.polys[poly] = new Poly(poly, 3);
                /**
                 * Must add/create our new indices for start and end.
                 * When we add the tile, the third vert will be
                 * generated for us.
                 */
                startTile.polys[poly].verts[0] = startTile.header.vertCount;
                startTile.polys[poly].verts[1] = startTile.header.vertCount + 1;
                //Set the poly's type to DT_POLYTYPE_OFFMESH_CONNECTION
                //so it is not seen as a regular poly when linking.
                startTile.polys[poly].setType(Poly.DT_POLYTYPE_OFFMESH_CONNECTION);
                //Make room for our start/end verts.
                startTile.verts = Arrays.copyOf(startTile.verts, startTile.verts.length + 6);
                //Increment our poly and vert counts.
                startTile.header.polyCount++;
                startTile.header.vertCount += 2;
                //Set our OffMeshLinks poly to this new poly.
                next.getValue().poly = poly;
                //Shorten names and make readable. Could just call directly.
                float[] start = startPoly.result.getNearestPos();
                float[] end = endPoly.result.getNearestPos();
                //Set the links position array values to nearest.
                next.getValue().pos = new float[] { start[0], start[1], start[2], end[0], end[1], end[2] };
                //Determine what side of the tile the vertx is on.
                next.getValue().side = startTile == endTile ? 0xFF
                        : NavMeshBuilder.classifyOffMeshPoint(new VectorPtr(next.getValue().pos, 3),
                                startTile.header.bmin, startTile.header.bmax);
                //Create new OffMeshConnection array.
                if (startTile.offMeshCons == null) {
                        startTile.offMeshCons = new org.recast4j.detour.OffMeshConnection[1];
                } else {
                        startTile.offMeshCons = Arrays.copyOf(startTile.offMeshCons, startTile.offMeshCons.length + 1);
                }

                //Add this connection.
                startTile.offMeshCons[startTile.offMeshCons.length - 1] = next.getValue();
                startTile.header.offMeshConCount++;

                //Set the polys area type and flags.
                startTile.polys[poly].flags = POLYFLAGS_JUMP;
                startTile.polys[poly].setArea(POLYAREA_TYPE_JUMP);

                /**
                 * Removing and adding the tile will rebuild all the
                 * links for the tile automatically. The number of links
                 * is : edges + portals * 2 + off-mesh con * 2.
                 */
                MeshData removeTile = navMesh.removeTile(navMesh.getTileRef(startTileByRef.result.first));
                navMesh.addTile(removeTile, 0, navMesh.getTileRef(startTileByRef.result.first));
            }
        }
    }
}

To set an OffMeshConnection, we create a new poly, but first we need to have a valid poly. Technically, when you build a tile, it will find the closest poly for you. However, since we are building this link ourselves, there is certain data that has to be taken from a valid poly in order to build it.

You use the standard methods for finding the closest poly for the start and end points. This assures us we will have a valid link. We will then use the data from these polys to finish building our connection.

//We will add a new poly that will become our "link"
//between start and end points so make room for it.
startTile.polys = Arrays.copyOf(startTile.polys, startTile.polys.length + 1);
//We shifted everything but haven't incremented polyCount
//yet so this will become our new poly's index.
int poly = startTile.header.polyCount;

First we make room for the poly.

startTile.polys[poly] = new Poly(poly, 3);
startTile.polys[poly].verts[0] = startTile.header.vertCount;
startTile.polys[poly].verts[1] = startTile.header.vertCount + 1;

Then we create the poly that will become our link and add the indicies to the tile. Off-mesh connections are stored in the navigation mesh as special 2-vertex polygons with a single edge. At least one of the vertices is expected to be inside a normal polygon. So an off-mesh connection is "entered" from a normal polygon at one of its endpoints. Jme requires 3 vertices per poly to build a debug mesh so we have to create a 3-vertex polygon here if using debug. The extra vertex position will be connected automatically when we add the tile back to the navmesh. For games, this would be a two vert poly.

//Set the poly's type to DT_POLYTYPE_OFFMESH_CONNECTION
//so it is not seen as a regular poly when linking.
startTile.polys[poly].setType(Poly.DT_POLYTYPE_OFFMESH_CONNECTION);

Next, set the new poly type. By doing this, this poly will not be considered to be a normal link between tiles so does not get processed as one when building the tile.

//Increment our poly and vert counts.
startTile.header.polyCount++;
startTile.header.vertCount += 2;

Now that we have built the poly, we increase the tiles poly and vert counts.

//Set our OffMeshLinks poly to this new poly.
next.getValue().poly = poly;
//Set the links position array values to nearest.
next.getValue().pos = new float[] { start[0], start[1], start[2], end[0], end[1], end[2] };

Then set the OffmeshConnection poly to our newly generated one and replace the start/end points for the link to point to our nearest positions for the closest polys.

//Determine what side of the tile the vertx is on.
next.getValue().side = startTile == endTile ? 0xFF
        : NavMeshBuilder.classifyOffMeshPoint(new VectorPtr(next.getValue().pos, 3),
                startTile.header.bmin, startTile.header.bmax);

Fill in the OffMeshConnection side of the tile. Basically, if this tile and end tile are the same, the tile is 0, ie the center of the surounding 8 tiles, otherwise whatever side the point is on.

//Create new OffMeshConnection array.
if (startTile.offMeshCons == null) {
        startTile.offMeshCons = new org.recast4j.detour.OffMeshConnection[1];
} else {
        startTile.offMeshCons = Arrays.copyOf(startTile.offMeshCons, startTile.offMeshCons.length + 1);
}

//Add this connection.
startTile.offMeshCons[startTile.offMeshCons.length - 1] = next.getValue();
startTile.header.offMeshConCount++;

Now, if the tiles OffmeshConnections array is empty, we just add to it else add it to the end and increase the tiles OffMeshConnection count.

//Set the polys area type and flags.
startTile.polys[poly].flags = POLYFLAGS_JUMP;
startTile.polys[poly].setArea(POLYAREA_TYPE_JUMP);

We set the poly Area Type and Ability Flag so we can detect it with path filtering.

/**
 * Removing and adding the tile will rebuild all the
 * links for the tile automatically. The number of links
 * is : edges + portals * 2 + off-mesh con * 2.
 */
 MeshData removeTile = navMesh.removeTile(navMesh.getTileRef(startTileByRef.result.first));
 navMesh.addTile(removeTile, 0, navMesh.getTileRef(startTileByRef.result.first));

This is the most important part. The links for this newly created poly to its neighbors have not yet been set. By removing and adding the tile back, using the same data, the links for the poly will be generated for us.

Animations And Movement Through OffMeshConnections

Tile Cache

Tile cache consists of creating a cache of tiles that are identicle to the tiles in a NavMesh and provides a super fast way to cut holes in the NavMesh. Tile cache is best for simple obstacles like crates and barrels which can move or are added and removed. These holes act as a natural filter for path finding in that the path will not travel through the hole but instead will try to navigate around it if possible.

As with all navMesh builds, the first steps are to gather your geometry, set your Area Types, and prepare your offMeshConnections if you have any. What comes next is as follows.

RecastConfig rcConfig = builder
        .withAgentRadius(radius)            // r
        .withAgentHeight(height)            // h
        //cs and ch should be .1 at min.
        .withCellSize(0.1f)                 // cs=r/2
        .withCellHeight(0.1f)               // ch=cs/2 but not < .1f
        .withAgentMaxClimb(maxClimb)        // > 2*ch
        .withAgentMaxSlope(45f)
        .withEdgeMaxLen(3.2f)               // r*8
        .withEdgeMaxError(1.3f)             // 1.1 - 1.5
        .withDetailSampleDistance(6.0f)     // increase if exception
        .withDetailSampleMaxError(6.0f)     // increase if exception
        .withVertsPerPoly(3)
        .withPartitionType(PartitionType.MONOTONE)
        .withTileSize(16).build();

//Build the tile cache which also builds the navMesh.
TileCache tc = getTileCache(geom, rcConfig);

/**
 * Layers represent heights for the tile cache. For example, a bridge
 * with an underpass would have a layer for travel under the bridge and
 * another for traveling over the bridge.
 */
TileLayerBuilder layerBuilder = new TileLayerBuilder(geom, rcConfig);

List<byte[]> layers = layerBuilder.build(ByteOrder.BIG_ENDIAN, false, 1);

for (byte[] data : layers) {
    try {
        /**
         * The way tile cache works is you have two tiles, one is for
         * the cache and is added here with addTile. The other is for
         * the NavMesh and is added with buildNavMeshTile.
         */
        long ref = tc.addTile(data, 0);
        tc.buildNavMeshTile(ref);
    } catch (IOException ex) {
        LOG.error("{} {}" + NavState.class.getName(), ex);
    }
}

The process consists of building a navmesh of tiles and a identicle cache of tiles in memory that will allow you to switch between the two tiles. A typical config is as follows.

RecastConfig rcConfig = builder
        .withAgentRadius(radius)            // r
        .withAgentHeight(height)            // h
        //cs and ch should be .1 at min.
        .withCellSize(0.1f)                 // cs=r/2
        .withCellHeight(0.1f)               // ch=cs/2 but not < .1f
        .withAgentMaxClimb(maxClimb)        // > 2*ch
        .withAgentMaxSlope(45f)
        .withEdgeMaxLen(3.2f)               // r*8
        .withEdgeMaxError(1.3f)             // 1.1 - 1.5
        .withDetailSampleDistance(6.0f)     // increase if exception
        .withDetailSampleMaxError(6.0f)     // increase if exception
        .withVertsPerPoly(3)
        .withPartitionType(PartitionType.MONOTONE)
        .withTileSize(16).build();

When building the tile cache, use Monotone Partitioning in your RecastConfig to build the mesh. Monotone partitioning is in general much faster than the other partitioning algorithms. In addition, for small tiles, the quality of the regions produced by monotone partitioning is comparable to the other partitioning algorithms. For those reasons it is the method used in the tile cache. Make sure you are using small tile sizes when using the tile cache in the range of 16 to 128 voxels.

//Build the tile cache which also builds the navMesh.
TileCache tc = getTileCache(geom, rcConfig);

After you set the parameters, you then create the tileCache. We wrap this part in a method call for code readabilty but you don’t have to.

//Build the tile cache.
private TileCache getTileCache(JmeInputGeomProvider geom, RecastConfig rcfg) {
    final int EXPECTED_LAYERS_PER_TILE = 4;

    TileCacheParams params = new TileCacheParams();

    int[] twh = Recast.calcTileCount(geom.getMeshBoundsMin(), geom.getMeshBoundsMax(), rcfg.cs, rcfg.tileSize);

    params.ch = rcfg.ch;
    params.cs = rcfg.cs;
    vCopy(params.orig, geom.getMeshBoundsMin());
    params.height = rcfg.tileSize;
    params.width = rcfg.tileSize;
    params.walkableHeight = height;
    params.walkableRadius = radius;
    params.walkableClimb = maxClimb;
    params.maxSimplificationError = rcfg.maxSimplificationError;
    params.maxTiles = twh[0] * twh[1] * EXPECTED_LAYERS_PER_TILE;
    params.maxObstacles = 128;
    NavMeshParams navMeshParams = new NavMeshParams();
    copy(navMeshParams.orig, geom.getMeshBoundsMin());
    navMeshParams.tileWidth = rcfg.tileSize * rcfg.cs;
    navMeshParams.tileHeight = rcfg.tileSize * rcfg.cs;
    navMeshParams.maxTiles = params.maxTiles;
    navMeshParams.maxPolys = 16384;
    NavMesh navMesh = new NavMesh(navMeshParams, 3);

    return new TileCache(params, new TileCacheStorageParams(ByteOrder.BIG_ENDIAN, false), navMesh, TileCacheCompressorFactory.get(false), new JmeTileCacheMeshProcess());
}

Layers represent heights for the tile cache. For example, a bridge with an underpass would have a layer for travel under the bridge and another for traveling over the bridge.

    final int EXPECTED_LAYERS_PER_TILE = 4;

There is no exact science to this setting so use your own judgement on the number of layers you will want for your given scene. The other individual settings are explained in detail throughout this wiki so they should be familar to you by now.

params.walkableHeight = height;
params.walkableRadius = radius;
params.walkableClimb = maxClimb;

It’s important to use the characters parameters that you expect to use the navMesh and not the calculated ones provided by a paramsBuilder object or config file. Calculated params are in voxels and you want world cordinates for these.

At this point you have a configured navMesh and empty tile cache built using the provided parameters. Now its time to fill the cache and add the tiles to the navMesh.

First you build the heightFields for the navMesh by using the TileLayerBuilder and passing it your gathered geometry and config file.

/**
 * Layers represent heights for the tile cache. For example, a bridge
 * with an underpass would have a layer for travel under the bridge and
 * another for traveling over the bridge.
 */
TileLayerBuilder layerBuilder = new TileLayerBuilder(geom, rcConfig);

List<byte[]> layers = layerBuilder.build(ByteOrder.BIG_ENDIAN, false, 1);

This will return to you a list of tile data based off the provided geometry and config. You now parse this list and create the tiles for the cache and navMesh.

for (byte[] data : layers) {
    try {
        /**
         * The way tile cache works is you have two tiles, one is for
         * the cache and is added here with addTile. The other is for
         * the NavMesh and is added with buildNavMeshTile.
         */
        long ref = tc.addTile(data, 0);
        tc.buildNavMeshTile(ref);
    } catch (IOException ex) {
        LOG.error("{} {}" + NavState.class.getName(), ex);
    }
}

As is noted above, you are creating a tile for the navMesh and another tile for the tile cache. You still need to set the Ability Flags for filtering which requires an additional object to do so.

/**
 * This is a mandatory class otherwise the tile cache build will not set
 * the areas. This gets call from the tc.buildNavMeshTile(ref) method.
 */
protected class JmeTileCacheMeshProcess implements TileCacheMeshProcess {

    @Override
    public void process(NavMeshDataCreateParams params) {
        // Update poly flags from areas.
        for (int i = 0; i < params.polyCount; ++i) {
            Poly p = new Poly(i, 6);

            if (params.polyAreas[i] == DT_TILECACHE_WALKABLE_AREA) {
                params.polyAreas[i] = POLYAREA_TYPE_GROUND;
            }

            if (params.polyAreas[i] == POLYAREA_TYPE_GROUND
            ||  params.polyAreas[i] == POLYAREA_TYPE_GRASS
            ||  params.polyAreas[i] == POLYAREA_TYPE_ROAD) {
                params.polyFlags[i] = POLYFLAGS_WALK;
            } else if (params.polyAreas[i] == POLYAREA_TYPE_WATER) {
                params.polyFlags[i] = POLYFLAGS_SWIM;
            } else if (params.polyAreas[i] == POLYAREA_TYPE_DOOR) {
                params.polyFlags[i] = POLYFLAGS_WALK | POLYFLAGS_DOOR;
            }
        }
    }

}

You set the Ability Flags by passing in a object that implements the TileCacheMeshProcess. This gets called with the buildNavMeshTile() method during the navMesh build part of the process. In theory, you should be able to set the offMeshConnections from here also but I was not able to find a way to do it.

Instead of setting offMeshConnections in the TileCacheMeshProcess, you can do it after you save the tiles out. Just place this after the tile building loop.

//Save and read back for testing.
TileCacheWriter writer = new TileCacheWriter();
TileCacheReader reader = new TileCacheReader();

try {
    //Write our file.
    writer.write(new FileOutputStream(new File("test.tc")), tc, ByteOrder.BIG_ENDIAN, false);
    //Create new tile cache.
    tc = reader.read(new FileInputStream("test.tc"), 3, new JmeTileCacheMeshProcess());

    //Get the navMesh and build a querry object.
    navMesh = tc.getNavMesh();
    query = new NavMeshQuery(navMesh);

    /**
     * Process OffMeshConnections. Since we are reading this in we do it
     * here. If we were just running with the tile cache we first
     * created we would just place this after building the tiles.
     * Basic flow:
     * Check each mapOffMeshConnection for an index > 0.
     * findNearestPoly() for the start/end positions of the link.
     * getTileAndPolyByRef() using the returned poly reference.
     * If both start and end are good values, set the connection properties.
     */
    Iterator<Map.Entry<String, org.recast4j.detour.OffMeshConnection>> itOffMesh = mapOffMeshCon.entrySet().iterator();
    while (itOffMesh.hasNext()) {
        Map.Entry<String, org.recast4j.detour.OffMeshConnection> next = itOffMesh.next();

        /**
         * If the OffMeshConnection id is 0, there is no paired bone for the
         * link so skip.
         */
        if (next.getValue().userId > 0) {
            //Create a new filter for findNearestPoly
            DefaultQueryFilter filter = new DefaultQueryFilter();

            //In our case, we only need swim or walk flags.
            int include = POLYFLAGS_WALK | POLYFLAGS_SWIM;
            filter.setIncludeFlags(include);

            //No excludes.
            int exclude = 0;
            filter.setExcludeFlags(exclude);

            //Get the start position for the link.
            float[] startPos = new float[3];
            System.arraycopy(next.getValue().pos, 0, startPos, 0, 3);
            //Get the end position for the link.
            float[] endPos = new float[3];
            System.arraycopy(next.getValue().pos, 3, endPos, 0, 3);

            //Find the nearest polys to start/end.
            Result<FindNearestPolyResult> startPoly = query.findNearestPoly(startPos, new float[] {radius,radius,radius}, filter);
            Result<FindNearestPolyResult> endPoly = query.findNearestPoly(endPos, new float[] {radius,radius,radius}, filter);

            /**
             * Note: not isFailure() here, because isSuccess guarantees us,
             * that the result isn't "RUNNING", which it could be if we only
             * check it's not failure.
             */
            if (!startPoly.status.isSuccess()
            ||  !endPoly.status.isSuccess()
            ||   startPoly.result.getNearestRef() == 0
            ||   endPoly.result.getNearestRef() == 0) {
                LOG.error("offmeshCon findNearestPoly unsuccessful or getNearestRef is not > 0.");
                LOG.error("Link [{}] pos {} id [{}]", next.getKey(), Arrays.toString(next.getValue().pos), next.getValue().userId);
                LOG.error("findNearestPoly startPoly [{}] getNearestRef [{}]", startPoly.status.isSuccess(), startPoly.result.getNearestRef());
                LOG.error("findNearestPoly endPoly [{}] getNearestRef [{}].", endPoly.status.isSuccess(), endPoly.result.getNearestRef());
            } else {
                //Get the tile and poly from reference.
                Result<Tupple2<MeshTile, Poly>> startTileByRef = navMesh.getTileAndPolyByRef(startPoly.result.getNearestRef());
                Result<Tupple2<MeshTile, Poly>> endTileByRef = navMesh.getTileAndPolyByRef(endPoly.result.getNearestRef());

                //Mesh data for the start/end tile.
                MeshData startTile = startTileByRef.result.first.data;
                MeshData endTile = endTileByRef.result.first.data;

                //Both start and end poly must be vailid.
                if (startTileByRef.result.second != null && endTileByRef.result.second != null) {
                    //We will add a new poly that will become our "link"
                    //between start and end points so make room for it.
                    startTile.polys = Arrays.copyOf(startTile.polys, startTile.polys.length + 1);
                    //We shifted everything but haven't incremented polyCount
                    //yet so this will become our new poly's index.
                    int poly = startTile.header.polyCount;
                    /**
                     * Off-mesh connections are stored in the navigation
                     * mesh as special 2-vertex polygons with a single edge.
                     * At least one of the vertices is expected to be inside
                     * a normal polygon. So an off-mesh connection is
                     * "entered" from a normal polygon at one of its
                     * endpoints. Jme requires 3 vertices per poly to
                     * build a debug mesh so we have to create a
                     * 3-vertex polygon here if using debug. The extra
                     * vertex position will be connected automatically
                     * when we add the tile back to the navmesh. For
                     * games, this would be a two vert poly.
                     *
                     * See: https://github.com/ppiastucki/recast4j/blob/3c532068d79fe0306fedf035e50216008c306cdf/detour/src/main/java/org/recast4j/detour/NavMesh.java#L406
                     */
                    startTile.polys[poly] = new Poly(poly, 3);
                    /**
                     * Must add/create our new indices for start and end.
                     * When we add the tile, the third vert will be
                     * generated for us.
                     */
                    startTile.polys[poly].verts[0] = startTile.header.vertCount;
                    startTile.polys[poly].verts[1] = startTile.header.vertCount + 1;
                    //Set the poly's type to DT_POLYTYPE_OFFMESH_CONNECTION
                    //so it is not seen as a regular poly when linking.
                    startTile.polys[poly].setType(Poly.DT_POLYTYPE_OFFMESH_CONNECTION);
                    //Make room for our start/end verts.
                    startTile.verts = Arrays.copyOf(startTile.verts, startTile.verts.length + 6);
                    //Increment our poly and vert counts.
                    startTile.header.polyCount++;
                    startTile.header.vertCount += 2;
                    //Set our OffMeshLinks poly to this new poly.
                    next.getValue().poly = poly;
                    //Shorten names and make readable. Could just call directly.
                    float[] start = startPoly.result.getNearestPos();
                    float[] end = endPoly.result.getNearestPos();
                    //Set the links position array values to nearest.
                    next.getValue().pos = new float[] { start[0], start[1], start[2], end[0], end[1], end[2] };
                    //Determine what side of the tile the vertx is on.
                    next.getValue().side = startTile == endTile ? 0xFF
                            : NavMeshBuilder.classifyOffMeshPoint(new VectorPtr(next.getValue().pos, 3),
                                    startTile.header.bmin, startTile.header.bmax);
                    //Create new OffMeshConnection array.
                    if (startTile.offMeshCons == null) {
                            startTile.offMeshCons = new org.recast4j.detour.OffMeshConnection[1];
                    } else {
                            startTile.offMeshCons = Arrays.copyOf(startTile.offMeshCons, startTile.offMeshCons.length + 1);
                    }

                    //Add this connection.
                    startTile.offMeshCons[startTile.offMeshCons.length - 1] = next.getValue();
                    startTile.header.offMeshConCount++;

                    //Set the polys area type and flags.
                    startTile.polys[poly].flags = POLYFLAGS_JUMP;
                    startTile.polys[poly].setArea(POLYAREA_TYPE_JUMP);

                    /**
                     * Removing and adding the tile will rebuild all the
                     * links for the tile automatically. The number of links
                     * is : edges + portals * 2 + off-mesh con * 2.
                     */
                    MeshData removeTile = navMesh.removeTile(navMesh.getTileRef(startTileByRef.result.first));
                    navMesh.addTile(removeTile, 0, navMesh.getTileRef(startTileByRef.result.first));
                }
            }
        }
    }
} catch (IOException ex) {
    LOG.error("{} {}", NavState.class.getName(), ex);
}
⚠️ **GitHub.com Fallback** ⚠️