Navmesh cell size

I have found that there is a problem with cell size in the NavMeshGenerator where I cannot set cell size less than .9f.

The problem is mentioned here by Sploreg,

Anyone fixed this in their fork or version?

Small update.

Been working on implementing Recast4j and the mesh it produces using a cell size of .3f works. Doesn’t take forever either. The mesh connects through levels with stairs, including curved ones, under roofs with many columns, up ramps and mountains, etc. All with a beautifully perfect mesh.

The only way I can think of to get jmeAI to work is scaling everything in the scene up by a factor of 10 to compensate for the minimum cell size of 1f but that isn’t a real solution.

1 Like

Not sure if you checked it out already, but there’s some wiki documentation for Recast in JME: https://jmonkeyengine.github.io/wiki/jme3/advanced/recast.html

Actually a decent recast and detour Implementation would be awesome, especially because of detour crowd

I used this to get an idea of how to get Recast4j to work.

At this point I have successfully generated a very accurate navmesh, the NavMeshQuery obj and a wireframe of the mesh for debugging. I am about to undertake the pathfinding part now. Crossing my fingers I can get it to work.

I had to learn jme3AI, critteraAI and jNavigation just to get to this point. I now know more about AI than I ever intended or wanted to.

This is a feature that I am interested in also.

3 Likes

I just started with recast4j. So it would be great if you can share some details about your implementation.

I have Recast and Detour finished. About to do Detour Crowd.

If you look at the recast4j site you will find that each part of recast has test cases. This will get you started.
https://groups.google.com/forum/#!msg/recastnavigation/zz69QMn4YeA/xMuU9AZKEAAJ

I used some of jme3AI methods, mixed with my own, mixed with the test cases, mixed with jNavigation, mixed with the SDK to build it.
https://jmonkeyengine.github.io/wiki/jme3/advanced/jme3_ai.html
https://jmonkeyengine.github.io/wiki/jme3/advanced/recast.html

I am almost done refactoring the code from my implementation of jme3I which will be used to finish the jme3AI tutorial I linked to. That will make it significantly easier for you to learn recast4j because its nearly identical.

These are the steps for recast I did,
Initialized all the variables for,

RecastConfig cfg = new RecastConfig();

These are my variables with notations gathered from creators on usage,

    /**
     * ***********************************************************************
     */
    //The width/height size of tile's on the xz-plane.
    //>=0
    private final int m_tileSize = 64;
    //The size of the non-navigable border around the heightfield.    
    //>=0
    private final int m_borderSize = 0;
    //The width and depth resolution used when sampling the source geometry. 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 size needed to allow mesh to travel up stairs.
    //Adjust m_cellSize and m_cellHeight for contour simplification exceptions.
    //> 0, outdoors = m_agentRadius/2, indoors = m_agentRadius/3, m_cellSize = 
    //m_agentRadius for very small cells.
    private final float m_cellSize = 0.25f;
    //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. 
    //m_agentHeight, m_agentMaxClimb, and m_detailSampleMaxError will 
    //need to be greater than this value in order to function correctly. 
    //m_agentMaxClimb is especially susceptible to impact from the value of 
    //m_cellHeight.
    //> 0, m_cellSize/2
    private final float m_cellHeight = 0.125f;
    //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 un-walkable. It can 
    //also be thought of as the maximum agent height.
    //This value should be at least two times the value of m_cellHeight in order 
    //to get good results. 
    //> 0 
    private final float m_agentHeight = 2.0f;
    //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.
    //This value must be greater than the m_cellSize to have an effect.
    //>= 0 
    private final float m_agentRadius = 0.5f;
    //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.
    //m_agentMaxClimb should be greater than two times m_cellHeight. 
    //(m_agentMaxClimb > m_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. 
    //>= 0, m_agentMaxClimb/m_cellHeight = voxels.
    private final float m_agentMaxClimb = 0.7f;
    //The maximum slope that is considered traversable. (In degrees.)
    //>= 0 
    private final float m_agentMaxSlope = 50.0f;
    //The minimum region size for unconnected (island) regions.
    //>= 0 
    private final int m_regionMinSize = 8;
    //Any regions smaller than this size will, if possible, be merged with 
    //larger regions.
    //>= 0 
    private final int m_regionMergeSize = 20;
    //The maximum length of polygon edges that represent the border of meshes.
    //Adjust to decrease dangling errors.
    //>= 0, m_agentRadius * 8
    private final float m_edgeMaxLen = 6.0f;
    //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.
    //>= 0, 1.1 to 1.5 for best results.
    private final float m_edgeMaxError = 1.3f;
    //The maximum number of vertices per polygon for polygons generated during 
    //the voxel to polygon conversion process.
    //>= 3 
    private final int m_vertsPerPoly = 6;
    //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 geometry's surface at the cost of a higher final triangle count 
    //and higher processing cost.
    //The difference between this parameter and m_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. 
    //m_edgeMaxError only matches edges of meshes to the contour of the original 
    //geometry. 
    //Decrease to reduce dangling errors.
    //>= 0 
    private final float m_detailSampleDist = 5.0f;
    //The maximum distance the surface of the detail mesh may deviate from the 
    //surface of the original geometry.
    //Increase to reduce dangling errors.
    //>= 0 
    private final float m_detailSampleMaxError = 5.0f;
    private final PartitionType m_partitionType = PartitionType.WATERSHED;

