@woosh/meep-engine 2.163.7 → 2.163.9
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/2d/line/line_segment_intersection_fraction_2d.d.ts +23 -0
- package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.d.ts.map +1 -0
- package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.js +44 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.d.ts +2 -2
- 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 +120 -179
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts +9 -10
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js +12 -13
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.d.ts +17 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.js +45 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.d.ts +40 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.js +84 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.js +53 -78
- package/src/core/geom/vec3/v3_matrix3_rotate.d.ts +16 -0
- package/src/core/geom/vec3/v3_matrix3_rotate.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_matrix3_rotate.js +49 -0
- package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.d.ts +2 -2
- package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.js +46 -46
- package/src/engine/graphics/sh3/path_tracer/sampling/getBiasedNormalSample.d.ts.map +1 -1
- package/src/engine/graphics/sh3/path_tracer/sampling/getBiasedNormalSample.js +6 -28
- package/src/engine/navigation/mesh/NavigationMesh.d.ts +6 -0
- package/src/engine/navigation/mesh/NavigationMesh.d.ts.map +1 -1
- package/src/engine/navigation/mesh/NavigationMesh.js +145 -234
- package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +229 -0
- package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts +11 -0
- package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts.map +1 -1
- package/src/engine/navigation/mesh/bt_mesh_face_find_path.js +623 -100
- package/src/engine/navigation/mesh/bt_mesh_face_find_path_polyanya.d.ts +17 -0
- package/src/engine/navigation/mesh/bt_mesh_face_find_path_polyanya.d.ts.map +1 -0
- package/src/engine/navigation/mesh/bt_mesh_face_find_path_polyanya.js +682 -0
- package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.d.ts.map +1 -1
- package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.js +354 -138
- package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.d.ts +21 -0
- package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.d.ts.map +1 -0
- package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.js +133 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clip_soup_against_overhangs.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/clip_soup_against_overhangs.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"clip_soup_against_overhangs.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/clip_soup_against_overhangs.js"],"names":[],"mappings":"AA+MA;;;;;;;;GAQG;AACH,kDARW,MAAM,EAAE,kBACR,MAAM,wCAEN,MAAM,gBACN,MAAM,gBAEJ,MAAM,EAAE,CAkOpB"}
|
|
@@ -14,100 +14,140 @@
|
|
|
14
14
|
* The subtraction uses the disjoint half-plane decomposition of a convex-polygon difference:
|
|
15
15
|
* T \ F = U_i ( T ∩ inside(e_0..e_{i-1}) ∩ outside(e_i) )
|
|
16
16
|
* each term convex, the terms disjoint, the union exactly T minus the convex (dilated) footprint F.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
*
|
|
18
|
+
* Everything is data-oriented: clipping happens in the 2D plane perpendicular to `up`, with polygons
|
|
19
|
+
* held as flat `Float64Array`s of (u,v) pairs and a small set of reused scratch buffers. World XYZ is
|
|
20
|
+
* recovered for each surviving vertex by barycentric reconstruction on its (planar) source triangle, so
|
|
21
|
+
* kept pieces stay exactly on the original - possibly tilted - walkable plane. Degenerate triangles are
|
|
22
|
+
* dropped, and triangles that no footprint overlaps are copied straight through without allocation.
|
|
23
|
+
*
|
|
24
|
+
* The footprints are indexed in a BVH (keyed by their (u,v) extent and carve height band), so each soup
|
|
25
|
+
* triangle only tests the obstacles that can actually overlap it - O(m log k) rather than O(m k) for m
|
|
26
|
+
* soup triangles and k obstacles.
|
|
19
27
|
*/
|
|
20
28
|
|
|
29
|
+
import { BVH } from "../../../../core/bvh2/bvh3/BVH.js";
|
|
30
|
+
import { bvh_query_user_data_overlaps_aabb } from "../../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js";
|
|
31
|
+
import { v3_dot } from "../../../../core/geom/vec3/v3_dot.js";
|
|
32
|
+
import { v3_orthonormal_matrix_from_normal } from "../../../../core/geom/vec3/v3_orthonormal_matrix_from_normal.js";
|
|
21
33
|
import { convex_hull_monotone_2d } from "../../../../core/geom/2d/convex-hull/convex_hull_monotone_2d.js";
|
|
34
|
+
import { compute_polygon_area_2d } from "../../../../core/geom/2d/compute_polygon_area_2d.js";
|
|
22
35
|
|
|
23
36
|
const POINT_EPS = 1e-9;
|
|
24
37
|
const AREA_EPS = 1e-9;
|
|
38
|
+
const HEIGHT_EPS = 1e-3;
|
|
25
39
|
const CORNER_SEGMENT_ANGLE = Math.PI / 6; // ~30 deg per rounded-corner segment
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
// Vertex coincidence key. Quantize each projected coordinate to QUANTIZE (1mm) and pack the two integer
|
|
42
|
+
// coordinates into one safe-integer key: cheaper than a string and with no per-probe allocation. Each
|
|
43
|
+
// axis occupies a biased 26-bit field, so the key stays unique while |u|,|v| < 2^25 / QUANTIZE
|
|
44
|
+
// (~33,000 units). A 16-bit-per-axis pack would only reach ~3.3 units and silently collide - navmesh
|
|
45
|
+
// coordinates routinely exceed that (e.g. geometry built 10,000 units from the origin). Coincident
|
|
46
|
+
// vertices are the same source vertex projected identically, so they match at any quantization; the
|
|
47
|
+
// precision only needs to keep genuinely distinct obstacle corners apart (the smallest is ~0.3 units).
|
|
48
|
+
const QUANTIZE = 1e3;
|
|
49
|
+
const KEY_BIAS = 1 << 25; // 33,554,432
|
|
50
|
+
const KEY_STRIDE = 1 << 26; // 67,108,864
|
|
51
|
+
|
|
52
|
+
// Reused clip scratch (stride 2: u,v). Grown on demand so a single allocation serves the whole soup.
|
|
53
|
+
let _cur = new Float64Array(64);
|
|
54
|
+
let _tmp = new Float64Array(64);
|
|
55
|
+
let _side = new Float64Array(64);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Ensure `buf` can hold `vertex_count` (u,v) pairs, returning a large-enough buffer (the same one when
|
|
59
|
+
* it already fits). Contents are not preserved - callers grow before writing.
|
|
60
|
+
*
|
|
61
|
+
* @param {Float64Array} buf
|
|
62
|
+
* @param {number} vertex_count
|
|
63
|
+
* @returns {Float64Array}
|
|
64
|
+
*/
|
|
65
|
+
function ensure_capacity(buf, vertex_count) {
|
|
66
|
+
const needed = vertex_count * 2;
|
|
67
|
+
if (buf.length >= needed) return buf;
|
|
68
|
+
let size = buf.length;
|
|
69
|
+
while (size < needed) size *= 2;
|
|
70
|
+
return new Float64Array(size);
|
|
39
71
|
}
|
|
40
72
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Sutherland-Hodgman clip of a 2D polygon against one half-plane of a directed edge - the general-edge
|
|
75
|
+
* analogue of {@link polygon2_clip_axis_halfplane}. The edge passes through (ex, ez) with direction
|
|
76
|
+
* (dx, dz); `keep_left` keeps the side with `cross(dir, P - e) >= 0`, otherwise the right side.
|
|
77
|
+
*
|
|
78
|
+
* @param {Float64Array} in_pts input vertices (stride 2)
|
|
79
|
+
* @param {number} in_n input vertex count
|
|
80
|
+
* @param {Float64Array} out_pts output buffer (stride 2), sized for at least in_n + 1 vertices
|
|
81
|
+
* @param {number} ex
|
|
82
|
+
* @param {number} ez a point on the edge
|
|
83
|
+
* @param {number} dx
|
|
84
|
+
* @param {number} dz the edge direction
|
|
85
|
+
* @param {boolean} keep_left keep the left half-plane when true
|
|
86
|
+
* @returns {number} surviving vertex count
|
|
87
|
+
*/
|
|
88
|
+
function clip_halfplane(in_pts, in_n, out_pts, ex, ez, dx, dz, keep_left) {
|
|
89
|
+
let out_n = 0;
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < in_n; i++) {
|
|
92
|
+
const j = (i + 1) % in_n;
|
|
44
93
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const last = out[out.length - 1];
|
|
48
|
-
if (last && Math.abs(last.u - p.u) < POINT_EPS && Math.abs(last.v - p.v) < POINT_EPS) return;
|
|
49
|
-
out.push(p);
|
|
50
|
-
};
|
|
94
|
+
const a_u = in_pts[i * 2], a_v = in_pts[i * 2 + 1];
|
|
95
|
+
const b_u = in_pts[j * 2], b_v = in_pts[j * 2 + 1];
|
|
51
96
|
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
push({
|
|
63
|
-
x: A.x + t * (C.x - A.x),
|
|
64
|
-
y: A.y + t * (C.y - A.y),
|
|
65
|
-
z: A.z + t * (C.z - A.z),
|
|
66
|
-
u: A.u + t * (C.u - A.u),
|
|
67
|
-
v: A.v + t * (C.v - A.v),
|
|
68
|
-
});
|
|
97
|
+
const sa = dx * (a_v - ez) - dz * (a_u - ex);
|
|
98
|
+
const sb = dx * (b_v - ez) - dz * (b_u - ex);
|
|
99
|
+
|
|
100
|
+
const in_a = keep_left ? (sa >= -POINT_EPS) : (sa <= POINT_EPS);
|
|
101
|
+
const in_b = keep_left ? (sb >= -POINT_EPS) : (sb <= POINT_EPS);
|
|
102
|
+
|
|
103
|
+
if (in_a) {
|
|
104
|
+
out_pts[out_n * 2] = a_u;
|
|
105
|
+
out_pts[out_n * 2 + 1] = a_v;
|
|
106
|
+
out_n++;
|
|
69
107
|
}
|
|
70
|
-
}
|
|
71
|
-
if (out.length > 1) {
|
|
72
|
-
const f = out[0], l = out[out.length - 1];
|
|
73
|
-
if (Math.abs(f.u - l.u) < POINT_EPS && Math.abs(f.v - l.v) < POINT_EPS) out.pop();
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
108
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
109
|
+
if (in_a !== in_b) {
|
|
110
|
+
const t = sa / (sa - sb);
|
|
111
|
+
out_pts[out_n * 2] = a_u + t * (b_u - a_u);
|
|
112
|
+
out_pts[out_n * 2 + 1] = a_v + t * (b_v - a_v);
|
|
113
|
+
out_n++;
|
|
114
|
+
}
|
|
83
115
|
}
|
|
84
|
-
return a * 0.5;
|
|
85
|
-
}
|
|
86
116
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const n = poly.length;
|
|
90
|
-
if (n < 3 || r <= 0) return poly;
|
|
117
|
+
return out_n;
|
|
118
|
+
}
|
|
91
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Outward Minkowski-with-disc offset of a convex CCW polygon by `r`: each edge is pushed out by `r` and
|
|
122
|
+
* each convex corner is replaced by a rounded arc. Input/output are flat (u,v) pairs.
|
|
123
|
+
*
|
|
124
|
+
* @param {Float64Array} pts convex CCW polygon (stride 2)
|
|
125
|
+
* @param {number} n vertex count
|
|
126
|
+
* @param {number} r offset radius (> 0)
|
|
127
|
+
* @returns {Float64Array} the offset polygon (CCW, stride 2)
|
|
128
|
+
*/
|
|
129
|
+
function offset_convex_ccw(pts, n, r) {
|
|
92
130
|
// outward edge normals (right of each CCW edge)
|
|
93
|
-
const enx = new
|
|
131
|
+
const enx = new Float64Array(n);
|
|
132
|
+
const enz = new Float64Array(n);
|
|
94
133
|
for (let i = 0; i < n; i++) {
|
|
95
|
-
const
|
|
96
|
-
let dx =
|
|
134
|
+
const j = (i + 1) % n;
|
|
135
|
+
let dx = pts[j * 2] - pts[i * 2];
|
|
136
|
+
let dz = pts[j * 2 + 1] - pts[i * 2 + 1];
|
|
97
137
|
const len = Math.hypot(dx, dz) || 1;
|
|
98
138
|
dx /= len; dz /= len;
|
|
99
|
-
enx[i] = dz;
|
|
139
|
+
enx[i] = dz; // (dir.v, -dir.u) = right/outward of a CCW edge
|
|
100
140
|
enz[i] = -dx;
|
|
101
141
|
}
|
|
102
142
|
|
|
103
143
|
const out = [];
|
|
104
144
|
for (let i = 0; i < n; i++) {
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
145
|
+
const prev_edge = (i - 1 + n) % n; // edge ending at vertex i
|
|
146
|
+
const next_edge = i; // edge starting at vertex i
|
|
147
|
+
const vu = pts[i * 2], vv = pts[i * 2 + 1];
|
|
108
148
|
|
|
109
|
-
|
|
110
|
-
|
|
149
|
+
const a1 = Math.atan2(enz[prev_edge], enx[prev_edge]);
|
|
150
|
+
const a2 = Math.atan2(enz[next_edge], enx[next_edge]);
|
|
111
151
|
let da = a2 - a1;
|
|
112
152
|
while (da < 0) da += 2 * Math.PI;
|
|
113
153
|
while (da > 2 * Math.PI) da -= 2 * Math.PI;
|
|
@@ -115,27 +155,54 @@ function offset_convex_ccw(poly, r) {
|
|
|
115
155
|
const steps = Math.max(1, Math.ceil(da / CORNER_SEGMENT_ANGLE));
|
|
116
156
|
for (let s = 0; s <= steps; s++) {
|
|
117
157
|
const a = a1 + da * (s / steps);
|
|
118
|
-
out.push(
|
|
158
|
+
out.push(vu + r * Math.cos(a), vv + r * Math.sin(a));
|
|
119
159
|
}
|
|
120
160
|
}
|
|
121
|
-
|
|
161
|
+
|
|
162
|
+
return Float64Array.from(out);
|
|
122
163
|
}
|
|
123
164
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Subtract convex footprint `fp` from a single convex polygon, appending the convex, disjoint pieces of
|
|
167
|
+
* (polygon \ fp) to `out_pieces` as right-sized `Float64Array`s. `fp` must be CCW (interior on the left
|
|
168
|
+
* of each edge). The polygon's interior remainder (the part inside `fp`) is discarded.
|
|
169
|
+
*
|
|
170
|
+
* @param {Float64Array} poly polygon vertices (stride 2)
|
|
171
|
+
* @param {number} poly_n polygon vertex count
|
|
172
|
+
* @param {Float64Array} fp footprint vertices (stride 2, CCW)
|
|
173
|
+
* @param {number} fp_n footprint vertex count
|
|
174
|
+
* @param {Float64Array[]} out_pieces destination list; surviving outside pieces are pushed
|
|
175
|
+
*/
|
|
176
|
+
function subtract_footprint(poly, poly_n, fp, fp_n, out_pieces) {
|
|
177
|
+
// current = the running intersection with the inside half-planes processed so far
|
|
178
|
+
_cur = ensure_capacity(_cur, poly_n + fp_n);
|
|
179
|
+
_tmp = ensure_capacity(_tmp, poly_n + fp_n);
|
|
180
|
+
_side = ensure_capacity(_side, poly_n + fp_n);
|
|
181
|
+
|
|
182
|
+
let cur = _cur;
|
|
183
|
+
let tmp = _tmp;
|
|
184
|
+
for (let i = 0; i < poly_n * 2; i++) cur[i] = poly[i];
|
|
185
|
+
let cur_n = poly_n;
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i < fp_n; i++) {
|
|
188
|
+
if (cur_n < 3) return; // fully consumed - nothing outside remains
|
|
189
|
+
|
|
190
|
+
const j = (i + 1) % fp_n;
|
|
191
|
+
const ex = fp[i * 2], ez = fp[i * 2 + 1];
|
|
192
|
+
const dx = fp[j * 2] - ex;
|
|
193
|
+
const dz = fp[j * 2 + 1] - ez;
|
|
194
|
+
if (Math.abs(dx) < POINT_EPS && Math.abs(dz) < POINT_EPS) continue; // degenerate edge
|
|
195
|
+
|
|
196
|
+
// piece outside this edge (and inside all earlier edges) leaves the footprint here -> it survives
|
|
197
|
+
const side_n = clip_halfplane(cur, cur_n, _side, ex, ez, dx, dz, false);
|
|
198
|
+
if (side_n >= 3 && Math.abs(compute_polygon_area_2d(_side, side_n)) > AREA_EPS) {
|
|
199
|
+
out_pieces.push(_side.slice(0, side_n * 2));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// keep marching with the part still inside this edge
|
|
203
|
+
cur_n = clip_halfplane(cur, cur_n, tmp, ex, ez, dx, dz, true);
|
|
204
|
+
const swap = cur; cur = tmp; tmp = swap;
|
|
137
205
|
}
|
|
138
|
-
return pieces;
|
|
139
206
|
}
|
|
140
207
|
|
|
141
208
|
/**
|
|
@@ -153,104 +220,253 @@ export function clip_soup_against_overhangs(soup, triangle_count, source, agent_
|
|
|
153
220
|
if (ul === 0) return soup.slice(0, triangle_count * 9);
|
|
154
221
|
upx /= ul; upy /= ul; upz /= ul;
|
|
155
222
|
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
const
|
|
223
|
+
// orthonormal basis: rows 0,1 span the plane perpendicular to up (our u,v axes), row 2 is up itself
|
|
224
|
+
const basis = new Float64Array(9);
|
|
225
|
+
v3_orthonormal_matrix_from_normal(basis, 0, upx, upy, upz);
|
|
226
|
+
const u_x = basis[0], u_y = basis[1], u_z = basis[2];
|
|
227
|
+
const v_x = basis[3], v_y = basis[4], v_z = basis[5];
|
|
160
228
|
|
|
161
|
-
// collect downward-facing (overhead) source triangles
|
|
162
|
-
const
|
|
229
|
+
// collect downward-facing (overhead) source triangles as flat (u,v) triples plus their mean height
|
|
230
|
+
const overhead_uv = []; // stride 6: u0,v0,u1,v1,u2,v2
|
|
231
|
+
const overhead_h = [];
|
|
163
232
|
const fn = new Float32Array(3);
|
|
164
233
|
const fc = new Float32Array(3);
|
|
165
|
-
const
|
|
166
|
-
for (let f = 0; f <
|
|
234
|
+
const face_count = source.faces.size;
|
|
235
|
+
for (let f = 0; f < face_count; f++) {
|
|
167
236
|
if (!source.faces.is_allocated(f)) continue;
|
|
168
237
|
source.face_read_normal(fn, 0, f);
|
|
169
|
-
if (fn[0] * upx + fn[1] * upy + fn[2] * upz >= -
|
|
238
|
+
if (fn[0] * upx + fn[1] * upy + fn[2] * upz >= -HEIGHT_EPS) continue; // not downward-facing
|
|
170
239
|
|
|
171
|
-
|
|
172
|
-
const lb = source.loop_read_next(la);
|
|
173
|
-
const lc = source.loop_read_next(lb);
|
|
174
|
-
const uv = [];
|
|
240
|
+
let l = source.face_read_loop(f);
|
|
175
241
|
let hsum = 0;
|
|
176
|
-
for (
|
|
242
|
+
for (let k = 0; k < 3; k++) {
|
|
177
243
|
source.vertex_read_coordinate(fc, 0, source.loop_read_vertex(l));
|
|
178
|
-
|
|
179
|
-
|
|
244
|
+
const x = fc[0], y = fc[1], z = fc[2];
|
|
245
|
+
overhead_uv.push(v3_dot(x, y, z, u_x, u_y, u_z), v3_dot(x, y, z, v_x, v_y, v_z));
|
|
246
|
+
hsum += v3_dot(x, y, z, upx, upy, upz);
|
|
247
|
+
l = source.loop_read_next(l);
|
|
180
248
|
}
|
|
181
|
-
|
|
249
|
+
overhead_h.push(hsum / 3);
|
|
182
250
|
}
|
|
183
251
|
|
|
184
|
-
|
|
252
|
+
const overhead_count = overhead_h.length;
|
|
253
|
+
if (overhead_count === 0) return soup.slice(0, triangle_count * 9);
|
|
185
254
|
|
|
186
255
|
// Group overhead triangles that share a (coincident) vertex into one obstacle. Each obstacle's
|
|
187
256
|
// footprint is the convex hull of its vertices, dilated by the agent radius. Treating the obstacle
|
|
188
257
|
// as one polygon (rather than per-triangle) avoids self-touching/pinched union boundaries.
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const parent = [];
|
|
258
|
+
const parent = new Int32Array(overhead_count);
|
|
259
|
+
for (let i = 0; i < overhead_count; i++) parent[i] = i;
|
|
192
260
|
const find = (a) => { while (parent[a] !== a) { parent[a] = parent[parent[a]]; a = parent[a]; } return a; };
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
for (let i = 0; i <
|
|
196
|
-
for (
|
|
197
|
-
const
|
|
198
|
-
|
|
261
|
+
|
|
262
|
+
const key_to_tri = new Map();
|
|
263
|
+
for (let i = 0; i < overhead_count; i++) {
|
|
264
|
+
for (let k = 0; k < 3; k++) {
|
|
265
|
+
const ku = Math.round(overhead_uv[i * 6 + k * 2] * QUANTIZE);
|
|
266
|
+
const kv = Math.round(overhead_uv[i * 6 + k * 2 + 1] * QUANTIZE);
|
|
267
|
+
const key = (ku + KEY_BIAS) * KEY_STRIDE + (kv + KEY_BIAS);
|
|
268
|
+
const seen = key_to_tri.get(key);
|
|
269
|
+
if (seen !== undefined) parent[find(i)] = find(seen); else key_to_tri.set(key, i);
|
|
199
270
|
}
|
|
200
271
|
}
|
|
272
|
+
|
|
273
|
+
/** @type {Map<number, {pts: number[], hsum: number, n: number}>} */
|
|
201
274
|
const groups = new Map();
|
|
202
|
-
for (let i = 0; i <
|
|
203
|
-
const
|
|
204
|
-
let g = groups.get(
|
|
205
|
-
if (
|
|
206
|
-
for (
|
|
207
|
-
g.hsum +=
|
|
275
|
+
for (let i = 0; i < overhead_count; i++) {
|
|
276
|
+
const root = find(i);
|
|
277
|
+
let g = groups.get(root);
|
|
278
|
+
if (g === undefined) { g = { pts: [], hsum: 0, n: 0 }; groups.set(root, g); }
|
|
279
|
+
for (let k = 0; k < 6; k++) g.pts.push(overhead_uv[i * 6 + k]);
|
|
280
|
+
g.hsum += overhead_h[i];
|
|
281
|
+
g.n++;
|
|
208
282
|
}
|
|
209
283
|
|
|
210
|
-
|
|
284
|
+
// each footprint: a CCW (dilated) convex polygon in (u,v) plus the obstacle's mean height
|
|
285
|
+
/** @type {Float64Array[]} */
|
|
286
|
+
const footprint_poly = [];
|
|
287
|
+
const footprint_h = [];
|
|
211
288
|
for (const g of groups.values()) {
|
|
212
289
|
const hull = convex_hull_monotone_2d(g.pts, g.pts.length / 2);
|
|
213
290
|
if (hull.length < 3) continue;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
291
|
+
|
|
292
|
+
let poly = new Float64Array(hull.length * 2);
|
|
293
|
+
for (let i = 0; i < hull.length; i++) {
|
|
294
|
+
poly[i * 2] = g.pts[hull[i] * 2];
|
|
295
|
+
poly[i * 2 + 1] = g.pts[hull[i] * 2 + 1];
|
|
296
|
+
}
|
|
297
|
+
// ensure CCW (interior to the left of each edge) for the subtraction
|
|
298
|
+
if (compute_polygon_area_2d(poly, hull.length) < 0) {
|
|
299
|
+
const rev = new Float64Array(poly.length);
|
|
300
|
+
for (let i = 0; i < hull.length; i++) {
|
|
301
|
+
rev[i * 2] = poly[(hull.length - 1 - i) * 2];
|
|
302
|
+
rev[i * 2 + 1] = poly[(hull.length - 1 - i) * 2 + 1];
|
|
303
|
+
}
|
|
304
|
+
poly = rev;
|
|
305
|
+
}
|
|
306
|
+
if (agent_radius > 0) poly = offset_convex_ccw(poly, poly.length / 2, agent_radius);
|
|
307
|
+
|
|
308
|
+
footprint_poly.push(poly);
|
|
309
|
+
footprint_h.push(g.hsum / g.n);
|
|
218
310
|
}
|
|
219
311
|
|
|
220
|
-
|
|
312
|
+
const footprint_count = footprint_poly.length;
|
|
313
|
+
if (footprint_count === 0) return soup.slice(0, triangle_count * 9);
|
|
314
|
+
|
|
315
|
+
// Broad-phase index: one leaf per footprint, in (u, v, height) space. The first two axes are the
|
|
316
|
+
// dilated footprint's (u,v) extent; the third is the band of soup heights it can carve - a triangle
|
|
317
|
+
// at mean height `th` is carved by an obstacle at height `h` iff 0 < h - th <= agent_height. A soup
|
|
318
|
+
// triangle then queries this BVH with its own (u,v) extent at `th` to get just the candidate
|
|
319
|
+
// obstacles, instead of scanning all of them.
|
|
320
|
+
const footprint_bvh = new BVH();
|
|
321
|
+
footprint_bvh.release_all();
|
|
322
|
+
for (let fi = 0; fi < footprint_count; fi++) {
|
|
323
|
+
const poly = footprint_poly[fi];
|
|
324
|
+
let min_u = Infinity, min_v = Infinity, max_u = -Infinity, max_v = -Infinity;
|
|
325
|
+
for (let i = 0; i < poly.length; i += 2) {
|
|
326
|
+
const u = poly[i], v = poly[i + 1];
|
|
327
|
+
if (u < min_u) min_u = u;
|
|
328
|
+
if (u > max_u) max_u = u;
|
|
329
|
+
if (v < min_v) min_v = v;
|
|
330
|
+
if (v > max_v) max_v = v;
|
|
331
|
+
}
|
|
332
|
+
const h = footprint_h[fi];
|
|
333
|
+
const node = footprint_bvh.allocate_node();
|
|
334
|
+
footprint_bvh.node_set_user_data(node, fi);
|
|
335
|
+
footprint_bvh.node_set_aabb_primitive(
|
|
336
|
+
node,
|
|
337
|
+
min_u, min_v, h - agent_height - HEIGHT_EPS,
|
|
338
|
+
max_u, max_v, h - HEIGHT_EPS
|
|
339
|
+
);
|
|
340
|
+
footprint_bvh.insert_leaf(node);
|
|
341
|
+
}
|
|
221
342
|
|
|
222
343
|
const out = [];
|
|
344
|
+
|
|
345
|
+
// per-triangle reusable storage
|
|
346
|
+
const tri_uv = new Float64Array(6); // u0,v0,u1,v1,u2,v2 of the source triangle
|
|
347
|
+
const px = new Float64Array(3), py = new Float64Array(3), pz = new Float64Array(3);
|
|
348
|
+
const query_aabb = new Float64Array(6);
|
|
349
|
+
const query_hits = new Int32Array(footprint_count);
|
|
350
|
+
/** @type {Float64Array[]} */
|
|
351
|
+
let pieces = [];
|
|
352
|
+
/** @type {Float64Array[]} */
|
|
353
|
+
let next_pieces = [];
|
|
354
|
+
|
|
223
355
|
for (let t = 0; t < triangle_count; t++) {
|
|
224
356
|
const o = t * 9;
|
|
225
|
-
|
|
357
|
+
|
|
226
358
|
let th = 0;
|
|
227
359
|
for (let k = 0; k < 3; k++) {
|
|
228
360
|
const x = soup[o + k * 3], y = soup[o + k * 3 + 1], z = soup[o + k * 3 + 2];
|
|
229
|
-
|
|
230
|
-
|
|
361
|
+
px[k] = x; py[k] = y; pz[k] = z;
|
|
362
|
+
tri_uv[k * 2] = v3_dot(x, y, z, u_x, u_y, u_z);
|
|
363
|
+
tri_uv[k * 2 + 1] = v3_dot(x, y, z, v_x, v_y, v_z);
|
|
364
|
+
th += v3_dot(x, y, z, upx, upy, upz);
|
|
231
365
|
}
|
|
232
366
|
th /= 3;
|
|
233
367
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
368
|
+
// barycentric denominator (twice the signed uv area of the triangle)
|
|
369
|
+
const e1u = tri_uv[2] - tri_uv[0], e1v = tri_uv[3] - tri_uv[1];
|
|
370
|
+
const e2u = tri_uv[4] - tri_uv[0], e2v = tri_uv[5] - tri_uv[1];
|
|
371
|
+
const det = e1u * e2v - e2u * e1v;
|
|
372
|
+
|
|
373
|
+
if (Math.abs(det) <= AREA_EPS) {
|
|
374
|
+
// degenerate in (u,v) (a near-vertical triangle): cannot carve meaningfully, pass through
|
|
375
|
+
for (let i = 0; i < 9; i++) out.push(soup[o + i]);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// broad-phase: only the obstacles whose (u,v) extent and height band overlap this triangle
|
|
380
|
+
let min_u = tri_uv[0], max_u = tri_uv[0], min_v = tri_uv[1], max_v = tri_uv[1];
|
|
381
|
+
for (let k = 1; k < 3; k++) {
|
|
382
|
+
const u = tri_uv[k * 2], v = tri_uv[k * 2 + 1];
|
|
383
|
+
if (u < min_u) min_u = u;
|
|
384
|
+
if (u > max_u) max_u = u;
|
|
385
|
+
if (v < min_v) min_v = v;
|
|
386
|
+
if (v > max_v) max_v = v;
|
|
387
|
+
}
|
|
388
|
+
query_aabb[0] = min_u; query_aabb[1] = min_v; query_aabb[2] = th;
|
|
389
|
+
query_aabb[3] = max_u; query_aabb[4] = max_v; query_aabb[5] = th;
|
|
390
|
+
|
|
391
|
+
const hit_count = bvh_query_user_data_overlaps_aabb(query_hits, 0, footprint_bvh, query_aabb);
|
|
392
|
+
if (hit_count === 0) {
|
|
393
|
+
// no obstacle overlaps here - copy straight through
|
|
394
|
+
for (let i = 0; i < 9; i++) out.push(soup[o + i]);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// start with the whole triangle as one piece, then subtract each overlapping footprint
|
|
399
|
+
pieces.length = 0;
|
|
400
|
+
pieces.push(Float64Array.of(tri_uv[0], tri_uv[1], tri_uv[2], tri_uv[3], tri_uv[4], tri_uv[5]));
|
|
401
|
+
|
|
402
|
+
for (let qi = 0; qi < hit_count; qi++) {
|
|
403
|
+
const fi = query_hits[qi];
|
|
404
|
+
const dh = footprint_h[fi] - th;
|
|
405
|
+
if (dh <= HEIGHT_EPS || dh > agent_height + HEIGHT_EPS) continue; // exact band test
|
|
406
|
+
|
|
407
|
+
const fp = footprint_poly[fi];
|
|
408
|
+
const fp_n = fp.length / 2;
|
|
409
|
+
|
|
410
|
+
next_pieces.length = 0;
|
|
411
|
+
for (let pi = 0; pi < pieces.length; pi++) {
|
|
412
|
+
subtract_footprint(pieces[pi], pieces[pi].length / 2, fp, fp_n, next_pieces);
|
|
241
413
|
}
|
|
242
|
-
|
|
414
|
+
|
|
415
|
+
const swap = pieces; pieces = next_pieces; next_pieces = swap;
|
|
243
416
|
if (pieces.length === 0) break;
|
|
244
417
|
}
|
|
245
418
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
419
|
+
const inv_det = 1 / det;
|
|
420
|
+
for (let pi = 0; pi < pieces.length; pi++) {
|
|
421
|
+
const poly = pieces[pi];
|
|
422
|
+
const poly_n = poly.length / 2;
|
|
423
|
+
|
|
424
|
+
// fan-triangulate the surviving piece and reconstruct world XYZ per vertex
|
|
425
|
+
for (let i = 1; i + 1 < poly_n; i++) {
|
|
426
|
+
const u0 = poly[0], v0 = poly[1];
|
|
427
|
+
const u1 = poly[i * 2], v1 = poly[i * 2 + 1];
|
|
428
|
+
const u2 = poly[(i + 1) * 2], v2 = poly[(i + 1) * 2 + 1];
|
|
429
|
+
|
|
430
|
+
const area2 = Math.abs((u1 - u0) * (v2 - v0) - (u2 - u0) * (v1 - v0));
|
|
250
431
|
if (area2 < 2 * AREA_EPS) continue;
|
|
251
|
-
|
|
432
|
+
|
|
433
|
+
emit_xyz(out, u0, v0, tri_uv, px, py, pz, e1u, e1v, e2u, e2v, inv_det);
|
|
434
|
+
emit_xyz(out, u1, v1, tri_uv, px, py, pz, e1u, e1v, e2u, e2v, inv_det);
|
|
435
|
+
emit_xyz(out, u2, v2, tri_uv, px, py, pz, e1u, e1v, e2u, e2v, inv_det);
|
|
252
436
|
}
|
|
253
437
|
}
|
|
254
438
|
}
|
|
439
|
+
|
|
255
440
|
return out;
|
|
256
441
|
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Reconstruct the world XYZ of a (u,v) point lying in a source triangle (barycentric on the triangle's
|
|
445
|
+
* planar uv coordinates) and append it to `out`.
|
|
446
|
+
*
|
|
447
|
+
* @param {number[]} out destination soup
|
|
448
|
+
* @param {number} u
|
|
449
|
+
* @param {number} v the point in (u,v)
|
|
450
|
+
* @param {Float64Array} tri_uv the triangle's uv corners (stride 2, vertex 0 is the origin)
|
|
451
|
+
* @param {Float64Array} px
|
|
452
|
+
* @param {Float64Array} py
|
|
453
|
+
* @param {Float64Array} pz the triangle's world corners
|
|
454
|
+
* @param {number} e1u
|
|
455
|
+
* @param {number} e1v edge (v1 - v0) in uv
|
|
456
|
+
* @param {number} e2u
|
|
457
|
+
* @param {number} e2v edge (v2 - v0) in uv
|
|
458
|
+
* @param {number} inv_det reciprocal of the uv area determinant
|
|
459
|
+
*/
|
|
460
|
+
function emit_xyz(out, u, v, tri_uv, px, py, pz, e1u, e1v, e2u, e2v, inv_det) {
|
|
461
|
+
const du = u - tri_uv[0];
|
|
462
|
+
const dv = v - tri_uv[1];
|
|
463
|
+
const w1 = (du * e2v - e2u * dv) * inv_det;
|
|
464
|
+
const w2 = (e1u * dv - du * e1v) * inv_det;
|
|
465
|
+
const w0 = 1 - w1 - w2;
|
|
466
|
+
|
|
467
|
+
out.push(
|
|
468
|
+
w0 * px[0] + w1 * px[1] + w2 * px[2],
|
|
469
|
+
w0 * py[0] + w1 * py[1] + w2 * py[2],
|
|
470
|
+
w0 * pz[0] + w1 * pz[1] + w2 * pz[2]
|
|
471
|
+
);
|
|
472
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does the segment (ax,ay,az)->(bx,by,bz) pass THROUGH the mesh surface -- cross the interior of any
|
|
3
|
+
* triangle transversally (entering one side, exiting the other) strictly between its endpoints?
|
|
4
|
+
*
|
|
5
|
+
* This is the "penetration" test for a navmesh path piece. A piece that hugs the surface (coplanar with
|
|
6
|
+
* it) or flies over / under it -- a corner-only waypoint chord on a non-planar surface -- does NOT
|
|
7
|
+
* penetrate; a piece that tunnels through a wall or floor does. Endpoints lying on the surface (every
|
|
8
|
+
* path waypoint) are not penetrations.
|
|
9
|
+
*
|
|
10
|
+
* @param {BVH} bvh built by bvh_build_from_bt_mesh over `mesh`
|
|
11
|
+
* @param {BinaryTopology} mesh triangle mesh
|
|
12
|
+
* @param {number} ax
|
|
13
|
+
* @param {number} ay
|
|
14
|
+
* @param {number} az
|
|
15
|
+
* @param {number} bx
|
|
16
|
+
* @param {number} by
|
|
17
|
+
* @param {number} bz
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
export function bvh_segment_penetrates_mesh(bvh: BVH, mesh: BinaryTopology, ax: number, ay: number, az: number, bx: number, by: number, bz: number): boolean;
|
|
21
|
+
//# sourceMappingURL=bvh_segment_penetrates_mesh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bvh_segment_penetrates_mesh.d.ts","sourceRoot":"","sources":["../../../../../src/engine/navigation/mesh/bvh_segment_penetrates_mesh.js"],"names":[],"mappings":"AAgEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,gFARW,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,GACJ,OAAO,CAmDnB"}
|