@woosh/meep-engine 2.163.3 → 2.163.4
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/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 +162 -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 +25 -24
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.4",
|
|
10
10
|
"main": "build/meep.module.js",
|
|
11
11
|
"module": "build/meep.module.js",
|
|
12
12
|
"exports": {
|
|
@@ -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;SA0H5B"}
|
|
@@ -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,32 @@ 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
|
|
255
|
+
const r = Math.max(agent_radius, 0);
|
|
256
|
+
const resolution = Math.max(agent_radius > 0 ? agent_radius : agent_height / 4, 0.3);
|
|
257
|
+
const coarse_guard = Math.max(2 * agent_radius, agent_height / 2, 0.8);
|
|
231
258
|
const res_sq = resolution * resolution;
|
|
259
|
+
const guard_sq = coarse_guard * coarse_guard;
|
|
260
|
+
|
|
261
|
+
// The agent fits at p iff nothing overhangs directly above it AND nothing overhangs within its body
|
|
262
|
+
// radius. (For r == 0 this is just the exact overhead ray.)
|
|
263
|
+
function agent_fits(px, py, pz) {
|
|
264
|
+
if (!point_has_clearance(source_bvh, source, px, py, pz, up_x, up_y, up_z, agent_height)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
if (r > 0 && overhead_within_radius(source_bvh, source, px, py, pz, up_x, up_y, up_z, agent_height, r)) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
232
272
|
|
|
233
|
-
// ---- Phase 1:
|
|
234
|
-
// Repeated fixed-point passes: split the longest edge of any face that (a) is still larger than
|
|
235
|
-
// the contour resolution and (b) has overhead geometry in its column. Splitting is conformal, so
|
|
236
|
-
// the mesh stays crack-free throughout.
|
|
273
|
+
// ---- Phase 1: tight, conformal refinement around the (dilated) obstacle outline ----
|
|
237
274
|
let changed = true;
|
|
238
275
|
let guard = 0;
|
|
239
276
|
|
|
@@ -244,53 +281,57 @@ export function bt_mesh_carve_height_clearance({
|
|
|
244
281
|
const face_count = mesh.faces.size;
|
|
245
282
|
|
|
246
283
|
for (let f = 0; f < face_count; f++) {
|
|
247
|
-
if (!mesh.faces.is_allocated(f))
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
284
|
+
if (!mesh.faces.is_allocated(f)) continue;
|
|
250
285
|
|
|
251
286
|
read_face_triangle(mesh, f);
|
|
252
287
|
|
|
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
288
|
const ab = (va[0] - vb[0]) ** 2 + (va[1] - vb[1]) ** 2 + (va[2] - vb[2]) ** 2;
|
|
256
289
|
const bc = (vb[0] - vc[0]) ** 2 + (vb[1] - vc[1]) ** 2 + (vb[2] - vc[2]) ** 2;
|
|
257
290
|
const ca = (vc[0] - va[0]) ** 2 + (vc[1] - va[1]) ** 2 + (vc[2] - va[2]) ** 2;
|
|
258
|
-
|
|
259
291
|
const longest_sq = Math.max(ab, bc, ca);
|
|
260
292
|
|
|
261
|
-
if (longest_sq <= res_sq)
|
|
262
|
-
|
|
263
|
-
|
|
293
|
+
if (longest_sq <= res_sq) continue;
|
|
294
|
+
|
|
295
|
+
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;
|
|
296
|
+
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;
|
|
297
|
+
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;
|
|
298
|
+
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;
|
|
299
|
+
|
|
300
|
+
const samples = [
|
|
301
|
+
va[0], va[1], va[2], vb[0], vb[1], vb[2], vc[0], vc[1], vc[2],
|
|
302
|
+
mab_x, mab_y, mab_z, mbc_x, mbc_y, mbc_z, mca_x, mca_y, mca_z,
|
|
303
|
+
cx, cy, cz,
|
|
304
|
+
];
|
|
305
|
+
let n_clear = 0, n_block = 0;
|
|
306
|
+
for (let s = 0; s < samples.length; s += 3) {
|
|
307
|
+
if (agent_fits(samples[s], samples[s + 1], samples[s + 2])) n_clear++; else n_block++;
|
|
264
308
|
}
|
|
265
309
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
310
|
+
let refine = false;
|
|
311
|
+
if (n_block > 0 && n_clear > 0) {
|
|
312
|
+
refine = true; // (dilated) outline crosses this face -> hug it at `resolution`
|
|
313
|
+
} else if (n_block === 0 && longest_sq > guard_sq) {
|
|
314
|
+
// all clear by sampling, but still large enough to hide an obstacle between samples:
|
|
315
|
+
// refine if any downward-facing overhang sits under the whole face footprint (+ radius)
|
|
316
|
+
if (footprint_has_overhead(source_bvh, source, up_x, up_y, up_z, agent_height, r)) {
|
|
317
|
+
refine = true;
|
|
318
|
+
}
|
|
275
319
|
}
|
|
320
|
+
// n_clear === 0 (fully blocked): leave it for the cull phase
|
|
321
|
+
|
|
322
|
+
if (!refine) continue;
|
|
276
323
|
|
|
277
324
|
const { la, lb, lc } = read_face_triangle(mesh, f);
|
|
278
325
|
let split_loop;
|
|
279
|
-
if (longest_sq === ab)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
split_loop = lb;
|
|
283
|
-
} else {
|
|
284
|
-
split_loop = lc;
|
|
285
|
-
}
|
|
326
|
+
if (longest_sq === ab) split_loop = la;
|
|
327
|
+
else if (longest_sq === bc) split_loop = lb;
|
|
328
|
+
else split_loop = lc;
|
|
286
329
|
|
|
287
330
|
bt_edge_split(mesh, mesh.loop_read_edge(split_loop), 0.5);
|
|
288
331
|
changed = true;
|
|
289
332
|
}
|
|
290
333
|
|
|
291
|
-
if (mesh.faces.size > MAX_FACES)
|
|
292
|
-
break;
|
|
293
|
-
}
|
|
334
|
+
if (mesh.faces.size > MAX_FACES) break;
|
|
294
335
|
}
|
|
295
336
|
|
|
296
337
|
// ---- Phase 2: cull blocked faces (whole-face, conformal) ----
|
|
@@ -298,19 +339,14 @@ export function bt_mesh_carve_height_clearance({
|
|
|
298
339
|
const face_count = mesh.faces.size;
|
|
299
340
|
|
|
300
341
|
for (let f = 0; f < face_count; f++) {
|
|
301
|
-
if (!mesh.faces.is_allocated(f))
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
342
|
+
if (!mesh.faces.is_allocated(f)) continue;
|
|
304
343
|
|
|
305
344
|
read_face_triangle(mesh, f);
|
|
306
|
-
|
|
307
345
|
const cx = (va[0] + vb[0] + vc[0]) / 3;
|
|
308
346
|
const cy = (va[1] + vb[1] + vc[1]) / 3;
|
|
309
347
|
const cz = (va[2] + vb[2] + vc[2]) / 3;
|
|
310
348
|
|
|
311
|
-
if (!
|
|
312
|
-
faces_to_kill.push(f);
|
|
313
|
-
}
|
|
349
|
+
if (!agent_fits(cx, cy, cz)) faces_to_kill.push(f);
|
|
314
350
|
}
|
|
315
351
|
|
|
316
352
|
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":"AA2CA;;;;;;;;;;GAUG;AACH,wKATW,cAAc,
|
|
1
|
+
{"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"AA2CA;;;;;;;;;;GAUG;AACH,wKATW,cAAc,QAkLxB;+BA7N8B,mEAAmE"}
|
|
@@ -164,10 +164,29 @@ export function navmesh_build_topology({
|
|
|
164
164
|
// which is what the loop-based neighbour queries rely on
|
|
165
165
|
bt_mesh_fuse_duplicate_edges(mesh);
|
|
166
166
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
167
|
+
// --- Agent-radius erosion FIRST, on the clean welded surface ---
|
|
168
|
+
// Erosion is reliable on a clean triangulation; running it BEFORE the obstacle carve avoids
|
|
169
|
+
// feeding it the irregular, hole-punched mesh the carve would otherwise produce (which made the
|
|
170
|
+
// vertex-sampled distance field mis-estimate the inset, both over-culling and fragmenting).
|
|
171
|
+
{
|
|
172
|
+
const islands = bt_mesh_compute_face_islands(mesh);
|
|
173
|
+
|
|
174
|
+
// decouple so islands can be eroded independently
|
|
175
|
+
bt_mesh_face_decouple_islands(mesh, islands);
|
|
176
|
+
|
|
177
|
+
for (const island of islands) {
|
|
178
|
+
bt_mesh_face_island_erode(mesh, island, agent_radius);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// remove dangling references, then compact (erosion frees faces, leaving holes in the ID space)
|
|
182
|
+
bt_mesh_cleanup_faceless_references(mesh);
|
|
183
|
+
bt_mesh_compact(mesh);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- Then carve the obstacle footprints (dilated by the agent radius) ---
|
|
187
|
+
// Runs on the already-eroded surface. Conformal edge-splits + whole-face culls only: this can
|
|
188
|
+
// never crack a passable region apart, it can only open holes where the agent genuinely cannot
|
|
189
|
+
// fit under an overhang.
|
|
171
190
|
if (agent_height > 0) {
|
|
172
191
|
bt_mesh_carve_height_clearance({
|
|
173
192
|
mesh,
|
|
@@ -177,29 +196,11 @@ export function navmesh_build_topology({
|
|
|
177
196
|
agent_radius,
|
|
178
197
|
up,
|
|
179
198
|
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const islands = bt_mesh_compute_face_islands(mesh);
|
|
183
199
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// shrink islands to agent_radius
|
|
188
|
-
for (const island of islands) {
|
|
189
|
-
bt_mesh_face_island_erode(mesh, island, agent_radius);
|
|
200
|
+
// carve kills faces via the free-list; compact back to a dense, hole-free topology
|
|
201
|
+
bt_mesh_compact(mesh);
|
|
190
202
|
}
|
|
191
203
|
|
|
192
|
-
// TODO attempt to reduce mesh complexity by re-triangulating flat areas or running a very constrained decimation
|
|
193
|
-
|
|
194
|
-
// remove dangling references
|
|
195
|
-
bt_mesh_cleanup_faceless_references(mesh);
|
|
196
|
-
|
|
197
|
-
// Island erosion (above) kills faces via the pool's free-list, leaving holes in the ID space.
|
|
198
|
-
// `faces.size` is a high-water mark, not a live count, so consumers that iterate `0..faces.size`
|
|
199
|
-
// (e.g. bvh_build_from_bt_mesh) would touch freed slots. Compact so every consumer sees a dense,
|
|
200
|
-
// hole-free topology - this is the single invariant downstream code relies on.
|
|
201
|
-
bt_mesh_compact(mesh);
|
|
202
|
-
|
|
203
204
|
// bridge across small steps / gaps so stair-separated or slightly-broken tiers are reachable.
|
|
204
205
|
// Opt-in: with both step params 0 (the default) the topology is left untouched.
|
|
205
206
|
if (agent_max_step_height > 0 || agent_max_step_distance > 0) {
|