@aethervtt/shard-nav 0.0.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/agents.d.ts +48 -0
- package/dist/agents.js +703 -0
- package/dist/bake.d.ts +26 -0
- package/dist/bake.js +710 -0
- package/dist/cache.d.ts +31 -0
- package/dist/cache.js +90 -0
- package/dist/components.d.ts +88 -0
- package/dist/components.js +279 -0
- package/dist/geometry.d.ts +33 -0
- package/dist/geometry.js +176 -0
- package/dist/grid.d.ts +96 -0
- package/dist/grid.js +463 -0
- package/dist/hash.d.ts +20 -0
- package/dist/hash.js +61 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +105 -0
- package/dist/methods.d.ts +97 -0
- package/dist/methods.js +179 -0
- package/dist/navmesh.d.ts +101 -0
- package/dist/navmesh.js +194 -0
- package/dist/overlay.d.ts +1 -0
- package/dist/overlay.js +166 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.js +79 -0
- package/dist/query.d.ts +83 -0
- package/dist/query.js +391 -0
- package/dist/recast.d.ts +39 -0
- package/dist/recast.js +161 -0
- package/dist/state.d.ts +119 -0
- package/dist/state.js +49 -0
- package/dist/test-level.d.ts +19 -0
- package/dist/test-level.js +55 -0
- package/dist/test-maze.d.ts +8 -0
- package/dist/test-maze.js +56 -0
- package/package.json +42 -0
package/dist/bake.js
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import { assetServer } from "@aethervtt/shard-assets";
|
|
2
|
+
import {
|
|
3
|
+
affine64,
|
|
4
|
+
Children,
|
|
5
|
+
defineSystem,
|
|
6
|
+
ShardError
|
|
7
|
+
} from "@aethervtt/shard-core";
|
|
8
|
+
import { BODY_KINDS, Collider, Physics } from "@aethervtt/shard-physics";
|
|
9
|
+
import { Mesh3d, Meshes } from "@aethervtt/shard-render";
|
|
10
|
+
import { LogResource } from "@aethervtt/shard-runtime";
|
|
11
|
+
import { Tilemap, TilemapDatas } from "@aethervtt/shard-sprite";
|
|
12
|
+
import { GlobalTransform } from "@aethervtt/shard-transform";
|
|
13
|
+
import { NavCache } from "./cache.js";
|
|
14
|
+
import { NavAreas, NavGrid, NavGridDatas, NavMesh, NavSource, OffMeshLink } from "./components.js";
|
|
15
|
+
import {
|
|
16
|
+
boxTriangles,
|
|
17
|
+
capsuleProfile,
|
|
18
|
+
heightfieldTriangles,
|
|
19
|
+
latheTriangles,
|
|
20
|
+
pushMesh,
|
|
21
|
+
pushTransformed,
|
|
22
|
+
sphereProfile
|
|
23
|
+
} from "./geometry.js";
|
|
24
|
+
import { DIAGONAL_MODES, NavGridData } from "./grid.js";
|
|
25
|
+
import { Hash64 } from "./hash.js";
|
|
26
|
+
import { NavMeshRuntime, packTile } from "./navmesh.js";
|
|
27
|
+
import { buildTile, voxelSettings } from "./recast.js";
|
|
28
|
+
import { Nav } from "./state.js";
|
|
29
|
+
const BAKE_FORMAT = "recast-navigation@0.43.1/shard-1";
|
|
30
|
+
function queries(world) {
|
|
31
|
+
return {
|
|
32
|
+
grids: world.query({ with: [NavGrid, GlobalTransform] }),
|
|
33
|
+
meshes: world.query({ with: [NavMesh] }),
|
|
34
|
+
sources: world.query({ with: [NavSource] }),
|
|
35
|
+
links: world.query({ with: [OffMeshLink] }),
|
|
36
|
+
colliders: world.query({ with: [Collider] })
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function updateNavigation(world, since, s = queries(world)) {
|
|
40
|
+
const nav = world.tryResource(Nav);
|
|
41
|
+
if (!nav) return;
|
|
42
|
+
let count = 0;
|
|
43
|
+
for (let t = 0; t < s.colliders.tables.length; t++) {
|
|
44
|
+
const table = s.colliders.tables[t];
|
|
45
|
+
count += table.count;
|
|
46
|
+
if (table.lastChanged(Collider) > since) nav.colliderTick = world.tick;
|
|
47
|
+
}
|
|
48
|
+
if (count !== nav.colliderCount) {
|
|
49
|
+
nav.colliderCount = count;
|
|
50
|
+
nav.colliderTick = world.tick;
|
|
51
|
+
}
|
|
52
|
+
updateGrids(s, world, nav, since, nav.colliderTick);
|
|
53
|
+
updateMeshes(s, world, nav, since);
|
|
54
|
+
}
|
|
55
|
+
const navBake = defineSystem({
|
|
56
|
+
name: "nav/bake",
|
|
57
|
+
description: "Builds NavGrids from data, tilemaps, or colliders, and rebakes NavMesh tiles whose NavSource geometry or OffMeshLinks changed (cached tiles load without Recast).",
|
|
58
|
+
setup: queries,
|
|
59
|
+
run: (s, world, ctx) => updateNavigation(world, ctx.lastRunTick, s)
|
|
60
|
+
});
|
|
61
|
+
function newGridRecord(entity) {
|
|
62
|
+
return {
|
|
63
|
+
entity,
|
|
64
|
+
source: "data",
|
|
65
|
+
grid: null,
|
|
66
|
+
ox: 0,
|
|
67
|
+
oy: 0,
|
|
68
|
+
csx: 1,
|
|
69
|
+
csy: 1,
|
|
70
|
+
z: 0,
|
|
71
|
+
diagonal: 0,
|
|
72
|
+
problem: null,
|
|
73
|
+
stamp: "",
|
|
74
|
+
layer: null,
|
|
75
|
+
editBase: -1,
|
|
76
|
+
editCursor: 0,
|
|
77
|
+
tilemapVersion: -1,
|
|
78
|
+
blocking: null,
|
|
79
|
+
colliderStamp: "",
|
|
80
|
+
version: 0,
|
|
81
|
+
seen: -1
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function updateGrids(s, world, nav, since, colliderTick) {
|
|
85
|
+
for (let t = 0; t < s.grids.tables.length; t++) {
|
|
86
|
+
const table = s.grids.tables[t];
|
|
87
|
+
const ticks = table.changedTicks(NavGrid);
|
|
88
|
+
const matrix = table.column(GlobalTransform, "matrix");
|
|
89
|
+
for (let row = 0; row < table.count; row++) {
|
|
90
|
+
const entity = table.entities[row];
|
|
91
|
+
let rec = nav.grids.get(entity);
|
|
92
|
+
if (!rec) {
|
|
93
|
+
rec = newGridRecord(entity);
|
|
94
|
+
nav.grids.set(entity, rec);
|
|
95
|
+
nav.gridList = [...nav.grids.values()];
|
|
96
|
+
}
|
|
97
|
+
const x = matrix[row * 12 + 3];
|
|
98
|
+
const y = matrix[row * 12 + 7];
|
|
99
|
+
rec.z = matrix[row * 12 + 11];
|
|
100
|
+
if (ticks[row] > since || rec.stamp === "") refreshGrid(world, rec, x, y, colliderTick);
|
|
101
|
+
else followGrid(world, rec, x, y, colliderTick);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (nav.grids.size > 0 && nav.grids.size !== s.grids.count()) {
|
|
105
|
+
for (const entity of nav.grids.keys()) {
|
|
106
|
+
if (!world.has(entity, NavGrid)) nav.grids.delete(entity);
|
|
107
|
+
}
|
|
108
|
+
nav.gridList = [...nav.grids.values()];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function refreshGrid(world, rec, x, y, colliderTick) {
|
|
112
|
+
const v = world.get(rec.entity, NavGrid);
|
|
113
|
+
const stamp = JSON.stringify([
|
|
114
|
+
v.source,
|
|
115
|
+
v.width,
|
|
116
|
+
v.height,
|
|
117
|
+
v.cellSize,
|
|
118
|
+
v.origin,
|
|
119
|
+
v.tilemap,
|
|
120
|
+
v.layer,
|
|
121
|
+
v.blockingTiles,
|
|
122
|
+
v.data?.guid ?? v.data?.path ?? null,
|
|
123
|
+
v.mask
|
|
124
|
+
]);
|
|
125
|
+
rec.diagonal = DIAGONAL_MODES.indexOf(v.diagonal);
|
|
126
|
+
if (stamp !== rec.stamp) {
|
|
127
|
+
rec.stamp = stamp;
|
|
128
|
+
rec.source = v.source;
|
|
129
|
+
rec.layer = null;
|
|
130
|
+
rec.editBase = -1;
|
|
131
|
+
rec.tilemapVersion = -1;
|
|
132
|
+
rec.colliderStamp = "";
|
|
133
|
+
rec.seen = -1;
|
|
134
|
+
rec.blocking = null;
|
|
135
|
+
if (v.source === "tilemap" && v.blockingTiles.length > 0) {
|
|
136
|
+
rec.blocking = new Uint8Array(65536);
|
|
137
|
+
for (const tile of v.blockingTiles) rec.blocking[tile] = 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
followGrid(world, rec, x, y, colliderTick);
|
|
141
|
+
}
|
|
142
|
+
function followGrid(world, rec, x, y, colliderTick) {
|
|
143
|
+
const entity = rec.entity;
|
|
144
|
+
if (rec.source === "data") {
|
|
145
|
+
const v2 = world.get(entity, NavGrid);
|
|
146
|
+
rec.ox = x + v2.origin[0];
|
|
147
|
+
rec.oy = y + v2.origin[1];
|
|
148
|
+
rec.csx = v2.cellSize[0];
|
|
149
|
+
rec.csy = v2.cellSize[1];
|
|
150
|
+
const grid2 = v2.data ? world.tryResource(NavGridDatas)?.get(v2.data) : void 0;
|
|
151
|
+
if (!grid2) {
|
|
152
|
+
rec.grid = null;
|
|
153
|
+
rec.problem = v2.data ? `waiting for ${v2.data.path ?? v2.data.guid}` : "no data asset";
|
|
154
|
+
if (v2.data) requestAsset(world, v2.data);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
rec.problem = null;
|
|
158
|
+
if (rec.grid !== grid2 || rec.seen !== grid2.version) {
|
|
159
|
+
rec.grid = grid2;
|
|
160
|
+
rec.seen = grid2.version;
|
|
161
|
+
rec.version++;
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (rec.source === "tilemap") {
|
|
166
|
+
followTilemap(world, rec);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const v = world.get(entity, NavGrid);
|
|
170
|
+
rec.ox = x + v.origin[0];
|
|
171
|
+
rec.oy = y + v.origin[1];
|
|
172
|
+
rec.csx = v.cellSize[0];
|
|
173
|
+
rec.csy = v.cellSize[1];
|
|
174
|
+
const physics = world.tryResource(Physics);
|
|
175
|
+
if (!physics) {
|
|
176
|
+
rec.problem = "no physics: add physics2d (or physics3d) to rasterize colliders";
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const stamp = `${colliderTick}:${x}:${y}`;
|
|
180
|
+
if (stamp === rec.colliderStamp && rec.grid) return;
|
|
181
|
+
rec.colliderStamp = stamp;
|
|
182
|
+
rec.problem = null;
|
|
183
|
+
const grid = rec.grid && rec.grid.width === v.width && rec.grid.height === v.height ? rec.grid : new NavGridData(v.width, v.height);
|
|
184
|
+
const costs = grid.costs;
|
|
185
|
+
const shape = {
|
|
186
|
+
shape: "cuboid",
|
|
187
|
+
halfExtents: [rec.csx * 0.49, rec.csy * 0.49, 0.5]
|
|
188
|
+
};
|
|
189
|
+
const at = [0, 0, rec.z];
|
|
190
|
+
const rotation = [0, 0, 0, 1];
|
|
191
|
+
let blocked = false;
|
|
192
|
+
const visit = (e) => {
|
|
193
|
+
const owner = physics.colliderOwner.get(e);
|
|
194
|
+
const body = owner === void 0 ? void 0 : physics.bodies.get(owner);
|
|
195
|
+
if (body === void 0 || BODY_KINDS[body.kind] === "fixed") {
|
|
196
|
+
blocked = true;
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
};
|
|
201
|
+
for (let cy = 0; cy < v.height; cy++) {
|
|
202
|
+
for (let cx = 0; cx < v.width; cx++) {
|
|
203
|
+
at[0] = rec.ox + (cx + 0.5) * rec.csx;
|
|
204
|
+
at[1] = rec.oy + (cy + 0.5) * rec.csy;
|
|
205
|
+
blocked = false;
|
|
206
|
+
physics.overlapShape(shape, at, rotation, { mask: v.mask }, visit);
|
|
207
|
+
costs[cy * v.width + cx] = blocked ? 0 : 1;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
grid.version++;
|
|
211
|
+
rec.grid = grid;
|
|
212
|
+
rec.version++;
|
|
213
|
+
}
|
|
214
|
+
function followTilemap(world, rec) {
|
|
215
|
+
const v = world.get(rec.entity, NavGrid);
|
|
216
|
+
const tilemapEntity = v.tilemap ?? rec.entity;
|
|
217
|
+
const tilemap = world.isAlive(tilemapEntity) ? world.tryGet(tilemapEntity, Tilemap) : void 0;
|
|
218
|
+
if (!tilemap) {
|
|
219
|
+
rec.grid = null;
|
|
220
|
+
rec.problem = `entity ${tilemapEntity} has no sprite/Tilemap`;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const data = tilemap.data ? world.tryResource(TilemapDatas)?.get(tilemap.data) : void 0;
|
|
224
|
+
if (!data) {
|
|
225
|
+
rec.grid = null;
|
|
226
|
+
rec.problem = "waiting for the tilemap data";
|
|
227
|
+
if (tilemap.data) requestAsset(world, tilemap.data);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const layer = v.layer ? data.layers.find((l) => l.name === v.layer) : data.layers[0];
|
|
231
|
+
if (!layer) {
|
|
232
|
+
rec.grid = null;
|
|
233
|
+
rec.problem = `the tilemap has no layer ${JSON.stringify(v.layer)}`;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
rec.problem = null;
|
|
237
|
+
const m = world.get(tilemapEntity, GlobalTransform).matrix;
|
|
238
|
+
const sx = Math.sqrt(m[0] * m[0] + m[4] * m[4] + m[8] * m[8]);
|
|
239
|
+
const sy = Math.sqrt(m[1] * m[1] + m[5] * m[5] + m[9] * m[9]);
|
|
240
|
+
rec.csx = tilemap.tileSize[0] * sx;
|
|
241
|
+
rec.csy = tilemap.tileSize[1] * sy;
|
|
242
|
+
rec.ox = m[3];
|
|
243
|
+
rec.oy = m[7] - layer.height * rec.csy;
|
|
244
|
+
const W = layer.width;
|
|
245
|
+
const H = layer.height;
|
|
246
|
+
const blocking = rec.blocking;
|
|
247
|
+
const blocks = (tile) => blocking ? blocking[tile] === 1 : tile !== 0;
|
|
248
|
+
const full = rec.layer !== layer || rec.editBase !== layer.editBase || rec.tilemapVersion !== data.version || !rec.grid || rec.grid.width !== W || rec.grid.height !== H;
|
|
249
|
+
if (full) {
|
|
250
|
+
const grid = rec.grid && rec.grid.width === W && rec.grid.height === H ? rec.grid : new NavGridData(W, H);
|
|
251
|
+
for (let y = 0; y < H; y++) {
|
|
252
|
+
for (let x = 0; x < W; x++)
|
|
253
|
+
grid.costs[(H - 1 - y) * W + x] = blocks(layer.tiles[y * W + x]) ? 0 : 1;
|
|
254
|
+
}
|
|
255
|
+
grid.version++;
|
|
256
|
+
rec.grid = grid;
|
|
257
|
+
rec.layer = layer;
|
|
258
|
+
rec.editBase = layer.editBase;
|
|
259
|
+
rec.editCursor = layer.edits.length;
|
|
260
|
+
rec.tilemapVersion = data.version;
|
|
261
|
+
rec.version++;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (rec.editCursor < layer.edits.length) {
|
|
265
|
+
const grid = rec.grid;
|
|
266
|
+
for (let k = rec.editCursor; k < layer.edits.length; k++) {
|
|
267
|
+
const i = layer.edits[k];
|
|
268
|
+
const x = i % W;
|
|
269
|
+
const y = (i - x) / W;
|
|
270
|
+
grid.costs[(H - 1 - y) * W + x] = blocks(layer.tiles[i]) ? 0 : 1;
|
|
271
|
+
}
|
|
272
|
+
rec.editCursor = layer.edits.length;
|
|
273
|
+
grid.version++;
|
|
274
|
+
rec.version++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function requestAsset(world, ref) {
|
|
278
|
+
try {
|
|
279
|
+
const server = assetServer(world);
|
|
280
|
+
const entry = server.entry(ref);
|
|
281
|
+
if (entry && entry.state === "unloaded") void server.request(entry.guid).catch(() => {
|
|
282
|
+
});
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const hash = new Hash64();
|
|
287
|
+
function areasSignature(areas) {
|
|
288
|
+
if (!areas) return -1;
|
|
289
|
+
let sig = 0;
|
|
290
|
+
for (const key in areas) {
|
|
291
|
+
const code = Math.imul((Number(key) | 0) + 1, 2654435761);
|
|
292
|
+
sig = sig + Math.imul(code ^ Math.round(areas[key] * 1e3), 2246822507) | 0;
|
|
293
|
+
}
|
|
294
|
+
return sig;
|
|
295
|
+
}
|
|
296
|
+
function settingsOf(table, row) {
|
|
297
|
+
return {
|
|
298
|
+
agentRadius: table.column(NavMesh, "agentRadius")[row],
|
|
299
|
+
agentHeight: table.column(NavMesh, "agentHeight")[row],
|
|
300
|
+
maxClimb: table.column(NavMesh, "maxClimb")[row],
|
|
301
|
+
maxSlope: table.column(NavMesh, "maxSlope")[row],
|
|
302
|
+
cellSize: table.column(NavMesh, "cellSize")[row],
|
|
303
|
+
cellHeight: table.column(NavMesh, "cellHeight")[row],
|
|
304
|
+
tileSize: table.column(NavMesh, "tileSize")[row]
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function sameSettings(a, b) {
|
|
308
|
+
return a.agentRadius === b.agentRadius && a.agentHeight === b.agentHeight && a.maxClimb === b.maxClimb && a.maxSlope === b.maxSlope && a.cellSize === b.cellSize && a.cellHeight === b.cellHeight && a.tileSize === b.tileSize;
|
|
309
|
+
}
|
|
310
|
+
const NO_RECAST = "nav/no-navmesh: Recast is not loaded; add the nav plugin (nav/grid only does grids)";
|
|
311
|
+
function updateMeshes(s, world, nav, since) {
|
|
312
|
+
const R = nav.R;
|
|
313
|
+
if (!R) return;
|
|
314
|
+
let changed = false;
|
|
315
|
+
for (let t = 0; t < s.meshes.tables.length; t++) {
|
|
316
|
+
const table = s.meshes.tables[t];
|
|
317
|
+
const ticks = table.changedTicks(NavMesh);
|
|
318
|
+
for (let row = 0; row < table.count; row++) {
|
|
319
|
+
const entity = table.entities[row];
|
|
320
|
+
const rt = nav.meshes.get(entity);
|
|
321
|
+
if (rt && ticks[row] <= since) continue;
|
|
322
|
+
const settings = settingsOf(table, row);
|
|
323
|
+
if (rt && sameSettings(rt.settings, settings)) {
|
|
324
|
+
changed = true;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (rt) {
|
|
328
|
+
rt.destroy();
|
|
329
|
+
for (const a of nav.agents.values()) if (a.nav === entity) a.crowdIndex = -1;
|
|
330
|
+
}
|
|
331
|
+
nav.meshes.set(entity, new NavMeshRuntime(R, entity, settings));
|
|
332
|
+
nav.meshList = [...nav.meshes.values()];
|
|
333
|
+
nav.areas = Number.NaN;
|
|
334
|
+
changed = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (nav.meshes.size > 0 && nav.meshes.size !== s.meshes.count()) {
|
|
338
|
+
for (const [entity, rt] of nav.meshes) {
|
|
339
|
+
if (!world.has(entity, NavMesh)) {
|
|
340
|
+
rt.destroy();
|
|
341
|
+
nav.meshes.delete(entity);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
nav.meshList = [...nav.meshes.values()];
|
|
345
|
+
}
|
|
346
|
+
if (nav.meshes.size === 0) return;
|
|
347
|
+
for (let i = 0; i < nav.meshList.length; i++) {
|
|
348
|
+
const rt = nav.meshList[i];
|
|
349
|
+
const frame = world.get(rt.entity, NavMesh).frame;
|
|
350
|
+
if (updateFrame(world, rt, frame)) changed = true;
|
|
351
|
+
}
|
|
352
|
+
const areas = world.tryResource(NavAreas);
|
|
353
|
+
const signature = areasSignature(areas);
|
|
354
|
+
if (nav.areas !== signature) {
|
|
355
|
+
nav.areas = signature;
|
|
356
|
+
for (let i = 0; i < nav.meshList.length; i++) nav.meshList[i].applyAreas(areas ?? {});
|
|
357
|
+
}
|
|
358
|
+
let pending = false;
|
|
359
|
+
for (let i = 0; i < nav.meshList.length; i++) if (nav.meshList[i].pending > 0) pending = true;
|
|
360
|
+
if (!nav.dirty && !changed && !pending && !watchedChanged(world, nav, since)) return;
|
|
361
|
+
gather(s, world, nav);
|
|
362
|
+
nav.dirty = false;
|
|
363
|
+
for (const rt of nav.meshes.values()) {
|
|
364
|
+
try {
|
|
365
|
+
bakeRuntime(world, nav, rt);
|
|
366
|
+
rt.problem = null;
|
|
367
|
+
} catch (err) {
|
|
368
|
+
rt.problem = err instanceof Error ? err.message : String(err);
|
|
369
|
+
world.tryResource(LogResource)?.error(err);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function updateFrame(world, rt, frame) {
|
|
374
|
+
const alive = frame !== null && frame >= 0 && world.isAlive(frame) && world.has(frame, GlobalTransform);
|
|
375
|
+
if (!alive) {
|
|
376
|
+
if (rt.frame === null) return false;
|
|
377
|
+
rt.frame = null;
|
|
378
|
+
rt.toLocal.set([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]);
|
|
379
|
+
rt.toWorld.set([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]);
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
const m = world.get(frame, GlobalTransform).matrix;
|
|
383
|
+
let same = rt.frame === frame;
|
|
384
|
+
for (let i = 0; i < 12 && same; i++) if (rt.toWorld[i] !== m[i]) same = false;
|
|
385
|
+
if (same) return false;
|
|
386
|
+
rt.frame = frame;
|
|
387
|
+
for (let i = 0; i < 12; i++) rt.toWorld[i] = m[i];
|
|
388
|
+
if (!affine64.invert(rt.toLocal, rt.toWorld)) rt.toLocal.set([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]);
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
function localize(nav, rt) {
|
|
392
|
+
const src = nav.soup;
|
|
393
|
+
const soup = rt.local;
|
|
394
|
+
soup.clear();
|
|
395
|
+
const m = rt.toLocal;
|
|
396
|
+
const p = src.positions;
|
|
397
|
+
const q = new Float64Array(9);
|
|
398
|
+
for (let t = 0; t < src.count; t++) {
|
|
399
|
+
for (let k = 0; k < 3; k++) {
|
|
400
|
+
const o = t * 9 + k * 3;
|
|
401
|
+
const x = p[o];
|
|
402
|
+
const y = p[o + 1];
|
|
403
|
+
const z = p[o + 2];
|
|
404
|
+
q[k * 3] = m[0] * x + m[1] * y + m[2] * z + m[3];
|
|
405
|
+
q[k * 3 + 1] = m[4] * x + m[5] * y + m[6] * z + m[7];
|
|
406
|
+
q[k * 3 + 2] = m[8] * x + m[9] * y + m[10] * z + m[11];
|
|
407
|
+
}
|
|
408
|
+
soup.push(q[0], q[1], q[2], q[3], q[4], q[5], q[6], q[7], q[8], src.areas[t]);
|
|
409
|
+
}
|
|
410
|
+
if (rt.localHashes.length < soup.count * 2) rt.localHashes = new Uint32Array(soup.count * 2);
|
|
411
|
+
const lp = soup.positions;
|
|
412
|
+
for (let t = 0; t < soup.count; t++) {
|
|
413
|
+
hash.reset();
|
|
414
|
+
for (let k = 0; k < 9; k++) hash.f32word(lp[t * 9 + k]);
|
|
415
|
+
hash.u32word(soup.areas[t]);
|
|
416
|
+
hash.lanes(rt.localHashes, t * 2);
|
|
417
|
+
}
|
|
418
|
+
const out = new Float64Array(3);
|
|
419
|
+
const links = nav.links.map((l) => {
|
|
420
|
+
rt.pointIn(l.start[0], l.start[1], l.start[2], out);
|
|
421
|
+
const start = [out[0], out[1], out[2]];
|
|
422
|
+
rt.pointIn(l.end[0], l.end[1], l.end[2], out);
|
|
423
|
+
return { ...l, start, end: [out[0], out[1], out[2]] };
|
|
424
|
+
});
|
|
425
|
+
return { soup, lanes: rt.localHashes, links };
|
|
426
|
+
}
|
|
427
|
+
function watchedChanged(world, nav, since) {
|
|
428
|
+
const list = nav.watchedList;
|
|
429
|
+
for (let i = 0; i < list.length; i++) {
|
|
430
|
+
const e = list[i];
|
|
431
|
+
if (!world.isAlive(e)) return true;
|
|
432
|
+
const table = world.entityTable(e);
|
|
433
|
+
const row = world.entityRow(e);
|
|
434
|
+
if (table.has(GlobalTransform) && table.changedTicks(GlobalTransform)[row] > since) return true;
|
|
435
|
+
if (table.has(Collider) && table.changedTicks(Collider)[row] > since) return true;
|
|
436
|
+
if (table.has(Mesh3d) && table.changedTicks(Mesh3d)[row] > since) return true;
|
|
437
|
+
if (table.has(NavSource) && table.changedTicks(NavSource)[row] > since) return true;
|
|
438
|
+
if (table.has(OffMeshLink) && table.changedTicks(OffMeshLink)[row] > since) return true;
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
const primitives = /* @__PURE__ */ new Map();
|
|
443
|
+
function primitive(key, make) {
|
|
444
|
+
let tris = primitives.get(key);
|
|
445
|
+
if (!tris) {
|
|
446
|
+
tris = make();
|
|
447
|
+
if (primitives.size > 256) primitives.clear();
|
|
448
|
+
primitives.set(key, tris);
|
|
449
|
+
}
|
|
450
|
+
return tris;
|
|
451
|
+
}
|
|
452
|
+
function gather(s, world, nav) {
|
|
453
|
+
const soup = nav.soup;
|
|
454
|
+
soup.clear();
|
|
455
|
+
nav.watched.clear();
|
|
456
|
+
nav.skipped = [];
|
|
457
|
+
nav.links = [];
|
|
458
|
+
let pending = 0;
|
|
459
|
+
const meshes = world.tryResource(Meshes);
|
|
460
|
+
const visit = (e, area) => {
|
|
461
|
+
nav.watched.add(e);
|
|
462
|
+
const m = world.get(e, GlobalTransform).matrix;
|
|
463
|
+
if (world.has(e, Collider)) {
|
|
464
|
+
const c = world.get(e, Collider);
|
|
465
|
+
if (!c.sensor) {
|
|
466
|
+
const [hx, hy, hz] = c.halfExtents;
|
|
467
|
+
const r = c.radius;
|
|
468
|
+
const hh = c.halfHeight;
|
|
469
|
+
let tris = null;
|
|
470
|
+
switch (c.shape) {
|
|
471
|
+
case "ball":
|
|
472
|
+
tris = primitive(`ball:${r}`, () => latheTriangles(sphereProfile(r)));
|
|
473
|
+
break;
|
|
474
|
+
case "cuboid":
|
|
475
|
+
tris = primitive(`box:${hx},${hy},${hz}`, () => boxTriangles(hx, hy, hz));
|
|
476
|
+
break;
|
|
477
|
+
case "capsule":
|
|
478
|
+
tris = primitive(`capsule:${r},${hh}`, () => latheTriangles(capsuleProfile(r, hh)));
|
|
479
|
+
break;
|
|
480
|
+
case "cylinder":
|
|
481
|
+
tris = primitive(
|
|
482
|
+
`cylinder:${r},${hh}`,
|
|
483
|
+
() => latheTriangles([
|
|
484
|
+
[0, -hh],
|
|
485
|
+
[r, -hh],
|
|
486
|
+
[r, hh],
|
|
487
|
+
[0, hh]
|
|
488
|
+
])
|
|
489
|
+
);
|
|
490
|
+
break;
|
|
491
|
+
case "cone":
|
|
492
|
+
tris = primitive(
|
|
493
|
+
`cone:${r},${hh}`,
|
|
494
|
+
() => latheTriangles([
|
|
495
|
+
[0, -hh],
|
|
496
|
+
[r, -hh],
|
|
497
|
+
[0, hh]
|
|
498
|
+
])
|
|
499
|
+
);
|
|
500
|
+
break;
|
|
501
|
+
case "heightfield":
|
|
502
|
+
tris = heightfieldTriangles(
|
|
503
|
+
c.heightfield.rows,
|
|
504
|
+
c.heightfield.cols,
|
|
505
|
+
c.heightfield.heights,
|
|
506
|
+
hx,
|
|
507
|
+
hy,
|
|
508
|
+
hz
|
|
509
|
+
);
|
|
510
|
+
break;
|
|
511
|
+
case "convex":
|
|
512
|
+
case "trimesh": {
|
|
513
|
+
const mesh = c.mesh ? meshes?.get(c.mesh) : void 0;
|
|
514
|
+
if (mesh) pushMesh(soup, mesh, m, area);
|
|
515
|
+
else if (c.mesh) {
|
|
516
|
+
pending++;
|
|
517
|
+
requestAsset(world, c.mesh);
|
|
518
|
+
} else nav.skipped.push({ entity: e, reason: `${c.shape} collider without a mesh` });
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
default:
|
|
522
|
+
nav.skipped.push({ entity: e, reason: `${c.shape} colliders are 2D` });
|
|
523
|
+
}
|
|
524
|
+
if (tris) pushTransformed(soup, tris, void 0, tris.length / 9, m, area);
|
|
525
|
+
}
|
|
526
|
+
} else if (world.has(e, Mesh3d)) {
|
|
527
|
+
const ref = world.get(e, Mesh3d).mesh;
|
|
528
|
+
const mesh = ref ? meshes?.get(ref) : void 0;
|
|
529
|
+
if (mesh) pushMesh(soup, mesh, m, area);
|
|
530
|
+
else if (ref) {
|
|
531
|
+
pending++;
|
|
532
|
+
requestAsset(world, ref);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const children = world.tryGet(e, Children)?.entities;
|
|
536
|
+
if (children) {
|
|
537
|
+
for (const child of children) {
|
|
538
|
+
if (child !== null && world.isAlive(child) && !world.has(child, NavSource))
|
|
539
|
+
visit(child, area);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
for (let t = 0; t < s.sources.tables.length; t++) {
|
|
544
|
+
const table = s.sources.tables[t];
|
|
545
|
+
const area = table.column(NavSource, "area");
|
|
546
|
+
if (!table.has(GlobalTransform)) continue;
|
|
547
|
+
for (let row = 0; row < table.count; row++) visit(table.entities[row], area[row]);
|
|
548
|
+
}
|
|
549
|
+
for (let t = 0; t < s.links.tables.length; t++) {
|
|
550
|
+
const table = s.links.tables[t];
|
|
551
|
+
for (let row = 0; row < table.count; row++) {
|
|
552
|
+
const e = table.entities[row];
|
|
553
|
+
nav.watched.add(e);
|
|
554
|
+
const link = world.get(e, OffMeshLink);
|
|
555
|
+
if (link.to === null || !world.isAlive(link.to) || !world.has(link.to, GlobalTransform)) {
|
|
556
|
+
nav.skipped.push({ entity: e, reason: "off-mesh link without a `to` entity" });
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
nav.watched.add(link.to);
|
|
560
|
+
const a = world.get(e, GlobalTransform).matrix;
|
|
561
|
+
const b = world.get(link.to, GlobalTransform).matrix;
|
|
562
|
+
nav.links.push({
|
|
563
|
+
start: [a[3], a[7], a[11]],
|
|
564
|
+
end: [b[3], b[7], b[11]],
|
|
565
|
+
radius: link.radius,
|
|
566
|
+
bidirectional: link.bidirectional,
|
|
567
|
+
area: link.area
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
nav.watchedList = [...nav.watched];
|
|
572
|
+
for (const rt of nav.meshes.values()) rt.pending = pending;
|
|
573
|
+
const n = soup.count;
|
|
574
|
+
if (nav.triHashes.length < n * 2) nav.triHashes = new Uint32Array(n * 2);
|
|
575
|
+
const p = soup.positions;
|
|
576
|
+
for (let t = 0; t < n; t++) {
|
|
577
|
+
hash.reset();
|
|
578
|
+
for (let k = 0; k < 9; k++) hash.f32word(p[t * 9 + k]);
|
|
579
|
+
hash.u32word(soup.areas[t]);
|
|
580
|
+
hash.lanes(nav.triHashes, t * 2);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function bakeRuntime(world, nav, rt) {
|
|
584
|
+
const t0 = performance.now();
|
|
585
|
+
const R = rt.R;
|
|
586
|
+
const framed = rt.frame !== null ? localize(nav, rt) : void 0;
|
|
587
|
+
const soup = framed ? framed.soup : nav.soup;
|
|
588
|
+
const allLinks = framed ? framed.links : nav.links;
|
|
589
|
+
const s = rt.settings;
|
|
590
|
+
const v = voxelSettings(s);
|
|
591
|
+
const tw = rt.tileWorld;
|
|
592
|
+
const border = v.borderSize * s.cellSize;
|
|
593
|
+
const nm = world.get(rt.entity, NavMesh);
|
|
594
|
+
const bounded = nm.boundsMin[0] < nm.boundsMax[0] || nm.boundsMin[1] < nm.boundsMax[1] || nm.boundsMin[2] < nm.boundsMax[2];
|
|
595
|
+
const [bx0, by0, bz0] = nm.boundsMin;
|
|
596
|
+
const [bx1, by1, bz1] = nm.boundsMax;
|
|
597
|
+
const tMinX = bounded ? Math.floor(bx0 / tw) : -Infinity;
|
|
598
|
+
const tMaxX = bounded ? Math.floor(bx1 / tw) : Infinity;
|
|
599
|
+
const tMinZ = bounded ? Math.floor(bz0 / tw) : -Infinity;
|
|
600
|
+
const tMaxZ = bounded ? Math.floor(bz1 / tw) : Infinity;
|
|
601
|
+
const perTile = /* @__PURE__ */ new Map();
|
|
602
|
+
const p = soup.positions;
|
|
603
|
+
for (let t = 0; t < soup.count; t++) {
|
|
604
|
+
const o = t * 9;
|
|
605
|
+
const minX = Math.min(p[o], p[o + 3], p[o + 6]);
|
|
606
|
+
const maxX = Math.max(p[o], p[o + 3], p[o + 6]);
|
|
607
|
+
const minY = Math.min(p[o + 1], p[o + 4], p[o + 7]);
|
|
608
|
+
const maxY = Math.max(p[o + 1], p[o + 4], p[o + 7]);
|
|
609
|
+
const minZ = Math.min(p[o + 2], p[o + 5], p[o + 8]);
|
|
610
|
+
const maxZ = Math.max(p[o + 2], p[o + 5], p[o + 8]);
|
|
611
|
+
if (bounded && (maxX < bx0 || minX > bx1 || maxY < by0 || minY > by1 || maxZ < bz0 || minZ > bz1))
|
|
612
|
+
continue;
|
|
613
|
+
const x0 = Math.max(tMinX, Math.floor((minX - border) / tw));
|
|
614
|
+
const x1 = Math.min(tMaxX, Math.floor((maxX + border) / tw));
|
|
615
|
+
const z0 = Math.max(tMinZ, Math.floor((minZ - border) / tw));
|
|
616
|
+
const z1 = Math.min(tMaxZ, Math.floor((maxZ + border) / tw));
|
|
617
|
+
for (let tz = z0; tz <= z1; tz++) {
|
|
618
|
+
for (let tx = x0; tx <= x1; tx++) {
|
|
619
|
+
const k = packTile(tx, tz);
|
|
620
|
+
let list = perTile.get(k);
|
|
621
|
+
if (!list) {
|
|
622
|
+
list = [];
|
|
623
|
+
perTile.set(k, list);
|
|
624
|
+
}
|
|
625
|
+
list.push(t);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const cache = world.initResource(NavCache);
|
|
630
|
+
rt.reserve(perTile.size);
|
|
631
|
+
const lanes = framed ? framed.lanes : nav.triHashes;
|
|
632
|
+
const stats = rt.stats;
|
|
633
|
+
stats.built = 0;
|
|
634
|
+
stats.cached = 0;
|
|
635
|
+
stats.kept = 0;
|
|
636
|
+
stats.removed = 0;
|
|
637
|
+
let indices = new Int32Array(64);
|
|
638
|
+
for (const [k, list] of perTile) {
|
|
639
|
+
const tx = Math.floor(k / 65536) - 32768;
|
|
640
|
+
const tz = k % 65536 - 32768;
|
|
641
|
+
let yMin = Infinity;
|
|
642
|
+
let yMax = -Infinity;
|
|
643
|
+
let sumA = 0;
|
|
644
|
+
let sumB = 0;
|
|
645
|
+
for (let i = 0; i < list.length; i++) {
|
|
646
|
+
const t = list[i];
|
|
647
|
+
const o = t * 9;
|
|
648
|
+
yMin = Math.min(yMin, p[o + 1], p[o + 4], p[o + 7]);
|
|
649
|
+
yMax = Math.max(yMax, p[o + 1], p[o + 4], p[o + 7]);
|
|
650
|
+
sumA = sumA + lanes[t * 2] >>> 0;
|
|
651
|
+
sumB = sumB + lanes[t * 2 + 1] >>> 0;
|
|
652
|
+
}
|
|
653
|
+
yMin = Math.floor(yMin) - 1;
|
|
654
|
+
yMax = Math.ceil(yMax) + 1;
|
|
655
|
+
const x0 = tx * tw;
|
|
656
|
+
const z0 = tz * tw;
|
|
657
|
+
const links = allLinks.filter(
|
|
658
|
+
(l) => l.start[0] >= x0 && l.start[0] < x0 + tw && l.start[2] >= z0 && l.start[2] < z0 + tw
|
|
659
|
+
);
|
|
660
|
+
hash.reset().string(BAKE_FORMAT);
|
|
661
|
+
hash.f32word(s.agentRadius).f32word(s.agentHeight).f32word(s.maxClimb).f32word(s.maxSlope).f32word(s.cellSize).f32word(s.cellHeight).u32word(s.tileSize).u32word(tx).u32word(tz).f32word(yMin).f32word(yMax).u32word(list.length).u32word(sumA).u32word(sumB);
|
|
662
|
+
for (const l of links) {
|
|
663
|
+
for (let i = 0; i < 3; i++) hash.f32word(l.start[i]).f32word(l.end[i]);
|
|
664
|
+
hash.f32word(l.radius).u32word(l.bidirectional ? 1 : 0).u32word(l.area);
|
|
665
|
+
}
|
|
666
|
+
const key = hash.hex();
|
|
667
|
+
const entry = rt.tiles.get(k);
|
|
668
|
+
if (entry && entry.key === key) {
|
|
669
|
+
stats.kept++;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
let bytes = cache.tiles.get(key);
|
|
673
|
+
if (bytes !== void 0) stats.cached++;
|
|
674
|
+
else {
|
|
675
|
+
if (indices.length < list.length) indices = new Int32Array(list.length * 2);
|
|
676
|
+
for (let i = 0; i < list.length; i++) indices[i] = list[i];
|
|
677
|
+
bytes = buildTile(R, s, tx, tz, yMin, yMax, soup, indices, list.length, links);
|
|
678
|
+
cache.tiles.set(key, bytes);
|
|
679
|
+
stats.built++;
|
|
680
|
+
}
|
|
681
|
+
rt.setTile(tx, tz, key, bytes);
|
|
682
|
+
}
|
|
683
|
+
for (const [k, entry] of rt.tiles) {
|
|
684
|
+
if (!perTile.has(k)) {
|
|
685
|
+
rt.removeTile(entry.tx, entry.ty);
|
|
686
|
+
stats.removed++;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
rt.updateBounds();
|
|
690
|
+
stats.bakes++;
|
|
691
|
+
stats.totalBuilt += stats.built;
|
|
692
|
+
stats.totalCached += stats.cached;
|
|
693
|
+
stats.ms = performance.now() - t0;
|
|
694
|
+
if (rt.tiles.size > 0 && rt.polygons() === 0 && rt.problem === null) {
|
|
695
|
+
throw new ShardError("nav/bake-failed", "The navmesh baked without any walkable polygons", {
|
|
696
|
+
hint: "Check the NavSource geometry has upward faces flatter than maxSlope, wide enough for agentRadius."
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function liveTileKeys(nav) {
|
|
701
|
+
const keys = [];
|
|
702
|
+
for (const rt of nav.meshes.values()) for (const t of rt.tiles.values()) keys.push(t.key);
|
|
703
|
+
return keys;
|
|
704
|
+
}
|
|
705
|
+
export {
|
|
706
|
+
NO_RECAST,
|
|
707
|
+
liveTileKeys,
|
|
708
|
+
navBake,
|
|
709
|
+
updateNavigation
|
|
710
|
+
};
|