feat(nav): 17.1/17.3 - ludic.nav over Recast & Detour v1.6.0 (zlib), built here from the pinned tag on the Mac and the PC (the DLL imports KERNEL32 alone): a navmesh per kind of walker from triangles with an area byte each and cylinders nothing stands in, saved and loaded as bytes; the nearest point, a path as corners (partial when the end cannot be reached), whether a straight line stays walkable, a cost per kind of ground. Tests on a hand-built meadow: round a post, over a ford, stopped at a bank, a saved mesh answering alike - Mac and PC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e397e4eee9
commit
9b8606483d
17 changed files with 587 additions and 0 deletions
108
packages/ludic.nav/native/shim/nav_build.inl
Normal file
108
packages/ludic.nav/native/shim/nav_build.inl
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// 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).
|
||||
|
||||
// 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) {
|
||||
for (int i = 0; i < nc; ++i) {
|
||||
float pos[3] = {cyl[i * 5], cyl[i * 5 + 1], cyl[i * 5 + 2]};
|
||||
rcMarkCylinderArea(&ctx, pos, cyl[i * 5 + 3], cyl[i * 5 + 4], RC_NULL_AREA, chf);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.cs = c[0];
|
||||
cfg.ch = c[1];
|
||||
cfg.walkableSlopeAngle = c[5];
|
||||
cfg.walkableHeight = (int)ceilf(c[2] / cfg.ch);
|
||||
cfg.walkableClimb = (int)floorf(c[4] / cfg.ch);
|
||||
cfg.walkableRadius = (int)ceilf(c[3] / cfg.cs);
|
||||
cfg.maxEdgeLen = (int)(12.0f / cfg.cs);
|
||||
cfg.maxSimplificationError = 1.3f;
|
||||
cfg.minRegionArea = 8 * 8;
|
||||
cfg.mergeRegionArea = 20 * 20;
|
||||
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());
|
||||
rcHeightfield *hf = rcAllocHeightfield();
|
||||
rcCompactHeightfield *chf = rcAllocCompactHeightfield();
|
||||
rcContourSet *cs = rcAllocContourSet();
|
||||
rcPolyMesh *pm = rcAllocPolyMesh();
|
||||
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);
|
||||
if (ok) {
|
||||
rcFilterLowHangingWalkableObstacles(&ctx, cfg.walkableClimb, *hf);
|
||||
rcFilterLedgeSpans(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf);
|
||||
rcFilterWalkableLowHeightSpans(&ctx, cfg.walkableHeight, *hf);
|
||||
ok = rcBuildCompactHeightfield(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf, *chf);
|
||||
}
|
||||
if (ok) {
|
||||
nav_mark_cylinders(ctx, *chf, cyl, nc);
|
||||
ok = rcErodeWalkableArea(&ctx, cfg.walkableRadius, *chf) && rcBuildDistanceField(&ctx, *chf) &&
|
||||
rcBuildRegions(&ctx, *chf, 0, 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) {
|
||||
for (int i = 0; i < pm->npolys; ++i) pm->flags[i] = pm->areas[i] ? 1 : 0;
|
||||
dtNavMeshCreateParams p;
|
||||
memset(&p, 0, sizeof(p));
|
||||
p.verts = pm->verts; p.vertCount = pm->nverts; p.polys = pm->polys; p.polyAreas = pm->areas;
|
||||
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];
|
||||
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);
|
||||
}
|
||||
rcFreeHeightField(hf); rcFreeCompactHeightfield(chf); rcFreeContourSet(cs); rcFreePolyMesh(pm); rcFreePolyMeshDetail(dm);
|
||||
if (!ok) return nullptr;
|
||||
return nav_from(data, size);
|
||||
}
|
||||
|
||||
// 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);
|
||||
return nav_from(data, size);
|
||||
}
|
||||
|
||||
NAV_SHIM void nav_free(void *h) {
|
||||
Nav *n = static_cast<Nav *>(h);
|
||||
if (!n) return;
|
||||
dtFreeNavMeshQuery(n->query);
|
||||
dtFreeNavMesh(n->mesh);
|
||||
delete n;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
53
packages/ludic.nav/native/shim/nav_query.inl
Normal file
53
packages/ludic.nav/native/shim/nav_query.inl
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// nav_query.inl - what a navmesh answers: the nearest point on it, a path as corner points, and
|
||||
// whether a straight line stays on it. Points go out through out[] (x, y, z each).
|
||||
|
||||
static dtPolyRef nav_poly_at(Nav *n, const float *p, float *on) {
|
||||
dtPolyRef r = 0;
|
||||
if (dtStatusFailed(n->query->findNearestPoly(p, n->ext, &n->filter, &r, on))) return 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
// the nearest point on the mesh to (x, y, z) into out; 0 when none is within reach
|
||||
NAV_SHIM int nav_nearest(void *h, float x, float y, float z, float *out) {
|
||||
float p[3] = {x, y, z};
|
||||
return nav_poly_at(static_cast<Nav *>(h), p, out) ? 1 : 0;
|
||||
}
|
||||
|
||||
// a cost per kind of ground (area 1..62): the filter every query uses
|
||||
NAV_SHIM void nav_area_cost(void *h, int area, float cost) {
|
||||
Nav *n = static_cast<Nav *>(h);
|
||||
if (area > 0 && area < DT_MAX_AREAS) n->filter.setAreaCost(area, cost);
|
||||
}
|
||||
|
||||
// the corners of a path from one point to another, at most max of them, into out: the count, or
|
||||
// -1 when either end is off the mesh. One that cannot reach the end stops as near it as the mesh
|
||||
// allows, and nav_partial says so
|
||||
NAV_SHIM int nav_path(void *h, float sx, float sy, float sz, float ex, float ey, float ez, float *out, int max) {
|
||||
Nav *n = static_cast<Nav *>(h);
|
||||
float s[3] = {sx, sy, sz}, e[3] = {ex, ey, ez}, so[3], eo[3];
|
||||
dtPolyRef a = nav_poly_at(n, s, so), b = nav_poly_at(n, e, eo);
|
||||
n->partial = 0;
|
||||
if (!a || !b) return -1;
|
||||
int np = 0;
|
||||
dtStatus st = n->query->findPath(a, b, so, eo, &n->filter, n->polys, &np, NAV_MAX_POLYS);
|
||||
if (dtStatusFailed(st) || np == 0) return -1;
|
||||
if (dtStatusDetail(st, DT_PARTIAL_RESULT) || n->polys[np - 1] != b) n->partial = 1;
|
||||
float end[3];
|
||||
dtVcopy(end, eo);
|
||||
if (n->polys[np - 1] != b) n->query->closestPointOnPoly(n->polys[np - 1], eo, end, nullptr);
|
||||
int count = 0;
|
||||
if (dtStatusFailed(n->query->findStraightPath(so, end, n->polys, np, out, nullptr, nullptr, &count, max, 0))) return -1;
|
||||
return count;
|
||||
}
|
||||
NAV_SHIM int nav_partial(void *h) { return static_cast<Nav *>(h)->partial; }
|
||||
|
||||
// does the straight line from one point to another stay on the mesh? 1 yes, 0 no, -1 off it
|
||||
NAV_SHIM int nav_raycast(void *h, float sx, float sy, float sz, float ex, float ez) {
|
||||
Nav *n = static_cast<Nav *>(h);
|
||||
float s[3] = {sx, sy, sz}, so[3], e[3] = {ex, sy, ez}, t = 0, nrm[3];
|
||||
dtPolyRef a = nav_poly_at(n, s, so);
|
||||
if (!a) return -1;
|
||||
int np = 0;
|
||||
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;
|
||||
}
|
||||
50
packages/ludic.nav/native/shim/nav_shim.cpp
Normal file
50
packages/ludic.nav/native/shim/nav_shim.cpp
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// nav_shim.cpp - ludic.nav's door into Recast & Detour (phase 17). The shim rules of
|
||||
// packages/README.md: int, float and opaque handles cross; no struct by value, no callback into
|
||||
// Ludic; what a query finds is written into a buffer the package owns; errors are return codes.
|
||||
#include <Recast.h>
|
||||
#include <DetourNavMesh.h>
|
||||
#include <DetourNavMeshBuilder.h>
|
||||
#include <DetourNavMeshQuery.h>
|
||||
#include <DetourCommon.h>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define NAV_SHIM extern "C" __declspec(dllexport)
|
||||
#else
|
||||
#define NAV_SHIM extern "C" __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
const int NAV_MAX_POLYS = 512; // polygons a path may cross; a longer one is cut there
|
||||
const int NAV_MAX_NODES = 4096; // the search's open list
|
||||
|
||||
// a navmesh and the one query that walks it, with the scratch a path needs
|
||||
struct Nav {
|
||||
dtNavMesh *mesh = nullptr;
|
||||
dtNavMeshQuery *query = nullptr;
|
||||
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) {
|
||||
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->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;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
#include "nav_build.inl"
|
||||
#include "nav_query.inl"
|
||||
Loading…
Add table
Add a link
Reference in a new issue