@woosh/meep-engine 2.163.3 → 2.163.5
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/package.json +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js +163 -3
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js +95 -0
- package/src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.d.ts +9 -8
- package/src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.d.ts.map +1 -1
- package/src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.js +161 -126
- package/src/engine/navigation/mesh/build/navmesh_build_topology.d.ts.map +1 -1
- package/src/engine/navigation/mesh/build/navmesh_build_topology.js +33 -22
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"description": "Pure JavaScript game engine. Fully featured and production ready.",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"author": "Alexander Goldring",
|
|
9
|
-
"version": "2.163.
|
|
9
|
+
"version": "2.163.5",
|
|
10
10
|
"main": "build/meep.module.js",
|
|
11
11
|
"module": "build/meep.module.js",
|
|
12
12
|
"exports": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bt_mesh_face_island_erode.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bt_mesh_face_island_erode.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js"],"names":[],"mappings":"AAoCA;;;;;;;;;;;;GAYG;AACH,uEAHW,MAAM,EAAE,kBACR,MAAM,QAgehB"}
|
|
@@ -6,13 +6,34 @@ import {
|
|
|
6
6
|
} from "../../../../line/line3_compute_segment_point_distance_eikonal.js";
|
|
7
7
|
import { NULL_POINTER } from "../BinaryTopology.js";
|
|
8
8
|
import { bt_face_get_incenter } from "../query/bt_face_get_incenter.js";
|
|
9
|
-
import { bt_mesh_build_boundary_distance_field } from "../query/bt_mesh_build_boundary_distance_field.js";
|
|
10
9
|
import { bt_mesh_cleanup_faceless_references } from "./bt_mesh_cleanup_faceless_references.js";
|
|
11
10
|
import { bt_edge_split } from "./edge/bt_edge_split.js";
|
|
12
11
|
import { bt_face_kill } from "./face/bt_face_kill.js";
|
|
13
12
|
import { bt_mesh_face_poke } from "./face/bt_face_poke.js";
|
|
14
13
|
import { bt_face_get_neighbour_faces } from "../query/bt_face_get_neighbour_faces.js";
|
|
15
14
|
|
|
15
|
+
const _eb_a = new Float32Array(3);
|
|
16
|
+
const _eb_b = new Float32Array(3);
|
|
17
|
+
const _eb_p = new Float32Array(3);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Squared straight-line distance from point p to segment a-b in 3D.
|
|
21
|
+
*/
|
|
22
|
+
function point_segment_distance_sq(px, py, pz, ax, ay, az, bx, by, bz) {
|
|
23
|
+
const abx = bx - ax, aby = by - ay, abz = bz - az;
|
|
24
|
+
const apx = px - ax, apy = py - ay, apz = pz - az;
|
|
25
|
+
const ab_len_sq = abx * abx + aby * aby + abz * abz;
|
|
26
|
+
|
|
27
|
+
let t = ab_len_sq > 0 ? (apx * abx + apy * aby + apz * abz) / ab_len_sq : 0;
|
|
28
|
+
if (t < 0) t = 0; else if (t > 1) t = 1;
|
|
29
|
+
|
|
30
|
+
const cx = ax + t * abx - px;
|
|
31
|
+
const cy = ay + t * aby - py;
|
|
32
|
+
const cz = az + t * abz - pz;
|
|
33
|
+
|
|
34
|
+
return cx * cx + cy * cy + cz * cz;
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
/**
|
|
17
38
|
* Shrinks an island of faces by a given distance.
|
|
18
39
|
* Conceptually, this moves the outline of the island inwards.
|
|
@@ -160,13 +181,152 @@ export function bt_mesh_face_island_erode(
|
|
|
160
181
|
}
|
|
161
182
|
}
|
|
162
183
|
|
|
163
|
-
// 3.
|
|
184
|
+
// 3. Distance field: EXACT straight-line distance from each island vertex to the nearest boundary
|
|
185
|
+
// edge. Agent clearance is a Euclidean (line-of-sight) distance to the nearest wall, NOT a geodesic
|
|
186
|
+
// distance measured along the surface. The previous eikonal field was both semantically wrong and
|
|
187
|
+
// sensitive to the triangulation - it produced a lumpy, non-uniform inset (e.g. a 14x14 platform
|
|
188
|
+
// built from strips eroded by 0.45-0.65 instead of a uniform 0.4). Exact distance is uniform.
|
|
164
189
|
/**
|
|
165
190
|
* Vertex -> Distance from boundary
|
|
166
191
|
* @type {Map<number, number>}
|
|
167
192
|
*/
|
|
168
193
|
const vertex_boundary_distances = new Map();
|
|
169
|
-
|
|
194
|
+
|
|
195
|
+
// boundary edge segments, cached by compute_field for reuse in the deviation test below
|
|
196
|
+
let boundary_segments = [];
|
|
197
|
+
|
|
198
|
+
// (re)compute the exact distance field for the current island vertices/boundary
|
|
199
|
+
function compute_field() {
|
|
200
|
+
vertex_boundary_distances.clear();
|
|
201
|
+
|
|
202
|
+
boundary_segments = [];
|
|
203
|
+
for (const e of boundary_edges_set) {
|
|
204
|
+
const bv1 = mesh.edge_read_vertex1(e);
|
|
205
|
+
const bv2 = mesh.edge_read_vertex2(e);
|
|
206
|
+
mesh.vertex_read_coordinate(_eb_a, 0, bv1);
|
|
207
|
+
mesh.vertex_read_coordinate(_eb_b, 0, bv2);
|
|
208
|
+
boundary_segments.push(_eb_a[0], _eb_a[1], _eb_a[2], _eb_b[0], _eb_b[1], _eb_b[2]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const v of island_vertices) {
|
|
212
|
+
mesh.vertex_read_coordinate(_eb_p, 0, v);
|
|
213
|
+
vertex_boundary_distances.set(v, dist_to_boundary(_eb_p[0], _eb_p[1], _eb_p[2]));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// exact straight-line distance from a point to the nearest boundary edge
|
|
218
|
+
function dist_to_boundary(px, py, pz) {
|
|
219
|
+
const seg = boundary_segments;
|
|
220
|
+
let best_sq = Infinity;
|
|
221
|
+
for (let i = 0; i < seg.length; i += 6) {
|
|
222
|
+
const d_sq = point_segment_distance_sq(
|
|
223
|
+
px, py, pz,
|
|
224
|
+
seg[i], seg[i + 1], seg[i + 2],
|
|
225
|
+
seg[i + 3], seg[i + 4], seg[i + 5]
|
|
226
|
+
);
|
|
227
|
+
if (d_sq < best_sq) best_sq = d_sq;
|
|
228
|
+
}
|
|
229
|
+
return best_sq === Infinity ? Infinity : Math.sqrt(best_sq);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
compute_field();
|
|
233
|
+
|
|
234
|
+
// 3a. CUT-BAND REFINEMENT. The inset contour is reconstructed piecewise-linearly from the faces the
|
|
235
|
+
// erosion threshold passes through, so on a coarse/skewed triangulation the cut chord deviates from
|
|
236
|
+
// the true offset (a strip-built platform eroded unevenly, 0.4 on one side and 0.6 on another). For
|
|
237
|
+
// each straddling face, place the two cut points and test whether their midpoint really lies at the
|
|
238
|
+
// erode distance; only refine (split the longest edge) when the chord deviates. Already-accurate
|
|
239
|
+
// cuts (e.g. a plain quad's straight sides) are left coarse, so detail is added only where needed.
|
|
240
|
+
{
|
|
241
|
+
const fine = Math.max(erode_distance * 0.5, 0.1);
|
|
242
|
+
const fine_sq = fine * fine;
|
|
243
|
+
const tol = Math.max(erode_distance * 0.15, 0.04);
|
|
244
|
+
let rounds = 0;
|
|
245
|
+
|
|
246
|
+
const ca = new Float32Array(3), cb = new Float32Array(3), cc = new Float32Array(3);
|
|
247
|
+
|
|
248
|
+
while (rounds < 48) {
|
|
249
|
+
rounds++;
|
|
250
|
+
|
|
251
|
+
const targets = new Set();
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < island_faces.length; i++) {
|
|
254
|
+
const f = island_faces[i];
|
|
255
|
+
if (!mesh.faces.is_allocated(f)) continue;
|
|
256
|
+
|
|
257
|
+
const la = mesh.face_read_loop(f);
|
|
258
|
+
const lb = mesh.loop_read_next(la);
|
|
259
|
+
const lc = mesh.loop_read_next(lb);
|
|
260
|
+
const v1 = mesh.loop_read_vertex(la);
|
|
261
|
+
const v2 = mesh.loop_read_vertex(lb);
|
|
262
|
+
const v3 = mesh.loop_read_vertex(lc);
|
|
263
|
+
|
|
264
|
+
const d1 = vertex_boundary_distances.get(v1);
|
|
265
|
+
const d2 = vertex_boundary_distances.get(v2);
|
|
266
|
+
const d3 = vertex_boundary_distances.get(v3);
|
|
267
|
+
|
|
268
|
+
const dmin = Math.min(d1, d2, d3);
|
|
269
|
+
const dmax = Math.max(d1, d2, d3);
|
|
270
|
+
|
|
271
|
+
// does the erode threshold pass through this face?
|
|
272
|
+
if (!(dmin < erode_distance && dmax > erode_distance)) continue;
|
|
273
|
+
|
|
274
|
+
mesh.vertex_read_coordinate(ca, 0, v1);
|
|
275
|
+
mesh.vertex_read_coordinate(cb, 0, v2);
|
|
276
|
+
mesh.vertex_read_coordinate(cc, 0, v3);
|
|
277
|
+
|
|
278
|
+
const e_ab = (ca[0] - cb[0]) ** 2 + (ca[1] - cb[1]) ** 2 + (ca[2] - cb[2]) ** 2;
|
|
279
|
+
const e_bc = (cb[0] - cc[0]) ** 2 + (cb[1] - cc[1]) ** 2 + (cb[2] - cc[2]) ** 2;
|
|
280
|
+
const e_ca = (cc[0] - ca[0]) ** 2 + (cc[1] - ca[1]) ** 2 + (cc[2] - ca[2]) ** 2;
|
|
281
|
+
const longest = Math.max(e_ab, e_bc, e_ca);
|
|
282
|
+
|
|
283
|
+
if (longest <= fine_sq) continue;
|
|
284
|
+
|
|
285
|
+
// the two cut points where the threshold crosses this face's edges
|
|
286
|
+
let mx = 0, my = 0, mz = 0, crossings = 0;
|
|
287
|
+
// edge (v1,v2)
|
|
288
|
+
if ((d1 < erode_distance) !== (d2 < erode_distance)) {
|
|
289
|
+
const t = (erode_distance - d1) / (d2 - d1);
|
|
290
|
+
mx += ca[0] + t * (cb[0] - ca[0]); my += ca[1] + t * (cb[1] - ca[1]); mz += ca[2] + t * (cb[2] - ca[2]); crossings++;
|
|
291
|
+
}
|
|
292
|
+
// edge (v2,v3)
|
|
293
|
+
if ((d2 < erode_distance) !== (d3 < erode_distance)) {
|
|
294
|
+
const t = (erode_distance - d2) / (d3 - d2);
|
|
295
|
+
mx += cb[0] + t * (cc[0] - cb[0]); my += cb[1] + t * (cc[1] - cb[1]); mz += cb[2] + t * (cc[2] - cb[2]); crossings++;
|
|
296
|
+
}
|
|
297
|
+
// edge (v3,v1)
|
|
298
|
+
if ((d3 < erode_distance) !== (d1 < erode_distance)) {
|
|
299
|
+
const t = (erode_distance - d3) / (d1 - d3);
|
|
300
|
+
mx += cc[0] + t * (ca[0] - cc[0]); my += cc[1] + t * (ca[1] - cc[1]); mz += cc[2] + t * (ca[2] - cc[2]); crossings++;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (crossings === 2) {
|
|
304
|
+
mx *= 0.5; my *= 0.5; mz *= 0.5;
|
|
305
|
+
const true_d = dist_to_boundary(mx, my, mz);
|
|
306
|
+
if (Math.abs(true_d - erode_distance) <= tol) {
|
|
307
|
+
// the straight cut here is already accurate - leave this face coarse
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const sl = (longest === e_ab) ? la : (longest === e_bc) ? lb : lc;
|
|
313
|
+
targets.add(mesh.loop_read_edge(sl));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (targets.size === 0) break;
|
|
317
|
+
|
|
318
|
+
for (const e of targets) {
|
|
319
|
+
if (mesh.edges.is_allocated(e)) bt_edge_split(mesh, e, 0.5);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
island_faces = recompute_island_faces(faces);
|
|
323
|
+
({ island_vertices, island_edges, boundary_vertices, boundary_edges_set } = collect_island_sets(island_faces));
|
|
324
|
+
|
|
325
|
+
compute_field();
|
|
326
|
+
|
|
327
|
+
if (mesh.faces.size > 200000) break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
170
330
|
// ---------------------------------------------------------
|
|
171
331
|
// 3.5. PEAK RESCUE PASS (The Fix)
|
|
172
332
|
// Handle cases where a face is "submerged" by vertex values
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve T-junctions: where a vertex lies on the interior of a boundary edge (rather than at one of
|
|
3
|
+
* its endpoints), split that edge at the vertex so the two pieces can later be welded/fused into a
|
|
4
|
+
* shared edge.
|
|
5
|
+
*
|
|
6
|
+
* This is required for meshes assembled from independently-authored faces of differing sizes - e.g. an
|
|
7
|
+
* L-shaped or ring floor built from rectangles, where one rectangle's corner falls in the middle of a
|
|
8
|
+
* neighbour's edge. Without it those faces touch only at a single vertex, so edge-based neighbour
|
|
9
|
+
* queries (island detection, erosion, path-finding) treat them as DISCONNECTED.
|
|
10
|
+
*
|
|
11
|
+
* Call AFTER an initial vertex-merge/edge-fuse, then merge + fuse again so the new split vertices weld
|
|
12
|
+
* onto the T-junction vertices and the resulting duplicate edges fuse.
|
|
13
|
+
*
|
|
14
|
+
* @param {BinaryTopology} mesh
|
|
15
|
+
* @param {number} [tolerance] max distance from a vertex to an edge for it to count as "on" the edge
|
|
16
|
+
* @returns {number} number of splits performed
|
|
17
|
+
*/
|
|
18
|
+
export function bt_mesh_resolve_t_junctions(mesh: BinaryTopology, tolerance?: number): number;
|
|
19
|
+
//# sourceMappingURL=bt_mesh_resolve_t_junctions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bt_mesh_resolve_t_junctions.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;GAgBG;AACH,8EAHW,MAAM,GACJ,MAAM,CAuElB"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { NULL_POINTER } from "../BinaryTopology.js";
|
|
2
|
+
import { bt_edge_split } from "./edge/bt_edge_split.js";
|
|
3
|
+
|
|
4
|
+
const _a = new Float32Array(3);
|
|
5
|
+
const _b = new Float32Array(3);
|
|
6
|
+
const _p = new Float32Array(3);
|
|
7
|
+
const _hit = new Float32Array(3);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve T-junctions: where a vertex lies on the interior of a boundary edge (rather than at one of
|
|
11
|
+
* its endpoints), split that edge at the vertex so the two pieces can later be welded/fused into a
|
|
12
|
+
* shared edge.
|
|
13
|
+
*
|
|
14
|
+
* This is required for meshes assembled from independently-authored faces of differing sizes - e.g. an
|
|
15
|
+
* L-shaped or ring floor built from rectangles, where one rectangle's corner falls in the middle of a
|
|
16
|
+
* neighbour's edge. Without it those faces touch only at a single vertex, so edge-based neighbour
|
|
17
|
+
* queries (island detection, erosion, path-finding) treat them as DISCONNECTED.
|
|
18
|
+
*
|
|
19
|
+
* Call AFTER an initial vertex-merge/edge-fuse, then merge + fuse again so the new split vertices weld
|
|
20
|
+
* onto the T-junction vertices and the resulting duplicate edges fuse.
|
|
21
|
+
*
|
|
22
|
+
* @param {BinaryTopology} mesh
|
|
23
|
+
* @param {number} [tolerance] max distance from a vertex to an edge for it to count as "on" the edge
|
|
24
|
+
* @returns {number} number of splits performed
|
|
25
|
+
*/
|
|
26
|
+
export function bt_mesh_resolve_t_junctions(mesh, tolerance = 1e-4) {
|
|
27
|
+
const tol_sq = tolerance * tolerance;
|
|
28
|
+
|
|
29
|
+
let total = 0;
|
|
30
|
+
let rounds = 0;
|
|
31
|
+
let changed = true;
|
|
32
|
+
|
|
33
|
+
while (changed && rounds < 256) {
|
|
34
|
+
changed = false;
|
|
35
|
+
rounds++;
|
|
36
|
+
|
|
37
|
+
const edge_count = mesh.edges.size;
|
|
38
|
+
|
|
39
|
+
for (let e = 0; e < edge_count; e++) {
|
|
40
|
+
if (!mesh.edges.is_allocated(e)) continue;
|
|
41
|
+
|
|
42
|
+
// only boundary edges (a single radial loop) can carry a T-junction
|
|
43
|
+
const l = mesh.edge_read_loop(e);
|
|
44
|
+
if (l === NULL_POINTER) continue;
|
|
45
|
+
if (mesh.loop_read_radial_next(l) !== l) continue;
|
|
46
|
+
|
|
47
|
+
const v1 = mesh.edge_read_vertex1(e);
|
|
48
|
+
const v2 = mesh.edge_read_vertex2(e);
|
|
49
|
+
|
|
50
|
+
mesh.vertex_read_coordinate(_a, 0, v1);
|
|
51
|
+
mesh.vertex_read_coordinate(_b, 0, v2);
|
|
52
|
+
|
|
53
|
+
const abx = _b[0] - _a[0], aby = _b[1] - _a[1], abz = _b[2] - _a[2];
|
|
54
|
+
const ab_sq = abx * abx + aby * aby + abz * abz;
|
|
55
|
+
if (ab_sq <= tol_sq) continue;
|
|
56
|
+
|
|
57
|
+
const t_margin = tolerance / Math.sqrt(ab_sq);
|
|
58
|
+
|
|
59
|
+
let split_t = -1;
|
|
60
|
+
let split_v = -1;
|
|
61
|
+
|
|
62
|
+
const vertex_count = mesh.vertices.size;
|
|
63
|
+
for (let v = 0; v < vertex_count; v++) {
|
|
64
|
+
if (v === v1 || v === v2) continue;
|
|
65
|
+
if (!mesh.vertices.is_allocated(v)) continue;
|
|
66
|
+
|
|
67
|
+
mesh.vertex_read_coordinate(_p, 0, v);
|
|
68
|
+
|
|
69
|
+
const t = ((_p[0] - _a[0]) * abx + (_p[1] - _a[1]) * aby + (_p[2] - _a[2]) * abz) / ab_sq;
|
|
70
|
+
if (t <= t_margin || t >= 1 - t_margin) continue;
|
|
71
|
+
|
|
72
|
+
const cx = _a[0] + t * abx - _p[0];
|
|
73
|
+
const cy = _a[1] + t * aby - _p[1];
|
|
74
|
+
const cz = _a[2] + t * abz - _p[2];
|
|
75
|
+
if (cx * cx + cy * cy + cz * cz > tol_sq) continue;
|
|
76
|
+
|
|
77
|
+
split_t = t;
|
|
78
|
+
split_v = v;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (split_t >= 0) {
|
|
83
|
+
// snapshot the on-edge vertex coordinate, split, then snap the new vertex exactly onto it
|
|
84
|
+
mesh.vertex_read_coordinate(_hit, 0, split_v);
|
|
85
|
+
const nv = bt_edge_split(mesh, e, split_t);
|
|
86
|
+
mesh.vertex_write_coordinate(nv, _hit, 0);
|
|
87
|
+
|
|
88
|
+
total++;
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return total;
|
|
95
|
+
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Carve holes into a (welded
|
|
3
|
-
*
|
|
2
|
+
* Carve holes into a (welded) walkable topology wherever an agent of the given height+radius would not
|
|
3
|
+
* fit under overhead geometry. INTENDED TO RUN AFTER the agent-radius boundary erosion: the boundary is
|
|
4
|
+
* already handled, so this only removes the obstacle footprints (dilated by the agent radius).
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* {@link
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* All subdivision is conformal ({@link bt_edge_split} re-triangulates every face around a split edge),
|
|
7
|
+
* and culling whole faces ({@link bt_face_kill}) never cracks neighbours - so this cannot disconnect a
|
|
8
|
+
* passable region. Refinement is tight: faces straddling the (dilated) obstacle outline are refined to
|
|
9
|
+
* `resolution`; a still-large all-clear face whose column has overhead is refined down to a coarse
|
|
10
|
+
* guard so a small obstacle cannot hide unsampled; everything else stays coarse.
|
|
10
11
|
*
|
|
11
12
|
* @param {object} params
|
|
12
13
|
* @param {BinaryTopology} params.mesh walkable topology to carve (modified in place)
|
|
13
14
|
* @param {BinaryTopology} params.source original source mesh (walkable + overhead geometry)
|
|
14
|
-
* @param {BVH} params.source_bvh BVH over `source`
|
|
15
|
+
* @param {BVH} params.source_bvh BVH over `source`
|
|
15
16
|
* @param {number} params.agent_height
|
|
16
17
|
* @param {number} params.agent_radius
|
|
17
18
|
* @param {Vector3} params.up world up direction
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bt_mesh_carve_height_clearance.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bt_mesh_carve_height_clearance.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.js"],"names":[],"mappings":"AA2NA;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAPkC,IAAI;IACJ,MAAM;IACjB,UAAU;IACP,YAAY,EAA3B,MAAM;IACS,YAAY,EAA3B,MAAM;IACU,EAAE;SAyH5B"}
|
|
@@ -15,26 +15,10 @@ import {
|
|
|
15
15
|
computeTriangleRayIntersectionBarycentric
|
|
16
16
|
} from "../../../../core/geom/3d/triangle/computeTriangleRayIntersectionBarycentric.js";
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
* Lift a ray origin off the surface so the upward clearance ray does not immediately re-hit the floor.
|
|
20
|
-
* @type {number}
|
|
21
|
-
*/
|
|
18
|
+
/** Lift a ray origin off the surface so the upward clearance ray does not re-hit the floor. */
|
|
22
19
|
const SURFACE_EPSILON = 1e-4;
|
|
23
20
|
|
|
24
|
-
/**
|
|
25
|
-
* Horizontal inset applied to a face footprint before testing for overhead geometry. Prevents a
|
|
26
|
-
* neighbour that merely *touches* a face along a shared edge (e.g. an adjacent platform at a slightly
|
|
27
|
-
* different height) from being mistaken for an overhang. A real overhang overlaps the interior with
|
|
28
|
-
* positive area and still registers.
|
|
29
|
-
* @type {number}
|
|
30
|
-
*/
|
|
31
|
-
const FOOTPRINT_INSET = 1e-2;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Safety cap on the number of faces the refinement is allowed to grow to, so a pathological input
|
|
35
|
-
* cannot loop forever.
|
|
36
|
-
* @type {number}
|
|
37
|
-
*/
|
|
21
|
+
/** Safety cap on the face count during refinement. */
|
|
38
22
|
const MAX_FACES = 200000;
|
|
39
23
|
|
|
40
24
|
// reused scratch
|
|
@@ -50,11 +34,9 @@ const vc = new Float32Array(3);
|
|
|
50
34
|
|
|
51
35
|
const overhead_hits = [];
|
|
52
36
|
const query_aabb = new Float32Array(6);
|
|
37
|
+
const scratch_normal = new Float32Array(3);
|
|
53
38
|
|
|
54
|
-
/**
|
|
55
|
-
* True if there is no source geometry within `agent_height` directly above the point along `up`.
|
|
56
|
-
* (Mirrors the clearance ray-cast used by the old soup-based pass.)
|
|
57
|
-
*/
|
|
39
|
+
/** True if nothing in `source` sits directly above the point within `agent_height` (exact ray test). */
|
|
58
40
|
function point_has_clearance(source_bvh, source, px, py, pz, up_x, up_y, up_z, agent_height) {
|
|
59
41
|
const origin_x = px + up_x * SURFACE_EPSILON;
|
|
60
42
|
const origin_y = py + up_y * SURFACE_EPSILON;
|
|
@@ -73,9 +55,15 @@ function point_has_clearance(source_bvh, source, px, py, pz, up_x, up_y, up_z, a
|
|
|
73
55
|
const face_id = source_bvh.node_get_user_data(node);
|
|
74
56
|
|
|
75
57
|
const loop_a = source.face_read_loop(face_id);
|
|
76
|
-
if (loop_a === NULL_POINTER)
|
|
58
|
+
if (loop_a === NULL_POINTER) continue;
|
|
59
|
+
|
|
60
|
+
// Only DOWNWARD-facing geometry (a ceiling / overhang underside) can obstruct head clearance.
|
|
61
|
+
// Up-facing walkable floors above (a higher tier, a step the agent climbs) must NOT block.
|
|
62
|
+
source.face_read_normal(scratch_normal, 0, face_id);
|
|
63
|
+
if (scratch_normal[0] * up_x + scratch_normal[1] * up_y + scratch_normal[2] * up_z >= -1e-3) {
|
|
77
64
|
continue;
|
|
78
65
|
}
|
|
66
|
+
|
|
79
67
|
const loop_b = source.loop_read_next(loop_a);
|
|
80
68
|
const loop_c = source.loop_read_next(loop_b);
|
|
81
69
|
|
|
@@ -91,62 +79,92 @@ function point_has_clearance(source_bvh, source, px, py, pz, up_x, up_y, up_z, a
|
|
|
91
79
|
tri_b[0], tri_b[1], tri_b[2],
|
|
92
80
|
tri_c[0], tri_c[1], tri_c[2]
|
|
93
81
|
);
|
|
94
|
-
if (!hit)
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
82
|
+
if (!hit) continue;
|
|
97
83
|
|
|
98
84
|
const t = intersection_result[0];
|
|
99
|
-
if (t > 0 && t <= agent_height)
|
|
100
|
-
// overhead obstruction within the agent's height
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
85
|
+
if (t > 0 && t <= agent_height) return false;
|
|
103
86
|
}
|
|
104
87
|
|
|
105
88
|
return true;
|
|
106
89
|
}
|
|
107
90
|
|
|
108
91
|
/**
|
|
109
|
-
*
|
|
92
|
+
* True if any source geometry sits above the point within `agent_height`, anywhere inside a horizontal
|
|
93
|
+
* disc of radius `r` (approximated by an axis-aligned box). Used to dilate the obstacle footprint by
|
|
94
|
+
* the agent radius - the agent's body cannot occupy a spot whose `r`-neighbourhood is overhung.
|
|
110
95
|
*/
|
|
111
|
-
function
|
|
112
|
-
const
|
|
113
|
-
const lb = mesh.loop_read_next(la);
|
|
114
|
-
const lc = mesh.loop_read_next(lb);
|
|
96
|
+
function overhead_within_radius(source_bvh, source, px, py, pz, up_x, up_y, up_z, agent_height, r) {
|
|
97
|
+
const p_along_up = px * up_x + py * up_y + pz * up_z;
|
|
115
98
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
99
|
+
// expand on the axes perpendicular to up by r, then sweep up by agent_height
|
|
100
|
+
const ex = r * (1 - Math.abs(up_x));
|
|
101
|
+
const ey = r * (1 - Math.abs(up_y));
|
|
102
|
+
const ez = r * (1 - Math.abs(up_z));
|
|
119
103
|
|
|
120
|
-
|
|
104
|
+
let min_x = px - ex, min_y = py - ey, min_z = pz - ez;
|
|
105
|
+
let max_x = px + ex, max_y = py + ey, max_z = pz + ez;
|
|
106
|
+
|
|
107
|
+
const dx = up_x * agent_height, dy = up_y * agent_height, dz = up_z * agent_height;
|
|
108
|
+
if (dx > 0) max_x += dx; else min_x += dx;
|
|
109
|
+
if (dy > 0) max_y += dy; else min_y += dy;
|
|
110
|
+
if (dz > 0) max_z += dz; else min_z += dz;
|
|
111
|
+
|
|
112
|
+
query_aabb[0] = min_x; query_aabb[1] = min_y; query_aabb[2] = min_z;
|
|
113
|
+
query_aabb[3] = max_x; query_aabb[4] = max_y; query_aabb[5] = max_z;
|
|
114
|
+
|
|
115
|
+
const count = bvh_query_user_data_overlaps_aabb(overhead_hits, 0, source_bvh, query_aabb);
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < count; i++) {
|
|
118
|
+
const fid = overhead_hits[i];
|
|
119
|
+
const la = source.face_read_loop(fid);
|
|
120
|
+
if (la === NULL_POINTER) continue;
|
|
121
|
+
|
|
122
|
+
// only downward-facing geometry counts as an overhang (see point_has_clearance)
|
|
123
|
+
source.face_read_normal(scratch_normal, 0, fid);
|
|
124
|
+
if (scratch_normal[0] * up_x + scratch_normal[1] * up_y + scratch_normal[2] * up_z >= -1e-3) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const lb = source.loop_read_next(la);
|
|
129
|
+
const lc = source.loop_read_next(lb);
|
|
130
|
+
|
|
131
|
+
source.vertex_read_coordinate(tri_a, 0, source.loop_read_vertex(la));
|
|
132
|
+
source.vertex_read_coordinate(tri_b, 0, source.loop_read_vertex(lb));
|
|
133
|
+
source.vertex_read_coordinate(tri_c, 0, source.loop_read_vertex(lc));
|
|
134
|
+
|
|
135
|
+
const cand_min_along_up = Math.min(
|
|
136
|
+
tri_a[0] * up_x + tri_a[1] * up_y + tri_a[2] * up_z,
|
|
137
|
+
tri_b[0] * up_x + tri_b[1] * up_y + tri_b[2] * up_z,
|
|
138
|
+
tri_c[0] * up_x + tri_c[1] * up_y + tri_c[2] * up_z
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (cand_min_along_up > p_along_up + 1e-3 && cand_min_along_up <= p_along_up + agent_height + 1e-3) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return false;
|
|
121
147
|
}
|
|
122
148
|
|
|
123
149
|
/**
|
|
124
|
-
* Does the
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* does not actually pass under the overhang, which only costs a little extra refinement, never a
|
|
128
|
-
* wrong cull.
|
|
150
|
+
* Does the column over this face's footprint (inflated horizontally by `pad`) contain any DOWNWARD-
|
|
151
|
+
* facing geometry above it within `agent_height`? Footprint-wide (not just the centroid), so an
|
|
152
|
+
* obstacle sitting anywhere under a large face is detected and the face is refined toward it.
|
|
129
153
|
*/
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
min_z += ez; max_z -= ez;
|
|
145
|
-
if (min_x > max_x) { const m = (min_x + max_x) * 0.5; min_x = m; max_x = m; }
|
|
146
|
-
if (min_y > max_y) { const m = (min_y + max_y) * 0.5; min_y = m; max_y = m; }
|
|
147
|
-
if (min_z > max_z) { const m = (min_z + max_z) * 0.5; min_z = m; max_z = m; }
|
|
148
|
-
|
|
149
|
-
// sweep the box by agent_height along the up direction
|
|
154
|
+
function footprint_has_overhead(source_bvh, source, up_x, up_y, up_z, agent_height, pad) {
|
|
155
|
+
const face_max_along_up = Math.max(
|
|
156
|
+
va[0] * up_x + va[1] * up_y + va[2] * up_z,
|
|
157
|
+
vb[0] * up_x + vb[1] * up_y + vb[2] * up_z,
|
|
158
|
+
vc[0] * up_x + vc[1] * up_y + vc[2] * up_z
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
let min_x = Math.min(va[0], vb[0], vc[0]) - pad;
|
|
162
|
+
let min_y = Math.min(va[1], vb[1], vc[1]) - pad;
|
|
163
|
+
let min_z = Math.min(va[2], vb[2], vc[2]) - pad;
|
|
164
|
+
let max_x = Math.max(va[0], vb[0], vc[0]) + pad;
|
|
165
|
+
let max_y = Math.max(va[1], vb[1], vc[1]) + pad;
|
|
166
|
+
let max_z = Math.max(va[2], vb[2], vc[2]) + pad;
|
|
167
|
+
|
|
150
168
|
const dx = up_x * agent_height, dy = up_y * agent_height, dz = up_z * agent_height;
|
|
151
169
|
if (dx > 0) max_x += dx; else min_x += dx;
|
|
152
170
|
if (dy > 0) max_y += dy; else min_y += dy;
|
|
@@ -159,24 +177,26 @@ function column_has_overhead(source_bvh, source, face_max_along_up, up_x, up_y,
|
|
|
159
177
|
|
|
160
178
|
for (let i = 0; i < count; i++) {
|
|
161
179
|
const fid = overhead_hits[i];
|
|
162
|
-
|
|
163
180
|
const la = source.face_read_loop(fid);
|
|
164
181
|
if (la === NULL_POINTER) continue;
|
|
182
|
+
|
|
183
|
+
source.face_read_normal(scratch_normal, 0, fid);
|
|
184
|
+
if (scratch_normal[0] * up_x + scratch_normal[1] * up_y + scratch_normal[2] * up_z >= -1e-3) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
165
188
|
const lb = source.loop_read_next(la);
|
|
166
189
|
const lc = source.loop_read_next(lb);
|
|
167
|
-
|
|
168
190
|
source.vertex_read_coordinate(tri_a, 0, source.loop_read_vertex(la));
|
|
169
191
|
source.vertex_read_coordinate(tri_b, 0, source.loop_read_vertex(lb));
|
|
170
192
|
source.vertex_read_coordinate(tri_c, 0, source.loop_read_vertex(lc));
|
|
171
193
|
|
|
172
|
-
// lowest extent of the candidate along up
|
|
173
194
|
const cand_min_along_up = Math.min(
|
|
174
195
|
tri_a[0] * up_x + tri_a[1] * up_y + tri_a[2] * up_z,
|
|
175
196
|
tri_b[0] * up_x + tri_b[1] * up_y + tri_b[2] * up_z,
|
|
176
197
|
tri_c[0] * up_x + tri_c[1] * up_y + tri_c[2] * up_z
|
|
177
198
|
);
|
|
178
199
|
|
|
179
|
-
// strictly above the face (the face's own/coplanar floor reads <= face_max_along_up)
|
|
180
200
|
if (cand_min_along_up > face_max_along_up + 1e-3) {
|
|
181
201
|
return true;
|
|
182
202
|
}
|
|
@@ -185,20 +205,33 @@ function column_has_overhead(source_bvh, source, face_max_along_up, up_x, up_y,
|
|
|
185
205
|
return false;
|
|
186
206
|
}
|
|
187
207
|
|
|
208
|
+
function read_face_triangle(mesh, face_id) {
|
|
209
|
+
const la = mesh.face_read_loop(face_id);
|
|
210
|
+
const lb = mesh.loop_read_next(la);
|
|
211
|
+
const lc = mesh.loop_read_next(lb);
|
|
212
|
+
|
|
213
|
+
mesh.vertex_read_coordinate(va, 0, mesh.loop_read_vertex(la));
|
|
214
|
+
mesh.vertex_read_coordinate(vb, 0, mesh.loop_read_vertex(lb));
|
|
215
|
+
mesh.vertex_read_coordinate(vc, 0, mesh.loop_read_vertex(lc));
|
|
216
|
+
|
|
217
|
+
return { la, lb, lc };
|
|
218
|
+
}
|
|
219
|
+
|
|
188
220
|
/**
|
|
189
|
-
* Carve holes into a (welded
|
|
190
|
-
*
|
|
221
|
+
* Carve holes into a (welded) walkable topology wherever an agent of the given height+radius would not
|
|
222
|
+
* fit under overhead geometry. INTENDED TO RUN AFTER the agent-radius boundary erosion: the boundary is
|
|
223
|
+
* already handled, so this only removes the obstacle footprints (dilated by the agent radius).
|
|
191
224
|
*
|
|
192
|
-
*
|
|
193
|
-
* {@link
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
225
|
+
* All subdivision is conformal ({@link bt_edge_split} re-triangulates every face around a split edge),
|
|
226
|
+
* and culling whole faces ({@link bt_face_kill}) never cracks neighbours - so this cannot disconnect a
|
|
227
|
+
* passable region. Refinement is tight: faces straddling the (dilated) obstacle outline are refined to
|
|
228
|
+
* `resolution`; a still-large all-clear face whose column has overhead is refined down to a coarse
|
|
229
|
+
* guard so a small obstacle cannot hide unsampled; everything else stays coarse.
|
|
197
230
|
*
|
|
198
231
|
* @param {object} params
|
|
199
232
|
* @param {BinaryTopology} params.mesh walkable topology to carve (modified in place)
|
|
200
233
|
* @param {BinaryTopology} params.source original source mesh (walkable + overhead geometry)
|
|
201
|
-
* @param {BVH} params.source_bvh BVH over `source`
|
|
234
|
+
* @param {BVH} params.source_bvh BVH over `source`
|
|
202
235
|
* @param {number} params.agent_height
|
|
203
236
|
* @param {number} params.agent_radius
|
|
204
237
|
* @param {Vector3} params.up world up direction
|
|
@@ -212,28 +245,30 @@ export function bt_mesh_carve_height_clearance({
|
|
|
212
245
|
up,
|
|
213
246
|
}) {
|
|
214
247
|
|
|
215
|
-
if (agent_height <= 0 || source_bvh.root === NULL_NODE)
|
|
216
|
-
// nothing overhead can obstruct anything
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
248
|
+
if (agent_height <= 0 || source_bvh.root === NULL_NODE) return;
|
|
219
249
|
|
|
220
|
-
// normalize up
|
|
221
250
|
let up_x = up.x, up_y = up.y, up_z = up.z;
|
|
222
251
|
const up_len = Math.sqrt(up_x * up_x + up_y * up_y + up_z * up_z);
|
|
223
|
-
if (up_len === 0)
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
252
|
+
if (up_len === 0) return;
|
|
226
253
|
up_x /= up_len; up_y /= up_len; up_z /= up_len;
|
|
227
254
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const resolution = Math.max(agent_radius > 0 ? agent_radius : agent_height / 4, 0.25);
|
|
255
|
+
const r = Math.max(agent_radius, 0);
|
|
256
|
+
const resolution = Math.max(agent_radius > 0 ? agent_radius : agent_height / 4, 0.3);
|
|
231
257
|
const res_sq = resolution * resolution;
|
|
232
258
|
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
259
|
+
// The agent fits at p iff nothing overhangs directly above it AND nothing overhangs within its body
|
|
260
|
+
// radius. (For r == 0 this is just the exact overhead ray.)
|
|
261
|
+
function agent_fits(px, py, pz) {
|
|
262
|
+
if (!point_has_clearance(source_bvh, source, px, py, pz, up_x, up_y, up_z, agent_height)) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
if (r > 0 && overhead_within_radius(source_bvh, source, px, py, pz, up_x, up_y, up_z, agent_height, r)) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---- Phase 1: tight, conformal refinement around the (dilated) obstacle outline ----
|
|
237
272
|
let changed = true;
|
|
238
273
|
let guard = 0;
|
|
239
274
|
|
|
@@ -244,53 +279,58 @@ export function bt_mesh_carve_height_clearance({
|
|
|
244
279
|
const face_count = mesh.faces.size;
|
|
245
280
|
|
|
246
281
|
for (let f = 0; f < face_count; f++) {
|
|
247
|
-
if (!mesh.faces.is_allocated(f))
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
282
|
+
if (!mesh.faces.is_allocated(f)) continue;
|
|
250
283
|
|
|
251
284
|
read_face_triangle(mesh, f);
|
|
252
285
|
|
|
253
|
-
// longest edge (by squared length); read_face_triangle puts va/vb/vc in loop order
|
|
254
|
-
// la=(a,b), lb=(b,c), lc=(c,a)
|
|
255
286
|
const ab = (va[0] - vb[0]) ** 2 + (va[1] - vb[1]) ** 2 + (va[2] - vb[2]) ** 2;
|
|
256
287
|
const bc = (vb[0] - vc[0]) ** 2 + (vb[1] - vc[1]) ** 2 + (vb[2] - vc[2]) ** 2;
|
|
257
288
|
const ca = (vc[0] - va[0]) ** 2 + (vc[1] - va[1]) ** 2 + (vc[2] - va[2]) ** 2;
|
|
258
|
-
|
|
259
289
|
const longest_sq = Math.max(ab, bc, ca);
|
|
260
290
|
|
|
261
|
-
if (longest_sq <= res_sq)
|
|
262
|
-
|
|
263
|
-
|
|
291
|
+
if (longest_sq <= res_sq) continue;
|
|
292
|
+
|
|
293
|
+
const mab_x = (va[0] + vb[0]) * 0.5, mab_y = (va[1] + vb[1]) * 0.5, mab_z = (va[2] + vb[2]) * 0.5;
|
|
294
|
+
const mbc_x = (vb[0] + vc[0]) * 0.5, mbc_y = (vb[1] + vc[1]) * 0.5, mbc_z = (vb[2] + vc[2]) * 0.5;
|
|
295
|
+
const mca_x = (vc[0] + va[0]) * 0.5, mca_y = (vc[1] + va[1]) * 0.5, mca_z = (vc[2] + va[2]) * 0.5;
|
|
296
|
+
const cx = (va[0] + vb[0] + vc[0]) / 3, cy = (va[1] + vb[1] + vc[1]) / 3, cz = (va[2] + vb[2] + vc[2]) / 3;
|
|
297
|
+
|
|
298
|
+
const samples = [
|
|
299
|
+
va[0], va[1], va[2], vb[0], vb[1], vb[2], vc[0], vc[1], vc[2],
|
|
300
|
+
mab_x, mab_y, mab_z, mbc_x, mbc_y, mbc_z, mca_x, mca_y, mca_z,
|
|
301
|
+
cx, cy, cz,
|
|
302
|
+
];
|
|
303
|
+
let n_clear = 0, n_block = 0;
|
|
304
|
+
for (let s = 0; s < samples.length; s += 3) {
|
|
305
|
+
if (agent_fits(samples[s], samples[s + 1], samples[s + 2])) n_clear++; else n_block++;
|
|
264
306
|
}
|
|
265
307
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
308
|
+
let refine = false;
|
|
309
|
+
if (n_block > 0 && n_clear > 0) {
|
|
310
|
+
refine = true; // (dilated) outline crosses this face -> hug it at `resolution`
|
|
311
|
+
} else if (n_block === 0) {
|
|
312
|
+
// all clear by sampling, but a small obstacle can hide entirely between the samples of a
|
|
313
|
+
// coarse face. If any downward-facing overhang sits under the face footprint, keep
|
|
314
|
+
// refining down to the contour resolution so even sub-sample obstacles get caught.
|
|
315
|
+
if (footprint_has_overhead(source_bvh, source, up_x, up_y, up_z, agent_height, r)) {
|
|
316
|
+
refine = true;
|
|
317
|
+
}
|
|
275
318
|
}
|
|
319
|
+
// n_clear === 0 (fully blocked): leave it for the cull phase
|
|
320
|
+
|
|
321
|
+
if (!refine) continue;
|
|
276
322
|
|
|
277
323
|
const { la, lb, lc } = read_face_triangle(mesh, f);
|
|
278
324
|
let split_loop;
|
|
279
|
-
if (longest_sq === ab)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
split_loop = lb;
|
|
283
|
-
} else {
|
|
284
|
-
split_loop = lc;
|
|
285
|
-
}
|
|
325
|
+
if (longest_sq === ab) split_loop = la;
|
|
326
|
+
else if (longest_sq === bc) split_loop = lb;
|
|
327
|
+
else split_loop = lc;
|
|
286
328
|
|
|
287
329
|
bt_edge_split(mesh, mesh.loop_read_edge(split_loop), 0.5);
|
|
288
330
|
changed = true;
|
|
289
331
|
}
|
|
290
332
|
|
|
291
|
-
if (mesh.faces.size > MAX_FACES)
|
|
292
|
-
break;
|
|
293
|
-
}
|
|
333
|
+
if (mesh.faces.size > MAX_FACES) break;
|
|
294
334
|
}
|
|
295
335
|
|
|
296
336
|
// ---- Phase 2: cull blocked faces (whole-face, conformal) ----
|
|
@@ -298,19 +338,14 @@ export function bt_mesh_carve_height_clearance({
|
|
|
298
338
|
const face_count = mesh.faces.size;
|
|
299
339
|
|
|
300
340
|
for (let f = 0; f < face_count; f++) {
|
|
301
|
-
if (!mesh.faces.is_allocated(f))
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
341
|
+
if (!mesh.faces.is_allocated(f)) continue;
|
|
304
342
|
|
|
305
343
|
read_face_triangle(mesh, f);
|
|
306
|
-
|
|
307
344
|
const cx = (va[0] + vb[0] + vc[0]) / 3;
|
|
308
345
|
const cy = (va[1] + vb[1] + vc[1]) / 3;
|
|
309
346
|
const cz = (va[2] + vb[2] + vc[2]) / 3;
|
|
310
347
|
|
|
311
|
-
if (!
|
|
312
|
-
faces_to_kill.push(f);
|
|
313
|
-
}
|
|
348
|
+
if (!agent_fits(cx, cy, cz)) faces_to_kill.push(f);
|
|
314
349
|
}
|
|
315
350
|
|
|
316
351
|
for (let i = 0; i < faces_to_kill.length; i++) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"AA8CA;;;;;;;;;;GAUG;AACH,wKATW,cAAc,QAyLxB;+BAvO8B,mEAAmE"}
|
|
@@ -39,6 +39,9 @@ import Vector3 from "../../../../core/geom/Vector3.js";
|
|
|
39
39
|
import { bvh_build_from_bt_mesh } from "../bvh_build_from_bt_mesh.js";
|
|
40
40
|
import { bvh_build_from_unindexed_triangles } from "./bvh_build_from_unindexed_triangles.js";
|
|
41
41
|
import { bt_mesh_carve_height_clearance } from "./bt_mesh_carve_height_clearance.js";
|
|
42
|
+
import {
|
|
43
|
+
bt_mesh_resolve_t_junctions
|
|
44
|
+
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js";
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
/**
|
|
@@ -164,41 +167,49 @@ export function navmesh_build_topology({
|
|
|
164
167
|
// which is what the loop-based neighbour queries rely on
|
|
165
168
|
bt_mesh_fuse_duplicate_edges(mesh);
|
|
166
169
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
170
|
+
// Resolve T-junctions: when faces of differing sizes abut (an L-shaped or ring floor built from
|
|
171
|
+
// rectangles, a small tile against a big one), one face's corner can land in the middle of
|
|
172
|
+
// another's edge. Those faces touch at a single vertex only, so edge-based neighbour queries
|
|
173
|
+
// treat them as disconnected. Split such edges at the stray vertex, then re-weld/fuse so the
|
|
174
|
+
// pieces share a real edge.
|
|
175
|
+
if (bt_mesh_resolve_t_junctions(mesh, 1e-4) > 0) {
|
|
176
|
+
bt_merge_verts_by_distance(mesh, 1e-6);
|
|
177
|
+
bt_mesh_fuse_duplicate_edges(mesh);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Carve obstacle footprints first (no radius dilation here) ---
|
|
181
|
+
// Cull the floor directly under any overhang, opening a hole at each obstacle footprint. The
|
|
182
|
+
// agent-radius dilation is handled by the erosion below, which treats these holes as boundaries
|
|
183
|
+
// and insets them (and the outer edge) by exactly agent_radius - giving both a uniform inset and
|
|
184
|
+
// a smooth, consistent outline around obstacles from a single, robust pass.
|
|
171
185
|
if (agent_height > 0) {
|
|
172
186
|
bt_mesh_carve_height_clearance({
|
|
173
187
|
mesh,
|
|
174
188
|
source,
|
|
175
189
|
source_bvh,
|
|
176
190
|
agent_height,
|
|
177
|
-
agent_radius,
|
|
191
|
+
agent_radius: 0,
|
|
178
192
|
up,
|
|
179
193
|
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const islands = bt_mesh_compute_face_islands(mesh);
|
|
183
194
|
|
|
184
|
-
|
|
185
|
-
bt_mesh_face_decouple_islands(mesh, islands);
|
|
186
|
-
|
|
187
|
-
// shrink islands to agent_radius
|
|
188
|
-
for (const island of islands) {
|
|
189
|
-
bt_mesh_face_island_erode(mesh, island, agent_radius);
|
|
195
|
+
bt_mesh_compact(mesh);
|
|
190
196
|
}
|
|
191
197
|
|
|
192
|
-
//
|
|
198
|
+
// --- Then erode every boundary (outer edge + obstacle holes) by the agent radius ---
|
|
199
|
+
// The erosion uses an exact Euclidean distance field with cut-band refinement, so it stays
|
|
200
|
+
// accurate and connected even on the hole-punched mesh the carve produces.
|
|
201
|
+
{
|
|
202
|
+
const islands = bt_mesh_compute_face_islands(mesh);
|
|
203
|
+
|
|
204
|
+
bt_mesh_face_decouple_islands(mesh, islands);
|
|
193
205
|
|
|
194
|
-
|
|
195
|
-
|
|
206
|
+
for (const island of islands) {
|
|
207
|
+
bt_mesh_face_island_erode(mesh, island, agent_radius);
|
|
208
|
+
}
|
|
196
209
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// hole-free topology - this is the single invariant downstream code relies on.
|
|
201
|
-
bt_mesh_compact(mesh);
|
|
210
|
+
bt_mesh_cleanup_faceless_references(mesh);
|
|
211
|
+
bt_mesh_compact(mesh);
|
|
212
|
+
}
|
|
202
213
|
|
|
203
214
|
// bridge across small steps / gaps so stair-separated or slightly-broken tiers are reachable.
|
|
204
215
|
// Opt-in: with both step params 0 (the default) the topology is left untouched.
|