trid3d 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -20
- package/package.json +1 -1
- package/trid/index.d.ts +210 -7
- package/trid/index.js +10 -3
- package/trid/math/projection.js +8 -2
- package/trid/math/transform.js +4 -5
- package/trid/modeling/query.js +706 -0
- package/trid/render/canvasRenderer.js +268 -5
- package/trid/render/draw.js +12 -2
- package/trid/render/lights.js +14 -1
- package/trid/render/post.js +155 -1
- package/trid/render/shadows.js +184 -0
- package/trid/render/shapes.js +42 -23
- package/trid/render/webglRenderer.js +348 -17
- package/trid/scene.js +1 -1
- package/trid/tools/index.js +1 -0
- package/trid/tools/selectionOutline.js +244 -0
- package/trid/tools/transformGizmo.js +9 -1
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Questions about a mesh's surface, asked in code: where a ray first meets
|
|
3
|
+
* it, whether a point is inside it, how far a point is from it, and whether
|
|
4
|
+
* it's closed. Also the same ray question against a whole scene.
|
|
5
|
+
*
|
|
6
|
+
* const q = meshQuery(body); // prepared once (a BVH)
|
|
7
|
+
* const hit = q.raycast(from, { x: 0, y: 0, z: 1 });
|
|
8
|
+
* q.contains(p); q.distance(p); q.closest(p);
|
|
9
|
+
*
|
|
10
|
+
* A query is a snapshot: it copies the mesh's triangles when it's made, so
|
|
11
|
+
* editing the mesh afterwards doesn't change its answers. Make a new one.
|
|
12
|
+
*
|
|
13
|
+
* Faces are clockwise from outside (like the built-in shapes), so normals
|
|
14
|
+
* here point out of the mesh, whichever side a ray comes from.
|
|
15
|
+
*/
|
|
16
|
+
import { invertMat4, determinantMat4, transformPoint, transformDirection, normalMatrix, transformNormal } from "../math/matrix.js";
|
|
17
|
+
import { worldObjects, ancestors } from "../sceneGraph.js";
|
|
18
|
+
import { isPickable } from "../tools/helpers.js";
|
|
19
|
+
import { samePlace } from "./cleanup.js";
|
|
20
|
+
|
|
21
|
+
/** Most triangles in a BVH leaf */
|
|
22
|
+
const LEAF = 4;
|
|
23
|
+
/** Bins per split when building the BVH (surface area heuristic) */
|
|
24
|
+
const BINS = 12;
|
|
25
|
+
|
|
26
|
+
const SIDES = ["both", "front", "back"];
|
|
27
|
+
|
|
28
|
+
/** Odd-looking directions, so rays rarely run exactly along edges (for `contains`) */
|
|
29
|
+
const PROBES = [
|
|
30
|
+
[0.5383, 0.6412, 0.5468], [-0.7071, 0.1934, 0.6797], [0.2311, -0.8417, -0.4878],
|
|
31
|
+
[-0.3122, -0.4519, 0.8357], [0.8829, -0.2711, 0.3833], [-0.5867, 0.7713, -0.2467],
|
|
32
|
+
[0.1471, 0.3378, -0.9296],
|
|
33
|
+
].map(([x, y, z]) => {
|
|
34
|
+
const l = Math.hypot(x, y, z);
|
|
35
|
+
return [x / l, y / l, z / l];
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const finite = (v) => !!v && Number.isFinite(v.x) && Number.isFinite(v.y) && Number.isFinite(v.z);
|
|
39
|
+
|
|
40
|
+
function checkPoint(p, what) {
|
|
41
|
+
if (!finite(p)) throw new Error(`${what} must be a point { x, y, z } of finite numbers`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function unitDirection(d) {
|
|
45
|
+
const l = finite(d) ? Math.hypot(d.x, d.y, d.z) : NaN;
|
|
46
|
+
if (!(l > 0) || !Number.isFinite(l)) throw new Error("A ray needs a direction { x, y, z } that isn't zero");
|
|
47
|
+
return { x: d.x / l, y: d.y / l, z: d.z / l };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function checkSide(side) {
|
|
51
|
+
if (!SIDES.includes(side)) throw new Error(`Unknown side "${side}": use "both", "front" or "back"`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Prepares a mesh for many questions: splits its faces into triangles (fans
|
|
56
|
+
* from each face's first corner, like `pickHit`) and builds a bounding volume
|
|
57
|
+
* hierarchy (boxes inside boxes) so each question only looks at the few
|
|
58
|
+
* triangles near it. Making it costs about as much as one slow question.
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} mesh - A mesh with `vertices` and `faces`
|
|
61
|
+
* @returns {MeshQuery}
|
|
62
|
+
*/
|
|
63
|
+
export function meshQuery(mesh) {
|
|
64
|
+
return new MeshQuery(mesh);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class MeshQuery {
|
|
68
|
+
constructor(mesh) {
|
|
69
|
+
const v = mesh?.vertices ?? [];
|
|
70
|
+
const corners = [], faceOf = [];
|
|
71
|
+
(mesh?.faces ?? []).forEach((f, fi) => {
|
|
72
|
+
if (!Array.isArray(f)) return;
|
|
73
|
+
for (let i = 1; i + 1 < f.length; i++) {
|
|
74
|
+
if (!finite(v[f[0]]) || !finite(v[f[i]]) || !finite(v[f[i + 1]])) continue;
|
|
75
|
+
corners.push(f[0], f[i], f[i + 1]);
|
|
76
|
+
faceOf.push(fi);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
const n = faceOf.length;
|
|
80
|
+
const tris = new Float64Array(n * 9);
|
|
81
|
+
for (let t = 0; t < n; t++) {
|
|
82
|
+
for (let k = 0; k < 3; k++) {
|
|
83
|
+
const p = v[corners[t * 3 + k]];
|
|
84
|
+
tris[t * 9 + k * 3] = p.x;
|
|
85
|
+
tris[t * 9 + k * 3 + 1] = p.y;
|
|
86
|
+
tris[t * 9 + k * 3 + 2] = p.z;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Triangles, 9 numbers each (in BVH order after building) */
|
|
90
|
+
this._tris = tris;
|
|
91
|
+
this._corners = Int32Array.from(corners);
|
|
92
|
+
this._faceOf = Int32Array.from(faceOf);
|
|
93
|
+
/** Number of triangles */
|
|
94
|
+
this.triangleCount = n;
|
|
95
|
+
buildBVH(this);
|
|
96
|
+
|
|
97
|
+
const b = this._box;
|
|
98
|
+
this.bounds = n
|
|
99
|
+
? { min: { x: b[0], y: b[1], z: b[2] }, max: { x: b[3], y: b[4], z: b[5] } }
|
|
100
|
+
: { min: { x: 0, y: 0, z: 0 }, max: { x: 0, y: 0, z: 0 } };
|
|
101
|
+
const size = n ? Math.max(b[3] - b[0], b[4] - b[1], b[5] - b[2]) || 1 : 1;
|
|
102
|
+
// Tolerances relative to the mesh's size
|
|
103
|
+
this._minDist = size * 1e-9;
|
|
104
|
+
this._detEps = size * size * 1e-14;
|
|
105
|
+
this._edgeEps = 1e-7;
|
|
106
|
+
this._hit = { t: 0, tri: -1, u: 0, w: 0 };
|
|
107
|
+
this._near = new Float64Array(5);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The first place a ray meets the surface, or null.
|
|
112
|
+
*
|
|
113
|
+
* @param {Vec3} origin
|
|
114
|
+
* @param {Vec3} direction - Any length but zero
|
|
115
|
+
* @param {{maxDistance?:number, side?:"both"|"front"|"back"}} [opts]
|
|
116
|
+
* `side`: "front" only counts faces hit from outside, "back" from inside
|
|
117
|
+
* @returns {{point, normal, distance, face, front, triangle, barycentric} | null}
|
|
118
|
+
*/
|
|
119
|
+
raycast(origin, direction, { maxDistance = Infinity, side = "both" } = {}) {
|
|
120
|
+
checkPoint(origin, "raycast: origin");
|
|
121
|
+
checkSide(side);
|
|
122
|
+
const d = unitDirection(direction);
|
|
123
|
+
if (!this._cast(origin, d, maxDistance, side)) return null;
|
|
124
|
+
const { t, tri, u, w } = this._hit;
|
|
125
|
+
return this._describe(tri, u, w, { x: origin.x + d.x * t, y: origin.y + d.y * t, z: origin.z + d.z * t }, t, d);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Whether a point is inside. Only meaningful for a closed mesh (see
|
|
130
|
+
* `isClosed`). Rays are sent in a few directions and their crossings
|
|
131
|
+
* counted; rays that pass too close to an edge are ignored.
|
|
132
|
+
*
|
|
133
|
+
* @param {Vec3} p
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
contains(p) {
|
|
137
|
+
checkPoint(p, "contains: the point");
|
|
138
|
+
const b = this._box;
|
|
139
|
+
if (!this.triangleCount || p.x < b[0] || p.y < b[1] || p.z < b[2] || p.x > b[3] || p.y > b[4] || p.z > b[5]) return false;
|
|
140
|
+
let inside = 0, outside = 0, unsureIn = 0, unsureOut = 0;
|
|
141
|
+
for (const dir of PROBES) {
|
|
142
|
+
const r = this._crossings(p, dir);
|
|
143
|
+
if (r.sure) (r.count % 2 ? inside++ : outside++);
|
|
144
|
+
else (r.count % 2 ? unsureIn++ : unsureOut++);
|
|
145
|
+
if (inside >= 2 || outside >= 2) break;
|
|
146
|
+
}
|
|
147
|
+
if (inside !== outside) return inside > outside;
|
|
148
|
+
return unsureIn > unsureOut;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The nearest point of the surface, or null when there's none within `maxDistance`.
|
|
153
|
+
*
|
|
154
|
+
* @param {Vec3} p
|
|
155
|
+
* @param {{maxDistance?:number}} [opts]
|
|
156
|
+
* @returns {{point, normal, distance, face, triangle, barycentric} | null}
|
|
157
|
+
*/
|
|
158
|
+
closest(p, { maxDistance = Infinity } = {}) {
|
|
159
|
+
checkPoint(p, "closest: the point");
|
|
160
|
+
const found = this._nearest(p, maxDistance);
|
|
161
|
+
if (found < 0) return null;
|
|
162
|
+
const c = this._near;
|
|
163
|
+
const w1 = c[3], w2 = c[4];
|
|
164
|
+
const point = { x: c[0], y: c[1], z: c[2] };
|
|
165
|
+
return this._describe(found, w1, w2, point, Math.hypot(p.x - c[0], p.y - c[1], p.z - c[2]), null);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Distance from a point to the surface (Infinity when nothing is within `maxDistance`).
|
|
170
|
+
*
|
|
171
|
+
* @param {Vec3} p
|
|
172
|
+
* @param {number} [maxDistance=Infinity] - Stop looking past this (faster)
|
|
173
|
+
* @returns {number}
|
|
174
|
+
*/
|
|
175
|
+
distance(p, maxDistance = Infinity) {
|
|
176
|
+
checkPoint(p, "distance: the point");
|
|
177
|
+
const found = this._nearest(p, maxDistance);
|
|
178
|
+
if (found < 0) return Infinity;
|
|
179
|
+
const c = this._near;
|
|
180
|
+
return Math.hypot(p.x - c[0], p.y - c[1], p.z - c[2]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Unit normal of triangle `tri`, pointing out (clockwise faces). */
|
|
184
|
+
_normal(tri) {
|
|
185
|
+
const q = this._tris, i = tri * 9;
|
|
186
|
+
const e1x = q[i + 3] - q[i], e1y = q[i + 4] - q[i + 1], e1z = q[i + 5] - q[i + 2];
|
|
187
|
+
const e2x = q[i + 6] - q[i], e2y = q[i + 7] - q[i + 1], e2z = q[i + 8] - q[i + 2];
|
|
188
|
+
const nx = e1y * e2z - e1z * e2y, ny = e1z * e2x - e1x * e2z, nz = e1x * e2y - e1y * e2x;
|
|
189
|
+
const l = Math.hypot(nx, ny, nz) || 1;
|
|
190
|
+
return { x: nx / l, y: ny / l, z: nz / l };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
_describe(tri, u, w, point, distance, dir) {
|
|
194
|
+
const normal = this._normal(tri);
|
|
195
|
+
const out = {
|
|
196
|
+
point,
|
|
197
|
+
normal,
|
|
198
|
+
distance,
|
|
199
|
+
face: this._faceOf[tri],
|
|
200
|
+
triangle: [this._corners[tri * 3], this._corners[tri * 3 + 1], this._corners[tri * 3 + 2]],
|
|
201
|
+
barycentric: [1 - u - w, u, w],
|
|
202
|
+
};
|
|
203
|
+
if (dir) out.front = dir.x * normal.x + dir.y * normal.y + dir.z * normal.z < 0;
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* First hit of the ray o + t·d (d any length), 0 < t < maxT in units of d.
|
|
209
|
+
* Fills `this._hit` and returns true when something is hit.
|
|
210
|
+
*/
|
|
211
|
+
_cast(o, d, maxT, side) {
|
|
212
|
+
if (!this.triangleCount) return false;
|
|
213
|
+
const dl = Math.hypot(d.x, d.y, d.z);
|
|
214
|
+
const minT = this._minDist / dl, detEps = this._detEps * dl;
|
|
215
|
+
const sign = side === "front" ? 1 : side === "back" ? -1 : 0;
|
|
216
|
+
const ix = 1 / d.x, iy = 1 / d.y, iz = 1 / d.z;
|
|
217
|
+
const box = this._box, first = this._first, count = this._count, tris = this._tris, stack = this._stack;
|
|
218
|
+
const hit = this._hit;
|
|
219
|
+
let best = maxT, found = -1, bu = 0, bw = 0;
|
|
220
|
+
let top = 0;
|
|
221
|
+
if (enter(box, 0, o, ix, iy, iz, best) === Infinity) return false;
|
|
222
|
+
stack[top++] = 0;
|
|
223
|
+
while (top) {
|
|
224
|
+
const node = stack[--top];
|
|
225
|
+
const n = count[node];
|
|
226
|
+
if (n > 0) {
|
|
227
|
+
for (let k = first[node], end = first[node] + n; k < end; k++) {
|
|
228
|
+
const i = k * 9;
|
|
229
|
+
const e1x = tris[i + 3] - tris[i], e1y = tris[i + 4] - tris[i + 1], e1z = tris[i + 5] - tris[i + 2];
|
|
230
|
+
const e2x = tris[i + 6] - tris[i], e2y = tris[i + 7] - tris[i + 1], e2z = tris[i + 8] - tris[i + 2];
|
|
231
|
+
const px = d.y * e2z - d.z * e2y, py = d.z * e2x - d.x * e2z, pz = d.x * e2y - d.y * e2x;
|
|
232
|
+
const det = e1x * px + e1y * py + e1z * pz;
|
|
233
|
+
// det > 0: the ray meets the face from outside
|
|
234
|
+
if (Math.abs(det) <= detEps || det * sign < 0) continue;
|
|
235
|
+
const inv = 1 / det;
|
|
236
|
+
const sx = o.x - tris[i], sy = o.y - tris[i + 1], sz = o.z - tris[i + 2];
|
|
237
|
+
const u = (sx * px + sy * py + sz * pz) * inv;
|
|
238
|
+
if (u < 0 || u > 1) continue;
|
|
239
|
+
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
|
|
240
|
+
const w = (d.x * qx + d.y * qy + d.z * qz) * inv;
|
|
241
|
+
if (w < 0 || u + w > 1) continue;
|
|
242
|
+
const t = (e2x * qx + e2y * qy + e2z * qz) * inv;
|
|
243
|
+
if (t > minT && t < best) {
|
|
244
|
+
best = t;
|
|
245
|
+
found = k;
|
|
246
|
+
bu = u;
|
|
247
|
+
bw = w;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
// Visit the nearer child first; skip boxes past the best hit
|
|
252
|
+
const a = first[node], b = a + 1;
|
|
253
|
+
const ta = enter(box, a * 6, o, ix, iy, iz, best), tb = enter(box, b * 6, o, ix, iy, iz, best);
|
|
254
|
+
if (ta <= tb) {
|
|
255
|
+
if (tb !== Infinity) stack[top++] = b;
|
|
256
|
+
if (ta !== Infinity) stack[top++] = a;
|
|
257
|
+
} else {
|
|
258
|
+
if (ta !== Infinity) stack[top++] = a;
|
|
259
|
+
stack[top++] = b;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (found < 0) return false;
|
|
264
|
+
hit.t = best;
|
|
265
|
+
hit.tri = found;
|
|
266
|
+
hit.u = bu;
|
|
267
|
+
hit.w = bw;
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** How many faces a ray from p crosses, and whether every crossing was clear of edges. */
|
|
272
|
+
_crossings(p, [dx, dy, dz]) {
|
|
273
|
+
const tris = this._tris, box = this._box, first = this._first, count = this._count, stack = this._stack;
|
|
274
|
+
const ix = 1 / dx, iy = 1 / dy, iz = 1 / dz, minT = this._minDist, eps = this._edgeEps, detEps = this._detEps;
|
|
275
|
+
let hits = 0, sure = true, top = 0;
|
|
276
|
+
stack[top++] = 0;
|
|
277
|
+
while (top) {
|
|
278
|
+
const node = stack[--top];
|
|
279
|
+
if (enter(box, node * 6, p, ix, iy, iz, Infinity) === Infinity) continue;
|
|
280
|
+
const n = count[node];
|
|
281
|
+
if (n === 0) {
|
|
282
|
+
stack[top++] = first[node];
|
|
283
|
+
stack[top++] = first[node] + 1;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
for (let k = first[node], end = first[node] + n; k < end; k++) {
|
|
287
|
+
const i = k * 9;
|
|
288
|
+
const e1x = tris[i + 3] - tris[i], e1y = tris[i + 4] - tris[i + 1], e1z = tris[i + 5] - tris[i + 2];
|
|
289
|
+
const e2x = tris[i + 6] - tris[i], e2y = tris[i + 7] - tris[i + 1], e2z = tris[i + 8] - tris[i + 2];
|
|
290
|
+
const px = dy * e2z - dz * e2y, py = dz * e2x - dx * e2z, pz = dx * e2y - dy * e2x;
|
|
291
|
+
const det = e1x * px + e1y * py + e1z * pz;
|
|
292
|
+
if (Math.abs(det) <= detEps) continue;
|
|
293
|
+
const inv = 1 / det;
|
|
294
|
+
const sx = p.x - tris[i], sy = p.y - tris[i + 1], sz = p.z - tris[i + 2];
|
|
295
|
+
const u = (sx * px + sy * py + sz * pz) * inv;
|
|
296
|
+
if (u < -eps || u > 1 + eps) continue;
|
|
297
|
+
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
|
|
298
|
+
const w = (dx * qx + dy * qy + dz * qz) * inv;
|
|
299
|
+
if (w < -eps || u + w > 1 + eps) continue;
|
|
300
|
+
const t = (e2x * qx + e2y * qy + e2z * qz) * inv;
|
|
301
|
+
if (t <= -minT) continue;
|
|
302
|
+
// Near an edge or the start point: this ray can't be trusted
|
|
303
|
+
if (u < eps || w < eps || u + w > 1 - eps || t <= minT) sure = false;
|
|
304
|
+
if (t > minT && u >= 0 && w >= 0 && u + w <= 1) hits++;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { count: hits, sure };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Nearest triangle to p within maxDist (its closest point in `this._near`), or -1. */
|
|
311
|
+
_nearest(p, maxDist) {
|
|
312
|
+
if (!this.triangleCount) return -1;
|
|
313
|
+
const box = this._box, first = this._first, count = this._count, tris = this._tris, stack = this._stack;
|
|
314
|
+
const near = this._near, tmp = new Float64Array(5);
|
|
315
|
+
let best2 = maxDist === Infinity ? Infinity : maxDist * maxDist, found = -1, top = 0;
|
|
316
|
+
if (boxDist2(box, 0, p) > best2) return -1;
|
|
317
|
+
stack[top++] = 0;
|
|
318
|
+
while (top) {
|
|
319
|
+
const node = stack[--top];
|
|
320
|
+
if (boxDist2(box, node * 6, p) > best2) continue;
|
|
321
|
+
const n = count[node];
|
|
322
|
+
if (n > 0) {
|
|
323
|
+
for (let k = first[node], end = first[node] + n; k < end; k++) {
|
|
324
|
+
const d2 = closestOnTriangle(tris, k * 9, p, tmp);
|
|
325
|
+
if (d2 <= best2) {
|
|
326
|
+
best2 = d2;
|
|
327
|
+
found = k;
|
|
328
|
+
near.set(tmp);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
const a = first[node], b = a + 1;
|
|
333
|
+
const da = boxDist2(box, a * 6, p), db = boxDist2(box, b * 6, p);
|
|
334
|
+
// Push the farther first, so the nearer is looked at first
|
|
335
|
+
if (da <= db) {
|
|
336
|
+
if (db <= best2) stack[top++] = b;
|
|
337
|
+
if (da <= best2) stack[top++] = a;
|
|
338
|
+
} else {
|
|
339
|
+
if (da <= best2) stack[top++] = a;
|
|
340
|
+
if (db <= best2) stack[top++] = b;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return found;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Builds the BVH on `q`, sorting its triangles so each leaf's are together.
|
|
350
|
+
* Nodes: `_box` (6 numbers each), `_count` (> 0 for a leaf: its triangles
|
|
351
|
+
* from `_first`; 0 for an inner node: its children are `_first` and `_first + 1`).
|
|
352
|
+
*/
|
|
353
|
+
function buildBVH(q) {
|
|
354
|
+
const n = q.triangleCount, src = q._tris;
|
|
355
|
+
const triBox = new Float64Array(n * 6), cen = new Float64Array(n * 3);
|
|
356
|
+
for (let t = 0; t < n; t++) {
|
|
357
|
+
for (let a = 0; a < 3; a++) {
|
|
358
|
+
const x = src[t * 9 + a], y = src[t * 9 + 3 + a], z = src[t * 9 + 6 + a];
|
|
359
|
+
const lo = Math.min(x, y, z), hi = Math.max(x, y, z);
|
|
360
|
+
triBox[t * 6 + a] = lo;
|
|
361
|
+
triBox[t * 6 + 3 + a] = hi;
|
|
362
|
+
cen[t * 3 + a] = (lo + hi) / 2;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const order = new Int32Array(n);
|
|
366
|
+
for (let t = 0; t < n; t++) order[t] = t;
|
|
367
|
+
|
|
368
|
+
const maxNodes = Math.max(1, 2 * n - 1);
|
|
369
|
+
const box = new Float64Array(maxNodes * 6), first = new Int32Array(maxNodes), count = new Int32Array(maxNodes);
|
|
370
|
+
let nodes = 1, depth = 1;
|
|
371
|
+
const binCount = new Int32Array(BINS), binBox = new Float64Array(BINS * 6);
|
|
372
|
+
const rightArea = new Float64Array(BINS), rightCount = new Int32Array(BINS);
|
|
373
|
+
|
|
374
|
+
const work = [[0, 0, n, 1]];
|
|
375
|
+
while (work.length) {
|
|
376
|
+
const [node, start, end, level] = work.pop();
|
|
377
|
+
depth = Math.max(depth, level);
|
|
378
|
+
// Node box and centroid box
|
|
379
|
+
const k = node * 6;
|
|
380
|
+
box.fill(Infinity, k, k + 3);
|
|
381
|
+
box.fill(-Infinity, k + 3, k + 6);
|
|
382
|
+
let cminx = Infinity, cminy = Infinity, cminz = Infinity, cmaxx = -Infinity, cmaxy = -Infinity, cmaxz = -Infinity;
|
|
383
|
+
for (let i = start; i < end; i++) {
|
|
384
|
+
const t = order[i];
|
|
385
|
+
for (let a = 0; a < 3; a++) {
|
|
386
|
+
if (triBox[t * 6 + a] < box[k + a]) box[k + a] = triBox[t * 6 + a];
|
|
387
|
+
if (triBox[t * 6 + 3 + a] > box[k + 3 + a]) box[k + 3 + a] = triBox[t * 6 + 3 + a];
|
|
388
|
+
}
|
|
389
|
+
const cx = cen[t * 3], cy = cen[t * 3 + 1], cz = cen[t * 3 + 2];
|
|
390
|
+
if (cx < cminx) cminx = cx;
|
|
391
|
+
if (cy < cminy) cminy = cy;
|
|
392
|
+
if (cz < cminz) cminz = cz;
|
|
393
|
+
if (cx > cmaxx) cmaxx = cx;
|
|
394
|
+
if (cy > cmaxy) cmaxy = cy;
|
|
395
|
+
if (cz > cmaxz) cmaxz = cz;
|
|
396
|
+
}
|
|
397
|
+
const size = end - start;
|
|
398
|
+
if (size <= LEAF) {
|
|
399
|
+
first[node] = start;
|
|
400
|
+
count[node] = size;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Split along the longest side of the centroids' box
|
|
405
|
+
const ex = cmaxx - cminx, ey = cmaxy - cminy, ez = cmaxz - cminz;
|
|
406
|
+
const axis = ex >= ey && ex >= ez ? 0 : ey >= ez ? 1 : 2;
|
|
407
|
+
const lo = axis === 0 ? cminx : axis === 1 ? cminy : cminz;
|
|
408
|
+
const extent = axis === 0 ? ex : axis === 1 ? ey : ez;
|
|
409
|
+
let mid = -1;
|
|
410
|
+
if (extent > 0) {
|
|
411
|
+
// Surface area heuristic over BINS bins
|
|
412
|
+
const scale = BINS / extent;
|
|
413
|
+
const binOf = (t) => Math.min(BINS - 1, Math.floor((cen[t * 3 + axis] - lo) * scale));
|
|
414
|
+
binCount.fill(0);
|
|
415
|
+
for (let b = 0; b < BINS; b++) {
|
|
416
|
+
binBox.fill(Infinity, b * 6, b * 6 + 3);
|
|
417
|
+
binBox.fill(-Infinity, b * 6 + 3, b * 6 + 6);
|
|
418
|
+
}
|
|
419
|
+
for (let i = start; i < end; i++) {
|
|
420
|
+
const t = order[i], b = binOf(t);
|
|
421
|
+
binCount[b]++;
|
|
422
|
+
for (let a = 0; a < 3; a++) {
|
|
423
|
+
if (triBox[t * 6 + a] < binBox[b * 6 + a]) binBox[b * 6 + a] = triBox[t * 6 + a];
|
|
424
|
+
if (triBox[t * 6 + 3 + a] > binBox[b * 6 + 3 + a]) binBox[b * 6 + 3 + a] = triBox[t * 6 + 3 + a];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Areas from the right, then sweep from the left
|
|
428
|
+
const acc = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
|
|
429
|
+
let c = 0;
|
|
430
|
+
for (let b = BINS - 1; b > 0; b--) {
|
|
431
|
+
grow(acc, binBox, b * 6);
|
|
432
|
+
c += binCount[b];
|
|
433
|
+
rightArea[b] = c ? area(acc) : 0;
|
|
434
|
+
rightCount[b] = c;
|
|
435
|
+
}
|
|
436
|
+
acc.splice(0, 6, Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity);
|
|
437
|
+
c = 0;
|
|
438
|
+
let bestCost = Infinity, bestBin = -1;
|
|
439
|
+
for (let b = 1; b < BINS; b++) {
|
|
440
|
+
grow(acc, binBox, (b - 1) * 6);
|
|
441
|
+
c += binCount[b - 1];
|
|
442
|
+
if (!c || !rightCount[b]) continue;
|
|
443
|
+
const cost = area(acc) * c + rightArea[b] * rightCount[b];
|
|
444
|
+
if (cost < bestCost) {
|
|
445
|
+
bestCost = cost;
|
|
446
|
+
bestBin = b;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (bestBin > 0) {
|
|
450
|
+
// Partition: bins below bestBin to the left
|
|
451
|
+
let i = start, j = end - 1;
|
|
452
|
+
while (i <= j) {
|
|
453
|
+
if (binOf(order[i]) < bestBin) i++;
|
|
454
|
+
else {
|
|
455
|
+
const s = order[i];
|
|
456
|
+
order[i] = order[j];
|
|
457
|
+
order[j--] = s;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
mid = i;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// All centroids at one place, or no useful split: cut the list in half
|
|
464
|
+
if (mid <= start || mid >= end) mid = (start + end) >> 1;
|
|
465
|
+
|
|
466
|
+
const left = nodes;
|
|
467
|
+
nodes += 2;
|
|
468
|
+
first[node] = left;
|
|
469
|
+
count[node] = 0;
|
|
470
|
+
work.push([left + 1, mid, end, level + 1], [left, start, mid, level + 1]);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Triangles in leaf order, so leaves read them in a row
|
|
474
|
+
const tris = new Float64Array(n * 9), corners = new Int32Array(n * 3), faceOf = new Int32Array(n);
|
|
475
|
+
for (let i = 0; i < n; i++) {
|
|
476
|
+
const t = order[i];
|
|
477
|
+
tris.set(src.subarray(t * 9, t * 9 + 9), i * 9);
|
|
478
|
+
corners.set(q._corners.subarray(t * 3, t * 3 + 3), i * 3);
|
|
479
|
+
faceOf[i] = q._faceOf[t];
|
|
480
|
+
}
|
|
481
|
+
q._tris = tris;
|
|
482
|
+
q._corners = corners;
|
|
483
|
+
q._faceOf = faceOf;
|
|
484
|
+
q._box = box.subarray(0, nodes * 6);
|
|
485
|
+
q._first = first.subarray(0, nodes);
|
|
486
|
+
q._count = count.subarray(0, nodes);
|
|
487
|
+
// An empty mesh: one empty leaf whose box nothing enters
|
|
488
|
+
if (!n) q._box.set([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]);
|
|
489
|
+
q._stack = new Int32Array(depth * 2 + 2);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function grow(acc, b, k) {
|
|
493
|
+
for (let a = 0; a < 3; a++) {
|
|
494
|
+
if (b[k + a] < acc[a]) acc[a] = b[k + a];
|
|
495
|
+
if (b[k + 3 + a] > acc[3 + a]) acc[3 + a] = b[k + 3 + a];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function area(b) {
|
|
500
|
+
const x = b[3] - b[0], y = b[4] - b[1], z = b[5] - b[2];
|
|
501
|
+
return x * y + y * z + z * x;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Where the ray o + t·(1/inv) enters box k, within [0, far]; Infinity if it doesn't. */
|
|
505
|
+
function enter(box, k, o, ix, iy, iz, far) {
|
|
506
|
+
let t0 = 0, t1 = far;
|
|
507
|
+
const lx = box[k], ly = box[k + 1], lz = box[k + 2], hx = box[k + 3], hy = box[k + 4], hz = box[k + 5];
|
|
508
|
+
if (ix === Infinity || ix === -Infinity) {
|
|
509
|
+
if (o.x < lx || o.x > hx) return Infinity;
|
|
510
|
+
} else {
|
|
511
|
+
let a = (lx - o.x) * ix, b = (hx - o.x) * ix;
|
|
512
|
+
if (a > b) [a, b] = [b, a];
|
|
513
|
+
if (a > t0) t0 = a;
|
|
514
|
+
if (b < t1) t1 = b;
|
|
515
|
+
}
|
|
516
|
+
if (iy === Infinity || iy === -Infinity) {
|
|
517
|
+
if (o.y < ly || o.y > hy) return Infinity;
|
|
518
|
+
} else {
|
|
519
|
+
let a = (ly - o.y) * iy, b = (hy - o.y) * iy;
|
|
520
|
+
if (a > b) [a, b] = [b, a];
|
|
521
|
+
if (a > t0) t0 = a;
|
|
522
|
+
if (b < t1) t1 = b;
|
|
523
|
+
}
|
|
524
|
+
if (iz === Infinity || iz === -Infinity) {
|
|
525
|
+
if (o.z < lz || o.z > hz) return Infinity;
|
|
526
|
+
} else {
|
|
527
|
+
let a = (lz - o.z) * iz, b = (hz - o.z) * iz;
|
|
528
|
+
if (a > b) [a, b] = [b, a];
|
|
529
|
+
if (a > t0) t0 = a;
|
|
530
|
+
if (b < t1) t1 = b;
|
|
531
|
+
}
|
|
532
|
+
// A little slack, so rays along a box's face aren't lost to rounding
|
|
533
|
+
return t0 <= t1 * (1 + 1e-12) + 1e-12 ? t0 : Infinity;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Squared distance from p to box k (0 inside). */
|
|
537
|
+
function boxDist2(box, k, p) {
|
|
538
|
+
const dx = Math.max(box[k] - p.x, 0, p.x - box[k + 3]);
|
|
539
|
+
const dy = Math.max(box[k + 1] - p.y, 0, p.y - box[k + 4]);
|
|
540
|
+
const dz = Math.max(box[k + 2] - p.z, 0, p.z - box[k + 5]);
|
|
541
|
+
return dx * dx + dy * dy + dz * dz;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Closest point on the triangle at `i` to p (Ericson's method): writes
|
|
546
|
+
* x, y, z and the weights of the 2nd and 3rd corners into `out`, returns
|
|
547
|
+
* the squared distance.
|
|
548
|
+
*/
|
|
549
|
+
function closestOnTriangle(q, i, p, out) {
|
|
550
|
+
const ax = q[i], ay = q[i + 1], az = q[i + 2];
|
|
551
|
+
const abx = q[i + 3] - ax, aby = q[i + 4] - ay, abz = q[i + 5] - az;
|
|
552
|
+
const acx = q[i + 6] - ax, acy = q[i + 7] - ay, acz = q[i + 8] - az;
|
|
553
|
+
const apx = p.x - ax, apy = p.y - ay, apz = p.z - az;
|
|
554
|
+
const d1 = abx * apx + aby * apy + abz * apz, d2 = acx * apx + acy * apy + acz * apz;
|
|
555
|
+
let v, w;
|
|
556
|
+
if (d1 <= 0 && d2 <= 0) {
|
|
557
|
+
v = 0; w = 0;
|
|
558
|
+
} else {
|
|
559
|
+
const bpx = p.x - q[i + 3], bpy = p.y - q[i + 4], bpz = p.z - q[i + 5];
|
|
560
|
+
const d3 = abx * bpx + aby * bpy + abz * bpz, d4 = acx * bpx + acy * bpy + acz * bpz;
|
|
561
|
+
const cpx = p.x - q[i + 6], cpy = p.y - q[i + 7], cpz = p.z - q[i + 8];
|
|
562
|
+
const d5 = abx * cpx + aby * cpy + abz * cpz, d6 = acx * cpx + acy * cpy + acz * cpz;
|
|
563
|
+
const vc = d1 * d4 - d3 * d2, vb = d5 * d2 - d1 * d6, va = d3 * d6 - d5 * d4;
|
|
564
|
+
if (d3 >= 0 && d4 <= d3) {
|
|
565
|
+
v = 1; w = 0;
|
|
566
|
+
} else if (d6 >= 0 && d5 <= d6) {
|
|
567
|
+
v = 0; w = 1;
|
|
568
|
+
} else if (vc <= 0 && d1 >= 0 && d3 <= 0) {
|
|
569
|
+
v = d1 / (d1 - d3); w = 0;
|
|
570
|
+
} else if (vb <= 0 && d2 >= 0 && d6 <= 0) {
|
|
571
|
+
v = 0; w = d2 / (d2 - d6);
|
|
572
|
+
} else if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
|
|
573
|
+
w = (d4 - d3) / (d4 - d3 + (d5 - d6)); v = 1 - w;
|
|
574
|
+
} else {
|
|
575
|
+
const den = 1 / (va + vb + vc);
|
|
576
|
+
v = vb * den; w = vc * den;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const x = ax + abx * v + acx * w, y = ay + aby * v + acy * w, z = az + abz * v + acz * w;
|
|
580
|
+
out[0] = x; out[1] = y; out[2] = z; out[3] = v; out[4] = w;
|
|
581
|
+
return (p.x - x) ** 2 + (p.y - y) ** 2 + (p.z - z) ** 2;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// One-off questions: each builds a query and throws it away. For many
|
|
585
|
+
// questions on one mesh, use meshQuery.
|
|
586
|
+
|
|
587
|
+
/** The first place a ray meets a mesh, or null. Same as `meshQuery(mesh).raycast(...)`. */
|
|
588
|
+
export function raycastMesh(mesh, origin, direction, opts) {
|
|
589
|
+
return meshQuery(mesh).raycast(origin, direction, opts);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Whether a point is inside a closed mesh. Same as `meshQuery(mesh).contains(p)`. */
|
|
593
|
+
export function pointInMesh(mesh, p) {
|
|
594
|
+
return meshQuery(mesh).contains(p);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Distance from a point to a mesh's surface. Same as `meshQuery(mesh).distance(p, maxDistance)`. */
|
|
598
|
+
export function distanceToMesh(mesh, p, maxDistance) {
|
|
599
|
+
return meshQuery(mesh).distance(p, maxDistance);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** The nearest point of a mesh's surface. Same as `meshQuery(mesh).closest(p, opts)`. */
|
|
603
|
+
export function closestPointOnMesh(mesh, p, opts) {
|
|
604
|
+
return meshQuery(mesh).closest(p, opts);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Whether a mesh is closed (watertight): every edge, matched by vertex
|
|
609
|
+
* position (texture seams split vertices), is shared by exactly two faces.
|
|
610
|
+
* Only a closed mesh has an inside.
|
|
611
|
+
*
|
|
612
|
+
* @param {Object} mesh
|
|
613
|
+
* @returns {boolean}
|
|
614
|
+
*/
|
|
615
|
+
export function isClosed(mesh) {
|
|
616
|
+
const v = mesh?.vertices ?? [];
|
|
617
|
+
if (!mesh?.faces?.length || !v.length) return false;
|
|
618
|
+
let size = 0;
|
|
619
|
+
for (const p of v) if (finite(p)) size = Math.max(size, Math.abs(p.x), Math.abs(p.y), Math.abs(p.z));
|
|
620
|
+
const same = samePlace(v, (size || 1) * 1e-7);
|
|
621
|
+
const edges = new Map();
|
|
622
|
+
for (const f of mesh.faces) {
|
|
623
|
+
if (!Array.isArray(f)) return false;
|
|
624
|
+
for (let i = 0; i < f.length; i++) {
|
|
625
|
+
const a = same[f[i]], b = same[f[(i + 1) % f.length]];
|
|
626
|
+
if (a === undefined || b === undefined) return false;
|
|
627
|
+
if (a === b) continue; // a collapsed edge (a sphere's poles)
|
|
628
|
+
const key = a < b ? `${a},${b}` : `${b},${a}`;
|
|
629
|
+
edges.set(key, (edges.get(key) ?? 0) + 1);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (!edges.size) return false;
|
|
633
|
+
for (const n of edges.values()) if (n !== 2) return false;
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Scene rays
|
|
638
|
+
|
|
639
|
+
/** Prepared queries for meshes in scenes, rebuilt when a mesh changes. */
|
|
640
|
+
const cache = new WeakMap();
|
|
641
|
+
|
|
642
|
+
function cachedQuery(mesh) {
|
|
643
|
+
const c = cache.get(mesh);
|
|
644
|
+
if (c && !mesh.dirty && c.vertices === mesh.vertices && c.faces === mesh.faces
|
|
645
|
+
&& c.vertexCount === mesh.vertices.length && c.faceCount === mesh.faces.length) return c.query;
|
|
646
|
+
const query = meshQuery(mesh);
|
|
647
|
+
cache.set(mesh, { query, vertices: mesh.vertices, faces: mesh.faces, vertexCount: mesh.vertices.length, faceCount: mesh.faces.length });
|
|
648
|
+
return query;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Forgets the prepared query `raycastScene` keeps for a mesh, so the next
|
|
653
|
+
* ray rebuilds it. Needed only after changing a mesh's vertices in place
|
|
654
|
+
* once a renderer has already cleared its `dirty` flag.
|
|
655
|
+
*
|
|
656
|
+
* @param {Object} mesh
|
|
657
|
+
*/
|
|
658
|
+
export function clearQueryCache(mesh) {
|
|
659
|
+
if (mesh) cache.delete(mesh);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* The first object a ray meets in a scene, in world space: the 3D version
|
|
664
|
+
* of `pickHit`. Each object's position, rotation, scale and groups are
|
|
665
|
+
* taken into account. Hidden objects, objects with `pickable: false` and
|
|
666
|
+
* objects without faces are skipped.
|
|
667
|
+
*
|
|
668
|
+
* Each mesh's query is prepared once and kept until the mesh changes
|
|
669
|
+
* (new arrays, a different count, or `mesh.dirty`).
|
|
670
|
+
*
|
|
671
|
+
* @param {Object} scene - A Scene, or a list of objects
|
|
672
|
+
* @param {Vec3} origin - In the world
|
|
673
|
+
* @param {Vec3} direction - In the world, any length but zero
|
|
674
|
+
* @param {Object} [opts]
|
|
675
|
+
* @param {(obj:Object) => boolean} [opts.filter] - Return false to skip an object
|
|
676
|
+
* @param {number} [opts.maxDistance=Infinity]
|
|
677
|
+
* @param {"both"|"front"|"back"} [opts.side="both"]
|
|
678
|
+
* @param {Object[]} [opts.objects] - Only these objects (and their children)
|
|
679
|
+
* @returns {Object|null} The hit: object, path, point, local, normal, distance, face, front, triangle, barycentric
|
|
680
|
+
*/
|
|
681
|
+
export function raycastScene(scene, origin, direction, { filter = () => true, maxDistance = Infinity, side = "both", objects } = {}) {
|
|
682
|
+
checkPoint(origin, "raycastScene: origin");
|
|
683
|
+
checkSide(side);
|
|
684
|
+
const d = unitDirection(direction);
|
|
685
|
+
let best = maxDistance, hit = null;
|
|
686
|
+
for (const object of worldObjects(objects ?? scene)) {
|
|
687
|
+
const mesh = object.mesh;
|
|
688
|
+
if (!mesh?.faces?.length || !Array.isArray(mesh.vertices) || !isPickable(object) || !filter(object)) continue;
|
|
689
|
+
const world = object.world;
|
|
690
|
+
if (!(Math.abs(determinantMat4(world)) > 1e-18)) continue; // scaled to nothing
|
|
691
|
+
const inv = invertMat4(world);
|
|
692
|
+
// The ray in the mesh's space; t stays the world distance since d is unit length
|
|
693
|
+
const lo = transformPoint(inv, origin), ld = transformDirection(inv, d);
|
|
694
|
+
const q = cachedQuery(mesh);
|
|
695
|
+
if (!q._cast(lo, ld, best, side)) continue;
|
|
696
|
+
const { t, tri, u, w } = q._hit;
|
|
697
|
+
best = t;
|
|
698
|
+
hit = { object, world, q, tri, u, w, t, lo, ld };
|
|
699
|
+
}
|
|
700
|
+
if (!hit) return null;
|
|
701
|
+
const { object, world, q, tri, u, w, t, lo, ld } = hit;
|
|
702
|
+
const local = { x: lo.x + ld.x * t, y: lo.y + ld.y * t, z: lo.z + ld.z * t };
|
|
703
|
+
const out = q._describe(tri, u, w, { x: origin.x + d.x * t, y: origin.y + d.y * t, z: origin.z + d.z * t }, t, ld);
|
|
704
|
+
out.normal = transformNormal(normalMatrix(world), out.normal);
|
|
705
|
+
return { object, path: ancestors(object), local, ...out };
|
|
706
|
+
}
|