@driftengine/nav 4.7.0 → 4.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -9
- package/dist/contour.js +120 -38
- package/dist/voxelise.d.ts +10 -0
- package/dist/voxelise.js +123 -19
- package/package.json +2 -2
- package/src/contour.ts +130 -51
- package/src/voxelise.ts +150 -20
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A navigation mesh: geometry in, convex walkable polygons out, and a straight line across them.
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**9,058 bytes gzipped**, measured by `scripts/size-gate.test.mjs` against
|
|
6
6
|
`scripts/fixtures/size/nav-only.ts`. Optional — nothing in `@driftengine/core` imports it, so a game
|
|
7
7
|
that does not path pays nothing.
|
|
8
8
|
|
|
@@ -36,6 +36,7 @@ const field = voxeliseWalkable(geometry, {
|
|
|
36
36
|
maxSlope: 45,
|
|
37
37
|
agentHeight: 2,
|
|
38
38
|
agentRadius: 0.5,
|
|
39
|
+
maxStep: 1,
|
|
39
40
|
});
|
|
40
41
|
const regions = buildRegions(field, { minRegionSpans: 8, maxStep: 1 });
|
|
41
42
|
const mesh = buildPolyMesh(buildContours(field, regions, 1.3), 6, field);
|
|
@@ -53,11 +54,28 @@ it has two components, and an agent that asks for the other side has to be told.
|
|
|
53
54
|
- **Steering.** §6.5 of the design reverses the refusal of a navigation _mesh_, which is a function
|
|
54
55
|
from geometry to a graph. What an agent does with a path stays a game's decision, and
|
|
55
56
|
`ai/src/entities/context.ts`'s refusal is upheld on exactly that line.
|
|
56
|
-
- **
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
- **A closed box's inside.** A column inside a box meets the floor, the box's bottom and its top,
|
|
58
|
+
and nothing there says the space between is solid short of trusting every mesh's winding. So the
|
|
59
|
+
floor a closed box encloses stays walkable: an island inside the box's walls that no route from
|
|
60
|
+
outside reaches, but `nearestPoly` can still snap a point beside the box onto it. A test pins it.
|
|
61
|
+
- **More than one layer of polygons.** The voxel field keeps both floors under a bridge, and the
|
|
62
|
+
regions keep them apart, but a contour follows the lowest walkable span in each column, so where
|
|
63
|
+
two floors overlap only the lower one becomes polygons.
|
|
64
|
+
|
|
65
|
+
## Walls, pillars and the shortest way round
|
|
66
|
+
|
|
67
|
+
- **A wall is a solid.** A face too steep to stand on is clipped to every cell it crosses and
|
|
68
|
+
stands in that column as a solid, so a building on the ground blocks, and so does a wall with no
|
|
69
|
+
thickness and no top. Erosion judges a neighbour at the span's own height, so the agent keeps its
|
|
70
|
+
radius from a wall even when the wall's top is walkable, and from a ledge higher than `maxStep`.
|
|
71
|
+
Give the voxeliser the same `maxStep` as `buildRegions`: the polygons are flat, so two edges that
|
|
72
|
+
touch are a portal whatever their heights, and erosion is what keeps a floor and a ledge apart.
|
|
73
|
+
- **A shared border is one line.** A room the watershed splits is several regions, and a portal is
|
|
74
|
+
where two polygons' edges overlap. The points where a border changes what it faces are held in
|
|
75
|
+
every outline through them, and each stretch between is simplified the same way from both sides,
|
|
76
|
+
so the regions meet edge to edge at any deviation.
|
|
77
|
+
- **A pillar is a hole.** A region's inner loops are bridged into its outline, so a path goes round
|
|
78
|
+
a free-standing pillar and no polygon claims the ground under it.
|
|
79
|
+
- **A path is the shortest way round.** The search runs over portals, the overlaps between
|
|
80
|
+
neighbouring polygons' edges, so the corridor it hands the funnel holds the shortest path: 9.81
|
|
81
|
+
on the wall in `query.test.ts`, where a search over polygon centres gives 10.75.
|
package/dist/contour.js
CHANGED
|
@@ -40,7 +40,7 @@ function regionAtColumn(field, regions, x, z) {
|
|
|
40
40
|
* are coincident.
|
|
41
41
|
*/
|
|
42
42
|
export function buildContours(field, regions, maxDeviation) {
|
|
43
|
-
const
|
|
43
|
+
const traced = [];
|
|
44
44
|
for (let region = 0; region < regions.count; region += 1) {
|
|
45
45
|
const edges = [];
|
|
46
46
|
for (let z = 0; z < field.depth; z += 1) {
|
|
@@ -78,10 +78,67 @@ export function buildContours(field, regions, maxDeviation) {
|
|
|
78
78
|
* fixes it exactly.
|
|
79
79
|
*/
|
|
80
80
|
const bridged = bridgeHoles(loops);
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
traced.push({ region, raw: Int32Array.from(bridged.loop), bridges: bridged.anchors });
|
|
82
|
+
}
|
|
83
|
+
/*
|
|
84
|
+
* **A border two regions share has to come out of simplification the same from both sides.** A
|
|
85
|
+
* portal is where two polygons' edges overlap, and each region's outline was simplified on its
|
|
86
|
+
* own: a jagged border between two regions came out as two different lines, which overlap
|
|
87
|
+
* nowhere, and a room the watershed had split was islands with no route between them. On a 16 by
|
|
88
|
+
* 10 room with a pillar, simplified to 1.3, there were two regions and no path across.
|
|
89
|
+
*
|
|
90
|
+
* So the points where a border changes what it faces are held in every loop that passes through
|
|
91
|
+
* them, which makes each stretch between two held points one border with the same two ends from
|
|
92
|
+
* both sides; a bridge's ends are held everywhere for the same reason, since the region on the
|
|
93
|
+
* far side of a hole meets them too. Each stretch is simplified in one canonical direction, so
|
|
94
|
+
* both sides take the same points. And the halving that keeps a loop from crossing itself is
|
|
95
|
+
* applied to every region together, because a shared stretch simplified at two deviations is two
|
|
96
|
+
* lines again. One awkward region makes every outline finer, which costs vertices and nothing
|
|
97
|
+
* else.
|
|
98
|
+
*/
|
|
99
|
+
const bridgeEnds = new Set();
|
|
100
|
+
for (const { raw, bridges } of traced) {
|
|
101
|
+
for (const at of bridges)
|
|
102
|
+
bridgeEnds.add(`${raw[at * 2]},${raw[at * 2 + 1]}`);
|
|
83
103
|
}
|
|
84
|
-
|
|
104
|
+
const held = traced.map(({ raw }) => {
|
|
105
|
+
const indices = [];
|
|
106
|
+
for (let at = 0; at < raw.length / 2; at += 1) {
|
|
107
|
+
const x = raw[at * 2];
|
|
108
|
+
const z = raw[at * 2 + 1];
|
|
109
|
+
if (bridgeEnds.has(`${x},${z}`) || isJunction(field, regions, x, z))
|
|
110
|
+
indices.push(at);
|
|
111
|
+
}
|
|
112
|
+
return indices;
|
|
113
|
+
});
|
|
114
|
+
for (let deviation = maxDeviation; deviation > 1e-4; deviation /= 2) {
|
|
115
|
+
const simplified = traced.map(({ raw }, at) => simplifyOnce(raw, deviation, held[at] ?? []));
|
|
116
|
+
if (simplified.some(contourSelfIntersects))
|
|
117
|
+
continue;
|
|
118
|
+
return traced.map(({ region, raw }, at) => ({
|
|
119
|
+
region,
|
|
120
|
+
points: simplified[at],
|
|
121
|
+
raw,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
return traced.map(({ region, raw }) => ({ region, points: raw, raw }));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Whether three or more different things meet at a cell corner: regions, or regions and nothing.
|
|
128
|
+
*
|
|
129
|
+
* Where a region's boundary stops facing one thing and starts facing another, the corner has the
|
|
130
|
+
* region and both of those around it, and that is the only way three can meet. So this is exactly
|
|
131
|
+
* the set of points where some loop through the corner changes what it borders, decided from the
|
|
132
|
+
* field alone, which is what lets every loop through it agree.
|
|
133
|
+
*/
|
|
134
|
+
function isJunction(field, regions, x, z) {
|
|
135
|
+
const around = [
|
|
136
|
+
regionAtColumn(field, regions, x - 1, z - 1),
|
|
137
|
+
regionAtColumn(field, regions, x, z - 1),
|
|
138
|
+
regionAtColumn(field, regions, x - 1, z),
|
|
139
|
+
regionAtColumn(field, regions, x, z),
|
|
140
|
+
];
|
|
141
|
+
return new Set(around).size >= 3;
|
|
85
142
|
}
|
|
86
143
|
/**
|
|
87
144
|
* Follow the directed edges into closed loops.
|
|
@@ -391,36 +448,27 @@ function segmentsCross(ax, az, bx, bz, cx, cz, dx, dz) {
|
|
|
391
448
|
return d1 !== d2 && d3 !== d4 && d1 !== 0 && d2 !== 0 && d3 !== 0 && d4 !== 0;
|
|
392
449
|
}
|
|
393
450
|
/**
|
|
394
|
-
* Douglas–Peucker around a closed loop,
|
|
451
|
+
* Douglas–Peucker around a closed loop, holding `held` and simplifying the stretches between.
|
|
395
452
|
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
453
|
+
* `buildContours` halves the deviation until no loop crosses itself. The raw boundary is a walk
|
|
454
|
+
* along cell edges and cannot cross itself, so that terminates at something valid however hostile
|
|
455
|
+
* the shape: it costs a few extra passes on a shape that needs them and nothing on one that does
|
|
456
|
+
* not.
|
|
400
457
|
*/
|
|
401
|
-
function
|
|
402
|
-
for (let deviation = maxDeviation; deviation > 1e-4; deviation /= 2) {
|
|
403
|
-
const candidate = simplifyOnce(raw, deviation, anchors);
|
|
404
|
-
if (!contourSelfIntersects(candidate))
|
|
405
|
-
return candidate;
|
|
406
|
-
}
|
|
407
|
-
return raw;
|
|
408
|
-
}
|
|
409
|
-
function simplifyOnce(raw, maxDeviation, anchors) {
|
|
458
|
+
function simplifyOnce(raw, maxDeviation, held) {
|
|
410
459
|
const count = raw.length / 2;
|
|
411
460
|
if (count < 4)
|
|
412
461
|
return raw;
|
|
413
462
|
const keep = new Uint8Array(count);
|
|
414
|
-
const bridged = anchors.length >= 2;
|
|
415
463
|
let fixed;
|
|
416
|
-
if (
|
|
464
|
+
if (held.length > 0) {
|
|
417
465
|
/*
|
|
418
|
-
* **
|
|
419
|
-
* two of them is an
|
|
420
|
-
* are what keep the two traversals of each bridge coincident,
|
|
421
|
-
* the
|
|
466
|
+
* **Held points are a bridge or a change of neighbour, and they are not negotiable.** Every
|
|
467
|
+
* stretch between two of them is an open polyline Douglas–Peucker may do as it likes with; the
|
|
468
|
+
* held points are what keep the two traversals of each bridge coincident, and each shared
|
|
469
|
+
* border the same from both of its regions.
|
|
422
470
|
*/
|
|
423
|
-
fixed = [...
|
|
471
|
+
fixed = [...held];
|
|
424
472
|
}
|
|
425
473
|
else {
|
|
426
474
|
/* Two anchors, so the loop becomes two open chains: the first point and the one furthest. */
|
|
@@ -440,12 +488,12 @@ function simplifyOnce(raw, maxDeviation, anchors) {
|
|
|
440
488
|
keep[at] = 1;
|
|
441
489
|
for (let at = 0; at < fixed.length; at += 1) {
|
|
442
490
|
const from = fixed[at];
|
|
443
|
-
const to = at + 1 < fixed.length ? fixed[at + 1] : count;
|
|
444
|
-
|
|
491
|
+
const to = at + 1 < fixed.length ? fixed[at + 1] : fixed[0] + count;
|
|
492
|
+
stretch(raw, from, to, maxDeviation, keep);
|
|
445
493
|
}
|
|
446
494
|
/*
|
|
447
|
-
* **Only a *
|
|
448
|
-
*
|
|
495
|
+
* **Only a *held* point is protected from the collinear pass, never the arbitrary pair.** The two
|
|
496
|
+
* anchors of a loop with nothing held are wherever the trace happened to start and whatever was
|
|
449
497
|
* furthest from it, so one is routinely a point in the middle of a straight edge — and
|
|
450
498
|
* `dropCollinear` exists precisely to remove that, which is what keeps an L six-cornered.
|
|
451
499
|
*/
|
|
@@ -454,7 +502,7 @@ function simplifyOnce(raw, maxDeviation, anchors) {
|
|
|
454
502
|
for (let at = 0; at < count; at += 1) {
|
|
455
503
|
if (keep[at] !== 1)
|
|
456
504
|
continue;
|
|
457
|
-
if (
|
|
505
|
+
if (held.length > 0 && fixed.includes(at))
|
|
458
506
|
protectedOut.push(out.length / 2);
|
|
459
507
|
out.push(raw[at * 2], raw[at * 2 + 1]);
|
|
460
508
|
}
|
|
@@ -493,16 +541,50 @@ function dropCollinear(points, protect = []) {
|
|
|
493
541
|
}
|
|
494
542
|
return out.length >= 6 ? Int32Array.from(out) : points;
|
|
495
543
|
}
|
|
496
|
-
/**
|
|
497
|
-
|
|
544
|
+
/**
|
|
545
|
+
* Mark the points of `raw` from `from` to `to` that have to stay. `to` may pass the end, which
|
|
546
|
+
* wraps; `to` equal to `from` plus the loop's length is the whole loop from one held point.
|
|
547
|
+
*
|
|
548
|
+
* **Walked in one canonical direction, whichever way the loop runs**, so a border two regions
|
|
549
|
+
* share, which each traces the other way round, keeps the same points from both sides: the
|
|
550
|
+
* stretch runs from its lesser end to its greater, comparing x then z, and a stretch that starts
|
|
551
|
+
* and ends at one point runs toward the lesser of its two neighbours.
|
|
552
|
+
*/
|
|
553
|
+
function stretch(raw, from, to, maxDeviation, keep) {
|
|
498
554
|
const count = raw.length / 2;
|
|
499
|
-
const endIndex = to % count;
|
|
500
555
|
if (to - from < 2)
|
|
501
556
|
return;
|
|
557
|
+
const order = [];
|
|
558
|
+
for (let at = from; at <= to; at += 1)
|
|
559
|
+
order.push(at % count);
|
|
560
|
+
const last = order.length - 1;
|
|
561
|
+
const first = order[0];
|
|
562
|
+
const end = order[last];
|
|
563
|
+
const sameEnds = raw[first * 2] === raw[end * 2] && raw[first * 2 + 1] === raw[end * 2 + 1];
|
|
564
|
+
const backwards = sameEnds
|
|
565
|
+
? lesser(raw, order[last - 1], order[1])
|
|
566
|
+
: lesser(raw, end, first);
|
|
567
|
+
if (backwards)
|
|
568
|
+
order.reverse();
|
|
569
|
+
douglasPeucker(raw, order, 0, last, maxDeviation, keep);
|
|
570
|
+
}
|
|
571
|
+
/** Whether point `a` comes before point `b`, by x and then by z. */
|
|
572
|
+
function lesser(raw, a, b) {
|
|
573
|
+
const ax = raw[a * 2];
|
|
574
|
+
const bx = raw[b * 2];
|
|
575
|
+
return ax < bx || (ax === bx && raw[a * 2 + 1] < raw[b * 2 + 1]);
|
|
576
|
+
}
|
|
577
|
+
/** Keep the furthest point of `order` between `lo` and `hi` while it is further than the bound. */
|
|
578
|
+
function douglasPeucker(raw, order, lo, hi, maxDeviation, keep) {
|
|
579
|
+
if (hi - lo < 2)
|
|
580
|
+
return;
|
|
581
|
+
const a = order[lo];
|
|
582
|
+
const b = order[hi];
|
|
502
583
|
let worst = -1;
|
|
503
584
|
let worstAt = -1;
|
|
504
|
-
for (let at =
|
|
505
|
-
const
|
|
585
|
+
for (let at = lo + 1; at < hi; at += 1) {
|
|
586
|
+
const point = order[at];
|
|
587
|
+
const d = distanceToSegment(raw[point * 2], raw[point * 2 + 1], raw[a * 2], raw[a * 2 + 1], raw[b * 2], raw[b * 2 + 1]);
|
|
506
588
|
if (d > worst) {
|
|
507
589
|
worst = d;
|
|
508
590
|
worstAt = at;
|
|
@@ -510,7 +592,7 @@ function chain(raw, from, to, maxDeviation, keep) {
|
|
|
510
592
|
}
|
|
511
593
|
if (worst <= maxDeviation || worstAt < 0)
|
|
512
594
|
return;
|
|
513
|
-
keep[worstAt] = 1;
|
|
514
|
-
|
|
515
|
-
|
|
595
|
+
keep[order[worstAt]] = 1;
|
|
596
|
+
douglasPeucker(raw, order, lo, worstAt, maxDeviation, keep);
|
|
597
|
+
douglasPeucker(raw, order, worstAt, hi, maxDeviation, keep);
|
|
516
598
|
}
|
package/dist/voxelise.d.ts
CHANGED
|
@@ -34,6 +34,16 @@ export interface VoxeliseSettings {
|
|
|
34
34
|
readonly agentHeight: number;
|
|
35
35
|
/** How far from an edge an agent's centre must stay. */
|
|
36
36
|
readonly agentRadius: number;
|
|
37
|
+
/**
|
|
38
|
+
* How many cells of height an agent can step across, the number `buildRegions` takes; 1 when
|
|
39
|
+
* absent. A neighbour further above or below than this is an edge, and erosion keeps the agent's
|
|
40
|
+
* radius from it.
|
|
41
|
+
*
|
|
42
|
+
* **Here as well as there because the polygons are flat.** Two regions whose edges touch are
|
|
43
|
+
* joined by a portal whatever their heights, so a floor and a ledge above it must be kept apart
|
|
44
|
+
* by erosion, or a route goes over the ledge.
|
|
45
|
+
*/
|
|
46
|
+
readonly maxStep?: number;
|
|
37
47
|
}
|
|
38
48
|
/**
|
|
39
49
|
* Walkable surfaces as spans, in columns on a horizontal grid.
|
package/dist/voxelise.js
CHANGED
|
@@ -59,6 +59,10 @@ export function spanWalkable(field, x, z, at) {
|
|
|
59
59
|
return false;
|
|
60
60
|
return field.spans[base + 2] === 1;
|
|
61
61
|
}
|
|
62
|
+
/** Scratch for clipping a triangle to one cell: four planes leave at most seven vertices. */
|
|
63
|
+
const CLIP_IN = new Float64Array(8 * 3);
|
|
64
|
+
const CLIP_OUT = new Float64Array(8 * 3);
|
|
65
|
+
const HEIGHTS = new Float64Array(2);
|
|
62
66
|
/**
|
|
63
67
|
* Rasterise, then decide.
|
|
64
68
|
*
|
|
@@ -116,18 +120,89 @@ export function voxeliseWalkable(geometry, settings) {
|
|
|
116
120
|
const hiZ = Math.min(depth - 1, Math.floor((Math.max(az, bz, cz) - minZ) / settings.cellSize));
|
|
117
121
|
for (let gz = loZ; gz <= hiZ; gz += 1) {
|
|
118
122
|
for (let gx = loX; gx <= hiX; gx += 1) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
if (walkable) {
|
|
124
|
+
const px = minX + (gx + 0.5) * settings.cellSize;
|
|
125
|
+
const pz = minZ + (gz + 0.5) * settings.cellSize;
|
|
126
|
+
const height = heightAt(ax, ay, az, bx, by, bz, cx, cy, cz, px, pz);
|
|
127
|
+
if (height === null)
|
|
128
|
+
continue;
|
|
129
|
+
const floor = Math.floor((height - minY) / settings.cellHeight);
|
|
130
|
+
columns[gx + gz * width]?.push({ bottom: floor, top: floor, walkable });
|
|
123
131
|
continue;
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
}
|
|
133
|
+
/*
|
|
134
|
+
* **A face too steep to stand on is a solid, over every cell it crosses.** Sampled at the
|
|
135
|
+
* cell's centre like a floor, a wall is never met at all: it is vertical, so it covers no
|
|
136
|
+
* area seen from above, and a building on the ground read as a roof over open floor that a
|
|
137
|
+
* route went straight through. So the face is clipped to the cell's square, and the heights
|
|
138
|
+
* it spans there stand in the column as one solid, whose top is not walkable.
|
|
139
|
+
*/
|
|
140
|
+
const x0 = minX + gx * settings.cellSize;
|
|
141
|
+
const z0 = minZ + gz * settings.cellSize;
|
|
142
|
+
CLIP_IN.set([ax, ay, az, bx, by, bz, cx, cy, cz]);
|
|
143
|
+
if (!clipToCell(x0, x0 + settings.cellSize, z0, z0 + settings.cellSize, HEIGHTS)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
columns[gx + gz * width]?.push({
|
|
147
|
+
bottom: Math.floor((HEIGHTS[0] - minY) / settings.cellHeight),
|
|
148
|
+
top: Math.floor((HEIGHTS[1] - minY) / settings.cellHeight),
|
|
149
|
+
walkable: false,
|
|
150
|
+
});
|
|
126
151
|
}
|
|
127
152
|
}
|
|
128
153
|
}
|
|
129
154
|
return pack(columns, width, depth, origin, settings);
|
|
130
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* The lowest and highest heights of the triangle in `CLIP_IN` within one cell's square, written to
|
|
158
|
+
* `out`; false where the triangle misses the cell.
|
|
159
|
+
*
|
|
160
|
+
* Sutherland–Hodgman against the square's four sides. Inclusive, so a wall lying exactly on a cell
|
|
161
|
+
* boundary still lands in the cell the bounding box chose for it.
|
|
162
|
+
*/
|
|
163
|
+
function clipToCell(x0, x1, z0, z1, out) {
|
|
164
|
+
let count = 3;
|
|
165
|
+
count = clipSide(CLIP_IN, count, CLIP_OUT, 0, x0, 1);
|
|
166
|
+
count = clipSide(CLIP_OUT, count, CLIP_IN, 0, x1, -1);
|
|
167
|
+
count = clipSide(CLIP_IN, count, CLIP_OUT, 2, z0, 1);
|
|
168
|
+
count = clipSide(CLIP_OUT, count, CLIP_IN, 2, z1, -1);
|
|
169
|
+
if (count === 0)
|
|
170
|
+
return false;
|
|
171
|
+
let low = Infinity;
|
|
172
|
+
let high = -Infinity;
|
|
173
|
+
for (let at = 0; at < count; at += 1) {
|
|
174
|
+
const y = CLIP_IN[at * 3 + 1];
|
|
175
|
+
low = Math.min(low, y);
|
|
176
|
+
high = Math.max(high, y);
|
|
177
|
+
}
|
|
178
|
+
out[0] = low;
|
|
179
|
+
out[1] = high;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
/** Keep the part of a polygon on one side of `axis = bound`: above it for `side` 1, below for -1. */
|
|
183
|
+
function clipSide(from, count, to, axis, bound, side) {
|
|
184
|
+
let written = 0;
|
|
185
|
+
for (let at = 0; at < count; at += 1) {
|
|
186
|
+
const next = (at + 1) % count;
|
|
187
|
+
const here = (from[at * 3 + axis] - bound) * side;
|
|
188
|
+
const there = (from[next * 3 + axis] - bound) * side;
|
|
189
|
+
if (here >= 0) {
|
|
190
|
+
to[written * 3] = from[at * 3];
|
|
191
|
+
to[written * 3 + 1] = from[at * 3 + 1];
|
|
192
|
+
to[written * 3 + 2] = from[at * 3 + 2];
|
|
193
|
+
written += 1;
|
|
194
|
+
}
|
|
195
|
+
if (here >= 0 !== there >= 0) {
|
|
196
|
+
const t = here / (here - there);
|
|
197
|
+
for (let k = 0; k < 3; k += 1) {
|
|
198
|
+
const a = from[at * 3 + k];
|
|
199
|
+
to[written * 3 + k] = a + (from[next * 3 + k] - a) * t;
|
|
200
|
+
}
|
|
201
|
+
written += 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return written;
|
|
205
|
+
}
|
|
131
206
|
/**
|
|
132
207
|
* The triangle's height at (px, pz), or null where the point is outside it.
|
|
133
208
|
*
|
|
@@ -157,20 +232,34 @@ function pack(columns, width, depth, origin, settings) {
|
|
|
157
232
|
const headroom = Math.ceil(settings.agentHeight / settings.cellHeight);
|
|
158
233
|
const merged = [];
|
|
159
234
|
for (const samples of columns) {
|
|
160
|
-
samples.sort((a, b) => a.
|
|
235
|
+
samples.sort((a, b) => a.bottom - b.bottom || a.top - b.top || Number(a.walkable) - Number(b.walkable));
|
|
161
236
|
const spans = [];
|
|
237
|
+
/* Where each span's solid starts, which is the ceiling of the span below it. */
|
|
238
|
+
const bottoms = [];
|
|
162
239
|
for (const sample of samples) {
|
|
163
240
|
const last = spans.length - SPAN_STRIDE;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
241
|
+
const top = spans[last];
|
|
242
|
+
/*
|
|
243
|
+
* A sample starting within one cell of a span's top is part of that solid, not a new one.
|
|
244
|
+
* The span stands on the highest top, and is walkable if anything ending within a cell of
|
|
245
|
+
* that top is: a wall rising past a floor takes the floor over, and a floor's two triangles
|
|
246
|
+
* at one height agree.
|
|
247
|
+
*/
|
|
248
|
+
if (last >= 0 && sample.bottom - top <= 1) {
|
|
249
|
+
const flag = sample.walkable ? 1 : 0;
|
|
250
|
+
if (sample.top > top + 1)
|
|
251
|
+
spans[last + 2] = flag;
|
|
252
|
+
else if (sample.top >= top - 1)
|
|
253
|
+
spans[last + 2] = spans[last + 2] | flag;
|
|
254
|
+
spans[last] = Math.max(top, sample.top);
|
|
167
255
|
continue;
|
|
168
256
|
}
|
|
169
|
-
spans.push(sample.
|
|
257
|
+
spans.push(sample.top, Number.MAX_SAFE_INTEGER, sample.walkable ? 1 : 0);
|
|
258
|
+
bottoms.push(sample.bottom);
|
|
170
259
|
}
|
|
171
|
-
/* A span's ceiling is the next
|
|
172
|
-
for (let at = 0; at + SPAN_STRIDE < spans.length; at += SPAN_STRIDE) {
|
|
173
|
-
spans[at + 1] =
|
|
260
|
+
/* A span's ceiling is where the next solid starts. The topmost has open sky. */
|
|
261
|
+
for (let at = 0, next = 1; at + SPAN_STRIDE < spans.length; at += SPAN_STRIDE, next += 1) {
|
|
262
|
+
spans[at + 1] = bottoms[next];
|
|
174
263
|
if (spans[at + 1] - spans[at] < headroom)
|
|
175
264
|
spans[at + 2] = 0;
|
|
176
265
|
}
|
|
@@ -209,28 +298,43 @@ function pack(columns, width, depth, origin, settings) {
|
|
|
209
298
|
* doing it as that many single-cell passes keeps the whole thing integer arithmetic with no
|
|
210
299
|
* distance to round. **Copied per pass**, because eroding in place erodes into what the same pass
|
|
211
300
|
* just cleared and eats the mesh from its edges inward.
|
|
301
|
+
*
|
|
302
|
+
* **A neighbour is judged at this span's height, not anywhere in its column.** Asked whether a
|
|
303
|
+
* neighbouring column had any walkable span at all, a wall answered yes, with the roof on top of
|
|
304
|
+
* it, and the floor beside the wall kept no margin. A neighbour counts as open when it has a
|
|
305
|
+
* walkable span within `maxStep` of this one, a step the agent can take. Further than that is a
|
|
306
|
+
* wall, a ledge or a drop, and the agent keeps its radius from it.
|
|
212
307
|
*/
|
|
213
308
|
function erode(columns, width, depth, settings) {
|
|
214
309
|
const steps = Math.ceil(settings.agentRadius / settings.cellSize);
|
|
310
|
+
const reach = settings.maxStep ?? 1;
|
|
215
311
|
for (let pass = 0; pass < steps; pass += 1) {
|
|
216
312
|
const before = columns.map((column) => [...column]);
|
|
217
|
-
const open = (gx, gz) => {
|
|
313
|
+
const open = (gx, gz, floor) => {
|
|
218
314
|
if (gx < 0 || gz < 0 || gx >= width || gz >= depth)
|
|
219
315
|
return false;
|
|
220
316
|
const column = before[gx + gz * width];
|
|
221
317
|
for (let at = 0; at < column.length; at += SPAN_STRIDE) {
|
|
222
|
-
if (column[at + 2] === 1)
|
|
318
|
+
if (column[at + 2] === 1 && Math.abs(column[at] - floor) <= reach)
|
|
223
319
|
return true;
|
|
224
320
|
}
|
|
225
321
|
return false;
|
|
226
322
|
};
|
|
227
323
|
for (let gz = 0; gz < depth; gz += 1) {
|
|
228
324
|
for (let gx = 0; gx < width; gx += 1) {
|
|
229
|
-
if (open(gx - 1, gz) && open(gx + 1, gz) && open(gx, gz - 1) && open(gx, gz + 1))
|
|
230
|
-
continue;
|
|
231
325
|
const column = columns[gx + gz * width];
|
|
232
|
-
for (let at = 0; at < column.length; at += SPAN_STRIDE)
|
|
326
|
+
for (let at = 0; at < column.length; at += SPAN_STRIDE) {
|
|
327
|
+
if (column[at + 2] !== 1)
|
|
328
|
+
continue;
|
|
329
|
+
const floor = column[at];
|
|
330
|
+
if (open(gx - 1, gz, floor) &&
|
|
331
|
+
open(gx + 1, gz, floor) &&
|
|
332
|
+
open(gx, gz - 1, floor) &&
|
|
333
|
+
open(gx, gz + 1, floor)) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
233
336
|
column[at + 2] = 0;
|
|
337
|
+
}
|
|
234
338
|
}
|
|
235
339
|
}
|
|
236
340
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/nav",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.2",
|
|
4
4
|
"description": "A navigation mesh: geometry to walkable convex polygons, and a straight line across them",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -53,6 +53,6 @@
|
|
|
53
53
|
"prepack": "node ../../scripts/build.mjs @driftengine/nav"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@driftengine/core": "4.7.
|
|
56
|
+
"@driftengine/core": "4.7.2"
|
|
57
57
|
}
|
|
58
58
|
}
|
package/src/contour.ts
CHANGED
|
@@ -69,7 +69,7 @@ export function buildContours(
|
|
|
69
69
|
regions: RegionField,
|
|
70
70
|
maxDeviation: number,
|
|
71
71
|
): Contour[] {
|
|
72
|
-
const
|
|
72
|
+
const traced: { region: number; raw: Int32Array; bridges: number[] }[] = [];
|
|
73
73
|
for (let region = 0; region < regions.count; region += 1) {
|
|
74
74
|
const edges: Edge[] = [];
|
|
75
75
|
for (let z = 0; z < field.depth; z += 1) {
|
|
@@ -105,10 +105,67 @@ export function buildContours(
|
|
|
105
105
|
* fixes it exactly.
|
|
106
106
|
*/
|
|
107
107
|
const bridged = bridgeHoles(loops);
|
|
108
|
-
|
|
109
|
-
out.push({ region, points: simplify(raw, maxDeviation, bridged.anchors), raw });
|
|
108
|
+
traced.push({ region, raw: Int32Array.from(bridged.loop), bridges: bridged.anchors });
|
|
110
109
|
}
|
|
111
|
-
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
* **A border two regions share has to come out of simplification the same from both sides.** A
|
|
113
|
+
* portal is where two polygons' edges overlap, and each region's outline was simplified on its
|
|
114
|
+
* own: a jagged border between two regions came out as two different lines, which overlap
|
|
115
|
+
* nowhere, and a room the watershed had split was islands with no route between them. On a 16 by
|
|
116
|
+
* 10 room with a pillar, simplified to 1.3, there were two regions and no path across.
|
|
117
|
+
*
|
|
118
|
+
* So the points where a border changes what it faces are held in every loop that passes through
|
|
119
|
+
* them, which makes each stretch between two held points one border with the same two ends from
|
|
120
|
+
* both sides; a bridge's ends are held everywhere for the same reason, since the region on the
|
|
121
|
+
* far side of a hole meets them too. Each stretch is simplified in one canonical direction, so
|
|
122
|
+
* both sides take the same points. And the halving that keeps a loop from crossing itself is
|
|
123
|
+
* applied to every region together, because a shared stretch simplified at two deviations is two
|
|
124
|
+
* lines again. One awkward region makes every outline finer, which costs vertices and nothing
|
|
125
|
+
* else.
|
|
126
|
+
*/
|
|
127
|
+
const bridgeEnds = new Set<string>();
|
|
128
|
+
for (const { raw, bridges } of traced) {
|
|
129
|
+
for (const at of bridges) bridgeEnds.add(`${raw[at * 2]},${raw[at * 2 + 1]}`);
|
|
130
|
+
}
|
|
131
|
+
const held = traced.map(({ raw }) => {
|
|
132
|
+
const indices: number[] = [];
|
|
133
|
+
for (let at = 0; at < raw.length / 2; at += 1) {
|
|
134
|
+
const x = raw[at * 2] as number;
|
|
135
|
+
const z = raw[at * 2 + 1] as number;
|
|
136
|
+
if (bridgeEnds.has(`${x},${z}`) || isJunction(field, regions, x, z)) indices.push(at);
|
|
137
|
+
}
|
|
138
|
+
return indices;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
for (let deviation = maxDeviation; deviation > 1e-4; deviation /= 2) {
|
|
142
|
+
const simplified = traced.map(({ raw }, at) => simplifyOnce(raw, deviation, held[at] ?? []));
|
|
143
|
+
if (simplified.some(contourSelfIntersects)) continue;
|
|
144
|
+
return traced.map(({ region, raw }, at) => ({
|
|
145
|
+
region,
|
|
146
|
+
points: simplified[at] as Int32Array,
|
|
147
|
+
raw,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
return traced.map(({ region, raw }) => ({ region, points: raw, raw }));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Whether three or more different things meet at a cell corner: regions, or regions and nothing.
|
|
155
|
+
*
|
|
156
|
+
* Where a region's boundary stops facing one thing and starts facing another, the corner has the
|
|
157
|
+
* region and both of those around it, and that is the only way three can meet. So this is exactly
|
|
158
|
+
* the set of points where some loop through the corner changes what it borders, decided from the
|
|
159
|
+
* field alone, which is what lets every loop through it agree.
|
|
160
|
+
*/
|
|
161
|
+
function isJunction(field: VoxelField, regions: RegionField, x: number, z: number): boolean {
|
|
162
|
+
const around = [
|
|
163
|
+
regionAtColumn(field, regions, x - 1, z - 1),
|
|
164
|
+
regionAtColumn(field, regions, x, z - 1),
|
|
165
|
+
regionAtColumn(field, regions, x - 1, z),
|
|
166
|
+
regionAtColumn(field, regions, x, z),
|
|
167
|
+
];
|
|
168
|
+
return new Set(around).size >= 3;
|
|
112
169
|
}
|
|
113
170
|
|
|
114
171
|
/** One side of one cell, pointing so the region stays on the same hand throughout. */
|
|
@@ -495,44 +552,27 @@ function segmentsCross(
|
|
|
495
552
|
}
|
|
496
553
|
|
|
497
554
|
/**
|
|
498
|
-
* Douglas–Peucker around a closed loop,
|
|
555
|
+
* Douglas–Peucker around a closed loop, holding `held` and simplifying the stretches between.
|
|
499
556
|
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
557
|
+
* `buildContours` halves the deviation until no loop crosses itself. The raw boundary is a walk
|
|
558
|
+
* along cell edges and cannot cross itself, so that terminates at something valid however hostile
|
|
559
|
+
* the shape: it costs a few extra passes on a shape that needs them and nothing on one that does
|
|
560
|
+
* not.
|
|
504
561
|
*/
|
|
505
|
-
function
|
|
506
|
-
raw: Int32Array,
|
|
507
|
-
maxDeviation: number,
|
|
508
|
-
anchors: readonly number[] = [],
|
|
509
|
-
): Int32Array {
|
|
510
|
-
for (let deviation = maxDeviation; deviation > 1e-4; deviation /= 2) {
|
|
511
|
-
const candidate = simplifyOnce(raw, deviation, anchors);
|
|
512
|
-
if (!contourSelfIntersects(candidate)) return candidate;
|
|
513
|
-
}
|
|
514
|
-
return raw;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function simplifyOnce(
|
|
518
|
-
raw: Int32Array,
|
|
519
|
-
maxDeviation: number,
|
|
520
|
-
anchors: readonly number[],
|
|
521
|
-
): Int32Array {
|
|
562
|
+
function simplifyOnce(raw: Int32Array, maxDeviation: number, held: readonly number[]): Int32Array {
|
|
522
563
|
const count = raw.length / 2;
|
|
523
564
|
if (count < 4) return raw;
|
|
524
565
|
|
|
525
566
|
const keep = new Uint8Array(count);
|
|
526
|
-
const bridged = anchors.length >= 2;
|
|
527
567
|
let fixed: number[];
|
|
528
|
-
if (
|
|
568
|
+
if (held.length > 0) {
|
|
529
569
|
/*
|
|
530
|
-
* **
|
|
531
|
-
* two of them is an
|
|
532
|
-
* are what keep the two traversals of each bridge coincident,
|
|
533
|
-
* the
|
|
570
|
+
* **Held points are a bridge or a change of neighbour, and they are not negotiable.** Every
|
|
571
|
+
* stretch between two of them is an open polyline Douglas–Peucker may do as it likes with; the
|
|
572
|
+
* held points are what keep the two traversals of each bridge coincident, and each shared
|
|
573
|
+
* border the same from both of its regions.
|
|
534
574
|
*/
|
|
535
|
-
fixed = [...
|
|
575
|
+
fixed = [...held];
|
|
536
576
|
} else {
|
|
537
577
|
/* Two anchors, so the loop becomes two open chains: the first point and the one furthest. */
|
|
538
578
|
let far = 0;
|
|
@@ -554,13 +594,13 @@ function simplifyOnce(
|
|
|
554
594
|
for (const at of fixed) keep[at] = 1;
|
|
555
595
|
for (let at = 0; at < fixed.length; at += 1) {
|
|
556
596
|
const from = fixed[at] as number;
|
|
557
|
-
const to = at + 1 < fixed.length ? (fixed[at + 1] as number) : count;
|
|
558
|
-
|
|
597
|
+
const to = at + 1 < fixed.length ? (fixed[at + 1] as number) : (fixed[0] as number) + count;
|
|
598
|
+
stretch(raw, from, to, maxDeviation, keep);
|
|
559
599
|
}
|
|
560
600
|
|
|
561
601
|
/*
|
|
562
|
-
* **Only a *
|
|
563
|
-
*
|
|
602
|
+
* **Only a *held* point is protected from the collinear pass, never the arbitrary pair.** The two
|
|
603
|
+
* anchors of a loop with nothing held are wherever the trace happened to start and whatever was
|
|
564
604
|
* furthest from it, so one is routinely a point in the middle of a straight edge — and
|
|
565
605
|
* `dropCollinear` exists precisely to remove that, which is what keeps an L six-cornered.
|
|
566
606
|
*/
|
|
@@ -568,7 +608,7 @@ function simplifyOnce(
|
|
|
568
608
|
const protectedOut: number[] = [];
|
|
569
609
|
for (let at = 0; at < count; at += 1) {
|
|
570
610
|
if (keep[at] !== 1) continue;
|
|
571
|
-
if (
|
|
611
|
+
if (held.length > 0 && fixed.includes(at)) protectedOut.push(out.length / 2);
|
|
572
612
|
out.push(raw[at * 2] as number, raw[at * 2 + 1] as number);
|
|
573
613
|
}
|
|
574
614
|
return dropCollinear(Int32Array.from(out), protectedOut);
|
|
@@ -607,8 +647,16 @@ function dropCollinear(points: Int32Array, protect: readonly number[] = []): Int
|
|
|
607
647
|
return out.length >= 6 ? Int32Array.from(out) : points;
|
|
608
648
|
}
|
|
609
649
|
|
|
610
|
-
/**
|
|
611
|
-
|
|
650
|
+
/**
|
|
651
|
+
* Mark the points of `raw` from `from` to `to` that have to stay. `to` may pass the end, which
|
|
652
|
+
* wraps; `to` equal to `from` plus the loop's length is the whole loop from one held point.
|
|
653
|
+
*
|
|
654
|
+
* **Walked in one canonical direction, whichever way the loop runs**, so a border two regions
|
|
655
|
+
* share, which each traces the other way round, keeps the same points from both sides: the
|
|
656
|
+
* stretch runs from its lesser end to its greater, comparing x then z, and a stretch that starts
|
|
657
|
+
* and ends at one point runs toward the lesser of its two neighbours.
|
|
658
|
+
*/
|
|
659
|
+
function stretch(
|
|
612
660
|
raw: Int32Array,
|
|
613
661
|
from: number,
|
|
614
662
|
to: number,
|
|
@@ -616,19 +664,50 @@ function chain(
|
|
|
616
664
|
keep: Uint8Array,
|
|
617
665
|
): void {
|
|
618
666
|
const count = raw.length / 2;
|
|
619
|
-
const endIndex = to % count;
|
|
620
667
|
if (to - from < 2) return;
|
|
668
|
+
const order: number[] = [];
|
|
669
|
+
for (let at = from; at <= to; at += 1) order.push(at % count);
|
|
670
|
+
const last = order.length - 1;
|
|
671
|
+
const first = order[0] as number;
|
|
672
|
+
const end = order[last] as number;
|
|
673
|
+
const sameEnds = raw[first * 2] === raw[end * 2] && raw[first * 2 + 1] === raw[end * 2 + 1];
|
|
674
|
+
const backwards = sameEnds
|
|
675
|
+
? lesser(raw, order[last - 1] as number, order[1] as number)
|
|
676
|
+
: lesser(raw, end, first);
|
|
677
|
+
if (backwards) order.reverse();
|
|
678
|
+
douglasPeucker(raw, order, 0, last, maxDeviation, keep);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Whether point `a` comes before point `b`, by x and then by z. */
|
|
682
|
+
function lesser(raw: Int32Array, a: number, b: number): boolean {
|
|
683
|
+
const ax = raw[a * 2] as number;
|
|
684
|
+
const bx = raw[b * 2] as number;
|
|
685
|
+
return ax < bx || (ax === bx && (raw[a * 2 + 1] as number) < (raw[b * 2 + 1] as number));
|
|
686
|
+
}
|
|
621
687
|
|
|
688
|
+
/** Keep the furthest point of `order` between `lo` and `hi` while it is further than the bound. */
|
|
689
|
+
function douglasPeucker(
|
|
690
|
+
raw: Int32Array,
|
|
691
|
+
order: readonly number[],
|
|
692
|
+
lo: number,
|
|
693
|
+
hi: number,
|
|
694
|
+
maxDeviation: number,
|
|
695
|
+
keep: Uint8Array,
|
|
696
|
+
): void {
|
|
697
|
+
if (hi - lo < 2) return;
|
|
698
|
+
const a = order[lo] as number;
|
|
699
|
+
const b = order[hi] as number;
|
|
622
700
|
let worst = -1;
|
|
623
701
|
let worstAt = -1;
|
|
624
|
-
for (let at =
|
|
702
|
+
for (let at = lo + 1; at < hi; at += 1) {
|
|
703
|
+
const point = order[at] as number;
|
|
625
704
|
const d = distanceToSegment(
|
|
626
|
-
raw[
|
|
627
|
-
raw[
|
|
628
|
-
raw[
|
|
629
|
-
raw[
|
|
630
|
-
raw[
|
|
631
|
-
raw[
|
|
705
|
+
raw[point * 2] as number,
|
|
706
|
+
raw[point * 2 + 1] as number,
|
|
707
|
+
raw[a * 2] as number,
|
|
708
|
+
raw[a * 2 + 1] as number,
|
|
709
|
+
raw[b * 2] as number,
|
|
710
|
+
raw[b * 2 + 1] as number,
|
|
632
711
|
);
|
|
633
712
|
if (d > worst) {
|
|
634
713
|
worst = d;
|
|
@@ -636,7 +715,7 @@ function chain(
|
|
|
636
715
|
}
|
|
637
716
|
}
|
|
638
717
|
if (worst <= maxDeviation || worstAt < 0) return;
|
|
639
|
-
keep[worstAt] = 1;
|
|
640
|
-
|
|
641
|
-
|
|
718
|
+
keep[order[worstAt] as number] = 1;
|
|
719
|
+
douglasPeucker(raw, order, lo, worstAt, maxDeviation, keep);
|
|
720
|
+
douglasPeucker(raw, order, worstAt, hi, maxDeviation, keep);
|
|
642
721
|
}
|
package/src/voxelise.ts
CHANGED
|
@@ -36,6 +36,16 @@ export interface VoxeliseSettings {
|
|
|
36
36
|
readonly agentHeight: number;
|
|
37
37
|
/** How far from an edge an agent's centre must stay. */
|
|
38
38
|
readonly agentRadius: number;
|
|
39
|
+
/**
|
|
40
|
+
* How many cells of height an agent can step across, the number `buildRegions` takes; 1 when
|
|
41
|
+
* absent. A neighbour further above or below than this is an edge, and erosion keeps the agent's
|
|
42
|
+
* radius from it.
|
|
43
|
+
*
|
|
44
|
+
* **Here as well as there because the polygons are flat.** Two regions whose edges touch are
|
|
45
|
+
* joined by a portal whatever their heights, so a floor and a ledge above it must be kept apart
|
|
46
|
+
* by erosion, or a route goes over the ledge.
|
|
47
|
+
*/
|
|
48
|
+
readonly maxStep?: number;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
/** Words per span: floor cell, ceiling cell, walkable. */
|
|
@@ -103,12 +113,21 @@ export function spanWalkable(field: VoxelField, x: number, z: number, at: number
|
|
|
103
113
|
return field.spans[base + 2] === 1;
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* One rasterised sample before the columns are packed: a solid from `bottom` to `top`, in cells,
|
|
118
|
+
* and whether its top can be stood on. A floor is a solid of no height.
|
|
119
|
+
*/
|
|
107
120
|
interface Sample {
|
|
108
|
-
|
|
121
|
+
bottom: number;
|
|
122
|
+
top: number;
|
|
109
123
|
walkable: boolean;
|
|
110
124
|
}
|
|
111
125
|
|
|
126
|
+
/** Scratch for clipping a triangle to one cell: four planes leave at most seven vertices. */
|
|
127
|
+
const CLIP_IN = new Float64Array(8 * 3);
|
|
128
|
+
const CLIP_OUT = new Float64Array(8 * 3);
|
|
129
|
+
const HEIGHTS = new Float64Array(2);
|
|
130
|
+
|
|
112
131
|
/**
|
|
113
132
|
* Rasterise, then decide.
|
|
114
133
|
*
|
|
@@ -172,12 +191,33 @@ export function voxeliseWalkable(geometry: NavGeometry, settings: VoxeliseSettin
|
|
|
172
191
|
|
|
173
192
|
for (let gz = loZ; gz <= hiZ; gz += 1) {
|
|
174
193
|
for (let gx = loX; gx <= hiX; gx += 1) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
194
|
+
if (walkable) {
|
|
195
|
+
const px = minX + (gx + 0.5) * settings.cellSize;
|
|
196
|
+
const pz = minZ + (gz + 0.5) * settings.cellSize;
|
|
197
|
+
const height = heightAt(ax, ay, az, bx, by, bz, cx, cy, cz, px, pz);
|
|
198
|
+
if (height === null) continue;
|
|
199
|
+
const floor = Math.floor((height - minY) / settings.cellHeight);
|
|
200
|
+
columns[gx + gz * width]?.push({ bottom: floor, top: floor, walkable });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
/*
|
|
204
|
+
* **A face too steep to stand on is a solid, over every cell it crosses.** Sampled at the
|
|
205
|
+
* cell's centre like a floor, a wall is never met at all: it is vertical, so it covers no
|
|
206
|
+
* area seen from above, and a building on the ground read as a roof over open floor that a
|
|
207
|
+
* route went straight through. So the face is clipped to the cell's square, and the heights
|
|
208
|
+
* it spans there stand in the column as one solid, whose top is not walkable.
|
|
209
|
+
*/
|
|
210
|
+
const x0 = minX + gx * settings.cellSize;
|
|
211
|
+
const z0 = minZ + gz * settings.cellSize;
|
|
212
|
+
CLIP_IN.set([ax, ay, az, bx, by, bz, cx, cy, cz]);
|
|
213
|
+
if (!clipToCell(x0, x0 + settings.cellSize, z0, z0 + settings.cellSize, HEIGHTS)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
columns[gx + gz * width]?.push({
|
|
217
|
+
bottom: Math.floor(((HEIGHTS[0] as number) - minY) / settings.cellHeight),
|
|
218
|
+
top: Math.floor(((HEIGHTS[1] as number) - minY) / settings.cellHeight),
|
|
219
|
+
walkable: false,
|
|
220
|
+
});
|
|
181
221
|
}
|
|
182
222
|
}
|
|
183
223
|
}
|
|
@@ -185,6 +225,64 @@ export function voxeliseWalkable(geometry: NavGeometry, settings: VoxeliseSettin
|
|
|
185
225
|
return pack(columns, width, depth, origin, settings);
|
|
186
226
|
}
|
|
187
227
|
|
|
228
|
+
/**
|
|
229
|
+
* The lowest and highest heights of the triangle in `CLIP_IN` within one cell's square, written to
|
|
230
|
+
* `out`; false where the triangle misses the cell.
|
|
231
|
+
*
|
|
232
|
+
* Sutherland–Hodgman against the square's four sides. Inclusive, so a wall lying exactly on a cell
|
|
233
|
+
* boundary still lands in the cell the bounding box chose for it.
|
|
234
|
+
*/
|
|
235
|
+
function clipToCell(x0: number, x1: number, z0: number, z1: number, out: Float64Array): boolean {
|
|
236
|
+
let count = 3;
|
|
237
|
+
count = clipSide(CLIP_IN, count, CLIP_OUT, 0, x0, 1);
|
|
238
|
+
count = clipSide(CLIP_OUT, count, CLIP_IN, 0, x1, -1);
|
|
239
|
+
count = clipSide(CLIP_IN, count, CLIP_OUT, 2, z0, 1);
|
|
240
|
+
count = clipSide(CLIP_OUT, count, CLIP_IN, 2, z1, -1);
|
|
241
|
+
if (count === 0) return false;
|
|
242
|
+
let low = Infinity;
|
|
243
|
+
let high = -Infinity;
|
|
244
|
+
for (let at = 0; at < count; at += 1) {
|
|
245
|
+
const y = CLIP_IN[at * 3 + 1] as number;
|
|
246
|
+
low = Math.min(low, y);
|
|
247
|
+
high = Math.max(high, y);
|
|
248
|
+
}
|
|
249
|
+
out[0] = low;
|
|
250
|
+
out[1] = high;
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Keep the part of a polygon on one side of `axis = bound`: above it for `side` 1, below for -1. */
|
|
255
|
+
function clipSide(
|
|
256
|
+
from: Float64Array,
|
|
257
|
+
count: number,
|
|
258
|
+
to: Float64Array,
|
|
259
|
+
axis: number,
|
|
260
|
+
bound: number,
|
|
261
|
+
side: number,
|
|
262
|
+
): number {
|
|
263
|
+
let written = 0;
|
|
264
|
+
for (let at = 0; at < count; at += 1) {
|
|
265
|
+
const next = (at + 1) % count;
|
|
266
|
+
const here = ((from[at * 3 + axis] as number) - bound) * side;
|
|
267
|
+
const there = ((from[next * 3 + axis] as number) - bound) * side;
|
|
268
|
+
if (here >= 0) {
|
|
269
|
+
to[written * 3] = from[at * 3] as number;
|
|
270
|
+
to[written * 3 + 1] = from[at * 3 + 1] as number;
|
|
271
|
+
to[written * 3 + 2] = from[at * 3 + 2] as number;
|
|
272
|
+
written += 1;
|
|
273
|
+
}
|
|
274
|
+
if (here >= 0 !== there >= 0) {
|
|
275
|
+
const t = here / (here - there);
|
|
276
|
+
for (let k = 0; k < 3; k += 1) {
|
|
277
|
+
const a = from[at * 3 + k] as number;
|
|
278
|
+
to[written * 3 + k] = a + ((from[next * 3 + k] as number) - a) * t;
|
|
279
|
+
}
|
|
280
|
+
written += 1;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return written;
|
|
284
|
+
}
|
|
285
|
+
|
|
188
286
|
/**
|
|
189
287
|
* The triangle's height at (px, pz), or null where the point is outside it.
|
|
190
288
|
*
|
|
@@ -232,20 +330,34 @@ function pack(
|
|
|
232
330
|
const merged: number[][] = [];
|
|
233
331
|
|
|
234
332
|
for (const samples of columns) {
|
|
235
|
-
samples.sort(
|
|
333
|
+
samples.sort(
|
|
334
|
+
(a, b) => a.bottom - b.bottom || a.top - b.top || Number(a.walkable) - Number(b.walkable),
|
|
335
|
+
);
|
|
236
336
|
const spans: number[] = [];
|
|
337
|
+
/* Where each span's solid starts, which is the ceiling of the span below it. */
|
|
338
|
+
const bottoms: number[] = [];
|
|
237
339
|
for (const sample of samples) {
|
|
238
340
|
const last = spans.length - SPAN_STRIDE;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
341
|
+
const top = spans[last] as number;
|
|
342
|
+
/*
|
|
343
|
+
* A sample starting within one cell of a span's top is part of that solid, not a new one.
|
|
344
|
+
* The span stands on the highest top, and is walkable if anything ending within a cell of
|
|
345
|
+
* that top is: a wall rising past a floor takes the floor over, and a floor's two triangles
|
|
346
|
+
* at one height agree.
|
|
347
|
+
*/
|
|
348
|
+
if (last >= 0 && sample.bottom - top <= 1) {
|
|
349
|
+
const flag = sample.walkable ? 1 : 0;
|
|
350
|
+
if (sample.top > top + 1) spans[last + 2] = flag;
|
|
351
|
+
else if (sample.top >= top - 1) spans[last + 2] = (spans[last + 2] as number) | flag;
|
|
352
|
+
spans[last] = Math.max(top, sample.top);
|
|
242
353
|
continue;
|
|
243
354
|
}
|
|
244
|
-
spans.push(sample.
|
|
355
|
+
spans.push(sample.top, Number.MAX_SAFE_INTEGER, sample.walkable ? 1 : 0);
|
|
356
|
+
bottoms.push(sample.bottom);
|
|
245
357
|
}
|
|
246
|
-
/* A span's ceiling is the next
|
|
247
|
-
for (let at = 0; at + SPAN_STRIDE < spans.length; at += SPAN_STRIDE) {
|
|
248
|
-
spans[at + 1] =
|
|
358
|
+
/* A span's ceiling is where the next solid starts. The topmost has open sky. */
|
|
359
|
+
for (let at = 0, next = 1; at + SPAN_STRIDE < spans.length; at += SPAN_STRIDE, next += 1) {
|
|
360
|
+
spans[at + 1] = bottoms[next] as number;
|
|
249
361
|
if ((spans[at + 1] as number) - (spans[at] as number) < headroom) spans[at + 2] = 0;
|
|
250
362
|
}
|
|
251
363
|
merged.push(spans);
|
|
@@ -288,6 +400,12 @@ function pack(
|
|
|
288
400
|
* doing it as that many single-cell passes keeps the whole thing integer arithmetic with no
|
|
289
401
|
* distance to round. **Copied per pass**, because eroding in place erodes into what the same pass
|
|
290
402
|
* just cleared and eats the mesh from its edges inward.
|
|
403
|
+
*
|
|
404
|
+
* **A neighbour is judged at this span's height, not anywhere in its column.** Asked whether a
|
|
405
|
+
* neighbouring column had any walkable span at all, a wall answered yes, with the roof on top of
|
|
406
|
+
* it, and the floor beside the wall kept no margin. A neighbour counts as open when it has a
|
|
407
|
+
* walkable span within `maxStep` of this one, a step the agent can take. Further than that is a
|
|
408
|
+
* wall, a ledge or a drop, and the agent keeps its radius from it.
|
|
291
409
|
*/
|
|
292
410
|
function erode(
|
|
293
411
|
columns: number[][],
|
|
@@ -296,22 +414,34 @@ function erode(
|
|
|
296
414
|
settings: VoxeliseSettings,
|
|
297
415
|
): void {
|
|
298
416
|
const steps = Math.ceil(settings.agentRadius / settings.cellSize);
|
|
417
|
+
const reach = settings.maxStep ?? 1;
|
|
299
418
|
for (let pass = 0; pass < steps; pass += 1) {
|
|
300
419
|
const before = columns.map((column) => [...column]);
|
|
301
|
-
const open = (gx: number, gz: number): boolean => {
|
|
420
|
+
const open = (gx: number, gz: number, floor: number): boolean => {
|
|
302
421
|
if (gx < 0 || gz < 0 || gx >= width || gz >= depth) return false;
|
|
303
422
|
const column = before[gx + gz * width] as number[];
|
|
304
423
|
for (let at = 0; at < column.length; at += SPAN_STRIDE) {
|
|
305
|
-
if (column[at + 2] === 1) return true;
|
|
424
|
+
if (column[at + 2] === 1 && Math.abs((column[at] as number) - floor) <= reach) return true;
|
|
306
425
|
}
|
|
307
426
|
return false;
|
|
308
427
|
};
|
|
309
428
|
|
|
310
429
|
for (let gz = 0; gz < depth; gz += 1) {
|
|
311
430
|
for (let gx = 0; gx < width; gx += 1) {
|
|
312
|
-
if (open(gx - 1, gz) && open(gx + 1, gz) && open(gx, gz - 1) && open(gx, gz + 1)) continue;
|
|
313
431
|
const column = columns[gx + gz * width] as number[];
|
|
314
|
-
for (let at = 0; at < column.length; at += SPAN_STRIDE)
|
|
432
|
+
for (let at = 0; at < column.length; at += SPAN_STRIDE) {
|
|
433
|
+
if (column[at + 2] !== 1) continue;
|
|
434
|
+
const floor = column[at] as number;
|
|
435
|
+
if (
|
|
436
|
+
open(gx - 1, gz, floor) &&
|
|
437
|
+
open(gx + 1, gz, floor) &&
|
|
438
|
+
open(gx, gz - 1, floor) &&
|
|
439
|
+
open(gx, gz + 1, floor)
|
|
440
|
+
) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
column[at + 2] = 0;
|
|
444
|
+
}
|
|
315
445
|
}
|
|
316
446
|
}
|
|
317
447
|
}
|