diff --git a/packages/ludic.nav/README.md b/packages/ludic.nav/README.md index 73528849..f209c10c 100644 --- a/packages/ludic.nav/README.md +++ b/packages/ludic.nav/README.md @@ -14,15 +14,21 @@ import "ludic.nav" `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. +- **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. @@ -32,12 +38,15 @@ import "ludic.nav" | | | | --- | --- | | `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) | +| `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 @@ -46,8 +55,10 @@ import "ludic.nav" 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. +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 diff --git a/packages/ludic.nav/build.ludic b/packages/ludic.nav/build.ludic index b92ef905..a8365d5e 100644 --- a/packages/ludic.nav/build.ludic +++ b/packages/ludic.nav/build.ludic @@ -1,8 +1,11 @@ -# 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 } +# 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 @@ -10,7 +13,28 @@ export function nav_build(nav_st: mut NavState, kind: int, v: []float, nv: int, 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)) + 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) @@ -25,6 +49,7 @@ 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) diff --git a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib index 9d6ee7d3..2ad8f0a1 100755 --- a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib +++ b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dd50af00e11ecd47c3c284818f153932b51ed4042be11447f17f44ed9a8e737 -size 159424 +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 index 95d31837..52c54d50 100644 --- a/packages/ludic.nav/lib/windows-x64/ludicnav.dll +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.dll @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef9b8a4cb718f3c47b0d7e042f9307b605288646f523df84777d6c53db00cf37 -size 333824 +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 index e8a20a66..46a507d1 100644 --- a/packages/ludic.nav/lib/windows-x64/ludicnav.lib +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14ea139aeddc712a0c580947ec3d4a90339aa92b54d80b1a8039bb0e88093405 -size 2922 +oid sha256:48f55cfffe5d3e99ae24b68a34b272bfc8ea693ab1f9d849e3e99f17797fee2b +size 3492 diff --git a/packages/ludic.nav/native.ludic b/packages/ludic.nav/native.ludic index c53a9a8f..61bb8f3c 100644 --- a/packages/ludic.nav/native.ludic +++ b/packages/ludic.nav/native.ludic @@ -1,6 +1,8 @@ # 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_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" @@ -10,3 +12,4 @@ extern function nvc_area_cost(h: pointer, area: int, cost: float) -> void = "nav 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/build.sh b/packages/ludic.nav/native/build.sh index c63bed78..6bc4323a 100755 --- a/packages/ludic.nav/native/build.sh +++ b/packages/ludic.nav/native/build.sh @@ -1,7 +1,8 @@ #!/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. Objects go to build/native; a few seconds on eight cores. +# 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" @@ -14,7 +15,7 @@ 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" +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)" diff --git a/packages/ludic.nav/native/shim/nav_build.inl b/packages/ludic.nav/native/shim/nav_build.inl index efda5577..85ad4b8b 100644 --- a/packages/ludic.nav/native/shim/nav_build.inl +++ b/packages/ludic.nav/native/shim/nav_build.inl @@ -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 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 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