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
|
|
@ -30,6 +30,7 @@ section. The rules are in [ludic.base](ludic.base/README.md).
|
|||
| [ludic.needs](ludic.needs/README.md) | a body's warmth, food, water and energy, and the countdown to a collapse |
|
||||
| [ludic.npc](ludic.npc/README.md) | the other people in a place: a routine by the hour, walking round what is in the way, facing a player who comes near, lines as data, a guest's copy |
|
||||
| [ludic.physics](ludic.physics/README.md) | rigid bodies, removable still shapes, queries and buoyancy over Jolt Physics (a native library, phase 16) |
|
||||
| [ludic.nav](ludic.nav/README.md) | a navmesh per kind of walker and the ways across it, over Recast & Detour (a native library, phase 17) |
|
||||
| [ludic.photo](ludic.photo/README.md) | a camera's photographs: what is in the frame, a grade on size, framing, light and the moment, the roll, its worth, the best of each subject |
|
||||
| [ludic.save](ludic.save/README.md) | versioned save files: a migration chain the game declares, torn writes told apart, a backup, a newer file refused and read-only |
|
||||
| [ludic.settings](ludic.settings/README.md) | a game's settings as data: one store, a fact per change, ranges, a safe set |
|
||||
|
|
|
|||
56
packages/ludic.nav/README.md
Normal file
56
packages/ludic.nav/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# ludic.nav
|
||||
|
||||
A walkable mesh of the world and the ways across it, over
|
||||
[Recast & Detour](https://github.com/recastnavigation/recastnavigation) (zlib) built here from a
|
||||
pinned tag (phase 17). Uses `ludic.base` and nothing else.
|
||||
|
||||
```ludic
|
||||
import "ludic.nav"
|
||||
```
|
||||
|
||||
## The rules it keeps
|
||||
|
||||
- **One mesh per kind of walker.** `NAV_PERSON` (0.35 m wide, the hiker's 0.55 m step),
|
||||
`NAV_LARGE` (an elk, a bear, a horse) and `NAV_SMALL` (a hare, a marmot) each have their own,
|
||||
because what a hare slips between a bear walks round. A `NavConfig` says what a mesh is built
|
||||
for: its voxels and its walker's height, radius, step and steepest slope.
|
||||
- **Built from triangles, or loaded from a bake.** `nav_build` rasterizes triangles - each with an
|
||||
area byte, 0 not walkable and 1..62 a kind of ground - and marks every cylinder given (a trunk, a
|
||||
post) as somewhere nothing stands; ground steeper than the slope is cleared whatever its area. A
|
||||
game bakes its maps once and loads the bytes (`nav_save_file` / `nav_load_file`): a map is never
|
||||
built at start-up.
|
||||
- **A path is corners.** `nav_path` answers the way from one point to another as the corners
|
||||
where it turns, the first the start and the last the end. When the end cannot be reached the
|
||||
path stops as near it as the mesh allows and `nav_partial` says so - a walker goes there and
|
||||
gives up, never through a river. Either end off the mesh is `-1`.
|
||||
- **A kind of ground has a cost** (`nav_area_cost`): a trail cheaper than a meadow, scree dearer.
|
||||
- **The same question gets the same answer.** Detour is deterministic for the same mesh and the
|
||||
same points, so co-op's order of dice is untouched by asking it.
|
||||
|
||||
## API
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `NAV_PERSON`, `NAV_LARGE`, `NAV_SMALL`, `NAV_CORNERS` | the kinds of walker, and a path's most corners (256) |
|
||||
| `NavConfig { cell, cell_h, height, radius, climb, slope }` | what a mesh is built for (a person by default) |
|
||||
| `nav_build(kind, v, nv, t, nt, area, cyl, nc, config) -> bool`, `nav_polygons(kind)` | a mesh from triangles and cylinders (`x, y, z, r, h` each) |
|
||||
| `nav_save_file(kind, path)`, `nav_load_file(kind, path)`, `nav_reset()` | a baked mesh written and read; every mesh let go |
|
||||
| `nav_nearest(kind, x, y, z) -> bool`, `nav_near_x/y/z()` | the nearest walkable point within a couple of metres |
|
||||
| `nav_path(kind, sx, sy, sz, ex, ey, ez) -> int`, `nav_corners()`, `nav_corner_x/y/z(i)`, `nav_partial(kind)` | a way as corners |
|
||||
| `nav_straight(kind, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable |
|
||||
| `nav_area_cost(kind, area, cost)` | how dear a kind of ground is to cross |
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
ludic test packages/ludic.nav
|
||||
```
|
||||
|
||||
A 40 m meadow built by hand: a path round a post, over a river's ford, stopping at the bank of a
|
||||
river with none, the nearest point, and a saved mesh answering as the built one did.
|
||||
|
||||
## The native library
|
||||
|
||||
`native/build.sh` fetches Recast & Detour v1.6.0, checks its SHA-256, and builds Recast, Detour
|
||||
and the shim (`native/shim/nav_shim.cpp`) into `lib/<target>/` - the same script on the Mac and on
|
||||
the PC (Git Bash, the LLVM installer's clang). `native/LICENSE-recastnavigation` ships with it.
|
||||
37
packages/ludic.nav/build.ludic
Normal file
37
packages/ludic.nav/build.ludic
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# build.ludic - a kind's mesh from triangles, or from a file a bake wrote
|
||||
# v: nv points (x, y, z); t: nt triangles (three indices each); area: one byte per triangle, 0 not
|
||||
# walkable, 1..62 a kind of ground; cyl: nc cylinders (x, y, z, r, h) nothing stands in
|
||||
export function nav_build(nav_st: mut NavState, kind: int, v: []float, nv: int, t: []int, nt: int, area: []byte, cyl: []float, nc: int, c: NavConfig) -> bool {
|
||||
if len(v) < nv * 3 or len(t) < nt * 3 or len(area) < nt or len(cyl) < nc * 5 { return false }
|
||||
let cfg = floats(6)
|
||||
cfg[0] = c.cell
|
||||
cfg[1] = c.cell_h
|
||||
cfg[2] = c.height
|
||||
cfg[3] = c.radius
|
||||
cfg[4] = c.climb
|
||||
cfg[5] = c.slope
|
||||
return nv_set(nav_st, kind, nvc_build(v, nv, t, nt, area, cyl, nc, cfg))
|
||||
}
|
||||
|
||||
# the polygons a kind's mesh has (0: none)
|
||||
export function nav_polygons(nav_st: NavState, kind: int) -> int {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
if h == null { return 0 }
|
||||
return nvc_polys(h)
|
||||
}
|
||||
|
||||
# a kind's mesh written to a file, and read back
|
||||
export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
if h == null { return false }
|
||||
let n = nvc_save(h, null, 0)
|
||||
let buf = buffer(n)
|
||||
if nvc_save(h, buf, n) != n { return false }
|
||||
return Fs.write_bytes(path, buf, n)
|
||||
}
|
||||
export function nav_load_file(nav_st: mut NavState, kind: int, path: string) -> bool {
|
||||
if not Fs.exists(path) { return false }
|
||||
let buf = Fs.read_bytes(path)
|
||||
if buf == null or len(buf) == 0 { return false }
|
||||
return nv_set(nav_st, kind, nvc_load(buf, len(buf)))
|
||||
}
|
||||
10
packages/ludic.nav/index.ludic
Normal file
10
packages/ludic.nav/index.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# ludic.nav - a navmesh per kind of walker (a person, a large animal, a small one): built from
|
||||
# triangles and the cylinders nothing stands in, or loaded from the bytes a bake saved; asked for
|
||||
# the nearest walkable point, a path as corners, and whether a straight line stays walkable.
|
||||
module ludic_nav uses ludic_base
|
||||
numbers float
|
||||
import "ludic.base"
|
||||
import "native.ludic"
|
||||
import "state.ludic"
|
||||
import "build.ludic"
|
||||
import "query.ludic"
|
||||
BIN
packages/ludic.nav/lib/macos-arm64/libludicnav.dylib
(Stored with Git LFS)
Executable file
BIN
packages/ludic.nav/lib/macos-arm64/libludicnav.dylib
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.dll
(Stored with Git LFS)
Normal file
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.dll
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.lib
(Stored with Git LFS)
Normal file
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.lib
(Stored with Git LFS)
Normal file
Binary file not shown.
12
packages/ludic.nav/native.ludic
Normal file
12
packages/ludic.nav/native.ludic
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# native.ludic - the shim's symbols (native/shim/nav_shim.cpp). A navmesh is a handle the package
|
||||
# keeps; points come back through a float buffer it owns. None of this is exported.
|
||||
extern function nvc_build(v: pointer, nv: int, t: pointer, nt: int, area: pointer, cyl: pointer, nc: int, c: pointer) -> pointer = "nav_build"
|
||||
extern function nvc_save(h: pointer, buf: pointer, cap: int) -> int = "nav_save"
|
||||
extern function nvc_load(buf: pointer, size: int) -> pointer = "nav_load"
|
||||
extern function nvc_free(h: pointer) -> void = "nav_free"
|
||||
extern function nvc_polys(h: pointer) -> int = "nav_polys"
|
||||
extern function nvc_nearest(h: pointer, x: float, y: float, z: float, out: pointer) -> int = "nav_nearest"
|
||||
extern function nvc_area_cost(h: pointer, area: int, cost: float) -> void = "nav_area_cost"
|
||||
extern function nvc_path(h: pointer, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float, out: pointer, max: int) -> int = "nav_path"
|
||||
extern function nvc_partial(h: pointer) -> int = "nav_partial"
|
||||
extern function nvc_raycast(h: pointer, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast"
|
||||
18
packages/ludic.nav/native/LICENSE-recastnavigation
Normal file
18
packages/ludic.nav/native/LICENSE-recastnavigation
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
Copyright (c) 2009 Mikko Mononen memon@inside.org
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
27
packages/ludic.nav/native/build.sh
Executable file
27
packages/ludic.nav/native/build.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/sh
|
||||
# builds lib/<target>/ for ludic.nav: Recast & Detour at a pinned tag, and the shim over them.
|
||||
# Recast builds the mesh, Detour answers it; DetourTileCache and DetourCrowd come with phases 17.8
|
||||
# and 18. Objects go to build/native; a few seconds on eight cores.
|
||||
set -eu
|
||||
PKG="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
. "$PKG/../../tools/native/lib.sh"
|
||||
RC_TAG=v1.6.0
|
||||
RC_SHA=d48ca0121962fa0639502c0f56c4e3ae72f98e55d88727225444f500775c0074
|
||||
SRC="$PKG/build/src/recast-$RC_TAG"
|
||||
native_fetch "$SRC" "https://github.com/recastnavigation/recastnavigation/archive/refs/tags/$RC_TAG.tar.gz" "$RC_SHA"
|
||||
|
||||
OBJ="$PKG/build/native/$(native_target)"
|
||||
mkdir -p "$OBJ"
|
||||
CXX="$(native_cxx)"
|
||||
INC="-I$SRC/Recast/Include -I$SRC/Detour/Include"
|
||||
FLAGS="$(native_cflags) -std=c++17 -ffp-contract=off -fno-exceptions -fno-rtti -DNDEBUG $INC"
|
||||
case "$(native_target)" in windows-*) FLAGS="$FLAGS -D_CRT_SECURE_NO_WARNINGS" ;; esac
|
||||
JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"
|
||||
|
||||
( cd "$SRC" && ls Recast/Source/*.cpp Detour/Source/*.cpp ) | while read -r f; do
|
||||
o="$OBJ/$(basename "$f" .cpp).o"
|
||||
[ "$o" -nt "$SRC/$f" ] || echo "$f $o"
|
||||
done | xargs -P "$JOBS" -n 2 sh -c '"$0" '"$FLAGS"' -c "'"$SRC"'/$1" -o "$2"' "$CXX"
|
||||
"$CXX" $FLAGS -I"$PKG/native/shim" -c "$PKG/native/shim/nav_shim.cpp" -o "$OBJ/nav_shim.o"
|
||||
|
||||
native_link "$PKG" ludicnav "$OBJ"/*.o
|
||||
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"
|
||||
7
packages/ludic.nav/package.ludic
Normal file
7
packages/ludic.nav/package.ludic
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# ludic.nav - a walkable mesh of the world and the ways across it, over Recast & Detour (phase 17),
|
||||
# built here from a pinned tag (native/build.sh). Uses ludic.base and nothing else.
|
||||
package "ludic.nav"
|
||||
version "0.1.0"
|
||||
kind source
|
||||
native "macos-arm64" "lib/macos-arm64/libludicnav.dylib"
|
||||
native "windows-x64" "lib/windows-x64/ludicnav.dll"
|
||||
43
packages/ludic.nav/query.ludic
Normal file
43
packages/ludic.nav/query.ludic
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# query.ludic - what a kind's mesh answers. A path is its corners, read back one at a time
|
||||
# (nav_corner_x / _y / _z); the nearest point the same (nav_near_x / _y / _z)
|
||||
|
||||
# the nearest walkable point to (x, y, z) within a couple of metres; false when there is none
|
||||
export function nav_nearest(nav_st: NavState, kind: int, x: float, y: float, z: float) -> bool {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
if h == null { return false }
|
||||
return nvc_nearest(h, x, y, z, nav_st.nv_near) == 1
|
||||
}
|
||||
export function nav_near_x(nav_st: NavState) -> float { return nav_st.nv_near[0] }
|
||||
export function nav_near_y(nav_st: NavState) -> float { return nav_st.nv_near[1] }
|
||||
export function nav_near_z(nav_st: NavState) -> float { return nav_st.nv_near[2] }
|
||||
|
||||
# a way from one point to another: its corners, first the start and last the end (or as near the end
|
||||
# as the mesh reaches - nav_partial says which); -1 when either end is off the mesh
|
||||
export function nav_path(nav_st: mut NavState, kind: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float) -> int {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
nav_st.nv_count = 0
|
||||
if h == null { return -1 }
|
||||
let n = nvc_path(h, sx, sy, sz, ex, ey, ez, nav_st.nv_out, NAV_CORNERS)
|
||||
if n > 0 { nav_st.nv_count = n }
|
||||
return n
|
||||
}
|
||||
export function nav_partial(nav_st: NavState, kind: int) -> bool {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
return h != null and nvc_partial(h) == 1
|
||||
}
|
||||
export function nav_corners(nav_st: NavState) -> int { return nav_st.nv_count }
|
||||
export function nav_corner_x(nav_st: NavState, i: int) -> float { return nav_st.nv_out[i * 3] }
|
||||
export function nav_corner_y(nav_st: NavState, i: int) -> float { return nav_st.nv_out[i * 3 + 1] }
|
||||
export function nav_corner_z(nav_st: NavState, i: int) -> float { return nav_st.nv_out[i * 3 + 2] }
|
||||
|
||||
# does the straight line from (sx, sy, sz) to (ex, ez) stay walkable?
|
||||
export function nav_straight(nav_st: NavState, kind: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> bool {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
return h != null and nvc_raycast(h, sx, sy, sz, ex, ez) == 1
|
||||
}
|
||||
|
||||
# how dear a kind of ground is to cross (1 is plain; a trail cheaper, scree dearer)
|
||||
export function nav_area_cost(nav_st: NavState, kind: int, area: int, cost: float) -> void {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
if h != null { nvc_area_cost(h, area, cost) }
|
||||
}
|
||||
46
packages/ludic.nav/state.ludic
Normal file
46
packages/ludic.nav/state.ludic
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# state.ludic - the kinds of walker, what a mesh is built for, and what the package holds: a mesh
|
||||
# per kind, and the last path's corners and nearest point, read back through the verbs
|
||||
export const NAV_PERSON: int = 0 # 0.35 m wide, steps what a hiker steps (CHAR_STEP)
|
||||
export const NAV_LARGE: int = 1 # an elk, a bear, a horse
|
||||
export const NAV_SMALL: int = 2 # a hare, a marmot
|
||||
export const NAV_KINDS: int = 3
|
||||
export const NAV_CORNERS: int = 256 # a path's corners at most; a longer one is cut there
|
||||
|
||||
# what a mesh is built for: its voxels (cell across, cell high) and its walker (height, radius,
|
||||
# the step it takes whatever its gradient, the steepest ground it walks, degrees)
|
||||
export property NavConfig {
|
||||
cell: float = 0.3
|
||||
cell_h: float = 0.2
|
||||
height: float = 1.8
|
||||
radius: float = 0.35
|
||||
climb: float = 0.55
|
||||
slope: float = 45.0
|
||||
}
|
||||
|
||||
export state NavState {
|
||||
nv_meshes: []pointer = nv_none()
|
||||
nv_out: []float = floats(NAV_CORNERS * 3)
|
||||
nv_count: int = 0
|
||||
nv_near: []float = floats(3)
|
||||
}
|
||||
function nv_none() -> []pointer {
|
||||
let m = new []pointer
|
||||
for k in 0 .. NAV_KINDS { push(m, null) }
|
||||
return m
|
||||
}
|
||||
function nv_mesh(nav_st: NavState, kind: int) -> pointer {
|
||||
if kind < 0 or kind >= NAV_KINDS { return null }
|
||||
return nav_st.nv_meshes[kind]
|
||||
}
|
||||
function nv_set(nav_st: mut NavState, kind: int, h: pointer) -> bool {
|
||||
if kind < 0 or kind >= NAV_KINDS { return false }
|
||||
if nav_st.nv_meshes[kind] != null { nvc_free(nav_st.nv_meshes[kind]) }
|
||||
nav_st.nv_meshes[kind] = h
|
||||
return h != null
|
||||
}
|
||||
|
||||
# every mesh let go: a new map, or the world closed
|
||||
export function nav_reset(nav_st: mut NavState) -> void {
|
||||
for k in 0 .. NAV_KINDS { nv_set(nav_st, k, null) }
|
||||
nav_st.nv_count = 0
|
||||
}
|
||||
110
packages/ludic.nav/tests/nav_test.ludic
Normal file
110
packages/ludic.nav/tests/nav_test.ludic
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# nav_test.ludic - ludic.nav on ground built by hand: a 40 m meadow of metre squares, a post in its
|
||||
# middle, a river across it (area 0) with a ford or without one. A path goes round the post and over
|
||||
# the ford, stops at the bank when there is none, and a saved mesh answers as the built one did.
|
||||
import "ludic.nav"
|
||||
import "ludic.base"
|
||||
program NavTest {
|
||||
numbers float
|
||||
|
||||
# the meadow, flat at y = 0; river 1 has a ford at z 30..34, river 2 none, river 0 is no river
|
||||
function meadow(nav_st: mut NavState, kind: int, river: int, post: bool) -> bool {
|
||||
let n = 40
|
||||
let v = floats((n + 1) * (n + 1) * 3)
|
||||
for j in 0 .. n + 1 {
|
||||
for i in 0 .. n + 1 {
|
||||
let o = (j * (n + 1) + i) * 3
|
||||
v[o] = float(i)
|
||||
v[o + 1] = 0.0
|
||||
v[o + 2] = float(j)
|
||||
}
|
||||
}
|
||||
let t = new []int
|
||||
let area = buffer(n * n * 2)
|
||||
for j in 0 .. n {
|
||||
for i in 0 .. n {
|
||||
let a = j * (n + 1) + i
|
||||
push(t, a)
|
||||
push(t, a + n + 1)
|
||||
push(t, a + 1)
|
||||
push(t, a + 1)
|
||||
push(t, a + n + 1)
|
||||
push(t, a + n + 2)
|
||||
var k = 1
|
||||
if river > 0 and i >= 18 and i < 22 and not (river == 1 and j >= 30 and j < 34) { k = 0 }
|
||||
area[(j * n + i) * 2] = k
|
||||
area[(j * n + i) * 2 + 1] = k
|
||||
}
|
||||
}
|
||||
let cyl = floats(5)
|
||||
cyl[0] = 20.0
|
||||
cyl[1] = -1.0
|
||||
cyl[2] = 20.0
|
||||
cyl[3] = 2.0
|
||||
cyl[4] = 4.0
|
||||
var nc = 0
|
||||
if post { nc = 1 }
|
||||
return nav_build(nav_st, kind, v, (n + 1) * (n + 1), t, n * n * 2, area, cyl, nc, new NavConfig)
|
||||
}
|
||||
function last_x(nav_st: NavState) -> float { return nav_corner_x(nav_st, nav_corners(nav_st) - 1) }
|
||||
function last_z(nav_st: NavState) -> float { return nav_corner_z(nav_st, nav_corners(nav_st) - 1) }
|
||||
|
||||
test "a path goes round a post, and the straight line through it is not walkable" (nav_st: mut NavState) {
|
||||
expect(meadow(nav_st, NAV_PERSON, 0, true))
|
||||
expect(nav_polygons(nav_st, NAV_PERSON) > 0)
|
||||
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0)
|
||||
expect(n >= 3)
|
||||
expect(not nav_partial(nav_st, NAV_PERSON))
|
||||
expect(Math.abs(last_x(nav_st) - 35.0) < 0.1)
|
||||
for i in 0 .. n {
|
||||
let dx = nav_corner_x(nav_st, i) - 20.0
|
||||
let dz = nav_corner_z(nav_st, i) - 20.0
|
||||
expect(dx * dx + dz * dz > 2.0 * 2.0)
|
||||
}
|
||||
expect(not nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 20.0))
|
||||
expect(nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 5.0, 35.0, 5.0))
|
||||
}
|
||||
|
||||
test "a river is crossed at its ford" (nav_st: mut NavState) {
|
||||
expect(meadow(nav_st, NAV_PERSON, 1, false))
|
||||
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
|
||||
expect(n >= 3)
|
||||
expect(not nav_partial(nav_st, NAV_PERSON))
|
||||
var ford = false
|
||||
for i in 0 .. n { if nav_corner_z(nav_st, i) > 29.0 { ford = true } }
|
||||
expect(ford)
|
||||
}
|
||||
|
||||
test "without a ford the path stops at the near bank and says so" (nav_st: mut NavState) {
|
||||
expect(meadow(nav_st, NAV_PERSON, 2, false))
|
||||
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
|
||||
expect(n >= 1)
|
||||
expect(nav_partial(nav_st, NAV_PERSON))
|
||||
expect(last_x(nav_st) < 18.0)
|
||||
}
|
||||
|
||||
test "the nearest walkable point, and nothing off the mesh" (nav_st: mut NavState) {
|
||||
expect(meadow(nav_st, NAV_PERSON, 0, false))
|
||||
expect(nav_nearest(nav_st, NAV_PERSON, 10.0, 1.0, 10.0))
|
||||
expect(Math.abs(nav_near_y(nav_st)) < 0.3)
|
||||
expect(Math.abs(nav_near_x(nav_st) - 10.0) < 0.1)
|
||||
expect(not nav_nearest(nav_st, NAV_PERSON, -50.0, 0.0, -50.0))
|
||||
expect_eq(nav_path(nav_st, NAV_PERSON, -50.0, 0.0, -50.0, 10.0, 0.0, 10.0), -1)
|
||||
expect_eq(nav_path(nav_st, NAV_LARGE, 5.0, 0.0, 5.0, 10.0, 0.0, 10.0), -1)
|
||||
}
|
||||
|
||||
test "a mesh saved and loaded answers as the built one did" (nav_st: mut NavState) {
|
||||
expect(meadow(nav_st, NAV_PERSON, 1, true))
|
||||
let path = Os.temp_dir() + "/ludic_nav_test.navmesh"
|
||||
expect(nav_save_file(nav_st, NAV_PERSON, path))
|
||||
expect(nav_load_file(nav_st, NAV_SMALL, path))
|
||||
let a = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
|
||||
let ax = last_x(nav_st)
|
||||
let az = last_z(nav_st)
|
||||
let b = nav_path(nav_st, NAV_SMALL, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
|
||||
expect_eq(a, b)
|
||||
expect_near(last_x(nav_st), ax, 0.0001)
|
||||
expect_near(last_z(nav_st), az, 0.0001)
|
||||
nav_reset(nav_st)
|
||||
expect_eq(nav_polygons(nav_st, NAV_SMALL), 0)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue