diff --git a/packages/README.md b/packages/README.md index 444d193b..44237109 100644 --- a/packages/README.md +++ b/packages/README.md @@ -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 | diff --git a/packages/ludic.nav/README.md b/packages/ludic.nav/README.md new file mode 100644 index 00000000..f209c10c --- /dev/null +++ b/packages/ludic.nav/README.md @@ -0,0 +1,67 @@ +# 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 ground, or loaded from a bake.** A `NavGround` is triangles - each with an area + byte, 0 not walkable and 1..62 a kind of ground - and what nothing stands in: cylinders (a trunk, + a post) and convex footprints (a boulder). Ground steeper than the slope is cleared whatever its + area. `nav_build` makes one mesh of it; a map is square tiles (`nav_tiled`, then `nav_tile_build` + for each tile from the ground under it and a few metres round), and a tile must be a whole number + of cells across or its seams never join - the shim refuses one that is not. A game bakes its maps + once and loads the bytes (`nav_save_file` / `nav_load_file`, one format for one tile or many): a + map is never built at start-up. Polygon refs are 64-bit, so a map may have 16384 tiles. +- **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 wander's goal is somewhere it can go.** `nav_random_near` answers a point about r away that a + walker standing at the start can reach - never across a river without a ford - from a seed the + caller draws from its own `Rng`, so the same seed is the same point on every machine. +- **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, 0.25 m cells) | +| `NavGround { v, nv, t, nt, area, cyl, nc, foot, nf }` | what it is built from: triangles, an area byte each, cylinders (`x, y, z, r, h`) and footprints (`n`, n points `x z`, `ymin`, `ymax`) | +| `nav_build(kind, ground, config) -> bool`, `nav_polygons(kind)` | one mesh over all of it | +| `nav_tiled(kind, ox, oz, tile, max_tiles, max_polys) -> bool`, `nav_tile_build(kind, tx, tz, ground, config) -> int` | a map of square tiles, a tile at a time (its polygons, 0 none, -1 failed) | +| `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_random_near(kind, x, y, z, r, seed) -> bool`, `nav_near_x/y/z()` | a reachable point about r away, from the caller's seed | +| `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 and a boulder's footprint, over a river's ford, +stopping at the bank of a river with none, the nearest point, random points that never cross the +river, and a saved mesh answering as the built one did. And a 128 m meadow as four tiles: a path +across the seams, a tile under water with no polygons, and the set saved and loaded. + +## 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//` - the same script on the Mac and on +the PC (Git Bash, the LLVM installer's clang). `native/LICENSE-recastnavigation` ships with it. diff --git a/packages/ludic.nav/build.ludic b/packages/ludic.nav/build.ludic new file mode 100644 index 00000000..a8365d5e --- /dev/null +++ b/packages/ludic.nav/build.ludic @@ -0,0 +1,62 @@ +# build.ludic - a kind's mesh: whole from its ground, or tile by tile (a map), or from a bake's file +function nv_ground_ok(g: NavGround) -> bool { + if g.v == null or g.t == null or g.area == null { return false } + if len(g.v) < g.nv * 3 or len(g.t) < g.nt * 3 or len(g.area) < g.nt { return false } + if g.nc > 0 and (g.cyl == null or len(g.cyl) < g.nc * 5) { return false } + return g.nf == 0 or (g.foot != null and len(g.foot) >= g.nf) +} +function nv_cfg(c: NavConfig) -> []float { + 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 cfg +} +function nv_or_none(xs: []float) -> []float { + if xs == null { return floats(1) } + return xs +} + +# one mesh over the whole of g +export function nav_build(nav_st: mut NavState, kind: int, g: NavGround, c: NavConfig) -> bool { + if not nv_ground_ok(g) { return false } + return nv_set(nav_st, kind, nvc_build(g.v, g.nv, g.t, g.nt, g.area, nv_or_none(g.cyl), g.nc, nv_or_none(g.foot), g.nf, nv_cfg(c))) +} + +# an empty mesh of square tiles `tile` metres across from (ox, oz), for a map built a tile at a time +export function nav_tiled(nav_st: mut NavState, kind: int, ox: float, oz: float, tile: float, max_tiles: int, max_polys: int) -> bool { + return nv_set(nav_st, kind, nvc_tiled(ox, oz, tile, max_tiles, max_polys)) +} +# tile (tx, tz) from ground g covering it and a few metres round it: its polygons, 0 none, -1 failed +export function nav_tile_build(nav_st: NavState, kind: int, tx: int, tz: int, g: NavGround, c: NavConfig) -> int { + let h = nv_mesh(nav_st, kind) + if h == null or not nv_ground_ok(g) { return -1 } + return nvc_tile_build(h, tx, tz, g.v, g.nv, g.t, g.nt, g.area, nv_or_none(g.cyl), g.nc, nv_or_none(g.foot), g.nf, nv_cfg(c)) +} + +# 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) + if n <= 0 { return false } + 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))) +} diff --git a/packages/ludic.nav/index.ludic b/packages/ludic.nav/index.ludic new file mode 100644 index 00000000..3f380a8d --- /dev/null +++ b/packages/ludic.nav/index.ludic @@ -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" diff --git a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib new file mode 100755 index 00000000..2ad8f0a1 --- /dev/null +++ b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24f64f1f9c6c4d4f4d97ecb92ea9b55cbfb6d0646a053c5e8210e76e7857aab2 +size 176736 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.dll b/packages/ludic.nav/lib/windows-x64/ludicnav.dll new file mode 100644 index 00000000..52c54d50 --- /dev/null +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38b77e51d87acfcb4d20569746bd174e40fda6c6e7a208a4ce05dc6af92eea06 +size 336384 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.lib b/packages/ludic.nav/lib/windows-x64/ludicnav.lib new file mode 100644 index 00000000..46a507d1 --- /dev/null +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.lib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48f55cfffe5d3e99ae24b68a34b272bfc8ea693ab1f9d849e3e99f17797fee2b +size 3492 diff --git a/packages/ludic.nav/native.ludic b/packages/ludic.nav/native.ludic new file mode 100644 index 00000000..61bb8f3c --- /dev/null +++ b/packages/ludic.nav/native.ludic @@ -0,0 +1,15 @@ +# 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, foot: pointer, nf: int, c: pointer) -> pointer = "nav_build" +extern function nvc_tiled(ox: float, oz: float, tile: float, max_tiles: int, max_polys: int) -> pointer = "nav_tiled" +extern function nvc_tile_build(h: pointer, tx: int, tz: int, v: pointer, nv: int, t: pointer, nt: int, area: pointer, cyl: pointer, nc: int, foot: pointer, nf: int, c: pointer) -> int = "nav_tile_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" +extern function nvc_random_near(h: pointer, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near" diff --git a/packages/ludic.nav/native/LICENSE-recastnavigation b/packages/ludic.nav/native/LICENSE-recastnavigation new file mode 100644 index 00000000..c259ddf6 --- /dev/null +++ b/packages/ludic.nav/native/LICENSE-recastnavigation @@ -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. + diff --git a/packages/ludic.nav/native/build.sh b/packages/ludic.nav/native/build.sh new file mode 100755 index 00000000..6bc4323a --- /dev/null +++ b/packages/ludic.nav/native/build.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# builds lib// 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. Polygon refs are 64-bit (DT_POLYREF64): an 8 km map in 64 m tiles is 16384 tiles, which +# 32-bit refs would leave 256 polygons each. 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 -DDT_POLYREF64 $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 diff --git a/packages/ludic.nav/native/shim/nav_build.inl b/packages/ludic.nav/native/shim/nav_build.inl new file mode 100644 index 00000000..85ad4b8b --- /dev/null +++ b/packages/ludic.nav/native/shim/nav_build.inl @@ -0,0 +1,128 @@ +// 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) { + 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); + } +} + +// 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]; + 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; +} + +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 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(); + 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, 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); + rcFilterWalkableLowHeightSpans(&ctx, cfg.walkableHeight, *hf); + ok = rcBuildCompactHeightfield(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf, *chf); + } + if (ok) { + 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, 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); + } + *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)); + 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 = 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); + } + rcFreeHeightField(hf); rcFreeCompactHeightfield(chf); rcFreeContourSet(cs); rcFreePolyMesh(pm); rcFreePolyMeshDetail(dm); + return ok; +} + +// 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); +} + +NAV_SHIM void nav_free(void *h) { + Nav *n = static_cast