Use findGeometries and terrain2mesh methods from jme3AI and SDK to gather the geometries.

GeometryBatchFactory.mergeGeometries(findGeometries(app.getRootNode(), new LinkedList<>()), mesh);
public Mesh terrain2mesh(Terrain terr) 

Create a list of Vertex positions and Index positions from that mesh.

/**
     * Get vertcices of a mesh boxed to Float.
     *
     * @param mesh
     * @return Returns boxed List of vertices.
     */
    private List<Float> getVertices(Mesh mesh) {
        FloatBuffer buffer = mesh.getFloatBuffer(VertexBuffer.Type.Position);
        float[] vertexArray = BufferUtils.getFloatArray(buffer);
        List<Float> vertexList = new ArrayList<>();

        for (float vertex : vertexArray) {
            vertexList.add(vertex);
        }
        return vertexList;
    }

    /**
     * Get all triangles from a mesh boxed to Integer.
     *
     * @param mesh
     * @return Returns boxed List of triangles.
     */
    private List<Integer> getIndices(Mesh mesh) {
        int[] indices = new int[3];
        Integer[] triangles = new Integer[mesh.getTriangleCount() * 3];

        for (int i = 0; i < triangles.length; i += 3) {
            mesh.getTriangle(i / 3, indices);
            triangles[i] = indices[0];
            triangles[i + 1] = indices[1];
            triangles[i + 2] = indices[2];
        }
        //Independent copy so Arrays.asList is garbage collected
        List<Integer> indicesList = new ArrayList<>(Arrays.asList(triangles));
        return indicesList;
    }

Feed those two lists into,

InputGeom geom = new InputGeom(vertexPositions, indices);

Let solo or tile mesh do its thang,

Hack saveObj to work with normals (didn’t have to but it shows any unwanted holes in the mesh if you do), export the PolyDetailMesh,

