feat(nav): 17.3 - a map in square tiles (nav_tiled, nav_tile_build; a tile must be a whole number of cells or its seams never join, and the shim refuses one that is not; 64-bit polygon refs for 16384 tiles), ground as a NavGround with boulders as convex footprints, one save format for one tile or many, and nav_random_near - a reachable point from the caller's seed, bit-identical on the Mac and the PC. 10 tests on both

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 15:42:37 +03:00
parent 9b8606483d
commit e6893f58be
16 changed files with 436 additions and 109 deletions

View file

@ -1,6 +1,6 @@
// nav_build.inl - a navmesh from triangles: Recast's solo pipeline (rasterize, filter, erode,
// regions, contours, polygons, detail) and one Detour tile. c[] is the configuration:
// cell size, cell height, agent height, agent radius, max climb, max slope (degrees).
// nav_build.inl - Recast's pipeline, shared by a whole mesh and a tile: rasterize the triangles,
// filter, mark what nothing stands in, erode, regions, contours, polygons, detail, Detour's data.
// c[] is the configuration: cell size, cell height, agent height, radius, max climb, max slope.
// cylinders (x, y, z, r, h each) no agent may stand in: trunks, posts, tents
static void nav_mark_cylinders(rcContext &ctx, rcCompactHeightfield &chf, const float *cyl, int nc) {
@ -10,12 +10,21 @@ static void nav_mark_cylinders(rcContext &ctx, rcCompactHeightfield &chf, const
}
}
// v: nv points (x, y, z); t: nt triangles; area: one per triangle, 0 not walkable, 1..62 a kind of
// ground (its cost is the filter's); the slope clears what is too steep whatever its area
NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const unsigned char *area,
const float *cyl, int nc, const float *c) {
rcContext ctx(false);
rcConfig cfg;
// convex footprints no agent may stand in (a boulder): each is n, n points (x, z), ymin, ymax
static void nav_mark_footprints(rcContext &ctx, rcCompactHeightfield &chf, const float *f, int nf) {
float pts[3 * 64];
int i = 0;
while (i < nf) {
int n = (int)f[i];
if (n < 3 || n > 64 || i + 1 + n * 2 + 2 > nf) return;
float y0 = f[i + 1 + n * 2], y1 = f[i + 2 + n * 2];
for (int k = 0; k < n; ++k) { pts[k * 3] = f[i + 1 + k * 2]; pts[k * 3 + 1] = y0; pts[k * 3 + 2] = f[i + 2 + k * 2]; }
rcMarkConvexPolyArea(&ctx, pts, n, y0, y1, RC_NULL_AREA, chf);
i += 3 + n * 2;
}
}
static void nav_config(rcConfig &cfg, const float *c) {
memset(&cfg, 0, sizeof(cfg));
cfg.cs = c[0];
cfg.ch = c[1];
@ -30,11 +39,18 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
cfg.maxVertsPerPoly = 6;
cfg.detailSampleDist = cfg.cs * 6.0f;
cfg.detailSampleMaxError = cfg.ch;
rcCalcBounds(v, nv, cfg.bmin, cfg.bmax);
rcCalcGridSize(cfg.bmin, cfg.bmax, cfg.cs, &cfg.width, &cfg.height);
}
std::vector<unsigned char> areas(area, area + nt);
rcClearUnwalkableTriangles(&ctx, cfg.walkableSlopeAngle, v, nv, t, nt, areas.data());
struct NavIn {
const float *v; int nv; const int *t; int nt; const unsigned char *area;
const float *cyl; int nc; const float *foot; int nf; const float *c;
};
// the pipeline over cfg's box; the tile's Detour data into data / size. 0 polygons is not an error
static bool nav_pipeline(rcConfig &cfg, const NavIn &in, int tx, int tz, unsigned char **data, int *size, int *polys) {
rcContext ctx(false);
std::vector<unsigned char> areas(in.area, in.area + in.nt);
rcClearUnwalkableTriangles(&ctx, cfg.walkableSlopeAngle, in.v, in.nv, in.t, in.nt, areas.data());
rcHeightfield *hf = rcAllocHeightfield();
rcCompactHeightfield *chf = rcAllocCompactHeightfield();
rcContourSet *cs = rcAllocContourSet();
@ -42,7 +58,7 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
rcPolyMeshDetail *dm = rcAllocPolyMeshDetail();
bool ok = hf && chf && cs && pm && dm &&
rcCreateHeightfield(&ctx, *hf, cfg.width, cfg.height, cfg.bmin, cfg.bmax, cfg.cs, cfg.ch) &&
rcRasterizeTriangles(&ctx, v, nv, t, areas.data(), nt, *hf, cfg.walkableClimb);
rcRasterizeTriangles(&ctx, in.v, in.nv, in.t, areas.data(), in.nt, *hf, cfg.walkableClimb);
if (ok) {
rcFilterLowHangingWalkableObstacles(&ctx, cfg.walkableClimb, *hf);
rcFilterLedgeSpans(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf);
@ -50,16 +66,16 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
ok = rcBuildCompactHeightfield(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf, *chf);
}
if (ok) {
nav_mark_cylinders(ctx, *chf, cyl, nc);
nav_mark_cylinders(ctx, *chf, in.cyl, in.nc);
nav_mark_footprints(ctx, *chf, in.foot, in.nf);
ok = rcErodeWalkableArea(&ctx, cfg.walkableRadius, *chf) && rcBuildDistanceField(&ctx, *chf) &&
rcBuildRegions(&ctx, *chf, 0, cfg.minRegionArea, cfg.mergeRegionArea) &&
rcBuildRegions(&ctx, *chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea) &&
rcBuildContours(&ctx, *chf, cfg.maxSimplificationError, cfg.maxEdgeLen, *cs) &&
rcBuildPolyMesh(&ctx, *cs, cfg.maxVertsPerPoly, *pm) &&
rcBuildPolyMeshDetail(&ctx, *pm, *chf, cfg.detailSampleDist, cfg.detailSampleMaxError, *dm);
}
unsigned char *data = nullptr;
int size = 0;
if (ok) {
*polys = ok ? pm->npolys : 0;
if (ok && pm->npolys > 0) {
for (int i = 0; i < pm->npolys; ++i) pm->flags[i] = pm->areas[i] ? 1 : 0;
dtNavMeshCreateParams p;
memset(&p, 0, sizeof(p));
@ -67,30 +83,28 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
p.polyFlags = pm->flags; p.polyCount = pm->npolys; p.nvp = pm->nvp;
p.detailMeshes = dm->meshes; p.detailVerts = dm->verts; p.detailVertsCount = dm->nverts;
p.detailTris = dm->tris; p.detailTriCount = dm->ntris;
p.walkableHeight = c[2]; p.walkableRadius = c[3]; p.walkableClimb = c[4];
p.walkableHeight = in.c[2]; p.walkableRadius = in.c[3]; p.walkableClimb = in.c[4];
p.tileX = tx; p.tileY = tz;
rcVcopy(p.bmin, pm->bmin); rcVcopy(p.bmax, pm->bmax);
p.cs = cfg.cs; p.ch = cfg.ch; p.buildBvTree = true;
ok = dtCreateNavMeshData(&p, &data, &size);
ok = dtCreateNavMeshData(&p, data, size);
}
rcFreeHeightField(hf); rcFreeCompactHeightfield(chf); rcFreeContourSet(cs); rcFreePolyMesh(pm); rcFreePolyMeshDetail(dm);
if (!ok) return nullptr;
return nav_from(data, size);
return ok;
}
// the tile's bytes, to write to a file: first the size (buf null), then the copy
NAV_SHIM int nav_save(void *h, unsigned char *buf, int cap) {
Nav *n = static_cast<Nav *>(h);
if (!buf) return n->size;
if (cap < n->size) return -1;
memcpy(buf, n->data, n->size);
return n->size;
}
// a navmesh from bytes nav_save wrote
NAV_SHIM void *nav_load(const unsigned char *buf, int size) {
unsigned char *data = static_cast<unsigned char *>(dtAlloc(size, DT_ALLOC_PERM));
if (!data) return nullptr;
memcpy(data, buf, size);
// one mesh over all the triangles given: v nv points (x, y, z); t nt triangles; area a byte each,
// 0 not walkable and 1..62 a kind of ground; the slope clears what is too steep whatever its area
NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const unsigned char *area,
const float *cyl, int nc, const float *foot, int nf, const float *c) {
rcConfig cfg;
nav_config(cfg, c);
rcCalcBounds(v, nv, cfg.bmin, cfg.bmax);
rcCalcGridSize(cfg.bmin, cfg.bmax, cfg.cs, &cfg.width, &cfg.height);
NavIn in = {v, nv, t, nt, area, cyl, nc, foot, nf, c};
unsigned char *data = nullptr;
int size = 0, polys = 0;
if (!nav_pipeline(cfg, in, 0, 0, &data, &size, &polys) || polys == 0) return nullptr;
return nav_from(data, size);
}
@ -102,7 +116,13 @@ NAV_SHIM void nav_free(void *h) {
delete n;
}
// every polygon in every tile
NAV_SHIM int nav_polys(void *h) {
const dtMeshTile *t = static_cast<const dtNavMesh *>(static_cast<Nav *>(h)->mesh)->getTile(0);
return t && t->header ? t->header->polyCount : 0;
const dtNavMesh *m = static_cast<Nav *>(h)->mesh;
int total = 0;
for (int i = 0; i < m->getMaxTiles(); ++i) {
const dtMeshTile *t = m->getTile(i);
if (t && t->header) total += t->header->polyCount;
}
return total;
}

View file

@ -51,3 +51,22 @@ NAV_SHIM int nav_raycast(void *h, float sx, float sy, float sz, float ex, float
if (dtStatusFailed(n->query->raycast(a, so, e, &n->filter, &t, nrm, n->polys, &np, NAV_MAX_POLYS))) return -1;
return t >= 1.0f ? 1 : 0;
}
// a point about r from (x, y, z) that a walker there can reach, into out; 0 when there is none.
// Detour asks a function for its dice: this one is seeded by the caller, so the same seed from the
// package's Rng gives the same point on every machine
static unsigned int nav_rs = 1;
static float nav_frand() {
nav_rs = nav_rs * 1664525u + 1013904223u;
return (float)(nav_rs >> 8) * (1.0f / 16777216.0f);
}
NAV_SHIM int nav_random_near(void *h, float x, float y, float z, float r, int seed, float *out) {
Nav *n = static_cast<Nav *>(h);
float p[3] = {x, y, z}, on[3];
dtPolyRef start = nav_poly_at(n, p, on);
if (!start) return 0;
nav_rs = (unsigned int)seed * 2654435761u + 1u;
dtPolyRef ref = 0;
if (dtStatusFailed(n->query->findRandomPointAroundCircle(start, on, r, &n->filter, nav_frand, &ref, out))) return 0;
return ref ? 1 : 0;
}

View file

@ -27,24 +27,26 @@ struct Nav {
dtQueryFilter filter;
float ext[3] = {2.0f, 4.0f, 2.0f}; // how far to look for the mesh around a point
dtPolyRef polys[NAV_MAX_POLYS];
unsigned char *data = nullptr; // the tile as built or loaded (the mesh frees it)
int size = 0;
int partial = 0; // the last path stopped short of its end
};
Nav *nav_from(unsigned char *data, int size) {
// a navmesh with its query, from params (a set of tiles) or from one tile's data
Nav *nav_open(dtNavMesh *mesh) {
Nav *n = new Nav();
n->mesh = dtAllocNavMesh();
if (!n->mesh || dtStatusFailed(n->mesh->init(data, size, DT_TILE_FREE_DATA))) { dtFree(data); dtFreeNavMesh(n->mesh); delete n; return nullptr; }
n->data = data;
n->size = size;
n->mesh = mesh;
n->query = dtAllocNavMeshQuery();
if (!n->query || dtStatusFailed(n->query->init(n->mesh, NAV_MAX_NODES))) { dtFreeNavMeshQuery(n->query); dtFreeNavMesh(n->mesh); delete n; return nullptr; }
n->filter.setIncludeFlags(0xffff);
n->filter.setExcludeFlags(0);
return n;
}
Nav *nav_from(unsigned char *data, int size) {
dtNavMesh *mesh = dtAllocNavMesh();
if (!mesh || dtStatusFailed(mesh->init(data, size, DT_TILE_FREE_DATA))) { dtFree(data); dtFreeNavMesh(mesh); return nullptr; }
return nav_open(mesh);
}
} // namespace
#include "nav_build.inl"
#include "nav_tiles.inl"
#include "nav_query.inl"

View file

@ -0,0 +1,102 @@
// nav_tiles.inl - a map as square tiles: a tiled mesh, one tile built from the triangles under it
// and a border round it, and every tile written and read back as one set of bytes
// an empty mesh of tiles `tile` metres across, the first at (ox, oz)
NAV_SHIM void *nav_tiled(float ox, float oz, float tile, int max_tiles, int max_polys) {
dtNavMeshParams p;
memset(&p, 0, sizeof(p));
p.orig[0] = ox; p.orig[1] = 0.0f; p.orig[2] = oz;
p.tileWidth = tile; p.tileHeight = tile;
p.maxTiles = max_tiles; p.maxPolys = max_polys;
dtNavMesh *mesh = dtAllocNavMesh();
if (!mesh || dtStatusFailed(mesh->init(&p))) { dtFreeNavMesh(mesh); return nullptr; }
return nav_open(mesh);
}
// tile (tx, tz) built from triangles that cover it and a border of the walker's radius and three
// cells round it, replacing whatever was there; its polygons, 0 for none, -1 when it failed
NAV_SHIM int nav_tile_build(void *h, int tx, int tz, const float *v, int nv, const int *t, int nt,
const unsigned char *area, const float *cyl, int nc, const float *foot, int nf, const float *c) {
Nav *n = static_cast<Nav *>(h);
const dtNavMeshParams *mp = n->mesh->getParams();
rcConfig cfg;
nav_config(cfg, c);
cfg.tileSize = (int)(mp->tileWidth / cfg.cs + 0.5f);
// a tile a cell short of its width leaves a seam its neighbour's polygons never meet
if (fabsf(cfg.tileSize * cfg.cs - mp->tileWidth) > 0.001f) return -1;
cfg.borderSize = cfg.walkableRadius + 3;
cfg.width = cfg.height = cfg.tileSize + cfg.borderSize * 2;
float lo[3], hi[3];
rcCalcBounds(v, nv, lo, hi);
cfg.bmin[0] = mp->orig[0] + tx * mp->tileWidth - cfg.borderSize * cfg.cs;
cfg.bmin[2] = mp->orig[2] + tz * mp->tileHeight - cfg.borderSize * cfg.cs;
cfg.bmax[0] = mp->orig[0] + (tx + 1) * mp->tileWidth + cfg.borderSize * cfg.cs;
cfg.bmax[2] = mp->orig[2] + (tz + 1) * mp->tileHeight + cfg.borderSize * cfg.cs;
cfg.bmin[1] = lo[1] - 1.0f;
cfg.bmax[1] = hi[1] + c[2] + 1.0f;
NavIn in = {v, nv, t, nt, area, cyl, nc, foot, nf, c};
unsigned char *data = nullptr;
int size = 0, polys = 0;
n->mesh->removeTile(n->mesh->getTileRefAt(tx, tz, 0), nullptr, nullptr);
if (!nav_pipeline(cfg, in, tx, tz, &data, &size, &polys)) return -1;
if (polys == 0) return 0;
if (dtStatusFailed(n->mesh->addTile(data, size, DT_TILE_FREE_DATA, 0, nullptr))) { dtFree(data); return -1; }
return polys;
}
// the set's bytes: "NAVT", a version, the mesh's params, then each tile's ref, size and data.
// First call with buf null for the size, then with a buffer that big
static const int NAV_MAGIC = 'N' << 24 | 'A' << 16 | 'V' << 8 | 'T';
static const int NAV_VERSION = 1;
static void nav_put(unsigned char *buf, int &at, const void *p, int n) {
if (buf) memcpy(buf + at, p, n);
at += n;
}
NAV_SHIM int nav_save(void *h, unsigned char *buf, int cap) {
const dtNavMesh *m = static_cast<Nav *>(h)->mesh;
const dtNavMeshParams *p = m->getParams();
int at = 0, count = 0;
for (int i = 0; i < m->getMaxTiles(); ++i) {
const dtMeshTile *t = m->getTile(i);
if (t && t->header && t->dataSize) count++;
}
nav_put(buf, at, &NAV_MAGIC, 4); nav_put(buf, at, &NAV_VERSION, 4); nav_put(buf, at, &count, 4);
nav_put(buf, at, p->orig, 12); nav_put(buf, at, &p->tileWidth, 4); nav_put(buf, at, &p->tileHeight, 4);
nav_put(buf, at, &p->maxTiles, 4); nav_put(buf, at, &p->maxPolys, 4);
for (int i = 0; i < m->getMaxTiles(); ++i) {
const dtMeshTile *t = m->getTile(i);
if (!t || !t->header || !t->dataSize) continue;
unsigned long long ref = (unsigned long long)m->getTileRef(t);
if (buf && at + 12 + t->dataSize > cap) return -1;
nav_put(buf, at, &ref, 8); nav_put(buf, at, &t->dataSize, 4); nav_put(buf, at, t->data, t->dataSize);
}
return at;
}
// a mesh from bytes nav_save wrote; null for anything else
NAV_SHIM void *nav_load(const unsigned char *buf, int size) {
int at = 0, magic = 0, version = 0, count = 0;
if (size < 40) return nullptr;
memcpy(&magic, buf, 4); memcpy(&version, buf + 4, 4); memcpy(&count, buf + 8, 4);
if (magic != NAV_MAGIC || version != NAV_VERSION) return nullptr;
dtNavMeshParams p;
memset(&p, 0, sizeof(p));
memcpy(p.orig, buf + 12, 12); memcpy(&p.tileWidth, buf + 24, 4); memcpy(&p.tileHeight, buf + 28, 4);
memcpy(&p.maxTiles, buf + 32, 4); memcpy(&p.maxPolys, buf + 36, 4);
at = 40;
dtNavMesh *mesh = dtAllocNavMesh();
if (!mesh || dtStatusFailed(mesh->init(&p))) { dtFreeNavMesh(mesh); return nullptr; }
for (int i = 0; i < count; ++i) {
unsigned long long ref = 0;
int n = 0;
if (at + 12 > size) break;
memcpy(&ref, buf + at, 8); memcpy(&n, buf + at + 8, 4);
at += 12;
if (n <= 0 || at + n > size) break;
unsigned char *data = static_cast<unsigned char *>(dtAlloc(n, DT_ALLOC_PERM));
memcpy(data, buf + at, n);
at += n;
if (dtStatusFailed(mesh->addTile(data, n, DT_TILE_FREE_DATA, (dtTileRef)ref, nullptr))) dtFree(data);
}
return nav_open(mesh);
}