@nakednous/tree 0.0.2 → 0.0.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/README.md +243 -57
- package/dist/index.js +838 -622
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -65,45 +65,36 @@ const _k = Object.freeze([0, 0, -1]);
|
|
|
65
65
|
*
|
|
66
66
|
* Every function uses only stack locals for intermediates (zero shared state).
|
|
67
67
|
* Every mutating function writes to a caller-provided `out` and returns `out`.
|
|
68
|
+
* Returns null on degeneracy (singular matrix, etc.).
|
|
68
69
|
*/
|
|
69
70
|
|
|
70
71
|
|
|
71
72
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
-
// Mat4
|
|
73
|
+
// Mat4 math
|
|
73
74
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
74
75
|
|
|
75
|
-
/** out =
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** out = a * b (column-major) */
|
|
85
|
-
function mat4Mul(out, a, b) {
|
|
86
|
-
const a0=a[0],a1=a[1],a2=a[2],a3=a[3],
|
|
87
|
-
a4=a[4],a5=a[5],a6=a[6],a7=a[7],
|
|
88
|
-
a8=a[8],a9=a[9],a10=a[10],a11=a[11],
|
|
89
|
-
a12=a[12],a13=a[13],a14=a[14],a15=a[15];
|
|
90
|
-
let b0,b1,b2,b3;
|
|
91
|
-
b0=b[0];b1=b[1];b2=b[2];b3=b[3];
|
|
76
|
+
/** out = A · B (column-major, standard math order) */
|
|
77
|
+
function mat4Mul(out, A, B) {
|
|
78
|
+
const a0=A[0],a1=A[1],a2=A[2],a3=A[3],
|
|
79
|
+
a4=A[4],a5=A[5],a6=A[6],a7=A[7],
|
|
80
|
+
a8=A[8],a9=A[9],a10=A[10],a11=A[11],
|
|
81
|
+
a12=A[12],a13=A[13],a14=A[14],a15=A[15];
|
|
82
|
+
let b0=B[0],b1=B[1],b2=B[2],b3=B[3];
|
|
92
83
|
out[0]=a0*b0+a4*b1+a8*b2+a12*b3;
|
|
93
84
|
out[1]=a1*b0+a5*b1+a9*b2+a13*b3;
|
|
94
85
|
out[2]=a2*b0+a6*b1+a10*b2+a14*b3;
|
|
95
86
|
out[3]=a3*b0+a7*b1+a11*b2+a15*b3;
|
|
96
|
-
b0=
|
|
87
|
+
b0=B[4];b1=B[5];b2=B[6];b3=B[7];
|
|
97
88
|
out[4]=a0*b0+a4*b1+a8*b2+a12*b3;
|
|
98
89
|
out[5]=a1*b0+a5*b1+a9*b2+a13*b3;
|
|
99
90
|
out[6]=a2*b0+a6*b1+a10*b2+a14*b3;
|
|
100
91
|
out[7]=a3*b0+a7*b1+a11*b2+a15*b3;
|
|
101
|
-
b0=
|
|
92
|
+
b0=B[8];b1=B[9];b2=B[10];b3=B[11];
|
|
102
93
|
out[8]=a0*b0+a4*b1+a8*b2+a12*b3;
|
|
103
94
|
out[9]=a1*b0+a5*b1+a9*b2+a13*b3;
|
|
104
95
|
out[10]=a2*b0+a6*b1+a10*b2+a14*b3;
|
|
105
96
|
out[11]=a3*b0+a7*b1+a11*b2+a15*b3;
|
|
106
|
-
b0=
|
|
97
|
+
b0=B[12];b1=B[13];b2=B[14];b3=B[15];
|
|
107
98
|
out[12]=a0*b0+a4*b1+a8*b2+a12*b3;
|
|
108
99
|
out[13]=a1*b0+a5*b1+a9*b2+a13*b3;
|
|
109
100
|
out[14]=a2*b0+a6*b1+a10*b2+a14*b3;
|
|
@@ -111,7 +102,7 @@ function mat4Mul(out, a, b) {
|
|
|
111
102
|
return out;
|
|
112
103
|
}
|
|
113
104
|
|
|
114
|
-
/** out = inverse(src).
|
|
105
|
+
/** out = inverse(src). Returns null if singular. */
|
|
115
106
|
function mat4Invert(out, src) {
|
|
116
107
|
const s=src;
|
|
117
108
|
const a00=s[0],a01=s[1],a02=s[2],a03=s[3],
|
|
@@ -202,32 +193,8 @@ function mat4MulPoint(out, m, x, y, z) {
|
|
|
202
193
|
return out;
|
|
203
194
|
}
|
|
204
195
|
|
|
205
|
-
/** out = mat4 * [x,y,z,0] (direction, no translation) */
|
|
206
|
-
function mat4MulDir(out, m, x, y, z) {
|
|
207
|
-
out[0] = m[0]*x + m[4]*y + m[8]*z;
|
|
208
|
-
out[1] = m[1]*x + m[5]*y + m[9]*z;
|
|
209
|
-
out[2] = m[2]*x + m[6]*y + m[10]*z;
|
|
210
|
-
return out;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** out = upper-left 3×3 transposed from mat4 (direction / dMatrix extraction) */
|
|
214
|
-
function mat3FromMat4T(out, m) {
|
|
215
|
-
out[0]=m[0]; out[1]=m[4]; out[2]=m[8];
|
|
216
|
-
out[3]=m[1]; out[4]=m[5]; out[5]=m[9];
|
|
217
|
-
out[6]=m[2]; out[7]=m[6]; out[8]=m[10];
|
|
218
|
-
return out;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** out = mat3 * vec3 */
|
|
222
|
-
function mat3MulVec3(out, m, x, y, z) {
|
|
223
|
-
out[0] = m[0]*x + m[3]*y + m[6]*z;
|
|
224
|
-
out[1] = m[1]*x + m[4]*y + m[7]*z;
|
|
225
|
-
out[2] = m[2]*x + m[5]*y + m[8]*z;
|
|
226
|
-
return out;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
196
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
230
|
-
// Projection queries
|
|
197
|
+
// Projection queries (read scalars from a projection mat4)
|
|
231
198
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
232
199
|
|
|
233
200
|
/** @returns {boolean} true if orthographic */
|
|
@@ -235,8 +202,8 @@ function projIsOrtho(p) { return p[15] !== 0; }
|
|
|
235
202
|
|
|
236
203
|
/**
|
|
237
204
|
* Near plane distance.
|
|
238
|
-
* @param {ArrayLike<number>} p Projection Mat4
|
|
239
|
-
* @param {number} ndcZMin WEBGL (−1) or WEBGPU (0)
|
|
205
|
+
* @param {ArrayLike<number>} p Projection Mat4.
|
|
206
|
+
* @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
|
|
240
207
|
*/
|
|
241
208
|
function projNear(p, ndcZMin) {
|
|
242
209
|
return p[15] === 0
|
|
@@ -289,107 +256,34 @@ function projHfov(p) {
|
|
|
289
256
|
// Derived matrices (convenience)
|
|
290
257
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
291
258
|
|
|
292
|
-
/** out =
|
|
259
|
+
/** out = P · V */
|
|
293
260
|
function mat4PV(out, proj, view) { return mat4Mul(out, proj, view); }
|
|
294
261
|
|
|
295
|
-
/** out =
|
|
262
|
+
/** out = V · M */
|
|
296
263
|
function mat4MV(out, model, view) { return mat4Mul(out, view, model); }
|
|
297
264
|
|
|
298
|
-
/** out = proj * view * model = P · V · M (standard GL) */
|
|
299
|
-
function mat4PMV(out, proj, model, view) {
|
|
300
|
-
// MV = view * model (V · M)
|
|
301
|
-
const t0=view[0],t1=view[1],t2=view[2],t3=view[3],
|
|
302
|
-
t4=view[4],t5=view[5],t6=view[6],t7=view[7],
|
|
303
|
-
t8=view[8],t9=view[9],t10=view[10],t11=view[11],
|
|
304
|
-
t12=view[12],t13=view[13],t14=view[14],t15=view[15];
|
|
305
|
-
let b0,b1,b2,b3;
|
|
306
|
-
b0=model[0];b1=model[1];b2=model[2];b3=model[3];
|
|
307
|
-
const mv0=t0*b0+t4*b1+t8*b2+t12*b3, mv1=t1*b0+t5*b1+t9*b2+t13*b3,
|
|
308
|
-
mv2=t2*b0+t6*b1+t10*b2+t14*b3, mv3=t3*b0+t7*b1+t11*b2+t15*b3;
|
|
309
|
-
b0=model[4];b1=model[5];b2=model[6];b3=model[7];
|
|
310
|
-
const mv4=t0*b0+t4*b1+t8*b2+t12*b3, mv5=t1*b0+t5*b1+t9*b2+t13*b3,
|
|
311
|
-
mv6=t2*b0+t6*b1+t10*b2+t14*b3, mv7=t3*b0+t7*b1+t11*b2+t15*b3;
|
|
312
|
-
b0=model[8];b1=model[9];b2=model[10];b3=model[11];
|
|
313
|
-
const mv8=t0*b0+t4*b1+t8*b2+t12*b3, mv9=t1*b0+t5*b1+t9*b2+t13*b3,
|
|
314
|
-
mv10=t2*b0+t6*b1+t10*b2+t14*b3, mv11=t3*b0+t7*b1+t11*b2+t15*b3;
|
|
315
|
-
b0=model[12];b1=model[13];b2=model[14];b3=model[15];
|
|
316
|
-
const mv12=t0*b0+t4*b1+t8*b2+t12*b3, mv13=t1*b0+t5*b1+t9*b2+t13*b3,
|
|
317
|
-
mv14=t2*b0+t6*b1+t10*b2+t14*b3, mv15=t3*b0+t7*b1+t11*b2+t15*b3;
|
|
318
|
-
// PMV = proj * MV (P · V · M)
|
|
319
|
-
const p0=proj[0],p1=proj[1],p2=proj[2],p3=proj[3],
|
|
320
|
-
p4=proj[4],p5=proj[5],p6=proj[6],p7=proj[7],
|
|
321
|
-
p8=proj[8],p9=proj[9],p10=proj[10],p11=proj[11],
|
|
322
|
-
p12=proj[12],p13=proj[13],p14=proj[14],p15=proj[15];
|
|
323
|
-
out[0]=p0*mv0+p4*mv1+p8*mv2+p12*mv3;
|
|
324
|
-
out[1]=p1*mv0+p5*mv1+p9*mv2+p13*mv3;
|
|
325
|
-
out[2]=p2*mv0+p6*mv1+p10*mv2+p14*mv3;
|
|
326
|
-
out[3]=p3*mv0+p7*mv1+p11*mv2+p15*mv3;
|
|
327
|
-
out[4]=p0*mv4+p4*mv5+p8*mv6+p12*mv7;
|
|
328
|
-
out[5]=p1*mv4+p5*mv5+p9*mv6+p13*mv7;
|
|
329
|
-
out[6]=p2*mv4+p6*mv5+p10*mv6+p14*mv7;
|
|
330
|
-
out[7]=p3*mv4+p7*mv5+p11*mv6+p15*mv7;
|
|
331
|
-
out[8]=p0*mv8+p4*mv9+p8*mv10+p12*mv11;
|
|
332
|
-
out[9]=p1*mv8+p5*mv9+p9*mv10+p13*mv11;
|
|
333
|
-
out[10]=p2*mv8+p6*mv9+p10*mv10+p14*mv11;
|
|
334
|
-
out[11]=p3*mv8+p7*mv9+p11*mv10+p15*mv11;
|
|
335
|
-
out[12]=p0*mv12+p4*mv13+p8*mv14+p12*mv15;
|
|
336
|
-
out[13]=p1*mv12+p5*mv13+p9*mv14+p13*mv15;
|
|
337
|
-
out[14]=p2*mv12+p6*mv13+p10*mv14+p14*mv15;
|
|
338
|
-
out[15]=p3*mv12+p7*mv13+p11*mv14+p15*mv15;
|
|
339
|
-
return out;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
265
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
343
|
-
//
|
|
266
|
+
// Location / Direction transforms
|
|
344
267
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
345
|
-
//
|
|
346
|
-
// FLAT DISPATCH: every from→to pair is a self-contained leaf.
|
|
347
|
-
// No path calls back into mapLocation/mapDirection (no reentrancy).
|
|
348
|
-
// All intermediates are stack locals (zero shared state).
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
// ── Location Transform ───────────────────────────────────────────────────
|
|
352
268
|
|
|
353
269
|
/**
|
|
354
|
-
* Relative transform for locations (points).
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
* p_to = out · p_from
|
|
361
|
-
*
|
|
362
|
-
* @param {ArrayLike<number>} out Destination 4×4 matrix (length 16).
|
|
363
|
-
* @param {ArrayLike<number>} from Source frame transform.
|
|
364
|
-
* @param {ArrayLike<number>} to Destination frame transform.
|
|
365
|
-
* @returns {ArrayLike<number>|null} `out`, or `null` if `to` is singular.
|
|
270
|
+
* Relative transform for locations (points): out = inv(to) · from.
|
|
271
|
+
* @param {ArrayLike<number>} out 16-element destination.
|
|
272
|
+
* @param {ArrayLike<number>} from Source frame transform.
|
|
273
|
+
* @param {ArrayLike<number>} to Destination frame transform.
|
|
274
|
+
* @returns {ArrayLike<number>|null} out, or null if to is singular.
|
|
366
275
|
*/
|
|
367
276
|
function mat4Location(out, from, to) {
|
|
368
277
|
return mat4Invert(out, to) && mat4Mul(out, out, from);
|
|
369
278
|
}
|
|
370
279
|
|
|
371
|
-
// ── Direction Transform ──────────────────────────────────────────────────
|
|
372
|
-
|
|
373
280
|
/**
|
|
374
|
-
* Relative transform for directions (vectors).
|
|
375
|
-
*
|
|
281
|
+
* Relative transform for directions (vectors): out = to₃ · inv(from₃).
|
|
376
282
|
* Uses only the upper-left 3×3 blocks, ignoring translation.
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
* and maps directions as:
|
|
383
|
-
*
|
|
384
|
-
* d_to = out · d_from
|
|
385
|
-
*
|
|
386
|
-
* Note: the final write is transposed so the result matches this module's
|
|
387
|
-
* matrix layout and multiplication convention.
|
|
388
|
-
*
|
|
389
|
-
* @param {ArrayLike<number>} out Destination 3×3 matrix (length 9).
|
|
390
|
-
* @param {ArrayLike<number>} from Source frame transform.
|
|
391
|
-
* @param {ArrayLike<number>} to Destination frame transform.
|
|
392
|
-
* @returns {ArrayLike<number>|null} `out`, or `null` if `from` is singular.
|
|
283
|
+
* @param {ArrayLike<number>} out 9-element destination.
|
|
284
|
+
* @param {ArrayLike<number>} from Source frame transform.
|
|
285
|
+
* @param {ArrayLike<number>} to Destination frame transform.
|
|
286
|
+
* @returns {ArrayLike<number>|null} out, or null if from is singular.
|
|
393
287
|
*/
|
|
394
288
|
function mat3Direction(out, from, to) {
|
|
395
289
|
const a00=from[0], a01=from[1], a02=from[2],
|
|
@@ -401,33 +295,41 @@ function mat3Direction(out, from, to) {
|
|
|
401
295
|
let det=a00*b01+a01*b11+a02*b21;
|
|
402
296
|
if (Math.abs(det) < 1e-12) return null;
|
|
403
297
|
det=1/det;
|
|
404
|
-
const i00=b01*det;
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
const i10=b11*det;
|
|
408
|
-
const i11=(a22*a00-a02*a20)*det;
|
|
409
|
-
const i12=(a02*a10-a12*a00)*det;
|
|
410
|
-
const i20=b21*det;
|
|
411
|
-
const i21=(a01*a20-a21*a00)*det;
|
|
412
|
-
const i22=(a11*a00-a01*a10)*det;
|
|
298
|
+
const i00=b01*det, i01=(a02*a21-a22*a01)*det, i02=(a12*a01-a02*a11)*det;
|
|
299
|
+
const i10=b11*det, i11=(a22*a00-a02*a20)*det, i12=(a02*a10-a12*a00)*det;
|
|
300
|
+
const i20=b21*det, i21=(a01*a20-a21*a00)*det, i22=(a11*a00-a01*a10)*det;
|
|
413
301
|
const t00=to[0], t01=to[1], t02=to[2],
|
|
414
302
|
t10=to[4], t11=to[5], t12=to[6],
|
|
415
303
|
t20=to[8], t21=to[9], t22=to[10];
|
|
416
|
-
const m00=t00*i00+t10*i01+t20*i02;
|
|
417
|
-
const
|
|
418
|
-
const
|
|
419
|
-
const m10=t00*i10+t10*i11+t20*i12;
|
|
420
|
-
const m11=t01*i10+t11*i11+t21*i12;
|
|
421
|
-
const m12=t02*i10+t12*i11+t22*i12;
|
|
422
|
-
const m20=t00*i20+t10*i21+t20*i22;
|
|
423
|
-
const m21=t01*i20+t11*i21+t21*i22;
|
|
424
|
-
const m22=t02*i20+t12*i21+t22*i22;
|
|
304
|
+
const m00=t00*i00+t10*i01+t20*i02, m01=t01*i00+t11*i01+t21*i02, m02=t02*i00+t12*i01+t22*i02;
|
|
305
|
+
const m10=t00*i10+t10*i11+t20*i12, m11=t01*i10+t11*i11+t21*i12, m12=t02*i10+t12*i11+t22*i12;
|
|
306
|
+
const m20=t00*i20+t10*i21+t20*i22, m21=t01*i20+t11*i21+t21*i22, m22=t02*i20+t12*i21+t22*i22;
|
|
425
307
|
out[0]=m00; out[1]=m10; out[2]=m20;
|
|
426
308
|
out[3]=m01; out[4]=m11; out[5]=m21;
|
|
427
309
|
out[6]=m02; out[7]=m12; out[8]=m22;
|
|
428
310
|
return out;
|
|
429
311
|
}
|
|
430
312
|
|
|
313
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
314
|
+
// Space transforms — mapLocation / mapDirection
|
|
315
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
316
|
+
//
|
|
317
|
+
// FLAT DISPATCH: every from→to pair is a self-contained leaf.
|
|
318
|
+
// No path calls back into mapLocation/mapDirection (no reentrancy).
|
|
319
|
+
// All intermediates are stack locals (zero shared state).
|
|
320
|
+
//
|
|
321
|
+
// Matrices bag m:
|
|
322
|
+
// {
|
|
323
|
+
// pMatrix: Float32Array(16) — projection (eye → clip)
|
|
324
|
+
// vMatrix: Float32Array(16) — view (world → eye)
|
|
325
|
+
// eMatrix?: Float32Array(16) — eye (eye → world, inv view); lazy
|
|
326
|
+
// pvMatrix?: Float32Array(16) — P · V; lazy
|
|
327
|
+
// ipvMatrix?:Float32Array(16) — inv(P · V); lazy
|
|
328
|
+
// fromFrame?:Float32Array(16) — MATRIX source frame (custom space)
|
|
329
|
+
// toFrameInv?:Float32Array(16) — inv(MATRIX dest frame)
|
|
330
|
+
// }
|
|
331
|
+
//
|
|
332
|
+
|
|
431
333
|
// ── Location leaf helpers ────────────────────────────────────────────────
|
|
432
334
|
|
|
433
335
|
function _worldToScreen(out, px, py, pz, pv, vp, ndcZMin) {
|
|
@@ -435,45 +337,43 @@ function _worldToScreen(out, px, py, pz, pv, vp, ndcZMin) {
|
|
|
435
337
|
const y = pv[1]*px+pv[5]*py+pv[9]*pz+pv[13];
|
|
436
338
|
const z = pv[2]*px+pv[6]*py+pv[10]*pz+pv[14];
|
|
437
339
|
const w = pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
|
|
438
|
-
|
|
439
|
-
const
|
|
340
|
+
const xi = (w !== 0 && w !== 1) ? x/w : x;
|
|
341
|
+
const yi = (w !== 0 && w !== 1) ? y/w : y;
|
|
342
|
+
const zi = (w !== 0 && w !== 1) ? z/w : z;
|
|
440
343
|
const ndcZRange = 1 - ndcZMin;
|
|
441
|
-
out[0] = (
|
|
442
|
-
out[1] = (
|
|
443
|
-
out[2] = (
|
|
344
|
+
out[0] = (xi*0.5+0.5)*vp[2]+vp[0];
|
|
345
|
+
out[1] = (yi*0.5+0.5)*vp[3]+vp[1];
|
|
346
|
+
out[2] = (zi - ndcZMin) / ndcZRange;
|
|
444
347
|
return out;
|
|
445
348
|
}
|
|
446
349
|
|
|
447
350
|
function _screenToWorld(out, px, py, pz, ipv, vp, ndcZMin) {
|
|
448
|
-
const sx=(px-vp[0])/vp[2], sy=(py-vp[1])/vp[3];
|
|
449
|
-
const nx=sx*2-1, ny=sy*2-1;
|
|
450
351
|
const ndcZRange = 1 - ndcZMin;
|
|
352
|
+
const nx = ((px-vp[0])/vp[2])*2-1;
|
|
353
|
+
const ny = ((py-vp[1])/vp[3])*2-1;
|
|
451
354
|
const nz = pz * ndcZRange + ndcZMin;
|
|
452
|
-
const x=ipv[0]*nx+ipv[4]*ny+ipv[8]*nz+ipv[12];
|
|
453
|
-
const y=ipv[1]*nx+ipv[5]*ny+ipv[9]*nz+ipv[13];
|
|
454
|
-
const z=ipv[2]*nx+ipv[6]*ny+ipv[10]*nz+ipv[14];
|
|
455
|
-
const w=ipv[3]*nx+ipv[7]*ny+ipv[11]*nz+ipv[15];
|
|
456
|
-
if (w === 0) { out[0]=px; out[1]=py; out[2]=pz; return out; }
|
|
355
|
+
const x = ipv[0]*nx+ipv[4]*ny+ipv[8]*nz+ipv[12];
|
|
356
|
+
const y = ipv[1]*nx+ipv[5]*ny+ipv[9]*nz+ipv[13];
|
|
357
|
+
const z = ipv[2]*nx+ipv[6]*ny+ipv[10]*nz+ipv[14];
|
|
358
|
+
const w = ipv[3]*nx+ipv[7]*ny+ipv[11]*nz+ipv[15];
|
|
457
359
|
out[0]=x/w; out[1]=y/w; out[2]=z/w;
|
|
458
360
|
return out;
|
|
459
361
|
}
|
|
460
362
|
|
|
461
363
|
function _worldToNDC(out, px, py, pz, pv) {
|
|
462
|
-
const x=pv[0]*px+pv[4]*py+pv[8]*pz+pv[12];
|
|
463
|
-
const y=pv[1]*px+pv[5]*py+pv[9]*pz+pv[13];
|
|
464
|
-
const z=pv[2]*px+pv[6]*py+pv[10]*pz+pv[14];
|
|
465
|
-
const w=pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
|
|
466
|
-
if (w === 0) { out[0]=px; out[1]=py; out[2]=pz; return out; }
|
|
364
|
+
const x = pv[0]*px+pv[4]*py+pv[8]*pz+pv[12];
|
|
365
|
+
const y = pv[1]*px+pv[5]*py+pv[9]*pz+pv[13];
|
|
366
|
+
const z = pv[2]*px+pv[6]*py+pv[10]*pz+pv[14];
|
|
367
|
+
const w = pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
|
|
467
368
|
out[0]=x/w; out[1]=y/w; out[2]=z/w;
|
|
468
369
|
return out;
|
|
469
370
|
}
|
|
470
371
|
|
|
471
372
|
function _ndcToWorld(out, px, py, pz, ipv) {
|
|
472
|
-
const x=ipv[0]*px+ipv[4]*py+ipv[8]*pz+ipv[12];
|
|
473
|
-
const y=ipv[1]*px+ipv[5]*py+ipv[9]*pz+ipv[13];
|
|
474
|
-
const z=ipv[2]*px+ipv[6]*py+ipv[10]*pz+ipv[14];
|
|
475
|
-
const w=ipv[3]*px+ipv[7]*py+ipv[11]*pz+ipv[15];
|
|
476
|
-
if (w === 0) { out[0]=px; out[1]=py; out[2]=pz; return out; }
|
|
373
|
+
const x = ipv[0]*px+ipv[4]*py+ipv[8]*pz+ipv[12];
|
|
374
|
+
const y = ipv[1]*px+ipv[5]*py+ipv[9]*pz+ipv[13];
|
|
375
|
+
const z = ipv[2]*px+ipv[6]*py+ipv[10]*pz+ipv[14];
|
|
376
|
+
const w = ipv[3]*px+ipv[7]*py+ipv[11]*pz+ipv[15];
|
|
477
377
|
out[0]=x/w; out[1]=y/w; out[2]=z/w;
|
|
478
378
|
return out;
|
|
479
379
|
}
|
|
@@ -494,12 +394,11 @@ function _ndcToScreen(out, px, py, pz, vp, ndcZMin) {
|
|
|
494
394
|
return out;
|
|
495
395
|
}
|
|
496
396
|
|
|
497
|
-
// ──
|
|
397
|
+
// ── _ensurePV — return pvMatrix from bag, computing inline if absent ──────
|
|
498
398
|
|
|
499
399
|
function _ensurePV(m) {
|
|
500
|
-
if (m.
|
|
501
|
-
|
|
502
|
-
const p = m.proj, v = m.view;
|
|
400
|
+
if (m.pvMatrix) return m.pvMatrix;
|
|
401
|
+
const p = m.pMatrix, v = m.vMatrix;
|
|
503
402
|
return [
|
|
504
403
|
p[0]*v[0]+p[4]*v[1]+p[8]*v[2]+p[12]*v[3],
|
|
505
404
|
p[1]*v[0]+p[5]*v[1]+p[9]*v[2]+p[13]*v[3],
|
|
@@ -523,27 +422,27 @@ function _ensurePV(m) {
|
|
|
523
422
|
/**
|
|
524
423
|
* Map a point between coordinate spaces.
|
|
525
424
|
*
|
|
526
|
-
* @param {Vec3} out
|
|
527
|
-
* @param {number} px,py,pz
|
|
528
|
-
* @param {string} from
|
|
529
|
-
* @param {string} to
|
|
530
|
-
* @param {object} m
|
|
531
|
-
*
|
|
532
|
-
* @param {Vec4} vp
|
|
533
|
-
* @param {number} ndcZMin
|
|
425
|
+
* @param {Vec3} out Result written here.
|
|
426
|
+
* @param {number} px,py,pz Input point.
|
|
427
|
+
* @param {string} from Source space constant.
|
|
428
|
+
* @param {string} to Target space constant.
|
|
429
|
+
* @param {object} m Matrices bag:
|
|
430
|
+
* { pMatrix, vMatrix, eMatrix?, pvMatrix?, ipvMatrix?, fromFrame?, toFrameInv? }
|
|
431
|
+
* @param {Vec4} vp Viewport [x, y, width, height].
|
|
432
|
+
* @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
|
|
534
433
|
*/
|
|
535
434
|
function mapLocation(out, px, py, pz, from, to, m, vp, ndcZMin) {
|
|
536
435
|
// WORLD ↔ SCREEN
|
|
537
436
|
if (from === WORLD && to === SCREEN)
|
|
538
437
|
return _worldToScreen(out, px,py,pz, _ensurePV(m), vp, ndcZMin);
|
|
539
438
|
if (from === SCREEN && to === WORLD)
|
|
540
|
-
return _screenToWorld(out, px,py,pz, m.
|
|
439
|
+
return _screenToWorld(out, px,py,pz, m.ipvMatrix, vp, ndcZMin);
|
|
541
440
|
|
|
542
441
|
// WORLD ↔ NDC
|
|
543
442
|
if (from === WORLD && to === NDC)
|
|
544
443
|
return _worldToNDC(out, px,py,pz, _ensurePV(m));
|
|
545
444
|
if (from === NDC && to === WORLD)
|
|
546
|
-
return _ndcToWorld(out, px,py,pz, m.
|
|
445
|
+
return _ndcToWorld(out, px,py,pz, m.ipvMatrix);
|
|
547
446
|
|
|
548
447
|
// SCREEN ↔ NDC
|
|
549
448
|
if (from === SCREEN && to === NDC)
|
|
@@ -553,34 +452,36 @@ function mapLocation(out, px, py, pz, from, to, m, vp, ndcZMin) {
|
|
|
553
452
|
|
|
554
453
|
// WORLD ↔ EYE
|
|
555
454
|
if (from === WORLD && to === EYE)
|
|
556
|
-
return mat4MulPoint(out, m.
|
|
455
|
+
return mat4MulPoint(out, m.vMatrix, px,py,pz);
|
|
557
456
|
if (from === EYE && to === WORLD)
|
|
558
|
-
return mat4MulPoint(out, m.
|
|
457
|
+
return mat4MulPoint(out, m.eMatrix, px,py,pz);
|
|
559
458
|
|
|
560
|
-
// EYE ↔ SCREEN
|
|
459
|
+
// EYE ↔ SCREEN
|
|
561
460
|
if (from === EYE && to === SCREEN) {
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
461
|
+
const e = m.eMatrix;
|
|
462
|
+
const ex=e[0]*px+e[4]*py+e[8]*pz+e[12],
|
|
463
|
+
ey=e[1]*px+e[5]*py+e[9]*pz+e[13],
|
|
464
|
+
ez=e[2]*px+e[6]*py+e[10]*pz+e[14];
|
|
565
465
|
return _worldToScreen(out, ex,ey,ez, _ensurePV(m), vp, ndcZMin);
|
|
566
466
|
}
|
|
567
467
|
if (from === SCREEN && to === EYE) {
|
|
568
|
-
_screenToWorld(out, px,py,pz, m.
|
|
468
|
+
_screenToWorld(out, px,py,pz, m.ipvMatrix, vp, ndcZMin);
|
|
569
469
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
570
|
-
return mat4MulPoint(out, m.
|
|
470
|
+
return mat4MulPoint(out, m.vMatrix, wx,wy,wz);
|
|
571
471
|
}
|
|
572
472
|
|
|
573
|
-
// EYE ↔ NDC
|
|
473
|
+
// EYE ↔ NDC
|
|
574
474
|
if (from === EYE && to === NDC) {
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
475
|
+
const e = m.eMatrix;
|
|
476
|
+
const ex=e[0]*px+e[4]*py+e[8]*pz+e[12],
|
|
477
|
+
ey=e[1]*px+e[5]*py+e[9]*pz+e[13],
|
|
478
|
+
ez=e[2]*px+e[6]*py+e[10]*pz+e[14];
|
|
578
479
|
return _worldToNDC(out, ex,ey,ez, _ensurePV(m));
|
|
579
480
|
}
|
|
580
481
|
if (from === NDC && to === EYE) {
|
|
581
|
-
_ndcToWorld(out, px,py,pz, m.
|
|
482
|
+
_ndcToWorld(out, px,py,pz, m.ipvMatrix);
|
|
582
483
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
583
|
-
return mat4MulPoint(out, m.
|
|
484
|
+
return mat4MulPoint(out, m.vMatrix, wx,wy,wz);
|
|
584
485
|
}
|
|
585
486
|
|
|
586
487
|
// MATRIX (custom frame) ↔ WORLD
|
|
@@ -591,49 +492,54 @@ function mapLocation(out, px, py, pz, from, to, m, vp, ndcZMin) {
|
|
|
591
492
|
|
|
592
493
|
// MATRIX ↔ EYE
|
|
593
494
|
if (from === MATRIX && to === EYE) {
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
495
|
+
const f = m.fromFrame;
|
|
496
|
+
const fx=f[0]*px+f[4]*py+f[8]*pz+f[12],
|
|
497
|
+
fy=f[1]*px+f[5]*py+f[9]*pz+f[13],
|
|
498
|
+
fz=f[2]*px+f[6]*py+f[10]*pz+f[14];
|
|
499
|
+
return mat4MulPoint(out, m.vMatrix, fx,fy,fz);
|
|
598
500
|
}
|
|
599
501
|
if (from === EYE && to === MATRIX) {
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
502
|
+
const e = m.eMatrix;
|
|
503
|
+
const ex=e[0]*px+e[4]*py+e[8]*pz+e[12],
|
|
504
|
+
ey=e[1]*px+e[5]*py+e[9]*pz+e[13],
|
|
505
|
+
ez=e[2]*px+e[6]*py+e[10]*pz+e[14];
|
|
603
506
|
return mat4MulPoint(out, m.toFrameInv, ex,ey,ez);
|
|
604
507
|
}
|
|
605
508
|
|
|
606
509
|
// MATRIX ↔ SCREEN
|
|
607
510
|
if (from === MATRIX && to === SCREEN) {
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
511
|
+
const f = m.fromFrame;
|
|
512
|
+
const fx=f[0]*px+f[4]*py+f[8]*pz+f[12],
|
|
513
|
+
fy=f[1]*px+f[5]*py+f[9]*pz+f[13],
|
|
514
|
+
fz=f[2]*px+f[6]*py+f[10]*pz+f[14];
|
|
611
515
|
return _worldToScreen(out, fx,fy,fz, _ensurePV(m), vp, ndcZMin);
|
|
612
516
|
}
|
|
613
517
|
if (from === SCREEN && to === MATRIX) {
|
|
614
|
-
_screenToWorld(out, px,py,pz, m.
|
|
518
|
+
_screenToWorld(out, px,py,pz, m.ipvMatrix, vp, ndcZMin);
|
|
615
519
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
616
520
|
return mat4MulPoint(out, m.toFrameInv, wx,wy,wz);
|
|
617
521
|
}
|
|
618
522
|
|
|
619
523
|
// MATRIX ↔ NDC
|
|
620
524
|
if (from === MATRIX && to === NDC) {
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
525
|
+
const f = m.fromFrame;
|
|
526
|
+
const fx=f[0]*px+f[4]*py+f[8]*pz+f[12],
|
|
527
|
+
fy=f[1]*px+f[5]*py+f[9]*pz+f[13],
|
|
528
|
+
fz=f[2]*px+f[6]*py+f[10]*pz+f[14];
|
|
624
529
|
return _worldToNDC(out, fx,fy,fz, _ensurePV(m));
|
|
625
530
|
}
|
|
626
531
|
if (from === NDC && to === MATRIX) {
|
|
627
|
-
_ndcToWorld(out, px,py,pz, m.
|
|
532
|
+
_ndcToWorld(out, px,py,pz, m.ipvMatrix);
|
|
628
533
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
629
534
|
return mat4MulPoint(out, m.toFrameInv, wx,wy,wz);
|
|
630
535
|
}
|
|
631
536
|
|
|
632
537
|
// MATRIX ↔ MATRIX
|
|
633
538
|
if (from === MATRIX && to === MATRIX) {
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
539
|
+
const f = m.fromFrame;
|
|
540
|
+
const fx=f[0]*px+f[4]*py+f[8]*pz+f[12],
|
|
541
|
+
fy=f[1]*px+f[5]*py+f[9]*pz+f[13],
|
|
542
|
+
fz=f[2]*px+f[6]*py+f[10]*pz+f[14];
|
|
637
543
|
return mat4MulPoint(out, m.toFrameInv, fx,fy,fz);
|
|
638
544
|
}
|
|
639
545
|
|
|
@@ -644,7 +550,7 @@ function mapLocation(out, px, py, pz, from, to, m, vp, ndcZMin) {
|
|
|
644
550
|
|
|
645
551
|
// ── Direction helpers ────────────────────────────────────────────────────
|
|
646
552
|
|
|
647
|
-
/** Apply the 3×3 linear part of a mat4 (rotation/scale, no translation) */
|
|
553
|
+
/** Apply the 3×3 linear part of a mat4 (rotation/scale, no translation). */
|
|
648
554
|
function _applyDir(out, mat, dx, dy, dz) {
|
|
649
555
|
out[0]=mat[0]*dx+mat[4]*dy+mat[8]*dz;
|
|
650
556
|
out[1]=mat[1]*dx+mat[5]*dy+mat[9]*dz;
|
|
@@ -652,37 +558,24 @@ function _applyDir(out, mat, dx, dy, dz) {
|
|
|
652
558
|
return out;
|
|
653
559
|
}
|
|
654
560
|
|
|
655
|
-
/**
|
|
656
|
-
* World→Screen direction. Self-contained: reads proj scalars + view mat.
|
|
657
|
-
* The existing p5.tree code nested _direction and _location calls here;
|
|
658
|
-
* this version inlines all math with stack locals.
|
|
659
|
-
*/
|
|
660
561
|
function _worldToScreenDir(out, dx, dy, dz, proj, view, vpW, vpH, ndcZMin) {
|
|
661
|
-
// 1. World → Eye direction: R · d (standard column-major mat × vec)
|
|
662
562
|
const edx = view[0]*dx + view[4]*dy + view[8]*dz;
|
|
663
563
|
const edy = view[1]*dx + view[5]*dy + view[9]*dz;
|
|
664
564
|
const edz = view[2]*dx + view[6]*dy + view[10]*dz;
|
|
665
|
-
|
|
666
565
|
const isPersp = proj[15] === 0;
|
|
667
566
|
let sdx = edx, sdy = edy;
|
|
668
|
-
|
|
669
567
|
if (isPersp) {
|
|
670
|
-
|
|
671
|
-
// view * [0,0,0,1] = column 3 of view
|
|
672
|
-
const zEye = view[8]*0 + view[9]*0 + view[10]*0 + view[14]; // = view[14]
|
|
568
|
+
const zEye = view[14];
|
|
673
569
|
const halfTan = Math.tan(projFov(proj) / 2);
|
|
674
570
|
const k = Math.abs(zEye * halfTan);
|
|
675
571
|
const pixPerUnit = vpH / (2 * k);
|
|
676
572
|
sdx *= pixPerUnit;
|
|
677
573
|
sdy *= pixPerUnit;
|
|
678
574
|
} else {
|
|
679
|
-
// Ortho: pixels per world unit along X/Y
|
|
680
575
|
const orthoW = Math.abs(projRight(proj, ndcZMin) - projLeft(proj, ndcZMin));
|
|
681
576
|
sdx *= vpW / orthoW;
|
|
682
577
|
sdy *= vpH / Math.abs(projTop(proj, ndcZMin) - projBottom(proj, ndcZMin));
|
|
683
578
|
}
|
|
684
|
-
|
|
685
|
-
// Z: map eye-space dz to screen-space dz
|
|
686
579
|
const near = projNear(proj, ndcZMin), far = projFar(proj);
|
|
687
580
|
const depthRange = near - far;
|
|
688
581
|
let sdz;
|
|
@@ -691,7 +584,6 @@ function _worldToScreenDir(out, dx, dy, dz, proj, view, vpW, vpH, ndcZMin) {
|
|
|
691
584
|
} else {
|
|
692
585
|
sdz = edz / (depthRange / (Math.abs(projRight(proj, ndcZMin) - projLeft(proj, ndcZMin)) / vpW));
|
|
693
586
|
}
|
|
694
|
-
|
|
695
587
|
out[0] = sdx; out[1] = sdy; out[2] = sdz;
|
|
696
588
|
return out;
|
|
697
589
|
}
|
|
@@ -699,7 +591,6 @@ function _worldToScreenDir(out, dx, dy, dz, proj, view, vpW, vpH, ndcZMin) {
|
|
|
699
591
|
function _screenToWorldDir(out, dx, dy, dz, proj, view, eye, vpW, vpH, ndcZMin) {
|
|
700
592
|
const isPersp = proj[15] === 0;
|
|
701
593
|
let edx = dx, edy = dy;
|
|
702
|
-
|
|
703
594
|
if (isPersp) {
|
|
704
595
|
const zEye = view[14];
|
|
705
596
|
const halfTan = Math.tan(projFov(proj) / 2);
|
|
@@ -711,7 +602,6 @@ function _screenToWorldDir(out, dx, dy, dz, proj, view, eye, vpW, vpH, ndcZMin)
|
|
|
711
602
|
edx *= orthoW / vpW;
|
|
712
603
|
edy *= Math.abs(projTop(proj, ndcZMin) - projBottom(proj, ndcZMin)) / vpH;
|
|
713
604
|
}
|
|
714
|
-
|
|
715
605
|
const near = projNear(proj, ndcZMin), far = projFar(proj);
|
|
716
606
|
const depthRange = near - far;
|
|
717
607
|
let edz;
|
|
@@ -720,8 +610,6 @@ function _screenToWorldDir(out, dx, dy, dz, proj, view, eye, vpW, vpH, ndcZMin)
|
|
|
720
610
|
} else {
|
|
721
611
|
edz = dz * (depthRange / (Math.abs(projRight(proj, ndcZMin) - projLeft(proj, ndcZMin)) / vpW));
|
|
722
612
|
}
|
|
723
|
-
|
|
724
|
-
// Eye → World direction (dMatrix = upper-left 3×3 of eye = inv(view))
|
|
725
613
|
_applyDir(out, eye, edx, edy, edz);
|
|
726
614
|
return out;
|
|
727
615
|
}
|
|
@@ -743,21 +631,21 @@ function _ndcToScreenDir(out, dx, dy, dz, vpW, vpH, ndcZMin) {
|
|
|
743
631
|
}
|
|
744
632
|
|
|
745
633
|
/**
|
|
746
|
-
* Map a direction between coordinate spaces.
|
|
747
|
-
* Same
|
|
634
|
+
* Map a direction vector between coordinate spaces.
|
|
635
|
+
* Same bag contract as mapLocation.
|
|
748
636
|
*/
|
|
749
637
|
function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
750
638
|
const vpW = Math.abs(vp[2]), vpH = Math.abs(vp[3]);
|
|
751
639
|
|
|
752
|
-
// EYE ↔ WORLD (most common
|
|
753
|
-
if (from === EYE && to === WORLD) return _applyDir(out, m.
|
|
754
|
-
if (from === WORLD && to === EYE) return _applyDir(out, m.
|
|
640
|
+
// EYE ↔ WORLD (most common)
|
|
641
|
+
if (from === EYE && to === WORLD) return _applyDir(out, m.eMatrix, dx, dy, dz);
|
|
642
|
+
if (from === WORLD && to === EYE) return _applyDir(out, m.vMatrix, dx, dy, dz);
|
|
755
643
|
|
|
756
644
|
// WORLD ↔ SCREEN
|
|
757
645
|
if (from === WORLD && to === SCREEN)
|
|
758
|
-
return _worldToScreenDir(out, dx,dy,dz, m.
|
|
646
|
+
return _worldToScreenDir(out, dx,dy,dz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
|
|
759
647
|
if (from === SCREEN && to === WORLD)
|
|
760
|
-
return _screenToWorldDir(out, dx,dy,dz, m.
|
|
648
|
+
return _screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.vMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
761
649
|
|
|
762
650
|
// SCREEN ↔ NDC
|
|
763
651
|
if (from === SCREEN && to === NDC)
|
|
@@ -765,45 +653,44 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
765
653
|
if (from === NDC && to === SCREEN)
|
|
766
654
|
return _ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
767
655
|
|
|
768
|
-
// WORLD ↔ NDC
|
|
656
|
+
// WORLD ↔ NDC
|
|
769
657
|
if (from === WORLD && to === NDC) {
|
|
770
|
-
_worldToScreenDir(out, dx,dy,dz, m.
|
|
658
|
+
_worldToScreenDir(out, dx,dy,dz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
|
|
771
659
|
const sx=out[0],sy=out[1],sz=out[2];
|
|
772
660
|
return _screenToNDCDir(out, sx,sy,sz, vpW, vpH, ndcZMin);
|
|
773
661
|
}
|
|
774
662
|
if (from === NDC && to === WORLD) {
|
|
775
663
|
_ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
776
664
|
const sx=out[0],sy=out[1],sz=out[2];
|
|
777
|
-
return _screenToWorldDir(out, sx,sy,sz, m.
|
|
665
|
+
return _screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.vMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
778
666
|
}
|
|
779
667
|
|
|
780
668
|
// EYE ↔ SCREEN
|
|
781
669
|
if (from === EYE && to === SCREEN) {
|
|
782
|
-
|
|
783
|
-
_applyDir(out, m.eye, dx,dy,dz);
|
|
670
|
+
_applyDir(out, m.eMatrix, dx,dy,dz);
|
|
784
671
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
785
|
-
return _worldToScreenDir(out, wx,wy,wz, m.
|
|
672
|
+
return _worldToScreenDir(out, wx,wy,wz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
|
|
786
673
|
}
|
|
787
674
|
if (from === SCREEN && to === EYE) {
|
|
788
|
-
_screenToWorldDir(out, dx,dy,dz, m.
|
|
675
|
+
_screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.vMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
789
676
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
790
|
-
return _applyDir(out, m.
|
|
677
|
+
return _applyDir(out, m.vMatrix, wx,wy,wz);
|
|
791
678
|
}
|
|
792
679
|
|
|
793
680
|
// EYE ↔ NDC
|
|
794
681
|
if (from === EYE && to === NDC) {
|
|
795
|
-
_applyDir(out, m.
|
|
682
|
+
_applyDir(out, m.eMatrix, dx,dy,dz);
|
|
796
683
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
797
|
-
_worldToScreenDir(out, wx,wy,wz, m.
|
|
684
|
+
_worldToScreenDir(out, wx,wy,wz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
|
|
798
685
|
const sx=out[0],sy=out[1],sz=out[2];
|
|
799
686
|
return _screenToNDCDir(out, sx,sy,sz, vpW, vpH, ndcZMin);
|
|
800
687
|
}
|
|
801
688
|
if (from === NDC && to === EYE) {
|
|
802
689
|
_ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
803
690
|
const sx=out[0],sy=out[1],sz=out[2];
|
|
804
|
-
_screenToWorldDir(out, sx,sy,sz, m.
|
|
691
|
+
_screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.vMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
805
692
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
806
|
-
return _applyDir(out, m.
|
|
693
|
+
return _applyDir(out, m.vMatrix, wx,wy,wz);
|
|
807
694
|
}
|
|
808
695
|
|
|
809
696
|
// MATRIX ↔ WORLD
|
|
@@ -814,10 +701,10 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
814
701
|
if (from === MATRIX && to === EYE) {
|
|
815
702
|
_applyDir(out, m.fromFrame, dx,dy,dz);
|
|
816
703
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
817
|
-
return _applyDir(out, m.
|
|
704
|
+
return _applyDir(out, m.vMatrix, wx,wy,wz);
|
|
818
705
|
}
|
|
819
706
|
if (from === EYE && to === MATRIX) {
|
|
820
|
-
_applyDir(out, m.
|
|
707
|
+
_applyDir(out, m.eMatrix, dx,dy,dz);
|
|
821
708
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
822
709
|
return _applyDir(out, m.toFrameInv, wx,wy,wz);
|
|
823
710
|
}
|
|
@@ -826,10 +713,10 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
826
713
|
if (from === MATRIX && to === SCREEN) {
|
|
827
714
|
_applyDir(out, m.fromFrame, dx,dy,dz);
|
|
828
715
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
829
|
-
return _worldToScreenDir(out, wx,wy,wz, m.
|
|
716
|
+
return _worldToScreenDir(out, wx,wy,wz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
|
|
830
717
|
}
|
|
831
718
|
if (from === SCREEN && to === MATRIX) {
|
|
832
|
-
_screenToWorldDir(out, dx,dy,dz, m.
|
|
719
|
+
_screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.vMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
833
720
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
834
721
|
return _applyDir(out, m.toFrameInv, wx,wy,wz);
|
|
835
722
|
}
|
|
@@ -838,14 +725,14 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
838
725
|
if (from === MATRIX && to === NDC) {
|
|
839
726
|
_applyDir(out, m.fromFrame, dx,dy,dz);
|
|
840
727
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
841
|
-
_worldToScreenDir(out, wx,wy,wz, m.
|
|
728
|
+
_worldToScreenDir(out, wx,wy,wz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
|
|
842
729
|
const sx=out[0],sy=out[1],sz=out[2];
|
|
843
730
|
return _screenToNDCDir(out, sx,sy,sz, vpW, vpH, ndcZMin);
|
|
844
731
|
}
|
|
845
732
|
if (from === NDC && to === MATRIX) {
|
|
846
733
|
_ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
847
734
|
const sx=out[0],sy=out[1],sz=out[2];
|
|
848
|
-
_screenToWorldDir(out, sx,sy,sz, m.
|
|
735
|
+
_screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.vMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
849
736
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
850
737
|
return _applyDir(out, m.toFrameInv, wx,wy,wz);
|
|
851
738
|
}
|
|
@@ -868,9 +755,9 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
868
755
|
|
|
869
756
|
/**
|
|
870
757
|
* World-units-per-pixel at a given eye-space Z depth.
|
|
871
|
-
* @param {ArrayLike<number>} proj Projection
|
|
758
|
+
* @param {ArrayLike<number>} proj Projection mat4.
|
|
872
759
|
* @param {number} vpH Viewport height (pixels).
|
|
873
|
-
* @param {number} eyeZ Eye-space Z
|
|
760
|
+
* @param {number} eyeZ Eye-space Z (negative for in-front-of camera).
|
|
874
761
|
* @param {number} ndcZMin WEBGL or WEBGPU.
|
|
875
762
|
*/
|
|
876
763
|
function pixelRatio(proj, vpH, eyeZ, ndcZMin) {
|
|
@@ -881,58 +768,62 @@ function pixelRatio(proj, vpH, eyeZ, ndcZMin) {
|
|
|
881
768
|
}
|
|
882
769
|
|
|
883
770
|
/**
|
|
884
|
-
* @file Pure quaternion/spline math +
|
|
771
|
+
* @file Pure quaternion/spline math + track state machines.
|
|
885
772
|
* @module tree/track
|
|
886
773
|
* @license GPL-3.0-only
|
|
887
774
|
*
|
|
888
775
|
* Zero dependencies. No p5, DOM, WebGL, or WebGPU usage.
|
|
889
|
-
* All quaternion operations use flat [x,y,z,w] arrays (w-last = glTF layout).
|
|
890
|
-
* PoseTrack is a pure state machine that stores {pos,rot,scl} keyframes
|
|
891
|
-
* and advances a cursor via tick().
|
|
892
776
|
*
|
|
893
777
|
* ── Exports ──────────────────────────────────────────────────────────────────
|
|
894
778
|
* Quaternion helpers
|
|
895
|
-
* qSet qCopy qDot qNormalize qNegate qMul qSlerp
|
|
779
|
+
* qSet qCopy qDot qNormalize qNegate qMul qSlerp qNlerp
|
|
896
780
|
* qFromAxisAngle qFromLookDir qFromRotMat3x3 qFromMat4 qToMat4
|
|
897
781
|
* quatToAxisAngle
|
|
898
782
|
* Spline / vector helpers
|
|
899
783
|
* catmullRomVec3 lerpVec3
|
|
900
784
|
* Transform / mat4 helpers
|
|
901
785
|
* transformToMat4 mat4ToTransform
|
|
902
|
-
*
|
|
903
|
-
* PoseTrack
|
|
786
|
+
* Tracks
|
|
787
|
+
* PoseTrack — { pos, rot, scl } TRS keyframes
|
|
788
|
+
* CameraTrack — { eye, center, up } lookat keyframes
|
|
789
|
+
*
|
|
790
|
+
* ── Class hierarchy ───────────────────────────────────────────────────────────
|
|
791
|
+
* Track (unexported, never instantiated directly)
|
|
792
|
+
* └── PoseTrack (exported)
|
|
793
|
+
* └── CameraTrack (exported)
|
|
794
|
+
*
|
|
795
|
+
* Track holds all transport machinery: cursor, play/stop/seek/tick,
|
|
796
|
+
* hooks, rate semantics. Subclasses add only keyframe storage and
|
|
797
|
+
* add() / eval() for their respective data shape.
|
|
904
798
|
*
|
|
905
799
|
* ── Hook architecture ─────────────────────────────────────────────────────────
|
|
906
800
|
* _onActivate / _onDeactivate — lib-space (underscore, set by host layer)
|
|
907
|
-
* Fire
|
|
908
|
-
* Used by the addon to register/unregister from the draw-loop tick set.
|
|
801
|
+
* Fire on playing transitions: false→true / true→false.
|
|
909
802
|
*
|
|
910
|
-
* onPlay / onEnd / onStop — user-space (public
|
|
911
|
-
* onPlay : fires in play()
|
|
912
|
-
* onEnd : fires in tick()
|
|
913
|
-
* onStop : fires in stop()
|
|
914
|
-
*
|
|
915
|
-
* onEnd and onStop are mutually exclusive per deactivation event.
|
|
916
|
-
* To react to any deactivation, chain both.
|
|
803
|
+
* onPlay / onEnd / onStop — user-space (public)
|
|
804
|
+
* onPlay : fires in play() on false→true transition.
|
|
805
|
+
* onEnd : fires in tick() at natural boundary (once mode only).
|
|
806
|
+
* onStop : fires in stop() / reset() — explicit deactivation.
|
|
807
|
+
* onEnd and onStop are mutually exclusive per event.
|
|
917
808
|
*
|
|
918
809
|
* Firing order:
|
|
919
810
|
* play() → onPlay → _onActivate
|
|
920
|
-
* tick() → onEnd → _onDeactivate
|
|
811
|
+
* tick() → onEnd → _onDeactivate
|
|
921
812
|
* stop() → onStop → _onDeactivate
|
|
922
813
|
* reset() → onStop → _onDeactivate
|
|
923
814
|
*
|
|
924
815
|
* ── Playback semantics (rate) ─────────────────────────────────────────────────
|
|
925
|
-
* rate > 0 forward
|
|
926
|
-
* rate < 0 backward
|
|
927
|
-
* rate === 0 frozen: tick()
|
|
816
|
+
* rate > 0 forward
|
|
817
|
+
* rate < 0 backward
|
|
818
|
+
* rate === 0 frozen: tick() no-op; playing unchanged
|
|
928
819
|
*
|
|
929
|
-
* play()
|
|
930
|
-
* stop()
|
|
931
|
-
* Assigning rate never
|
|
820
|
+
* play() is the sole setter of playing = true.
|
|
821
|
+
* stop() is the sole setter of playing = false.
|
|
822
|
+
* Assigning rate never starts or stops playback.
|
|
932
823
|
*
|
|
933
824
|
* ── One-keyframe behaviour ────────────────────────────────────────────────────
|
|
934
825
|
* play() with exactly one keyframe snaps eval() to that keyframe without
|
|
935
|
-
* setting playing = true and without
|
|
826
|
+
* setting playing = true and without firing hooks.
|
|
936
827
|
*/
|
|
937
828
|
|
|
938
829
|
|
|
@@ -953,68 +844,62 @@ const qCopy = (out, a) => {
|
|
|
953
844
|
/** Dot product of two quaternions. */
|
|
954
845
|
const qDot = (a, b) => a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3];
|
|
955
846
|
|
|
956
|
-
/** Normalise in-place. @returns {number[]} out */
|
|
847
|
+
/** Normalise quaternion in-place. @returns {number[]} out */
|
|
957
848
|
const qNormalize = (out) => {
|
|
958
|
-
const
|
|
959
|
-
out[0]
|
|
960
|
-
return out;
|
|
849
|
+
const l = Math.sqrt(out[0]*out[0]+out[1]*out[1]+out[2]*out[2]+out[3]*out[3]) || 1;
|
|
850
|
+
out[0]/=l; out[1]/=l; out[2]/=l; out[3]/=l; return out;
|
|
961
851
|
};
|
|
962
852
|
|
|
963
|
-
/** Negate
|
|
964
|
-
const qNegate = (out) => {
|
|
965
|
-
out[0]
|
|
966
|
-
return out;
|
|
853
|
+
/** Negate quaternion (same rotation, different hemisphere). @returns {number[]} out */
|
|
854
|
+
const qNegate = (out, a) => {
|
|
855
|
+
out[0]=-a[0]; out[1]=-a[1]; out[2]=-a[2]; out[3]=-a[3]; return out;
|
|
967
856
|
};
|
|
968
857
|
|
|
969
|
-
/** out = a * b
|
|
858
|
+
/** Hamilton product out = a * b. @returns {number[]} out */
|
|
970
859
|
const qMul = (out, a, b) => {
|
|
971
|
-
const ax
|
|
972
|
-
|
|
973
|
-
out[
|
|
974
|
-
out[
|
|
975
|
-
out[
|
|
976
|
-
out[3] = aw*bw - ax*bx - ay*by - az*bz;
|
|
860
|
+
const ax=a[0],ay=a[1],az=a[2],aw=a[3], bx=b[0],by=b[1],bz=b[2],bw=b[3];
|
|
861
|
+
out[0]=aw*bx+ax*bw+ay*bz-az*by;
|
|
862
|
+
out[1]=aw*by-ax*bz+ay*bw+az*bx;
|
|
863
|
+
out[2]=aw*bz+ax*by-ay*bx+az*bw;
|
|
864
|
+
out[3]=aw*bw-ax*bx-ay*by-az*bz;
|
|
977
865
|
return out;
|
|
978
866
|
};
|
|
979
867
|
|
|
868
|
+
/** Spherical linear interpolation. @returns {number[]} out */
|
|
869
|
+
const qSlerp = (out, a, b, t) => {
|
|
870
|
+
let bx=b[0],by=b[1],bz=b[2],bw=b[3];
|
|
871
|
+
let d = a[0]*bx+a[1]*by+a[2]*bz+a[3]*bw;
|
|
872
|
+
if (d < 0) { bx=-bx; by=-by; bz=-bz; bw=-bw; d=-d; }
|
|
873
|
+
let f0, f1;
|
|
874
|
+
if (1-d > 1e-10) {
|
|
875
|
+
const th=Math.acos(d), st=Math.sin(th);
|
|
876
|
+
f0=Math.sin((1-t)*th)/st; f1=Math.sin(t*th)/st;
|
|
877
|
+
} else {
|
|
878
|
+
f0=1-t; f1=t;
|
|
879
|
+
}
|
|
880
|
+
out[0]=a[0]*f0+bx*f1; out[1]=a[1]*f0+by*f1;
|
|
881
|
+
out[2]=a[2]*f0+bz*f1; out[3]=a[3]*f0+bw*f1;
|
|
882
|
+
return qNormalize(out);
|
|
883
|
+
};
|
|
884
|
+
|
|
980
885
|
/**
|
|
981
|
-
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
* @param {number[]} out 4-element result array.
|
|
985
|
-
* @param {number[]} a Start quaternion [x,y,z,w].
|
|
986
|
-
* @param {number[]} b End quaternion [x,y,z,w].
|
|
987
|
-
* @param {number} t Blend [0, 1].
|
|
886
|
+
* Normalised linear interpolation (nlerp).
|
|
887
|
+
* Cheaper than slerp; slightly non-constant angular velocity.
|
|
888
|
+
* Handles antipodal quats by flipping b when dot < 0.
|
|
988
889
|
* @returns {number[]} out
|
|
989
890
|
*/
|
|
990
|
-
const
|
|
991
|
-
let
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
out[1] = a[1] + t*(by - a[1]);
|
|
997
|
-
out[2] = a[2] + t*(bz - a[2]);
|
|
998
|
-
out[3] = a[3] + t*(bw - a[3]);
|
|
999
|
-
return qNormalize(out);
|
|
1000
|
-
}
|
|
1001
|
-
const theta = Math.acos(d), sinT = Math.sin(theta);
|
|
1002
|
-
const s0 = Math.sin((1 - t) * theta) / sinT;
|
|
1003
|
-
const s1 = Math.sin(t * theta) / sinT;
|
|
1004
|
-
out[0] = s0*a[0] + s1*bx;
|
|
1005
|
-
out[1] = s0*a[1] + s1*by;
|
|
1006
|
-
out[2] = s0*a[2] + s1*bz;
|
|
1007
|
-
out[3] = s0*a[3] + s1*bw;
|
|
1008
|
-
return out;
|
|
891
|
+
const qNlerp = (out, a, b, t) => {
|
|
892
|
+
let bx=b[0],by=b[1],bz=b[2],bw=b[3];
|
|
893
|
+
if (a[0]*bx+a[1]*by+a[2]*bz+a[3]*bw < 0) { bx=-bx; by=-by; bz=-bz; bw=-bw; }
|
|
894
|
+
out[0]=a[0]+t*(bx-a[0]); out[1]=a[1]+t*(by-a[1]);
|
|
895
|
+
out[2]=a[2]+t*(bz-a[2]); out[3]=a[3]+t*(bw-a[3]);
|
|
896
|
+
return qNormalize(out);
|
|
1009
897
|
};
|
|
1010
898
|
|
|
1011
899
|
/**
|
|
1012
|
-
* Build a quaternion from
|
|
1013
|
-
* The axis need not be normalised.
|
|
900
|
+
* Build a quaternion from axis-angle.
|
|
1014
901
|
* @param {number[]} out
|
|
1015
|
-
* @param {number} ax Axis
|
|
1016
|
-
* @param {number} ay Axis y.
|
|
1017
|
-
* @param {number} az Axis z.
|
|
902
|
+
* @param {number} ax @param {number} ay @param {number} az Axis (need not be unit).
|
|
1018
903
|
* @param {number} angle Radians.
|
|
1019
904
|
* @returns {number[]} out
|
|
1020
905
|
*/
|
|
@@ -1022,93 +907,76 @@ const qFromAxisAngle = (out, ax, ay, az, angle) => {
|
|
|
1022
907
|
const half = angle * 0.5;
|
|
1023
908
|
const s = Math.sin(half);
|
|
1024
909
|
const len = Math.sqrt(ax*ax + ay*ay + az*az) || 1;
|
|
1025
|
-
out[0] = s * ax / len;
|
|
1026
|
-
out[1] = s * ay / len;
|
|
1027
|
-
out[2] = s * az / len;
|
|
910
|
+
out[0] = s * ax / len; out[1] = s * ay / len; out[2] = s * az / len;
|
|
1028
911
|
out[3] = Math.cos(half);
|
|
1029
912
|
return out;
|
|
1030
913
|
};
|
|
1031
914
|
|
|
1032
915
|
/**
|
|
1033
|
-
* Build a quaternion from a look direction (
|
|
1034
|
-
* and an optional up vector (defaults to +Y).
|
|
916
|
+
* Build a quaternion from a look direction (−Z forward) and optional up (default +Y).
|
|
1035
917
|
* @param {number[]} out
|
|
1036
918
|
* @param {number[]} dir Forward direction [x,y,z].
|
|
1037
919
|
* @param {number[]} [up] Up vector [x,y,z].
|
|
1038
920
|
* @returns {number[]} out
|
|
1039
921
|
*/
|
|
1040
922
|
const qFromLookDir = (out, dir, up) => {
|
|
1041
|
-
let fx
|
|
1042
|
-
const
|
|
1043
|
-
fx
|
|
1044
|
-
let ux
|
|
1045
|
-
let rx
|
|
1046
|
-
const
|
|
1047
|
-
rx
|
|
1048
|
-
ux
|
|
1049
|
-
return qFromRotMat3x3(out, rx,
|
|
923
|
+
let fx=dir[0],fy=dir[1],fz=dir[2];
|
|
924
|
+
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
925
|
+
fx/=fl; fy/=fl; fz/=fl;
|
|
926
|
+
let ux=up?up[0]:0, uy=up?up[1]:1, uz=up?up[2]:0;
|
|
927
|
+
let rx=uy*fz-uz*fy, ry=uz*fx-ux*fz, rz=ux*fy-uy*fx;
|
|
928
|
+
const rl=Math.sqrt(rx*rx+ry*ry+rz*rz)||1;
|
|
929
|
+
rx/=rl; ry/=rl; rz/=rl;
|
|
930
|
+
ux=fy*rz-fz*ry; uy=fz*rx-fx*rz; uz=fx*ry-fy*rx;
|
|
931
|
+
return qFromRotMat3x3(out, rx,ry,rz, ux,uy,uz, -fx,-fy,-fz);
|
|
1050
932
|
};
|
|
1051
933
|
|
|
1052
934
|
/**
|
|
1053
|
-
* Build a quaternion from a
|
|
935
|
+
* Build a quaternion from a 3×3 rotation matrix (9 row-major scalars).
|
|
1054
936
|
* @returns {number[]} out (normalised)
|
|
1055
937
|
*/
|
|
1056
|
-
const qFromRotMat3x3 = (out, m00,
|
|
1057
|
-
const tr = m00
|
|
938
|
+
const qFromRotMat3x3 = (out, m00,m01,m02, m10,m11,m12, m20,m21,m22) => {
|
|
939
|
+
const tr = m00+m11+m22;
|
|
1058
940
|
if (tr > 0) {
|
|
1059
|
-
const s
|
|
1060
|
-
out[3]
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
out[
|
|
1064
|
-
} else if (
|
|
1065
|
-
const s
|
|
1066
|
-
out[3] =
|
|
1067
|
-
out[0] = 0.25 * s;
|
|
1068
|
-
out[1] = (m01 + m10) / s;
|
|
1069
|
-
out[2] = (m02 + m20) / s;
|
|
1070
|
-
} else if (m11 > m22) {
|
|
1071
|
-
const s = 2 * Math.sqrt(1 + m11 - m00 - m22);
|
|
1072
|
-
out[3] = (m02 - m20) / s;
|
|
1073
|
-
out[0] = (m01 + m10) / s;
|
|
1074
|
-
out[1] = 0.25 * s;
|
|
1075
|
-
out[2] = (m12 + m21) / s;
|
|
941
|
+
const s=0.5/Math.sqrt(tr+1);
|
|
942
|
+
out[3]=0.25/s; out[0]=(m21-m12)*s; out[1]=(m02-m20)*s; out[2]=(m10-m01)*s;
|
|
943
|
+
} else if (m00>m11 && m00>m22) {
|
|
944
|
+
const s=2*Math.sqrt(1+m00-m11-m22);
|
|
945
|
+
out[3]=(m21-m12)/s; out[0]=0.25*s; out[1]=(m01+m10)/s; out[2]=(m02+m20)/s;
|
|
946
|
+
} else if (m11>m22) {
|
|
947
|
+
const s=2*Math.sqrt(1+m11-m00-m22);
|
|
948
|
+
out[3]=(m02-m20)/s; out[0]=(m01+m10)/s; out[1]=0.25*s; out[2]=(m12+m21)/s;
|
|
1076
949
|
} else {
|
|
1077
|
-
const s
|
|
1078
|
-
out[3]
|
|
1079
|
-
out[0] = (m02 + m20) / s;
|
|
1080
|
-
out[1] = (m12 + m21) / s;
|
|
1081
|
-
out[2] = 0.25 * s;
|
|
950
|
+
const s=2*Math.sqrt(1+m22-m00-m11);
|
|
951
|
+
out[3]=(m10-m01)/s; out[0]=(m02+m20)/s; out[1]=(m12+m21)/s; out[2]=0.25*s;
|
|
1082
952
|
}
|
|
1083
953
|
return qNormalize(out);
|
|
1084
954
|
};
|
|
1085
955
|
|
|
1086
956
|
/**
|
|
1087
|
-
* Extract a unit quaternion from the upper-left
|
|
957
|
+
* Extract a unit quaternion from the upper-left 3×3 of a column-major mat4.
|
|
1088
958
|
* @param {number[]} out
|
|
1089
959
|
* @param {Float32Array|number[]} m Column-major mat4.
|
|
1090
960
|
* @returns {number[]} out
|
|
1091
961
|
*/
|
|
1092
962
|
const qFromMat4 = (out, m) =>
|
|
1093
|
-
qFromRotMat3x3(out, m[0],
|
|
963
|
+
qFromRotMat3x3(out, m[0],m[4],m[8], m[1],m[5],m[9], m[2],m[6],m[10]);
|
|
1094
964
|
|
|
1095
965
|
/**
|
|
1096
|
-
* Write a quaternion into a column-major mat4
|
|
1097
|
-
*
|
|
966
|
+
* Write a quaternion into the rotation block of a column-major mat4.
|
|
967
|
+
* Translation and perspective rows/cols are set to identity values.
|
|
1098
968
|
* @param {Float32Array|number[]} out 16-element array.
|
|
1099
969
|
* @param {number[]} q [x,y,z,w].
|
|
1100
970
|
* @returns {Float32Array|number[]} out
|
|
1101
971
|
*/
|
|
1102
972
|
const qToMat4 = (out, q) => {
|
|
1103
|
-
const x
|
|
1104
|
-
const x2
|
|
1105
|
-
const xx
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
out[
|
|
1109
|
-
out[
|
|
1110
|
-
out[8] = xz+wy; out[9] = yz-wx; out[10] = 1-(xx+yy); out[11] = 0;
|
|
1111
|
-
out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1;
|
|
973
|
+
const x=q[0],y=q[1],z=q[2],w=q[3];
|
|
974
|
+
const x2=x+x,y2=y+y,z2=z+z;
|
|
975
|
+
const xx=x*x2,xy=x*y2,xz=x*z2,yy=y*y2,yz=y*z2,zz=z*z2,wx=w*x2,wy=w*y2,wz=w*z2;
|
|
976
|
+
out[0]=1-(yy+zz); out[1]=xy+wz; out[2]=xz-wy; out[3]=0;
|
|
977
|
+
out[4]=xy-wz; out[5]=1-(xx+zz); out[6]=yz+wx; out[7]=0;
|
|
978
|
+
out[8]=xz+wy; out[9]=yz-wx; out[10]=1-(xx+yy); out[11]=0;
|
|
979
|
+
out[12]=0; out[13]=0; out[14]=0; out[15]=1;
|
|
1112
980
|
return out;
|
|
1113
981
|
};
|
|
1114
982
|
|
|
@@ -1120,11 +988,11 @@ const qToMat4 = (out, q) => {
|
|
|
1120
988
|
*/
|
|
1121
989
|
const quatToAxisAngle = (q, out) => {
|
|
1122
990
|
out = out || {};
|
|
1123
|
-
const x
|
|
1124
|
-
const sinHalf = Math.sqrt(x*x
|
|
1125
|
-
if (sinHalf < 1e-8) { out.axis
|
|
1126
|
-
out.angle = 2
|
|
1127
|
-
out.axis = [x
|
|
991
|
+
const x=q[0],y=q[1],z=q[2],w=q[3];
|
|
992
|
+
const sinHalf = Math.sqrt(x*x+y*y+z*z);
|
|
993
|
+
if (sinHalf < 1e-8) { out.axis=[0,1,0]; out.angle=0; return out; }
|
|
994
|
+
out.angle = 2*Math.atan2(sinHalf, w);
|
|
995
|
+
out.axis = [x/sinHalf, y/sinHalf, z/sinHalf];
|
|
1128
996
|
return out;
|
|
1129
997
|
};
|
|
1130
998
|
|
|
@@ -1133,35 +1001,34 @@ const quatToAxisAngle = (q, out) => {
|
|
|
1133
1001
|
// =========================================================================
|
|
1134
1002
|
|
|
1135
1003
|
function _dist3(a, b) {
|
|
1136
|
-
const dx
|
|
1137
|
-
return Math.sqrt(dx*dx
|
|
1004
|
+
const dx=a[0]-b[0], dy=a[1]-b[1], dz=a[2]-b[2];
|
|
1005
|
+
return Math.sqrt(dx*dx+dy*dy+dz*dz);
|
|
1138
1006
|
}
|
|
1139
1007
|
|
|
1140
1008
|
/**
|
|
1141
|
-
* Centripetal Catmull-Rom interpolation (alpha
|
|
1142
|
-
* out = interp(p0, p1, p2, p3, t) where t
|
|
1143
|
-
* Boundary:
|
|
1009
|
+
* Centripetal Catmull-Rom interpolation (alpha=0.5, Barry-Goldman).
|
|
1010
|
+
* out = interp(p0, p1, p2, p3, t) where t∈[0,1] maps p1→p2.
|
|
1011
|
+
* Boundary: p0===p1 or p2===p3 clamps the end tangent.
|
|
1144
1012
|
* @param {number[]} out 3-element result.
|
|
1145
|
-
* @param {number[]} p0
|
|
1146
|
-
* @param {number[]} p1
|
|
1147
|
-
* @param {number[]} p2
|
|
1148
|
-
* @param {number[]} p3
|
|
1149
|
-
* @param {number} t
|
|
1013
|
+
* @param {number[]} p0 Control point before p1.
|
|
1014
|
+
* @param {number[]} p1 Segment start.
|
|
1015
|
+
* @param {number[]} p2 Segment end.
|
|
1016
|
+
* @param {number[]} p3 Control point after p2.
|
|
1017
|
+
* @param {number} t Blend [0, 1].
|
|
1150
1018
|
* @returns {number[]} out
|
|
1151
1019
|
*/
|
|
1152
1020
|
const catmullRomVec3 = (out, p0, p1, p2, p3, t) => {
|
|
1153
1021
|
const alpha = 0.5;
|
|
1154
|
-
const dt0
|
|
1155
|
-
const dt1
|
|
1156
|
-
const dt2
|
|
1022
|
+
const dt0 = Math.pow(_dist3(p0,p1), alpha) || 1;
|
|
1023
|
+
const dt1 = Math.pow(_dist3(p1,p2), alpha) || 1;
|
|
1024
|
+
const dt2 = Math.pow(_dist3(p2,p3), alpha) || 1;
|
|
1157
1025
|
for (let i = 0; i < 3; i++) {
|
|
1158
1026
|
const t1_0 = (p1[i]-p0[i])/dt0 - (p2[i]-p0[i])/(dt0+dt1) + (p2[i]-p1[i])/dt1;
|
|
1159
1027
|
const t2_0 = (p2[i]-p1[i])/dt1 - (p3[i]-p1[i])/(dt1+dt2) + (p3[i]-p2[i])/dt2;
|
|
1160
|
-
const m1
|
|
1161
|
-
const
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
out[i] = a*t*t*t + b*t*t + m1*t + p1[i];
|
|
1028
|
+
const m1=t1_0*dt1, m2=t2_0*dt1;
|
|
1029
|
+
const a= 2*p1[i]-2*p2[i]+m1+m2;
|
|
1030
|
+
const b=-3*p1[i]+3*p2[i]-2*m1-m2;
|
|
1031
|
+
out[i] = a*t*t*t + b*t*t + m1*t + p1[i];
|
|
1165
1032
|
}
|
|
1166
1033
|
return out;
|
|
1167
1034
|
};
|
|
@@ -1175,9 +1042,9 @@ const catmullRomVec3 = (out, p0, p1, p2, p3, t) => {
|
|
|
1175
1042
|
* @returns {number[]} out
|
|
1176
1043
|
*/
|
|
1177
1044
|
const lerpVec3 = (out, a, b, t) => {
|
|
1178
|
-
out[0]
|
|
1179
|
-
out[1]
|
|
1180
|
-
out[2]
|
|
1045
|
+
out[0]=a[0]+t*(b[0]-a[0]);
|
|
1046
|
+
out[1]=a[1]+t*(b[1]-a[1]);
|
|
1047
|
+
out[2]=a[2]+t*(b[2]-a[2]);
|
|
1181
1048
|
return out;
|
|
1182
1049
|
};
|
|
1183
1050
|
|
|
@@ -1187,45 +1054,42 @@ const lerpVec3 = (out, a, b, t) => {
|
|
|
1187
1054
|
|
|
1188
1055
|
/**
|
|
1189
1056
|
* Write a TRS transform into a column-major mat4.
|
|
1190
|
-
* Rotation is encoded as a quaternion; scale is baked into rotation columns.
|
|
1191
1057
|
* @param {Float32Array|number[]} out 16-element column-major mat4.
|
|
1192
1058
|
* @param {{ pos:number[], rot:number[], scl:number[] }} xform
|
|
1193
1059
|
* @returns {Float32Array|number[]} out
|
|
1194
1060
|
*/
|
|
1195
1061
|
const transformToMat4 = (out, xform) => {
|
|
1196
1062
|
qToMat4(out, xform.rot);
|
|
1197
|
-
const sx
|
|
1198
|
-
out[0]
|
|
1199
|
-
out[4]
|
|
1200
|
-
out[8]
|
|
1201
|
-
out[12] = xform.pos[
|
|
1202
|
-
out[13] = xform.pos[1];
|
|
1203
|
-
out[14] = xform.pos[2];
|
|
1063
|
+
const sx=xform.scl[0], sy=xform.scl[1], sz=xform.scl[2];
|
|
1064
|
+
out[0]*=sx; out[1]*=sx; out[2]*=sx;
|
|
1065
|
+
out[4]*=sy; out[5]*=sy; out[6]*=sy;
|
|
1066
|
+
out[8]*=sz; out[9]*=sz; out[10]*=sz;
|
|
1067
|
+
out[12]=xform.pos[0]; out[13]=xform.pos[1]; out[14]=xform.pos[2];
|
|
1204
1068
|
return out;
|
|
1205
1069
|
};
|
|
1206
1070
|
|
|
1207
1071
|
/**
|
|
1208
1072
|
* Decompose a column-major mat4 into a TRS transform.
|
|
1209
|
-
* Assumes no shear. Scale
|
|
1073
|
+
* Assumes no shear. Scale extracted from column lengths.
|
|
1210
1074
|
* @param {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1211
1075
|
* @param {Float32Array|number[]} m Column-major mat4.
|
|
1212
1076
|
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1213
1077
|
*/
|
|
1214
1078
|
const mat4ToTransform = (out, m) => {
|
|
1215
|
-
out.pos[0]
|
|
1216
|
-
const sx
|
|
1217
|
-
const sy
|
|
1218
|
-
const sz
|
|
1219
|
-
out.scl[0]
|
|
1079
|
+
out.pos[0]=m[12]; out.pos[1]=m[13]; out.pos[2]=m[14];
|
|
1080
|
+
const sx=Math.sqrt(m[0]*m[0]+m[1]*m[1]+m[2]*m[2]);
|
|
1081
|
+
const sy=Math.sqrt(m[4]*m[4]+m[5]*m[5]+m[6]*m[6]);
|
|
1082
|
+
const sz=Math.sqrt(m[8]*m[8]+m[9]*m[9]+m[10]*m[10]);
|
|
1083
|
+
out.scl[0]=sx; out.scl[1]=sy; out.scl[2]=sz;
|
|
1220
1084
|
qFromRotMat3x3(out.rot,
|
|
1221
|
-
m[0]/sx,
|
|
1222
|
-
m[1]/sx,
|
|
1223
|
-
m[2]/sx,
|
|
1085
|
+
m[0]/sx,m[4]/sy,m[8]/sz,
|
|
1086
|
+
m[1]/sx,m[5]/sy,m[9]/sz,
|
|
1087
|
+
m[2]/sx,m[6]/sy,m[10]/sz);
|
|
1224
1088
|
return out;
|
|
1225
1089
|
};
|
|
1226
1090
|
|
|
1227
1091
|
// =========================================================================
|
|
1228
|
-
//
|
|
1092
|
+
// S4a Spec parser — PoseTrack
|
|
1229
1093
|
// =========================================================================
|
|
1230
1094
|
|
|
1231
1095
|
const _isNum = (x) => typeof x === 'number' && Number.isFinite(x);
|
|
@@ -1234,198 +1098,303 @@ const _clampS = (x, lo, hi) => x < lo ? lo : (x > hi ? hi : x);
|
|
|
1234
1098
|
|
|
1235
1099
|
function _parseVec3(v) {
|
|
1236
1100
|
if (!v) return null;
|
|
1237
|
-
if (Array.isArray(v) && v.length >= 3 && v.every(n => typeof n === 'number')) return [v[0],
|
|
1238
|
-
if (typeof v === 'object' && 'x' in v) return [v.x
|
|
1101
|
+
if (Array.isArray(v) && v.length >= 3 && v.every(n => typeof n === 'number')) return [v[0],v[1],v[2]];
|
|
1102
|
+
if (typeof v === 'object' && 'x' in v) return [v.x||0, v.y||0, v.z||0];
|
|
1239
1103
|
return null;
|
|
1240
1104
|
}
|
|
1241
1105
|
|
|
1106
|
+
// Euler: unit axis vectors and the six valid intrinsic orderings.
|
|
1107
|
+
const _EULER_AXES = { X:[1,0,0], Y:[0,1,0], Z:[0,0,1] };
|
|
1108
|
+
const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Parse any rotation representation into a unit quaternion [x,y,z,w].
|
|
1112
|
+
*
|
|
1113
|
+
* Accepted forms:
|
|
1114
|
+
*
|
|
1115
|
+
* [x,y,z,w]
|
|
1116
|
+
* Raw quaternion array.
|
|
1117
|
+
*
|
|
1118
|
+
* { axis:[x,y,z], angle }
|
|
1119
|
+
* Axis-angle. Axis need not be unit.
|
|
1120
|
+
*
|
|
1121
|
+
* { dir:[x,y,z], up?:[x,y,z] }
|
|
1122
|
+
* Object orientation — forward direction (−Z) with optional up hint.
|
|
1123
|
+
*
|
|
1124
|
+
* { eMatrix: mat4 }
|
|
1125
|
+
* Extract rotation block from an eye (eye→world) matrix.
|
|
1126
|
+
* Column-major Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1127
|
+
*
|
|
1128
|
+
* { mat3: mat3 }
|
|
1129
|
+
* Column-major 3×3 rotation matrix — Float32Array(9) or plain Array.
|
|
1130
|
+
*
|
|
1131
|
+
* { euler:[rx,ry,rz], order?:'YXZ' }
|
|
1132
|
+
* Intrinsic Euler angles (radians). Angles are indexed by order position:
|
|
1133
|
+
* e[0] rotates around order[0] axis, e[1] around order[1], e[2] around order[2].
|
|
1134
|
+
* Supported orders: YXZ (default), XYZ, ZYX, ZXY, XZY, YZX.
|
|
1135
|
+
* Note: intrinsic ABC = extrinsic CBA with the same angles — to use
|
|
1136
|
+
* extrinsic order ABC, reverse the string and use intrinsic CBA.
|
|
1137
|
+
*
|
|
1138
|
+
* { from:[x,y,z], to:[x,y,z] }
|
|
1139
|
+
* Shortest-arc rotation from one direction onto another.
|
|
1140
|
+
* Both vectors are normalised internally.
|
|
1141
|
+
* Antiparallel input: 180° rotation around a perpendicular axis.
|
|
1142
|
+
*
|
|
1143
|
+
* @param {*} v
|
|
1144
|
+
* @returns {number[]|null} [x,y,z,w] or null if unparseable.
|
|
1145
|
+
*/
|
|
1242
1146
|
function _parseQuat(v) {
|
|
1243
1147
|
if (!v) return null;
|
|
1244
|
-
|
|
1148
|
+
|
|
1149
|
+
// raw [x,y,z,w]
|
|
1150
|
+
if (Array.isArray(v) && v.length === 4 && v.every(n => typeof n === 'number'))
|
|
1151
|
+
return [v[0],v[1],v[2],v[3]];
|
|
1152
|
+
|
|
1153
|
+
// { axis, angle }
|
|
1245
1154
|
if (v.axis && typeof v.angle === 'number') {
|
|
1246
|
-
const a = Array.isArray(v.axis) ? v.axis : [v.axis.x
|
|
1247
|
-
return qFromAxisAngle([0,
|
|
1155
|
+
const a = Array.isArray(v.axis) ? v.axis : [v.axis.x||0, v.axis.y||0, v.axis.z||0];
|
|
1156
|
+
return qFromAxisAngle([0,0,0,1], a[0],a[1],a[2], v.angle);
|
|
1248
1157
|
}
|
|
1158
|
+
|
|
1159
|
+
// { dir, up? }
|
|
1249
1160
|
if (v.dir) {
|
|
1250
|
-
const d = Array.isArray(v.dir) ? v.dir : [v.dir.x
|
|
1251
|
-
const u = v.up ? (Array.isArray(v.up) ? v.up : [v.up.x
|
|
1252
|
-
return qFromLookDir([0,
|
|
1161
|
+
const d = Array.isArray(v.dir) ? v.dir : [v.dir.x||0, v.dir.y||0, v.dir.z||0];
|
|
1162
|
+
const u = v.up ? (Array.isArray(v.up) ? v.up : [v.up.x||0, v.up.y||0, v.up.z||0]) : null;
|
|
1163
|
+
return qFromLookDir([0,0,0,1], d, u);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// { eMatrix } — rotation block from eye (eye→world) matrix, col-major mat4
|
|
1167
|
+
if (v.eMatrix != null) {
|
|
1168
|
+
const m = (ArrayBuffer.isView(v.eMatrix) || Array.isArray(v.eMatrix))
|
|
1169
|
+
? v.eMatrix : (v.eMatrix.mat4 ?? null);
|
|
1170
|
+
if (m && m.length >= 16) return qFromMat4([0,0,0,1], m);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// { mat3 } — column-major 3×3 rotation matrix
|
|
1174
|
+
// col0=[m0,m1,m2], col1=[m3,m4,m5], col2=[m6,m7,m8]
|
|
1175
|
+
// row-major for qFromRotMat3x3: row0=[m0,m3,m6], row1=[m1,m4,m7], row2=[m2,m5,m8]
|
|
1176
|
+
if (v.mat3 != null) {
|
|
1177
|
+
const m = v.mat3;
|
|
1178
|
+
if ((ArrayBuffer.isView(m) || Array.isArray(m)) && m.length >= 9)
|
|
1179
|
+
return qFromRotMat3x3([0,0,0,1], m[0],m[3],m[6], m[1],m[4],m[7], m[2],m[5],m[8]);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// { euler, order? } — intrinsic Euler angles (radians), default order YXZ
|
|
1183
|
+
if (v.euler != null) {
|
|
1184
|
+
const e = v.euler;
|
|
1185
|
+
if (!Array.isArray(e) || e.length < 3) return null;
|
|
1186
|
+
const order = (typeof v.order === 'string' && _EULER_ORDERS.has(v.order))
|
|
1187
|
+
? v.order : 'YXZ';
|
|
1188
|
+
const q = [0,0,0,1];
|
|
1189
|
+
const s = [0,0,0,1]; // scratch — reused each step
|
|
1190
|
+
for (let i = 0; i < 3; i++) {
|
|
1191
|
+
const ax = _EULER_AXES[order[i]];
|
|
1192
|
+
qMul(q, q, qFromAxisAngle(s, ax[0],ax[1],ax[2], e[i]));
|
|
1193
|
+
}
|
|
1194
|
+
return q;
|
|
1253
1195
|
}
|
|
1196
|
+
|
|
1197
|
+
// { from, to } — shortest-arc rotation from one direction onto another
|
|
1198
|
+
if (v.from != null && v.to != null) {
|
|
1199
|
+
const f = Array.isArray(v.from) ? v.from : [v.from.x||0, v.from.y||0, v.from.z||0];
|
|
1200
|
+
const t = Array.isArray(v.to) ? v.to : [v.to.x||0, v.to.y||0, v.to.z||0];
|
|
1201
|
+
const fl = Math.sqrt(f[0]*f[0]+f[1]*f[1]+f[2]*f[2]) || 1;
|
|
1202
|
+
const tl = Math.sqrt(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]) || 1;
|
|
1203
|
+
const fx=f[0]/fl, fy=f[1]/fl, fz=f[2]/fl;
|
|
1204
|
+
const tx=t[0]/tl, ty=t[1]/tl, tz=t[2]/tl;
|
|
1205
|
+
const dot = fx*tx + fy*ty + fz*tz;
|
|
1206
|
+
// parallel — identity
|
|
1207
|
+
if (dot >= 1 - 1e-8) return [0,0,0,1];
|
|
1208
|
+
// antiparallel — 180° around any perpendicular axis
|
|
1209
|
+
if (dot <= -1 + 1e-8) {
|
|
1210
|
+
// cross(from, X=[1,0,0]) = [0, fz, -fy]
|
|
1211
|
+
let px=0, py=fz, pz=-fy;
|
|
1212
|
+
let pl = Math.sqrt(px*px+py*py+pz*pz);
|
|
1213
|
+
if (pl < 1e-8) {
|
|
1214
|
+
// from ≈ ±X; try cross(from, Z=[0,0,1]) = [fy, -fx, 0]
|
|
1215
|
+
px=fy; py=-fx; pz=0;
|
|
1216
|
+
pl = Math.sqrt(px*px+py*py+pz*pz);
|
|
1217
|
+
}
|
|
1218
|
+
if (pl < 1e-8) return [0,0,0,1];
|
|
1219
|
+
return qFromAxisAngle([0,0,0,1], px/pl,py/pl,pz/pl, Math.PI);
|
|
1220
|
+
}
|
|
1221
|
+
// general case — axis = normalize(cross(from, to))
|
|
1222
|
+
let ax=fy*tz-fz*ty, ay=fz*tx-fx*tz, az=fx*ty-fy*tx;
|
|
1223
|
+
const al = Math.sqrt(ax*ax+ay*ay+az*az) || 1;
|
|
1224
|
+
return qFromAxisAngle([0,0,0,1], ax/al,ay/al,az/al,
|
|
1225
|
+
Math.acos(Math.max(-1, Math.min(1, dot))));
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1254
1228
|
return null;
|
|
1255
1229
|
}
|
|
1256
1230
|
|
|
1231
|
+
/**
|
|
1232
|
+
* Parse a PoseTrack keyframe spec.
|
|
1233
|
+
*
|
|
1234
|
+
* Accepted forms:
|
|
1235
|
+
*
|
|
1236
|
+
* { mMatrix }
|
|
1237
|
+
* Decompose a column-major mat4 into TRS via mat4ToTransform.
|
|
1238
|
+
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1239
|
+
* pos from col3, scl from column lengths, rot from normalised rotation block.
|
|
1240
|
+
*
|
|
1241
|
+
* { pos, rot, scl }
|
|
1242
|
+
* Explicit TRS. pos and scl are vec3, rot accepts any form from _parseQuat.
|
|
1243
|
+
* All fields are optional — missing pos/scl default to [0,0,0] / [1,1,1],
|
|
1244
|
+
* missing rot defaults to identity.
|
|
1245
|
+
*
|
|
1246
|
+
* @param {Object} spec
|
|
1247
|
+
* @returns {{ pos:number[], rot:number[], scl:number[] }|null}
|
|
1248
|
+
*/
|
|
1257
1249
|
function _parseSpec(spec) {
|
|
1258
1250
|
if (!spec || typeof spec !== 'object') return null;
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1251
|
+
|
|
1252
|
+
// { mMatrix } — full TRS decomposition from model matrix
|
|
1253
|
+
if (spec.mMatrix != null) {
|
|
1254
|
+
const m = (ArrayBuffer.isView(spec.mMatrix) || Array.isArray(spec.mMatrix))
|
|
1255
|
+
? spec.mMatrix : (spec.mMatrix.mat4 ?? null);
|
|
1256
|
+
if (!m || m.length < 16) return null;
|
|
1257
|
+
return mat4ToTransform({ pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] }, m);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const pos = _parseVec3(spec.pos) || [0,0,0];
|
|
1261
|
+
const rot = _parseQuat(spec.rot) || [0,0,0,1];
|
|
1262
|
+
const scl = _parseVec3(spec.scl) || [1,1,1];
|
|
1262
1263
|
return { pos, rot, scl };
|
|
1263
1264
|
}
|
|
1264
1265
|
|
|
1265
1266
|
function _sameTransform(a, b) {
|
|
1266
|
-
for (let i
|
|
1267
|
-
for (let i
|
|
1267
|
+
for (let i=0;i<3;i++) if (a.pos[i]!==b.pos[i]||a.scl[i]!==b.scl[i]) return false;
|
|
1268
|
+
for (let i=0;i<4;i++) if (a.rot[i]!==b.rot[i]) return false;
|
|
1268
1269
|
return true;
|
|
1269
1270
|
}
|
|
1270
1271
|
|
|
1271
1272
|
// =========================================================================
|
|
1272
|
-
//
|
|
1273
|
+
// S4b Spec parser — CameraTrack
|
|
1273
1274
|
// =========================================================================
|
|
1274
1275
|
|
|
1275
1276
|
/**
|
|
1276
|
-
*
|
|
1277
|
+
* Parse a camera keyframe spec into internal { eye, center, up } form.
|
|
1277
1278
|
*
|
|
1278
|
-
*
|
|
1279
|
-
* The track maintains a scalar cursor (seg, f) that advances each tick().
|
|
1279
|
+
* Accepted forms:
|
|
1280
1280
|
*
|
|
1281
|
-
*
|
|
1282
|
-
*
|
|
1281
|
+
* { eye, center, up? }
|
|
1282
|
+
* Explicit lookat. up defaults to [0,1,0] and is normalised on storage.
|
|
1283
|
+
* Note: eye here is a vec3 — distinguished from { eMatrix } by length.
|
|
1283
1284
|
*
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
1285
|
+
* { vMatrix: mat4 }
|
|
1286
|
+
* Column-major view matrix (world→eye).
|
|
1287
|
+
* eye reconstructed via -R^T·t; center = eye + forward·1; up = [0,1,0].
|
|
1288
|
+
* The matrix's up_ortho (col1) is intentionally NOT used as up —
|
|
1289
|
+
* passing it to cam.camera() shifts orbitControl's orbit reference.
|
|
1290
|
+
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1288
1291
|
*
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1292
|
+
* { eMatrix: mat4 }
|
|
1293
|
+
* Column-major eye matrix (eye→world, i.e. inverse view).
|
|
1294
|
+
* eye read directly from col3; center = eye + forward·1; up = [0,1,0].
|
|
1295
|
+
* Simpler extraction than vMatrix; prefer this form when eMatrix is available.
|
|
1296
|
+
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1291
1297
|
*
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
* without setting playing = true and without firing hooks.
|
|
1295
|
-
*
|
|
1296
|
-
* @example
|
|
1297
|
-
* const track = new PoseTrack()
|
|
1298
|
-
* track.add({ pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] })
|
|
1299
|
-
* track.add({ pos:[0,100,0], rot:[0,0,0,1], scl:[1,1,1] })
|
|
1300
|
-
* track.play({ loop: true, onStop: t => console.log('stopped at', t.time()) })
|
|
1298
|
+
* @param {Object} spec
|
|
1299
|
+
* @returns {{ eye:number[], center:number[], up:number[] }|null}
|
|
1301
1300
|
*/
|
|
1302
|
-
|
|
1301
|
+
function _parseCameraSpec(spec) {
|
|
1302
|
+
if (!spec || typeof spec !== 'object') return null;
|
|
1303
|
+
|
|
1304
|
+
// { vMatrix } — view matrix (world→eye); reconstruct eye via -R^T·t
|
|
1305
|
+
if (spec.vMatrix != null) {
|
|
1306
|
+
const m = (ArrayBuffer.isView(spec.vMatrix) || Array.isArray(spec.vMatrix))
|
|
1307
|
+
? spec.vMatrix : (spec.vMatrix.mat4 ?? null);
|
|
1308
|
+
if (!m || m.length < 16) return null;
|
|
1309
|
+
const ex = -(m[0]*m[12] + m[4]*m[13] + m[8]*m[14]);
|
|
1310
|
+
const ey = -(m[1]*m[12] + m[5]*m[13] + m[9]*m[14]);
|
|
1311
|
+
const ez = -(m[2]*m[12] + m[6]*m[13] + m[10]*m[14]);
|
|
1312
|
+
const fx=-m[8], fy=-m[9], fz=-m[10];
|
|
1313
|
+
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
1314
|
+
return { eye:[ex,ey,ez], center:[ex+fx/fl,ey+fy/fl,ez+fz/fl], up:[0,1,0] };
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// { eMatrix } — eye matrix (eye→world); eye = col3, forward = -col2
|
|
1318
|
+
if (spec.eMatrix != null) {
|
|
1319
|
+
const m = (ArrayBuffer.isView(spec.eMatrix) || Array.isArray(spec.eMatrix))
|
|
1320
|
+
? spec.eMatrix : (spec.eMatrix.mat4 ?? null);
|
|
1321
|
+
if (!m || m.length < 16) return null;
|
|
1322
|
+
const ex=m[12], ey=m[13], ez=m[14];
|
|
1323
|
+
const fx=-m[8], fy=-m[9], fz=-m[10];
|
|
1324
|
+
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
1325
|
+
return { eye:[ex,ey,ez], center:[ex+fx/fl,ey+fy/fl,ez+fz/fl], up:[0,1,0] };
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// { eye, center, up? } — explicit lookat (eye is a vec3, not a mat4)
|
|
1329
|
+
const eye = _parseVec3(spec.eye);
|
|
1330
|
+
const center = _parseVec3(spec.center);
|
|
1331
|
+
if (!eye || !center) return null;
|
|
1332
|
+
const upRaw = spec.up ? _parseVec3(spec.up) : null;
|
|
1333
|
+
const up = upRaw || [0,1,0];
|
|
1334
|
+
const ul = Math.sqrt(up[0]*up[0]+up[1]*up[1]+up[2]*up[2]) || 1;
|
|
1335
|
+
return { eye, center, up:[up[0]/ul, up[1]/ul, up[2]/ul] };
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function _sameCameraKeyframe(a, b) {
|
|
1339
|
+
for (let i=0;i<3;i++) {
|
|
1340
|
+
if (a.eye[i]!==b.eye[i]) return false;
|
|
1341
|
+
if (a.center[i]!==b.center[i]) return false;
|
|
1342
|
+
if (a.up[i]!==b.up[i]) return false;
|
|
1343
|
+
}
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// =========================================================================
|
|
1348
|
+
// S5 Track — unexported base class (transport machinery only)
|
|
1349
|
+
// =========================================================================
|
|
1350
|
+
|
|
1351
|
+
class Track {
|
|
1303
1352
|
constructor() {
|
|
1304
|
-
/** @type {Array
|
|
1353
|
+
/** @type {Array} Keyframe array — shape depends on subclass. */
|
|
1305
1354
|
this.keyframes = [];
|
|
1306
1355
|
/** Whether playback is active. @type {boolean} */
|
|
1307
1356
|
this.playing = false;
|
|
1308
|
-
/** Loop
|
|
1357
|
+
/** Loop at boundaries. @type {boolean} */
|
|
1309
1358
|
this.loop = false;
|
|
1310
|
-
/** Ping-pong bounce
|
|
1359
|
+
/** Ping-pong bounce (takes precedence over loop). @type {boolean} */
|
|
1311
1360
|
this.pingPong = false;
|
|
1312
|
-
/** Frames per segment (
|
|
1361
|
+
/** Frames per segment (≥1). @type {number} */
|
|
1313
1362
|
this.duration = 30;
|
|
1314
1363
|
/** Current segment index. @type {number} */
|
|
1315
1364
|
this.seg = 0;
|
|
1316
|
-
/** Frame offset within
|
|
1365
|
+
/** Frame offset within segment (can be fractional). @type {number} */
|
|
1317
1366
|
this.f = 0;
|
|
1318
|
-
/**
|
|
1319
|
-
* Position interpolation mode.
|
|
1320
|
-
* @type {'catmullrom'|'linear'}
|
|
1321
|
-
*/
|
|
1322
|
-
this.posInterp = 'catmullrom';
|
|
1323
1367
|
|
|
1324
|
-
//
|
|
1325
|
-
this._pos = [0, 0, 0];
|
|
1326
|
-
this._rot = [0, 0, 0, 1];
|
|
1327
|
-
this._scl = [1, 1, 1];
|
|
1328
|
-
|
|
1329
|
-
// Internal rate — assigning never touches playing
|
|
1368
|
+
// Internal rate — never directly starts/stops playback
|
|
1330
1369
|
this._rate = 1;
|
|
1331
1370
|
|
|
1332
|
-
// User-space hooks
|
|
1333
|
-
/**
|
|
1334
|
-
this.
|
|
1335
|
-
/**
|
|
1336
|
-
this.onEnd = null;
|
|
1337
|
-
/** Fires on explicit stop() or reset(). Mutually exclusive with onEnd per event. @type {Function|null} */
|
|
1338
|
-
this.onStop = null;
|
|
1371
|
+
// User-space hooks
|
|
1372
|
+
/** @type {Function|null} */ this.onPlay = null;
|
|
1373
|
+
/** @type {Function|null} */ this.onEnd = null;
|
|
1374
|
+
/** @type {Function|null} */ this.onStop = null;
|
|
1339
1375
|
|
|
1340
|
-
// Lib-space hooks (set by host layer
|
|
1341
|
-
/** @type {Function|null} */
|
|
1342
|
-
this.
|
|
1343
|
-
/** @type {Function|null} */
|
|
1344
|
-
this._onDeactivate = null;
|
|
1376
|
+
// Lib-space hooks (set by host layer, e.g. p5 bridge)
|
|
1377
|
+
/** @type {Function|null} */ this._onActivate = null;
|
|
1378
|
+
/** @type {Function|null} */ this._onDeactivate = null;
|
|
1345
1379
|
}
|
|
1346
1380
|
|
|
1347
|
-
|
|
1348
|
-
// Getter/setter so future consumers get the right value from track.rate,
|
|
1349
|
-
// while the setter intentionally has NO side effects on playing.
|
|
1350
|
-
|
|
1351
|
-
/** Playback rate. 0 = frozen (playing flag unchanged). @type {number} */
|
|
1381
|
+
/** Playback rate. Assigning never starts/stops playback. @type {number} */
|
|
1352
1382
|
get rate() { return this._rate; }
|
|
1353
|
-
set rate(v) {
|
|
1354
|
-
this._rate = (typeof v === 'number' && Number.isFinite(v)) ? v : 1;
|
|
1355
|
-
// Intentionally does NOT start or stop playback.
|
|
1356
|
-
}
|
|
1383
|
+
set rate(v) { this._rate = (_isNum(v)) ? v : 1; }
|
|
1357
1384
|
|
|
1358
|
-
/** Number of interpolatable segments (keyframes.length
|
|
1385
|
+
/** Number of interpolatable segments (keyframes.length − 1, min 0). @type {number} */
|
|
1359
1386
|
get segments() { return Math.max(0, this.keyframes.length - 1); }
|
|
1360
1387
|
|
|
1361
|
-
// ── Keyframe management ──────────────────────────────────────────────────
|
|
1362
|
-
|
|
1363
|
-
/**
|
|
1364
|
-
* Append a keyframe. Adjacent duplicates are skipped by default.
|
|
1365
|
-
* @param {{ pos?, rot?, scl? }} spec pos/rot/scl arrays, {x,y,z}, axis-angle, look-dir.
|
|
1366
|
-
* @param {{ deduplicate?: boolean }} [opts]
|
|
1367
|
-
*/
|
|
1368
|
-
add(spec, opts) {
|
|
1369
|
-
const kf = _parseSpec(spec);
|
|
1370
|
-
if (!kf) return;
|
|
1371
|
-
const dedup = !opts || opts.deduplicate !== false;
|
|
1372
|
-
if (dedup && this.keyframes.length > 0) {
|
|
1373
|
-
if (_sameTransform(this.keyframes[this.keyframes.length - 1], kf)) return;
|
|
1374
|
-
}
|
|
1375
|
-
this.keyframes.push(kf);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
/**
|
|
1379
|
-
* Replace (or append at end) the keyframe at index.
|
|
1380
|
-
* @param {number} index Existing index or keyframes.length to append.
|
|
1381
|
-
* @param {{ pos?, rot?, scl? }} spec
|
|
1382
|
-
* @returns {boolean}
|
|
1383
|
-
*/
|
|
1384
|
-
set(index, spec) {
|
|
1385
|
-
if (!_isNum(index)) return false;
|
|
1386
|
-
const i = index | 0;
|
|
1387
|
-
const kf = _parseSpec(spec);
|
|
1388
|
-
if (!kf || i < 0 || i > this.keyframes.length) return false;
|
|
1389
|
-
if (i === this.keyframes.length) { this.keyframes.push(kf); }
|
|
1390
|
-
else { this.keyframes[i] = kf; }
|
|
1391
|
-
return true;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
/**
|
|
1395
|
-
* Remove the keyframe at index. Adjusts cursor if needed.
|
|
1396
|
-
* @param {number} index
|
|
1397
|
-
* @returns {boolean}
|
|
1398
|
-
*/
|
|
1399
|
-
remove(index) {
|
|
1400
|
-
if (!_isNum(index)) return false;
|
|
1401
|
-
const i = index | 0;
|
|
1402
|
-
if (i < 0 || i >= this.keyframes.length) return false;
|
|
1403
|
-
this.keyframes.splice(i, 1);
|
|
1404
|
-
const nSeg = this.segments;
|
|
1405
|
-
if (nSeg === 0) { this.seg = 0; this.f = 0; }
|
|
1406
|
-
else if (this.seg >= nSeg) { this.seg = nSeg - 1; }
|
|
1407
|
-
return true;
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
// ── Transport ────────────────────────────────────────────────────────────
|
|
1411
|
-
|
|
1412
1388
|
/**
|
|
1413
1389
|
* Start or update playback.
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
* Zero keyframes: no-op.
|
|
1418
|
-
* One keyframe: snaps cursor (seg=0, f=0); no playing=true, no hooks.
|
|
1419
|
-
* Already playing: updates params in place; hooks are not re-fired.
|
|
1420
|
-
* rate=0 is valid: track will be playing but frozen until rate changes.
|
|
1421
|
-
*
|
|
1422
|
-
* @param {number|Object} [rateOrOpts]
|
|
1423
|
-
* @returns {PoseTrack} this
|
|
1390
|
+
* @param {number|Object} [rateOrOpts] Numeric rate or options object:
|
|
1391
|
+
* { rate, duration, loop, pingPong, onPlay, onEnd, onStop }
|
|
1392
|
+
* @returns {Track} this
|
|
1424
1393
|
*/
|
|
1425
1394
|
play(rateOrOpts) {
|
|
1426
1395
|
if (this.keyframes.length === 0) return this;
|
|
1427
1396
|
|
|
1428
|
-
// One keyframe: snap
|
|
1397
|
+
// One keyframe: snap cursor, no animation
|
|
1429
1398
|
if (this.keyframes.length === 1) {
|
|
1430
1399
|
this.seg = 0; this.f = 0;
|
|
1431
1400
|
return this;
|
|
@@ -1444,9 +1413,7 @@ class PoseTrack {
|
|
|
1444
1413
|
if (_isNum(o.rate)) this._rate = o.rate;
|
|
1445
1414
|
}
|
|
1446
1415
|
|
|
1447
|
-
|
|
1448
|
-
const nSeg = this.segments;
|
|
1449
|
-
const dur = Math.max(1, this.duration | 0);
|
|
1416
|
+
const nSeg = this.segments, dur = Math.max(1, this.duration | 0);
|
|
1450
1417
|
if (this.seg < 0) this.seg = 0;
|
|
1451
1418
|
if (this.seg >= nSeg) this.seg = nSeg - 1;
|
|
1452
1419
|
if (this.f < 0) this.f = 0;
|
|
@@ -1454,7 +1421,6 @@ class PoseTrack {
|
|
|
1454
1421
|
|
|
1455
1422
|
const wasPlaying = this.playing;
|
|
1456
1423
|
this.playing = true;
|
|
1457
|
-
|
|
1458
1424
|
if (!wasPlaying) {
|
|
1459
1425
|
if (typeof this.onPlay === 'function') { try { this.onPlay(this); } catch (_) {} }
|
|
1460
1426
|
this._onActivate?.();
|
|
@@ -1463,11 +1429,9 @@ class PoseTrack {
|
|
|
1463
1429
|
}
|
|
1464
1430
|
|
|
1465
1431
|
/**
|
|
1466
|
-
* Stop playback.
|
|
1467
|
-
*
|
|
1468
|
-
*
|
|
1469
|
-
* @param {boolean} [rewind=false] Seek to playback origin after stopping.
|
|
1470
|
-
* @returns {PoseTrack} this
|
|
1432
|
+
* Stop playback.
|
|
1433
|
+
* @param {boolean} [rewind=false] Seek to origin after stopping.
|
|
1434
|
+
* @returns {Track} this
|
|
1471
1435
|
*/
|
|
1472
1436
|
stop(rewind) {
|
|
1473
1437
|
const wasPlaying = this.playing;
|
|
@@ -1482,8 +1446,7 @@ class PoseTrack {
|
|
|
1482
1446
|
|
|
1483
1447
|
/**
|
|
1484
1448
|
* Clear all keyframes and stop.
|
|
1485
|
-
*
|
|
1486
|
-
* @returns {PoseTrack} this
|
|
1449
|
+
* @returns {Track} this
|
|
1487
1450
|
*/
|
|
1488
1451
|
reset() {
|
|
1489
1452
|
const wasPlaying = this.playing;
|
|
@@ -1497,13 +1460,27 @@ class PoseTrack {
|
|
|
1497
1460
|
return this;
|
|
1498
1461
|
}
|
|
1499
1462
|
|
|
1463
|
+
/**
|
|
1464
|
+
* Remove the keyframe at index. Adjusts cursor if needed.
|
|
1465
|
+
* @param {number} index
|
|
1466
|
+
* @returns {boolean}
|
|
1467
|
+
*/
|
|
1468
|
+
remove(index) {
|
|
1469
|
+
if (!_isNum(index)) return false;
|
|
1470
|
+
const i = index | 0;
|
|
1471
|
+
if (i < 0 || i >= this.keyframes.length) return false;
|
|
1472
|
+
this.keyframes.splice(i, 1);
|
|
1473
|
+
const nSeg = this.segments;
|
|
1474
|
+
if (nSeg === 0) { this.seg = 0; this.f = 0; }
|
|
1475
|
+
else if (this.seg >= nSeg) { this.seg = nSeg - 1; }
|
|
1476
|
+
return true;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1500
1479
|
/**
|
|
1501
1480
|
* Seek to a normalised position [0,1] across the full path.
|
|
1502
|
-
* Can optionally target a specific segment (t is then local to that segment).
|
|
1503
|
-
* Does not change the playing flag.
|
|
1504
1481
|
* @param {number} t Normalised time [0, 1].
|
|
1505
1482
|
* @param {number} [segIndex] Optional segment override.
|
|
1506
|
-
* @returns {
|
|
1483
|
+
* @returns {Track} this
|
|
1507
1484
|
*/
|
|
1508
1485
|
seek(t, segIndex) {
|
|
1509
1486
|
const nSeg = this.segments;
|
|
@@ -1519,8 +1496,7 @@ class PoseTrack {
|
|
|
1519
1496
|
}
|
|
1520
1497
|
|
|
1521
1498
|
/**
|
|
1522
|
-
* Normalised playback
|
|
1523
|
-
* Returns 0 when fewer than 2 keyframes exist.
|
|
1499
|
+
* Normalised playback position [0,1].
|
|
1524
1500
|
* @returns {number}
|
|
1525
1501
|
*/
|
|
1526
1502
|
time() {
|
|
@@ -1531,10 +1507,8 @@ class PoseTrack {
|
|
|
1531
1507
|
}
|
|
1532
1508
|
|
|
1533
1509
|
/**
|
|
1534
|
-
* Snapshot of
|
|
1535
|
-
* @returns {
|
|
1536
|
-
* time:number, playing:boolean, loop:boolean, pingPong:boolean,
|
|
1537
|
-
* rate:number, duration:number }}
|
|
1510
|
+
* Snapshot of transport state.
|
|
1511
|
+
* @returns {Object}
|
|
1538
1512
|
*/
|
|
1539
1513
|
info() {
|
|
1540
1514
|
return {
|
|
@@ -1552,12 +1526,8 @@ class PoseTrack {
|
|
|
1552
1526
|
}
|
|
1553
1527
|
|
|
1554
1528
|
/**
|
|
1555
|
-
* Advance
|
|
1556
|
-
*
|
|
1557
|
-
* rate === 0: frozen — returns this.playing without moving (no-op).
|
|
1558
|
-
* Returns false and fires onEnd → _onDeactivate when a once-mode boundary is hit.
|
|
1559
|
-
* Returns true while playing and continuing.
|
|
1560
|
-
*
|
|
1529
|
+
* Advance cursor by rate frames.
|
|
1530
|
+
* Returns true while playing, false when stopping.
|
|
1561
1531
|
* @returns {boolean}
|
|
1562
1532
|
*/
|
|
1563
1533
|
tick() {
|
|
@@ -1566,8 +1536,6 @@ class PoseTrack {
|
|
|
1566
1536
|
if (nSeg === 0) {
|
|
1567
1537
|
this.playing = false; this._onDeactivate?.(); return false;
|
|
1568
1538
|
}
|
|
1569
|
-
|
|
1570
|
-
// Frozen: position does not advance, playing stays true
|
|
1571
1539
|
if (this._rate === 0) return true;
|
|
1572
1540
|
|
|
1573
1541
|
const dur = Math.max(1, this.duration | 0);
|
|
@@ -1575,25 +1543,22 @@ class PoseTrack {
|
|
|
1575
1543
|
const s = _clampS(this.seg * dur + this.f, 0, total);
|
|
1576
1544
|
const next = s + this._rate;
|
|
1577
1545
|
|
|
1578
|
-
// ── pingPong ──
|
|
1579
1546
|
if (this.pingPong) {
|
|
1580
1547
|
let pos = next, flips = 0;
|
|
1581
1548
|
while (pos < 0 || pos > total) {
|
|
1582
|
-
if (pos < 0)
|
|
1583
|
-
else
|
|
1549
|
+
if (pos < 0) { pos = -pos; flips++; }
|
|
1550
|
+
else { pos = 2 * total - pos; flips++; }
|
|
1584
1551
|
}
|
|
1585
1552
|
if (flips & 1) this._rate = -this._rate;
|
|
1586
1553
|
this._setCursorFromScalar(pos);
|
|
1587
1554
|
return true;
|
|
1588
1555
|
}
|
|
1589
1556
|
|
|
1590
|
-
// ── loop ──
|
|
1591
1557
|
if (this.loop) {
|
|
1592
1558
|
this._setCursorFromScalar(((next % total) + total) % total);
|
|
1593
1559
|
return true;
|
|
1594
1560
|
}
|
|
1595
1561
|
|
|
1596
|
-
// ── once — boundary check ──
|
|
1597
1562
|
if (next <= 0) {
|
|
1598
1563
|
this._setCursorFromScalar(0);
|
|
1599
1564
|
this.playing = false;
|
|
@@ -1613,15 +1578,117 @@ class PoseTrack {
|
|
|
1613
1578
|
return true;
|
|
1614
1579
|
}
|
|
1615
1580
|
|
|
1581
|
+
/** @private */
|
|
1582
|
+
_setCursorFromScalar(s) {
|
|
1583
|
+
const dur = Math.max(1, this.duration | 0);
|
|
1584
|
+
const nSeg = this.segments;
|
|
1585
|
+
this.seg = Math.floor(s / dur);
|
|
1586
|
+
this.f = s - this.seg * dur;
|
|
1587
|
+
if (this.seg >= nSeg) { this.seg = nSeg - 1; this.f = dur; }
|
|
1588
|
+
if (this.seg < 0) { this.seg = 0; this.f = 0; }
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// =========================================================================
|
|
1593
|
+
// S6 PoseTrack
|
|
1594
|
+
// =========================================================================
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Renderer-agnostic TRS keyframe track.
|
|
1598
|
+
*
|
|
1599
|
+
* Keyframe shape: { pos:[x,y,z], rot:[x,y,z,w], scl:[x,y,z] }
|
|
1600
|
+
*
|
|
1601
|
+
* add() accepts individual specs or a bulk array of specs:
|
|
1602
|
+
*
|
|
1603
|
+
* { mMatrix } — full TRS from model matrix
|
|
1604
|
+
* { pos, rot, scl } — direct TRS
|
|
1605
|
+
* { pos, rot: [x,y,z,w] } — explicit quaternion
|
|
1606
|
+
* { pos, rot: { axis, angle } } — axis-angle
|
|
1607
|
+
* { pos, rot: { dir, up? } } — look direction
|
|
1608
|
+
* { pos, rot: { eMatrix: mat4 } } — rotation from eye matrix
|
|
1609
|
+
* { pos, rot: { mat3 } } — column-major 3×3 rotation matrix
|
|
1610
|
+
* { pos, rot: { euler, order? } } — intrinsic Euler angles (default YXZ)
|
|
1611
|
+
* { pos, rot: { from, to } } — shortest-arc between two directions
|
|
1612
|
+
* [ spec, spec, ... ] — bulk
|
|
1613
|
+
*
|
|
1614
|
+
* eval() writes { pos, rot, scl }:
|
|
1615
|
+
* pos — Catmull-Rom (posInterp='catmullrom') or lerp
|
|
1616
|
+
* rot — slerp (rotInterp='slerp') or nlerp
|
|
1617
|
+
* scl — lerp
|
|
1618
|
+
*
|
|
1619
|
+
* @example
|
|
1620
|
+
* const track = new PoseTrack()
|
|
1621
|
+
* track.add({ pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] })
|
|
1622
|
+
* track.add({ pos:[100,0,0], rot: { euler:[0, Math.PI/2, 0] } })
|
|
1623
|
+
* track.add({ mMatrix: someModelMatrix })
|
|
1624
|
+
* track.play({ loop: true })
|
|
1625
|
+
* // per frame:
|
|
1626
|
+
* track.tick()
|
|
1627
|
+
* const out = { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] }
|
|
1628
|
+
* track.eval(out)
|
|
1629
|
+
*/
|
|
1630
|
+
class PoseTrack extends Track {
|
|
1631
|
+
constructor() {
|
|
1632
|
+
super();
|
|
1633
|
+
/**
|
|
1634
|
+
* Position interpolation mode.
|
|
1635
|
+
* @type {'catmullrom'|'linear'}
|
|
1636
|
+
*/
|
|
1637
|
+
this.posInterp = 'catmullrom';
|
|
1638
|
+
/**
|
|
1639
|
+
* Rotation interpolation mode.
|
|
1640
|
+
* - 'slerp' — constant angular velocity (default)
|
|
1641
|
+
* - 'nlerp' — normalised lerp; cheaper, slightly non-constant speed
|
|
1642
|
+
* @type {'slerp'|'nlerp'}
|
|
1643
|
+
*/
|
|
1644
|
+
this.rotInterp = 'slerp';
|
|
1645
|
+
// Scratch for toMatrix() — avoids hot-path allocations
|
|
1646
|
+
this._pos = [0,0,0];
|
|
1647
|
+
this._rot = [0,0,0,1];
|
|
1648
|
+
this._scl = [1,1,1];
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Append one or more keyframes. Adjacent duplicates are skipped by default.
|
|
1653
|
+
* @param {Object|Object[]} spec
|
|
1654
|
+
* @param {{ deduplicate?: boolean }} [opts]
|
|
1655
|
+
*/
|
|
1656
|
+
add(spec, opts) {
|
|
1657
|
+
if (Array.isArray(spec)) {
|
|
1658
|
+
for (const s of spec) this.add(s, opts);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
const kf = _parseSpec(spec);
|
|
1662
|
+
if (!kf) return;
|
|
1663
|
+
const dedup = !opts || opts.deduplicate !== false;
|
|
1664
|
+
if (dedup && this.keyframes.length > 0) {
|
|
1665
|
+
if (_sameTransform(this.keyframes[this.keyframes.length - 1], kf)) return;
|
|
1666
|
+
}
|
|
1667
|
+
this.keyframes.push(kf);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* Replace (or append at end) the keyframe at index.
|
|
1672
|
+
* @param {number} index
|
|
1673
|
+
* @param {Object} spec
|
|
1674
|
+
* @returns {boolean}
|
|
1675
|
+
*/
|
|
1676
|
+
set(index, spec) {
|
|
1677
|
+
if (!_isNum(index)) return false;
|
|
1678
|
+
const i = index | 0, kf = _parseSpec(spec);
|
|
1679
|
+
if (!kf || i < 0 || i > this.keyframes.length) return false;
|
|
1680
|
+
if (i === this.keyframes.length) this.keyframes.push(kf);
|
|
1681
|
+
else this.keyframes[i] = kf;
|
|
1682
|
+
return true;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1616
1685
|
/**
|
|
1617
|
-
* Evaluate
|
|
1618
|
-
* If out is omitted a new object is allocated (avoid in hot paths).
|
|
1619
|
-
* Uses centripetal Catmull-Rom for position (posInterp === 'catmullrom') or lerp.
|
|
1686
|
+
* Evaluate interpolated TRS pose at current cursor.
|
|
1620
1687
|
* @param {{ pos:number[], rot:number[], scl:number[] }} [out]
|
|
1621
1688
|
* @returns {{ pos:number[], rot:number[], scl:number[] }} out
|
|
1622
1689
|
*/
|
|
1623
1690
|
eval(out) {
|
|
1624
|
-
out = out || { pos:
|
|
1691
|
+
out = out || { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] };
|
|
1625
1692
|
const n = this.keyframes.length;
|
|
1626
1693
|
if (n === 0) return out;
|
|
1627
1694
|
|
|
@@ -1640,27 +1707,30 @@ class PoseTrack {
|
|
|
1640
1707
|
const k0 = this.keyframes[seg];
|
|
1641
1708
|
const k1 = this.keyframes[seg + 1];
|
|
1642
1709
|
|
|
1643
|
-
//
|
|
1710
|
+
// pos — Catmull-Rom or lerp
|
|
1644
1711
|
if (this.posInterp === 'catmullrom') {
|
|
1645
|
-
const p0 = seg > 0
|
|
1712
|
+
const p0 = seg > 0 ? this.keyframes[seg - 1].pos : k0.pos;
|
|
1646
1713
|
const p3 = seg + 2 < n ? this.keyframes[seg + 2].pos : k1.pos;
|
|
1647
1714
|
catmullRomVec3(out.pos, p0, k0.pos, k1.pos, p3, t);
|
|
1648
1715
|
} else {
|
|
1649
1716
|
lerpVec3(out.pos, k0.pos, k1.pos, t);
|
|
1650
1717
|
}
|
|
1651
1718
|
|
|
1652
|
-
//
|
|
1653
|
-
|
|
1719
|
+
// rot — slerp or nlerp
|
|
1720
|
+
if (this.rotInterp === 'nlerp') {
|
|
1721
|
+
qNlerp(out.rot, k0.rot, k1.rot, t);
|
|
1722
|
+
} else {
|
|
1723
|
+
qSlerp(out.rot, k0.rot, k1.rot, t);
|
|
1724
|
+
}
|
|
1654
1725
|
|
|
1655
|
-
//
|
|
1726
|
+
// scl — lerp
|
|
1656
1727
|
lerpVec3(out.scl, k0.scl, k1.scl, t);
|
|
1657
1728
|
|
|
1658
1729
|
return out;
|
|
1659
1730
|
}
|
|
1660
1731
|
|
|
1661
1732
|
/**
|
|
1662
|
-
* Evaluate
|
|
1663
|
-
* Reuses internal scratch arrays — no allocation per call.
|
|
1733
|
+
* Evaluate into an existing column-major mat4.
|
|
1664
1734
|
* @param {Float32Array|number[]} outMat4 16-element array.
|
|
1665
1735
|
* @returns {Float32Array|number[]} outMat4
|
|
1666
1736
|
*/
|
|
@@ -1668,17 +1738,163 @@ class PoseTrack {
|
|
|
1668
1738
|
const xf = this.eval({ pos: this._pos, rot: this._rot, scl: this._scl });
|
|
1669
1739
|
return transformToMat4(outMat4, xf);
|
|
1670
1740
|
}
|
|
1741
|
+
}
|
|
1671
1742
|
|
|
1672
|
-
|
|
1743
|
+
// =========================================================================
|
|
1744
|
+
// S7 CameraTrack
|
|
1745
|
+
// =========================================================================
|
|
1673
1746
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1747
|
+
/**
|
|
1748
|
+
* Lookat camera keyframe track.
|
|
1749
|
+
*
|
|
1750
|
+
* Keyframe shape: { eye:[x,y,z], center:[x,y,z], up:[x,y,z] }
|
|
1751
|
+
*
|
|
1752
|
+
* Each field is independently interpolated — eye and center along their
|
|
1753
|
+
* own paths, up nlerped on the unit sphere. This correctly handles cameras
|
|
1754
|
+
* that always look at a fixed target (center stays at origin throughout)
|
|
1755
|
+
* as well as free-fly paths where center moves independently.
|
|
1756
|
+
*
|
|
1757
|
+
* add() accepts individual specs or a bulk array of specs:
|
|
1758
|
+
*
|
|
1759
|
+
* { eye, center, up? } explicit lookat; up defaults to [0,1,0]
|
|
1760
|
+
* { vMatrix: mat4 } view matrix (world→eye); eye reconstructed via -R^T·t
|
|
1761
|
+
* { eMatrix: mat4 } eye matrix (eye→world); eye read from col3 directly
|
|
1762
|
+
* [ spec, spec, ... ] bulk
|
|
1763
|
+
*
|
|
1764
|
+
* Note on up for matrix forms:
|
|
1765
|
+
* up is always [0,1,0]. The matrix's col1 (up_ortho) is intentionally
|
|
1766
|
+
* not used — it differs from the hint [0,1,0] for upright cameras and
|
|
1767
|
+
* passing it to cam.camera() shifts orbitControl's orbit reference.
|
|
1768
|
+
* Use capturePose() (p5.tree bridge) when the real up hint is needed.
|
|
1769
|
+
*
|
|
1770
|
+
* eval() writes { eye, center, up }:
|
|
1771
|
+
* eye — Catmull-Rom (eyeInterp='catmullrom') or lerp
|
|
1772
|
+
* center — Catmull-Rom (centerInterp='catmullrom') or lerp
|
|
1773
|
+
* up — nlerp (normalize-after-lerp on unit sphere)
|
|
1774
|
+
*
|
|
1775
|
+
* @example
|
|
1776
|
+
* const track = new CameraTrack()
|
|
1777
|
+
* track.add({ eye:[0,0,500], center:[0,0,0] })
|
|
1778
|
+
* track.add({ eMatrix: myEyeMatrix })
|
|
1779
|
+
* track.add({ vMatrix: myViewMatrix })
|
|
1780
|
+
* track.play({ loop: true })
|
|
1781
|
+
* // per frame:
|
|
1782
|
+
* track.tick()
|
|
1783
|
+
* const out = { eye:[0,0,0], center:[0,0,0], up:[0,1,0] }
|
|
1784
|
+
* track.eval(out)
|
|
1785
|
+
* cam.camera(out.eye[0],out.eye[1],out.eye[2],
|
|
1786
|
+
* out.center[0],out.center[1],out.center[2],
|
|
1787
|
+
* out.up[0],out.up[1],out.up[2])
|
|
1788
|
+
*/
|
|
1789
|
+
class CameraTrack extends Track {
|
|
1790
|
+
constructor() {
|
|
1791
|
+
super();
|
|
1792
|
+
/**
|
|
1793
|
+
* Eye position interpolation mode.
|
|
1794
|
+
* @type {'catmullrom'|'linear'}
|
|
1795
|
+
*/
|
|
1796
|
+
this.eyeInterp = 'catmullrom';
|
|
1797
|
+
/**
|
|
1798
|
+
* Center (lookat target) interpolation mode.
|
|
1799
|
+
* 'linear' suits fixed or predictably moving targets.
|
|
1800
|
+
* 'catmullrom' gives smoother paths when center is also flying freely.
|
|
1801
|
+
* @type {'catmullrom'|'linear'}
|
|
1802
|
+
*/
|
|
1803
|
+
this.centerInterp = 'linear';
|
|
1804
|
+
// Scratch for toCamera() — avoids hot-path allocations
|
|
1805
|
+
this._eye = [0,0,0];
|
|
1806
|
+
this._center = [0,0,0];
|
|
1807
|
+
this._up = [0,1,0];
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* Append one or more camera keyframes. Adjacent duplicates are skipped by default.
|
|
1812
|
+
*
|
|
1813
|
+
* @param {Object|Object[]} spec
|
|
1814
|
+
* { eye, center, up? } or { vMatrix: mat4 } or { eMatrix: mat4 } or an array of either.
|
|
1815
|
+
* @param {{ deduplicate?: boolean }} [opts]
|
|
1816
|
+
*/
|
|
1817
|
+
add(spec, opts) {
|
|
1818
|
+
if (Array.isArray(spec)) {
|
|
1819
|
+
for (const s of spec) this.add(s, opts);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const kf = _parseCameraSpec(spec);
|
|
1823
|
+
if (!kf) return;
|
|
1824
|
+
const dedup = !opts || opts.deduplicate !== false;
|
|
1825
|
+
if (dedup && this.keyframes.length > 0) {
|
|
1826
|
+
if (_sameCameraKeyframe(this.keyframes[this.keyframes.length - 1], kf)) return;
|
|
1827
|
+
}
|
|
1828
|
+
this.keyframes.push(kf);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* Replace (or append at end) the keyframe at index.
|
|
1833
|
+
* @param {number} index
|
|
1834
|
+
* @param {Object} spec
|
|
1835
|
+
* @returns {boolean}
|
|
1836
|
+
*/
|
|
1837
|
+
set(index, spec) {
|
|
1838
|
+
if (!_isNum(index)) return false;
|
|
1839
|
+
const i = index | 0, kf = _parseCameraSpec(spec);
|
|
1840
|
+
if (!kf || i < 0 || i > this.keyframes.length) return false;
|
|
1841
|
+
if (i === this.keyframes.length) this.keyframes.push(kf);
|
|
1842
|
+
else this.keyframes[i] = kf;
|
|
1843
|
+
return true;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* Evaluate interpolated camera pose at current cursor.
|
|
1848
|
+
*
|
|
1849
|
+
* @param {{ eye:number[], center:number[], up:number[] }} [out]
|
|
1850
|
+
* @returns {{ eye:number[], center:number[], up:number[] }} out
|
|
1851
|
+
*/
|
|
1852
|
+
eval(out) {
|
|
1853
|
+
out = out || { eye:[0,0,0], center:[0,0,0], up:[0,1,0] };
|
|
1854
|
+
const n = this.keyframes.length;
|
|
1855
|
+
if (n === 0) return out;
|
|
1856
|
+
|
|
1857
|
+
if (n === 1) {
|
|
1858
|
+
const k = this.keyframes[0];
|
|
1859
|
+
out.eye[0]=k.eye[0]; out.eye[1]=k.eye[1]; out.eye[2]=k.eye[2];
|
|
1860
|
+
out.center[0]=k.center[0]; out.center[1]=k.center[1]; out.center[2]=k.center[2];
|
|
1861
|
+
out.up[0]=k.up[0]; out.up[1]=k.up[1]; out.up[2]=k.up[2];
|
|
1862
|
+
return out;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const nSeg = n - 1;
|
|
1676
1866
|
const dur = Math.max(1, this.duration | 0);
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1867
|
+
const seg = _clampS(this.seg, 0, nSeg - 1);
|
|
1868
|
+
const t = _clamp01(this.f / dur);
|
|
1869
|
+
const k0 = this.keyframes[seg];
|
|
1870
|
+
const k1 = this.keyframes[seg + 1];
|
|
1871
|
+
|
|
1872
|
+
// eye — Catmull-Rom or lerp
|
|
1873
|
+
if (this.eyeInterp === 'catmullrom') {
|
|
1874
|
+
const p0 = seg > 0 ? this.keyframes[seg - 1].eye : k0.eye;
|
|
1875
|
+
const p3 = seg + 2 < n ? this.keyframes[seg + 2].eye : k1.eye;
|
|
1876
|
+
catmullRomVec3(out.eye, p0, k0.eye, k1.eye, p3, t);
|
|
1877
|
+
} else {
|
|
1878
|
+
lerpVec3(out.eye, k0.eye, k1.eye, t);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// center — Catmull-Rom or lerp (independent lookat target)
|
|
1882
|
+
if (this.centerInterp === 'catmullrom') {
|
|
1883
|
+
const c0 = seg > 0 ? this.keyframes[seg - 1].center : k0.center;
|
|
1884
|
+
const c3 = seg + 2 < n ? this.keyframes[seg + 2].center : k1.center;
|
|
1885
|
+
catmullRomVec3(out.center, c0, k0.center, k1.center, c3, t);
|
|
1886
|
+
} else {
|
|
1887
|
+
lerpVec3(out.center, k0.center, k1.center, t);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// up — nlerp (normalize after lerp; correct for typical near-upright cameras)
|
|
1891
|
+
const ux = k0.up[0] + t*(k1.up[0]-k0.up[0]);
|
|
1892
|
+
const uy = k0.up[1] + t*(k1.up[1]-k0.up[1]);
|
|
1893
|
+
const uz = k0.up[2] + t*(k1.up[2]-k0.up[2]);
|
|
1894
|
+
const ul = Math.sqrt(ux*ux+uy*uy+uz*uz) || 1;
|
|
1895
|
+
out.up[0]=ux/ul; out.up[1]=uy/ul; out.up[2]=uz/ul;
|
|
1896
|
+
|
|
1897
|
+
return out;
|
|
1682
1898
|
}
|
|
1683
1899
|
}
|
|
1684
1900
|
|
|
@@ -1839,5 +2055,5 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
|
|
|
1839
2055
|
return allIn ? VISIBLE : SEMIVISIBLE;
|
|
1840
2056
|
}
|
|
1841
2057
|
|
|
1842
|
-
export { EYE, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, PoseTrack, SCREEN, SEMIVISIBLE, VISIBLE, WEBGL, WEBGPU, WORLD, _i, _j, _k, boxVisibility, catmullRomVec3, distanceToPlane, frustumPlanes, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction,
|
|
2058
|
+
export { CameraTrack, EYE, INVISIBLE, MATRIX, MODEL, NDC, ORIGIN, PLANE_BOTTOM, PLANE_FAR, PLANE_LEFT, PLANE_NEAR, PLANE_RIGHT, PLANE_TOP, PoseTrack, SCREEN, SEMIVISIBLE, VISIBLE, WEBGL, WEBGPU, WORLD, _i, _j, _k, boxVisibility, catmullRomVec3, distanceToPlane, frustumPlanes, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction, mat3NormalFromMat4, mat4Invert, mat4Location, mat4MV, mat4Mul, mat4MulPoint, mat4PV, mat4ToTransform, mat4Transpose, pixelRatio, pointVisibility, projBottom, projFar, projFov, projHfov, projIsOrtho, projLeft, projNear, projRight, projTop, qCopy, qDot, qFromAxisAngle, qFromLookDir, qFromMat4, qFromRotMat3x3, qMul, qNegate, qNlerp, qNormalize, qSet, qSlerp, qToMat4, quatToAxisAngle, sphereVisibility, transformToMat4 };
|
|
1843
2059
|
//# sourceMappingURL=index.js.map
|