private void exportObj(String filename, PolyMeshDetail dmesh) {

        File file = new File(filename);
        try (FileWriter out = new FileWriter(file)) {
             //vertex
            for (int v = 0; v < dmesh.nverts; v++) {
                out.write("v " + dmesh.verts[v * 3] + " "
                        + dmesh.verts[v * 3 + 1] + " "
                        + dmesh.verts[v * 3 + 2] + "\n");
            }
            //vertex normal
            Vector3f[] vector3Array = getVector3Array(dmesh);
            for (int m = 0; m < dmesh.nmeshes; m++) {
                int vfirst = dmesh.meshes[m * 4];
                int tfirst = dmesh.meshes[m * 4 + 2];
                for (int f = 0; f < dmesh.meshes[m * 4 + 3]; f++) {
                    Vector3f normal = Triangle.computeTriangleNormal(
                            vector3Array[(vfirst + dmesh.tris[(tfirst + f) * 4])],
                            vector3Array[(vfirst + dmesh.tris[(tfirst + f) * 4 + 1])],
                            vector3Array[(vfirst + dmesh.tris[(tfirst + f) * 4 + 2])],
                            null);
                    out.write("vn " + normal.x + " " + normal.y + " " + normal.z + "\n");
                }
            }
            //face
            int count = 1;
            for (int m = 0; m < dmesh.nmeshes; m++) {
                int vfirst = dmesh.meshes[m * 4];
                int tfirst = dmesh.meshes[m * 4 + 2];
                for (int f = 0; f < dmesh.meshes[m * 4 + 3]; f++) {
                    out.write("f "
                            + (vfirst + dmesh.tris[(tfirst + f) * 4] + 1)
                            + "//" + count + " "
                            + (vfirst + dmesh.tris[(tfirst + f) * 4 + 1] + 1)
                            + "//" + count + " "
                            + (vfirst + dmesh.tris[(tfirst + f) * 4 + 2] + 1)
                            + "//" + count + "\n");
                    count++;
                }
            }
            out.close();
        } catch (IOException ex) {
            Logger.getLogger(RecastNavState.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

Heres the getter for the PolyMeshDetail Vector3f[] arrays,

private Vector3f[] getVector3Array(PolyMeshDetail dmesh) {
        Vector3f[] vector3Array = new Vector3f[dmesh.nverts];
        for (int i = 0; i < dmesh.nverts; i++) {
            vector3Array[i] = new Vector3f(dmesh.verts[i * 3],
                    dmesh.verts[i * 3 + 1], dmesh.verts[i * 3 + 2]);
        }
        return vector3Array;
    }

Wrote this to read in the saved object for display.

private void showDebugMesh(String name) {
        final Geometry meshGeom = (Geometry) getApplication().getAssetManager().loadModel(name);
        meshGeom.setName("NavMesh2");
        meshGeom.setCullHint(CullHint.Never);
        Material mat = new Material(getApplication().getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Green);
        mat.getAdditionalRenderState().setWireframe(true);
        meshGeom.setMaterial(mat);
        app.getRootNode().attachChild(meshGeom);
    }

Once you get SoloMesh up and running do TiledMesh because that’s where its at. Its at least a third faster and can produce the same exact mesh as Solo, if you want it to, with just changing m_tileSize. The A* pathfinding is amazingly accurate. I test mine with stairs with straight, 45, 90, 180 turns, overhangs, tables and it builds a beautifully perfect mesh.

TileMesh only needs these two changes, set this variable like this,

navMeshParams.maxTiles = tw * th;

gather up the dmesh in params into a list and hack exportOb to use this list,

private void exportObj(String filename, List<PolyMeshDetail> dmeshList) {

        File file = new File(filename);
        try (FileWriter out = new FileWriter(file)) {
            //vertex
            for (int j = 0; j < dmeshList.size(); j++) {
                PolyMeshDetail dmesh = dmeshList.get(j);
                for (int v = 0; v < dmesh.nverts; v++) {
                    out.write("v " + dmesh.verts[v * 3] + " "
                            + dmesh.verts[v * 3 + 1] + " "
                            + dmesh.verts[v * 3 + 2] + "\n");
                }
            }
             //vertex normal
            for (int j = 0; j < dmeshList.size(); j++) {
                PolyMeshDetail dmesh = dmeshList.get(j);
                Vector3f[] vector3Array = getVector3Array(dmesh);
                for (int m = 0; m < dmesh.nmeshes; m++) {
                    int vfirst = dmesh.meshes[m * 4];
                    int tfirst = dmesh.meshes[m * 4 + 2];
                    for (int f = 0; f < dmesh.meshes[m * 4 + 3]; f++) {
                        Vector3f normal = Triangle.computeTriangleNormal(
                                vector3Array[(vfirst + dmesh.tris[(tfirst + f) * 4])],
                                vector3Array[(vfirst + dmesh.tris[(tfirst + f) * 4 + 1])],
                                vector3Array[(vfirst + dmesh.tris[(tfirst + f) * 4 + 2])],
                                null);
                        out.write("vn " + normal.x + " " + normal.y + " " + normal.z + "\n");
                    }
                }
            }
            //face
            int count = 1;
            int offset = 0;
            for (int j = 0; j < dmeshList.size(); j++) {
                PolyMeshDetail dmesh = dmeshList.get(j);
                for (int m = 0; m < dmesh.nmeshes; m++) {
                    int vfirst = dmesh.meshes[m * 4];
                    int tfirst = dmesh.meshes[m * 4 + 2];
                    for (int f = 0; f < dmesh.meshes[m * 4 + 3]; f++) {
                        out.write("f "
                                + ((vfirst + dmesh.tris[(tfirst + f) * 4] + 1) + offset)
                                + "//" + count + " "
                                + ((vfirst + dmesh.tris[(tfirst + f) * 4 + 1] + 1) + offset)
                                + "//" + count + " "
                                + ((vfirst + dmesh.tris[(tfirst + f) * 4 + 2] + 1) + offset)
                                + "//" + count + "\n");
                        count++;
                    }
                }
                offset += dmesh.nverts;
            }
            out.close();
        } catch (IOException ex) {
            Logger.getLogger(RecastNavState.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

As for the actual use of the output, that’s also very similar to jme3AI. This whole recast is worthy of a tutorial of its own but I want to finish jme3AI tutorial. ill be posting that code for review shortly.

Edit: Forgot to mention, this runs from a BaseAppState.

5 Likes

I forgot to mention I think I found the cause for the original problem this thread started with.

There is not an actual code fix for it because what cause’s the

problem also happened in recast.

I think this method in jme3AI

private TriangleMesh buildNavMesh(float[] positions, int[] indices, IntermediateData intermediateData)

Is similar to this method in recast,

public RecastBuilderResult build(InputGeom geom, RecastBuilderConfig bcfg)

Both rely on a thread to build the mesh, recast uses these.

private RecastBuilderResult[][] buildSingleThread(InputGeom geom, RecastConfig cfg, float[] bmin, float[] bmax, int tw, int th) 

or

private RecastBuilderResult[][] buildMultiThread(InputGeom geom, RecastConfig cfg, float[] bmin, float[] bmax, int tw, int th, int threads)

Both of these methods have a executor timeout of,

ec.awaitTermination(1000, TimeUnit.HOURS);

Heres whats going on. When these two variables

int tw 
int th 

initialize they are tile width and tile height so a tile that is m_tileSize 64 for instance calculates to a 32x32 iteration in this for loop in either of the two methods,

for (int x = 0; x < tw; ++x) {
	for (int y = 0; y < th; ++y) {
		final int tx = x;
		final int ty = y;
		ec.submit((Runnable) () -> {
			result[tx][ty] = build(geom, new RecastBuilderConfig(cfg, bmin, bmax, tx, ty, true));
		});
	}
}

In this instance that’s 32 iterations tw and 32 iterations of th. The th part of this loop actually builds the mesh. This can take anywhere from 300ms to 1100ms per iteration.

On a rather small mesh that’s 32x32 = 1024 iterations x min of 300 ms or about 5 minutes. On large meshes with different tile sizes this can increase to 25k+ iterations at 1000 ms each or in laymen terms, 8+ hours to complete this loop.

So its not a problem that can be resolved by anything other than more computing power or just use smaller meshes.

2 Likes

Sorry for the late response here, was on vacation. I really appreciate that you share you implementation. At the moment I check the recast mesh but it looks like is has some holes:

Did you experience the same? I have no idea why these wholes are their…

I can’t tell what the terrain looks like.

Based off the small triangles at the edges of the mesh id say hills.

The steepness of the terrain determines if the mesh will climb the edge.
m_agentMaxSlope

The m_agentMaxClimb also affects if it will climb based on step height between ledges. Lower it and it will climb higher ledges.

You can also try modifying the terrain to have less defined edges on hills where you want it to travel.

You can also start with the default settings and see what happens.
m_cellSize = 0.30f
m_cellHeight =0.20f
m_agentHeight = 2.0f
m_agentRadius = 0.6f
m_agentMaxClimb=0.90f
m_agentMaxSlope =45
m_regionMinSize = 8
m_regionMergeSize = 20
m_edgeMaxLen = 12.0f
m_edgeMaxError = 1.3f
m_vertsPerPoly = 6
m_detailSampleDist = 6.0f
m_detailSampleMaxError = 1.0f

It generally comes down to either tweaking the parameters or mesh to get what you want in the end.

Edit: Also try lowering the m_regionMinSize .

This will maybe help some also,

Hi Mitm,

trying to implement Recast Navigation as done by you, I cannot find InputGeom class anymore in detour or recast.
Is your implementation more recent as this example here?

https://jmonkeyengine.github.io/wiki/jme3/advanced/recast.html

Its now called inputGeometryProvider.

InputGeomProvider geom = new SimpleInputGeomProvider(vertexPositions, indices);

Here is solo,

    private void soloNavMeshBuilder() {

        Mesh mesh = new Mesh();
        GeometryBatchFactory.mergeGeometries(findGeometries(app.getRootNode(),
                new LinkedList<>()), mesh);
        List<Float> vertexPositions = getVertices(mesh);
        List<Integer> indices = getIndices(mesh);
        InputGeomProvider geomProvider = new SimpleInputGeomProvider(vertexPositions, indices);
        long time1 = System.nanoTime();
        float[] bmin = geomProvider.getMeshBoundsMin();
        float[] bmax = geomProvider.getMeshBoundsMax();
//        float[] verts = geomProvider.getVerts();
//        int[] tris = geomProvider.getTris();
//        int ntris = tris.length / 3;

        // Step 1. Initialize build config.
        RecastConfig cfg = new RecastConfig(m_partitionType, m_cellSize,
                m_cellHeight, m_agentHeight, m_agentRadius,
                m_agentMaxClimb, m_agentMaxSlope, m_regionMinSize,
                m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError,
                m_vertsPerPoly, m_detailSampleDist, m_detailSampleMaxError,
                m_tileSize, SampleAreaModifications.SAMPLE_AREAMOD_GROUND);
        RecastBuilderConfig bcfg = new RecastBuilderConfig(cfg, bmin, bmax);
        Context m_ctx = new Context();

        // Step 2. Rasterize input polygon soup.
        // Allocate voxel heightfield where we rasterize our input data to.
        Heightfield m_solid = new Heightfield(bcfg.width, bcfg.height, bcfg.bmin,
                bcfg.bmax, cfg.cs, cfg.ch);

        for (TriMesh geom : geomProvider.meshes()) {
            float[] verts = geom.getVerts();
            int[] tris = geom.getTris();
            int ntris = tris.length / 3;
            // Allocate array that can hold triangle area types.
            // If you have multiple meshes you need to process, allocate
            // and array which can hold the max number of triangles you need to
            // process.

            // Find triangles which are walkable based on their slope and rasterize
            // them.
            // If your input data is multiple meshes, you can transform them here,
            // calculate
            // the are type for each of the meshes and rasterize them.
            int[] m_triareas = Recast.markWalkableTriangles(m_ctx, cfg.walkableSlopeAngle, verts, tris, ntris, cfg.walkableAreaMod);
            RecastRasterization.rasterizeTriangles(m_ctx, verts, tris, m_triareas, ntris, m_solid, cfg.walkableClimb);
        }

        // Step 3. Filter walkables surfaces.
        // Once all geometry is rasterized, we do initial pass of filtering to
        // remove unwanted overhangs caused by the conservative rasterization
        // as well as filter spans where the character cannot possibly stand.
        RecastFilter.filterLowHangingWalkableObstacles(m_ctx, cfg.walkableClimb, m_solid);
        RecastFilter.filterLedgeSpans(m_ctx, cfg.walkableHeight, cfg.walkableClimb, m_solid);
        RecastFilter.filterWalkableLowHeightSpans(m_ctx, cfg.walkableHeight, m_solid);

        // Step 4. Partition walkable surface to simple regions.
        // Compact the heightfield so that it is faster to handle from now on.
        // This will result more cache coherent data as well as the neighbours
        // between walkable cells will be calculated.
        CompactHeightfield m_chf = Recast.buildCompactHeightfield(m_ctx,cfg.walkableHeight, cfg.walkableClimb, m_solid);

        // Erode the walkable area by agent radius.
        RecastArea.erodeWalkableArea(m_ctx, cfg.walkableRadius, m_chf);

        // (Optional) Mark areas.
        /*
         * ConvexVolume vols = m_geom->getConvexVolumes(); for (int i = 0; i < m_geom->getConvexVolumeCount(); ++i)
         * rcMarkConvexPolyArea(m_ctx, vols[i].verts, vols[i].nverts, vols[i].hmin, vols[i].hmax, (unsigned
         * char)vols[i].area, *m_chf);
         */

        // Partition the heightfield so that we can use simple algorithm later
        // to triangulate the walkable areas.
        // There are 3 martitioning 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
        // - the are some corner cases where this method creates produces holes
        // and overlaps
        // - holes may appear when a small obstacles is close to large open area
        // (triangulation can handle this)
        // - overlaps may occur if you have narrow spiral corridors (i.e
        // stairs), this make triangulation to fail
        // * generally the best choice if you precompute the nacmesh, use this
        // if you have large open areas
        // 2) Monotone partioning
        // - 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 partitoining
        // - quite fast
        // - partitions the heighfield 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

        if (m_partitionType == PartitionType.WATERSHED) {
                // Prepare for region partitioning, by calculating distance field
                // along the walkable surface.
                RecastRegion.buildDistanceField(m_ctx, m_chf);
                // Partition the walkable surface into simple regions without holes.
                RecastRegion.buildRegions(m_ctx, m_chf, m_borderSize, cfg.minRegionArea, cfg.mergeRegionArea);
        } else if (m_partitionType == PartitionType.MONOTONE) {
                // Partition the walkable surface into simple regions without holes.
                // Monotone partitioning does not need distancefield.
                RecastRegion.buildRegionsMonotone(m_ctx, m_chf, m_borderSize, cfg.minRegionArea, cfg.mergeRegionArea);
        } else {
                // Partition the walkable surface into simple regions without holes.
                RecastRegion.buildLayerRegions(m_ctx, m_chf, m_borderSize, cfg.minRegionArea);
        }

        // Step 5. Trace and simplify region contours.
        // Create contours.
//        ContourSet m_cset = RecastContour.buildContours(m_ctx, m_chf, cfg.maxSimplificationError, cfg.maxEdgeLen, RecastConstants.RC_CONTOUR_TESS_WALL_EDGES);
        ContourSet m_cset = RecastContour.buildContours(m_ctx, m_chf, cfg.maxSimplificationError, cfg.maxEdgeLen, 0x01);
        // Step 6. Build polygons mesh from contours.
        // Build polygon navmesh from the contours.
        PolyMesh m_pmesh = RecastMesh.buildPolyMesh(m_ctx, m_cset, cfg.maxVertsPerPoly);

        // Step 7. Create detail mesh which allows to access approximate height
        // on each polygon.
        PolyMeshDetail m_dmesh = RecastMeshDetail.buildPolyMeshDetail(m_ctx,
                m_pmesh, m_chf, cfg.detailSampleDist, cfg.detailSampleMaxError);
        long time2 = System.nanoTime() - time1;
        System.out.println(exportFilename + " : " + m_partitionType + "  "
                + TimeUnit.SECONDS.convert(time2, TimeUnit.NANOSECONDS) + " s");
        exportObj(exportFilename.substring(0, exportFilename.lastIndexOf('.'))
                + "_debug.obj", m_dmesh);

        //must set flags for navigation controls to work
        for (int i = 0; i < m_pmesh.npolys; ++i) {
            m_pmesh.flags[i] = SampleAreaModifications.SAMPLE_POLYFLAGS_WALK;
        }
//        NavMeshDataCreateParams params = new NavMeshDataCreateParams();
        NavMeshDataCreateParams params = new NavMeshDataParameters();
        
        params.verts = m_pmesh.verts;
        params.vertCount = m_pmesh.nverts;
        params.polys = m_pmesh.polys;
        params.polyAreas = m_pmesh.areas;
        params.polyFlags = m_pmesh.flags;
        params.polyCount = m_pmesh.npolys;
        params.nvp = m_pmesh.nvp;
        params.detailMeshes = m_dmesh.meshes;
        params.detailVerts = m_dmesh.verts;
        params.detailVertsCount = m_dmesh.nverts;
        params.detailTris = m_dmesh.tris;
        params.detailTriCount = m_dmesh.ntris;
        params.walkableHeight = m_agentHeight;
        params.walkableRadius = m_agentRadius;
        params.walkableClimb = m_agentMaxClimb;
        params.bmin = m_pmesh.bmin;
        params.bmax = m_pmesh.bmax;
        params.cs = m_cellSize;
        params.ch = m_cellHeight;
        params.buildBvTree = true;
        
        MeshData meshData = NavMeshBuilder.createNavMeshData(params);
        navMesh = new NavMesh(meshData, params.nvp, 0);
        
        //create object to save solo NavMesh parameters
        MeshParameters meshParams = new MeshParameters();
        meshParams.addMeshDataParameters((NavMeshDataParameters) params);
        //save NavMesh parameters as .j3o
        saveNavMesh(meshParams, exportFilename.substring(0, exportFilename.lastIndexOf('.'))
                                                            + "_solo_C" + m_tileSize);

        //display NavMesh.obj
//        showDebugMesh("Scenes/Recast/recastmesh_debug.obj", ColorRGBA.Green);
        //save as obj no normals
//        saveObj(exportFilename.substring(0, exportFilename.lastIndexOf('.')) + "_solo_" + m_partitionType + "_C" + m_tileSize + "_detail.obj", m_dmesh);
        saveObj(exportFilename.substring(0, exportFilename.lastIndexOf('.')) + "_solo_" + m_partitionType + "_C" + m_tileSize + ".obj", m_pmesh);
        //save as obj with normals
        exportObj(exportFilename.substring(0, exportFilename.lastIndexOf('.')) + "_solo_" + m_partitionType + "_C" + m_tileSize + "_detail.obj", m_dmesh);
    }

Here is tiled.

    private void tiledNavMeshBuilder() {
        Mesh mesh = new Mesh();        
        GeometryBatchFactory.mergeGeometries(findGeometries(app.getRootNode(),
                new LinkedList<>()), mesh);
        List<Float> vertexPositions = getVertices(mesh);
        List<Integer> indices = getIndices(mesh);
        InputGeomProvider geom = new SimpleInputGeomProvider(vertexPositions, indices);
        long time1 = System.nanoTime();

        // Initialize build config.
        RecastConfig cfg = new RecastConfig(m_partitionType, m_cellSize,
                m_cellHeight, m_agentHeight, m_agentRadius,
                m_agentMaxClimb, m_agentMaxSlope, m_regionMinSize,
                m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError,
                m_vertsPerPoly, m_detailSampleDist, m_detailSampleMaxError,
                m_tileSize, SampleAreaModifications.SAMPLE_AREAMOD_GROUND);

        // Build all tiles
//        RecastBuilder builder = new RecastBuilder();
        time3 = System.nanoTime();
        //uses listener param (this)
        RecastBuilder builder = new RecastBuilder(this);
        //use listener when calling buildTiles
        RecastBuilderResult[][] rcResult = builder.buildTiles(geom, cfg, 1);
        long time2 = System.nanoTime() - time1;
        System.out.println(exportFilename + " : " + m_partitionType + "  "
                + TimeUnit.SECONDS.convert(time2, TimeUnit.NANOSECONDS) + " s");

        // Add tiles to nav mesh
        int tw = rcResult.length;
        int th = rcResult[0].length;

        // Create empty nav mesh
//        NavMeshParams navMeshParams = new NavMeshParams();
        NavMeshParams navMeshParams = new NavMeshParameters();
        copy(navMeshParams.orig, geom.getMeshBoundsMin());
        navMeshParams.tileWidth = m_tileSize * m_cellSize;
        navMeshParams.tileHeight = m_tileSize * m_cellSize;
        navMeshParams.maxTiles = tw * th;
        navMeshParams.maxPolys = 32768;
        //Create object to save NavMeshParameters for tiled NavMesh
        MeshParameters meshParams = new MeshParameters();
        meshParams.addMeshParameters((NavMeshParameters) navMeshParams);
        navMesh = new NavMesh(navMeshParams, 6);
        
        List<PolyMeshDetail> dmeshList = new ArrayList<>();

        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;
                }
                for (int i = 0; i < pmesh.npolys; ++i) {
                    pmesh.flags[i] = 1;
                }
//                NavMeshDataCreateParams params = new NavMeshDataCreateParams();
                NavMeshDataCreateParams params = new NavMeshDataParameters();
                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;
                PolyMeshDetail dmesh = rcResult[x][y].getMeshDetail();
                dmeshList.add(dmesh);
                params.detailMeshes = dmesh.meshes;
                params.detailVerts = dmesh.verts;
                params.detailVertsCount = dmesh.nverts;
                params.detailTris = dmesh.tris;
                params.detailTriCount = dmesh.ntris;
                params.walkableHeight = m_agentHeight;
                params.walkableRadius = m_agentRadius;
                params.walkableClimb = m_agentMaxClimb;
                params.bmin = pmesh.bmin;
                params.bmax = pmesh.bmax;
                params.cs = m_cellSize;
                params.ch = m_cellHeight;
                params.tileX = x;
                params.tileY = y;
                params.buildBvTree = true;
                navMesh.addTile(NavMeshBuilder.createNavMeshData(params), 0, 0);
                //Add parameters to the 
                meshParams.addMeshDataParameters((NavMeshDataParameters) params);
            }
        }

        //save NavMesh parameters as .j3o
        saveNavMesh(meshParams, exportFilename.substring(0, exportFilename.lastIndexOf('.')) 
                                + "_tiled_C" + m_tileSize);

        //save NavMesh as .obj
        exportObj(exportFilename.substring(0, exportFilename.lastIndexOf('.'))
                + "_tiled_C" + m_tileSize + ".obj", dmeshList);
        //display NavMesh.obj
//        showDebugMesh("Scenes/Recast/recastmesh_tiled64_debug.obj", ColorRGBA.Green);
    }

With tiled there is now an option to use a interface to setup a listener so you can know how many min, hours or days it would take to build the Navmesh. I say days because I pushed four 512x512 areas through with millions of vertices for testing. You don’t have to use a listener though.

Here is my listener that goes with the tiledNavMeshBuilder.

    long time3;
    
    long elapsedTime;
    long elapsedTimeHr;
    long elapsedTimeMin;
    long elapsedTimeSec;
    
    long buildTime;
    
    long avBuildTime;
    long buildTimeNano;
    
    long estTotalTime;
    long totalTimeHr;
    long totalTimeMin;
    long totalTimeSec;
    
    long estTimeRemain;
    long timeRemainHr;
    long timeRemainMin;
    long timeRemainSec;
     
    long convert;
    String minSec;
    /**
     * Listenr for result times to complete one tile in RecastBuilder.
     * @param completed
     * @param total
     */
    @Override
    public void onProgress(int completed, int total) {
        buildTime = System.nanoTime() - time3;
        elapsedTime += buildTime;
        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
        time3 = System.nanoTime();
    }

I also completely integrated recast4j to jme so you will need to change these two lines in the tiled builder.

//        NavMeshParams navMeshParams = new NavMeshParams();
        NavMeshParams navMeshParams = new NavMeshParameters();
//                NavMeshDataCreateParams params = new NavMeshDataCreateParams();
                NavMeshDataCreateParams params = new NavMeshDataParameters();

or this in solo,

//        NavMeshDataCreateParams params = new NavMeshDataCreateParams();
        NavMeshDataCreateParams params = new NavMeshDataParameters();

If I missed something let me know.

1 Like

Forgot, the interface is named,

RecastBuilderProgressListener

One last thing, I build this from source and create a library after giving feedback to the creator of recast4j so the most up to date version will be 1.8 whenever it is released. I am not sure what happens when using Maven and my code so if you get any errors let me know and I will try and lead you to the fix.

1 Like

Thank you a lot mitm. I will do it next days and let you know if something missed, or post my implementation directly so others can copy from it too.

I was able to build navigation mesh object based on your implementation. Thanks a lot!

If someone needs implementation for solo (not tiled) mesh, you can just copy it from git project, just copy two files from AI folder:

Another question - is there some code on navigation query and how to use navigation mesh to get the route to end point?

I also see that this link: https://jmonkeyengine.github.io/wiki/jme3/advanced/recast.html
is not there anymore.

Are you probably adding new documentation now?