@nakednous/tree 0.0.9 → 0.0.11
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 +51 -14
- package/dist/index.js +788 -563
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,40 +31,580 @@ const _j = Object.freeze([0, -1, 0]);
|
|
|
31
31
|
const _k = Object.freeze([0, 0, -1]);
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* @file
|
|
35
|
-
* @module tree/
|
|
34
|
+
* @file Quaternion algebra and mat4/mat3 conversions.
|
|
35
|
+
* @module tree/quat
|
|
36
36
|
* @license AGPL-3.0-only
|
|
37
37
|
*
|
|
38
|
-
*
|
|
38
|
+
* Quaternions are stored as flat [x, y, z, w] arrays (w-last, glTF layout).
|
|
39
39
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* All functions follow the out-first, zero-allocation contract.
|
|
41
|
+
* Conversion functions bridge between quaternion and matrix representations
|
|
42
|
+
* but do not perform any higher-level graphics operations — those belong
|
|
43
|
+
* in form.js (matrix construction from specs) or track.js (animation).
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// =========================================================================
|
|
48
|
+
// Basic ops
|
|
49
|
+
// =========================================================================
|
|
50
|
+
|
|
51
|
+
/** Set all four components. @returns {number[]} out */
|
|
52
|
+
const qSet = (out, x, y, z, w) => {
|
|
53
|
+
out[0] = x; out[1] = y; out[2] = z; out[3] = w; return out;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Copy quaternion a into out. @returns {number[]} out */
|
|
57
|
+
const qCopy = (out, a) => {
|
|
58
|
+
out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; return out;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Dot product of two quaternions. */
|
|
62
|
+
const qDot = (a, b) => a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3];
|
|
63
|
+
|
|
64
|
+
/** Normalise quaternion in-place. @returns {number[]} out */
|
|
65
|
+
const qNormalize = (out) => {
|
|
66
|
+
const l = Math.sqrt(out[0]*out[0]+out[1]*out[1]+out[2]*out[2]+out[3]*out[3]) || 1;
|
|
67
|
+
out[0]/=l; out[1]/=l; out[2]/=l; out[3]/=l; return out;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Negate quaternion (same rotation, different hemisphere). @returns {number[]} out */
|
|
71
|
+
const qNegate = (out, a) => {
|
|
72
|
+
out[0]=-a[0]; out[1]=-a[1]; out[2]=-a[2]; out[3]=-a[3]; return out;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Hamilton product out = a * b. @returns {number[]} out */
|
|
76
|
+
const qMul = (out, a, b) => {
|
|
77
|
+
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];
|
|
78
|
+
out[0]=aw*bx+ax*bw+ay*bz-az*by;
|
|
79
|
+
out[1]=aw*by-ax*bz+ay*bw+az*bx;
|
|
80
|
+
out[2]=aw*bz+ax*by-ay*bx+az*bw;
|
|
81
|
+
out[3]=aw*bw-ax*bx-ay*by-az*bz;
|
|
82
|
+
return out;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// =========================================================================
|
|
86
|
+
// Interpolation
|
|
87
|
+
// =========================================================================
|
|
88
|
+
|
|
89
|
+
/** Spherical linear interpolation. @returns {number[]} out */
|
|
90
|
+
const qSlerp = (out, a, b, t) => {
|
|
91
|
+
let bx=b[0],by=b[1],bz=b[2],bw=b[3];
|
|
92
|
+
let d = a[0]*bx+a[1]*by+a[2]*bz+a[3]*bw;
|
|
93
|
+
if (d < 0) { bx=-bx; by=-by; bz=-bz; bw=-bw; d=-d; }
|
|
94
|
+
let f0, f1;
|
|
95
|
+
if (1-d > 1e-10) {
|
|
96
|
+
const th=Math.acos(d), st=Math.sin(th);
|
|
97
|
+
f0=Math.sin((1-t)*th)/st; f1=Math.sin(t*th)/st;
|
|
98
|
+
} else {
|
|
99
|
+
f0=1-t; f1=t;
|
|
100
|
+
}
|
|
101
|
+
out[0]=a[0]*f0+bx*f1; out[1]=a[1]*f0+by*f1;
|
|
102
|
+
out[2]=a[2]*f0+bz*f1; out[3]=a[3]*f0+bw*f1;
|
|
103
|
+
return qNormalize(out);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Normalised linear interpolation (nlerp).
|
|
108
|
+
* Cheaper than slerp; slightly non-constant angular velocity.
|
|
109
|
+
* Handles antipodal quats by flipping b when dot < 0.
|
|
110
|
+
* @returns {number[]} out
|
|
111
|
+
*/
|
|
112
|
+
const qNlerp = (out, a, b, t) => {
|
|
113
|
+
let bx=b[0],by=b[1],bz=b[2],bw=b[3];
|
|
114
|
+
if (a[0]*bx+a[1]*by+a[2]*bz+a[3]*bw < 0) { bx=-bx; by=-by; bz=-bz; bw=-bw; }
|
|
115
|
+
out[0]=a[0]+t*(bx-a[0]); out[1]=a[1]+t*(by-a[1]);
|
|
116
|
+
out[2]=a[2]+t*(bz-a[2]); out[3]=a[3]+t*(bw-a[3]);
|
|
117
|
+
return qNormalize(out);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// =========================================================================
|
|
121
|
+
// Construction
|
|
122
|
+
// =========================================================================
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build a quaternion from axis-angle.
|
|
126
|
+
* @param {number[]} out
|
|
127
|
+
* @param {number} ax @param {number} ay @param {number} az Axis (need not be unit).
|
|
128
|
+
* @param {number} angle Radians.
|
|
129
|
+
* @returns {number[]} out
|
|
130
|
+
*/
|
|
131
|
+
const qFromAxisAngle = (out, ax, ay, az, angle) => {
|
|
132
|
+
const half = angle * 0.5;
|
|
133
|
+
const s = Math.sin(half);
|
|
134
|
+
const len = Math.sqrt(ax*ax + ay*ay + az*az) || 1;
|
|
135
|
+
out[0] = s * ax / len; out[1] = s * ay / len; out[2] = s * az / len;
|
|
136
|
+
out[3] = Math.cos(half);
|
|
137
|
+
return out;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build a quaternion from a look direction (−Z forward) and optional up (default +Y).
|
|
142
|
+
* @param {number[]} out
|
|
143
|
+
* @param {number[]} dir Forward direction [x,y,z].
|
|
144
|
+
* @param {number[]} [up] Up vector [x,y,z].
|
|
145
|
+
* @returns {number[]} out
|
|
146
|
+
*/
|
|
147
|
+
const qFromLookDir = (out, dir, up) => {
|
|
148
|
+
let fx=dir[0],fy=dir[1],fz=dir[2];
|
|
149
|
+
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
150
|
+
fx/=fl; fy/=fl; fz/=fl;
|
|
151
|
+
let ux=up?up[0]:0, uy=up?up[1]:1, uz=up?up[2]:0;
|
|
152
|
+
let rx=uy*fz-uz*fy, ry=uz*fx-ux*fz, rz=ux*fy-uy*fx;
|
|
153
|
+
const rl=Math.sqrt(rx*rx+ry*ry+rz*rz)||1;
|
|
154
|
+
rx/=rl; ry/=rl; rz/=rl;
|
|
155
|
+
ux=fy*rz-fz*ry; uy=fz*rx-fx*rz; uz=fx*ry-fy*rx;
|
|
156
|
+
return qFromRotMat3x3(out, rx,ry,rz, ux,uy,uz, -fx,-fy,-fz);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build a quaternion from a 3×3 rotation matrix (9 row-major scalars).
|
|
161
|
+
* @returns {number[]} out (normalised)
|
|
162
|
+
*/
|
|
163
|
+
const qFromRotMat3x3 = (out, m00,m01,m02, m10,m11,m12, m20,m21,m22) => {
|
|
164
|
+
const tr = m00+m11+m22;
|
|
165
|
+
if (tr > 0) {
|
|
166
|
+
const s=0.5/Math.sqrt(tr+1);
|
|
167
|
+
out[3]=0.25/s; out[0]=(m21-m12)*s; out[1]=(m02-m20)*s; out[2]=(m10-m01)*s;
|
|
168
|
+
} else if (m00>m11 && m00>m22) {
|
|
169
|
+
const s=2*Math.sqrt(1+m00-m11-m22);
|
|
170
|
+
out[3]=(m21-m12)/s; out[0]=0.25*s; out[1]=(m01+m10)/s; out[2]=(m02+m20)/s;
|
|
171
|
+
} else if (m11>m22) {
|
|
172
|
+
const s=2*Math.sqrt(1+m11-m00-m22);
|
|
173
|
+
out[3]=(m02-m20)/s; out[0]=(m01+m10)/s; out[1]=0.25*s; out[2]=(m12+m21)/s;
|
|
174
|
+
} else {
|
|
175
|
+
const s=2*Math.sqrt(1+m22-m00-m11);
|
|
176
|
+
out[3]=(m10-m01)/s; out[0]=(m02+m20)/s; out[1]=(m12+m21)/s; out[2]=0.25*s;
|
|
177
|
+
}
|
|
178
|
+
return qNormalize(out);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extract a unit quaternion from the upper-left 3×3 of a column-major mat4.
|
|
183
|
+
* @param {number[]} out
|
|
184
|
+
* @param {Float32Array|number[]} m Column-major mat4.
|
|
185
|
+
* @returns {number[]} out
|
|
186
|
+
*/
|
|
187
|
+
const qFromMat4 = (out, m) =>
|
|
188
|
+
qFromRotMat3x3(out, m[0],m[4],m[8], m[1],m[5],m[9], m[2],m[6],m[10]);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Write a quaternion into the rotation block of a column-major mat4.
|
|
192
|
+
* Translation and perspective rows/cols are set to identity values.
|
|
193
|
+
* @param {Float32Array|number[]} out 16-element array.
|
|
194
|
+
* @param {number[]} q [x,y,z,w].
|
|
195
|
+
* @returns {Float32Array|number[]} out
|
|
196
|
+
*/
|
|
197
|
+
const qToMat4 = (out, q) => {
|
|
198
|
+
const x=q[0],y=q[1],z=q[2],w=q[3];
|
|
199
|
+
const x2=x+x,y2=y+y,z2=z+z;
|
|
200
|
+
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;
|
|
201
|
+
out[0]=1-(yy+zz); out[1]=xy+wz; out[2]=xz-wy; out[3]=0;
|
|
202
|
+
out[4]=xy-wz; out[5]=1-(xx+zz); out[6]=yz+wx; out[7]=0;
|
|
203
|
+
out[8]=xz+wy; out[9]=yz-wx; out[10]=1-(xx+yy); out[11]=0;
|
|
204
|
+
out[12]=0; out[13]=0; out[14]=0; out[15]=1;
|
|
205
|
+
return out;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// =========================================================================
|
|
209
|
+
// Decomposition
|
|
210
|
+
// =========================================================================
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Decompose a unit quaternion into { axis:[x,y,z], angle } (radians).
|
|
214
|
+
* @param {number[]} q [x,y,z,w].
|
|
215
|
+
* @param {Object} [out]
|
|
216
|
+
* @returns {{ axis: number[], angle: number }}
|
|
217
|
+
*/
|
|
218
|
+
const quatToAxisAngle = (q, out) => {
|
|
219
|
+
out = out || {};
|
|
220
|
+
const x=q[0],y=q[1],z=q[2],w=q[3];
|
|
221
|
+
const sinHalf = Math.sqrt(x*x+y*y+z*z);
|
|
222
|
+
if (sinHalf < 1e-8) { out.axis=[0,1,0]; out.angle=0; return out; }
|
|
223
|
+
out.angle = 2*Math.atan2(sinHalf, w);
|
|
224
|
+
out.axis = [x/sinHalf, y/sinHalf, z/sinHalf];
|
|
225
|
+
return out;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @file Matrix construction from geometric specs and partial decomposition.
|
|
230
|
+
* @module tree/form
|
|
231
|
+
* @license AGPL-3.0-only
|
|
232
|
+
*
|
|
233
|
+
* Constructs mat4s from higher-level specs: TRS transforms, orthonormal
|
|
234
|
+
* bases, lookat parameters, projection parameters, and special-purpose
|
|
235
|
+
* matrices (bias, reflection).
|
|
236
|
+
*
|
|
237
|
+
* Design invariant: form.js has no dependency on query.js. Construction
|
|
238
|
+
* from specs requires only scalar arithmetic and quaternion conversions.
|
|
239
|
+
* Callers compose the resulting matrices using query.js (mat4Mul etc.).
|
|
240
|
+
*
|
|
241
|
+
* Lookat constructors live here because a camera is just a frame — the eye
|
|
242
|
+
* matrix is the camera object's model matrix, not a camera-specific concept.
|
|
243
|
+
* There is no camera module; mat4LookAt and mat4EyeMatrix are frame
|
|
244
|
+
* constructions that happen to use lookat parameterisation.
|
|
245
|
+
*
|
|
246
|
+
* Projection constructors live here because they construct matrices from
|
|
247
|
+
* geometric parameters. Projection scalar reads (projNear, projFov, etc.)
|
|
248
|
+
* live in query.js — they interrogate an existing projection matrix.
|
|
249
|
+
*
|
|
250
|
+
* Partial decomposers (mat4To___) are the inverse of construction — they
|
|
251
|
+
* extract a single component from an existing matrix. Kept alongside
|
|
252
|
+
* constructors because they are paired operations on the same components.
|
|
253
|
+
*
|
|
254
|
+
* Imports quat.js only. No dependency on query.js, visibility.js, or track.js.
|
|
255
|
+
*
|
|
256
|
+
* All functions follow the out-first, zero-allocation contract.
|
|
257
|
+
* Returns null on degeneracy where applicable.
|
|
258
|
+
*/
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
// =========================================================================
|
|
262
|
+
// Frame construction
|
|
263
|
+
// =========================================================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Rigid frame from orthonormal basis + translation.
|
|
267
|
+
* The primitive that lookat constructors use internally.
|
|
268
|
+
*
|
|
269
|
+
* Column-major layout: col0=right, col1=up, col2=forward, col3=translation.
|
|
270
|
+
*
|
|
271
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
272
|
+
* @param {number} rx,ry,rz Right vector (col 0).
|
|
273
|
+
* @param {number} ux,uy,uz Up vector (col 1).
|
|
274
|
+
* @param {number} fx,fy,fz Forward vec (col 2).
|
|
275
|
+
* @param {number} tx,ty,tz Translation (col 3).
|
|
276
|
+
* @returns {Float32Array|number[]} out
|
|
277
|
+
*/
|
|
278
|
+
function mat4FromBasis(out, rx,ry,rz, ux,uy,uz, fx,fy,fz, tx,ty,tz) {
|
|
279
|
+
out[0]=rx; out[1]=ry; out[2]=rz; out[3]=0;
|
|
280
|
+
out[4]=ux; out[5]=uy; out[6]=uz; out[7]=0;
|
|
281
|
+
out[8]=fx; out[9]=fy; out[10]=fz; out[11]=0;
|
|
282
|
+
out[12]=tx; out[13]=ty; out[14]=tz; out[15]=1;
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* View matrix (world→eye) from lookat parameters.
|
|
288
|
+
* Cheaper than building the eye matrix and inverting.
|
|
289
|
+
*
|
|
290
|
+
* Convention: −Z axis points toward center (camera looks along −Z in eye space).
|
|
291
|
+
*
|
|
292
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
293
|
+
* @param {number} ex,ey,ez Eye (camera) position.
|
|
294
|
+
* @param {number} cx,cy,cz Center (look-at target).
|
|
295
|
+
* @param {number} ux,uy,uz World up hint (need not be unit).
|
|
296
|
+
* @returns {Float32Array|number[]} out
|
|
297
|
+
*/
|
|
298
|
+
function mat4LookAt(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
|
|
299
|
+
// z = normalize(eye - center) (camera +Z away from target)
|
|
300
|
+
let zx=ex-cx, zy=ey-cy, zz=ez-cz;
|
|
301
|
+
const zl=Math.sqrt(zx*zx+zy*zy+zz*zz)||1;
|
|
302
|
+
zx/=zl; zy/=zl; zz/=zl;
|
|
303
|
+
// x = normalize(up × z) (right)
|
|
304
|
+
let xx=uy*zz-uz*zy, xy=uz*zx-ux*zz, xz=ux*zy-uy*zx;
|
|
305
|
+
const xl=Math.sqrt(xx*xx+xy*xy+xz*xz)||1;
|
|
306
|
+
xx/=xl; xy/=xl; xz/=xl;
|
|
307
|
+
// y = z × x (up_ortho, guaranteed perpendicular)
|
|
308
|
+
const yx=zy*xz-zz*xy, yy=zz*xx-zx*xz, yz=zx*xy-zy*xx;
|
|
309
|
+
// View = [R | -R·t] (column-major)
|
|
310
|
+
out[0]=xx; out[1]=yx; out[2]=zx; out[3]=0;
|
|
311
|
+
out[4]=xy; out[5]=yy; out[6]=zy; out[7]=0;
|
|
312
|
+
out[8]=xz; out[9]=yz; out[10]=zz; out[11]=0;
|
|
313
|
+
out[12]=-(xx*ex+xy*ey+xz*ez);
|
|
314
|
+
out[13]=-(yx*ex+yy*ey+yz*ez);
|
|
315
|
+
out[14]=-(zx*ex+zy*ey+zz*ez);
|
|
316
|
+
out[15]=1;
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Eye matrix (eye→world) from lookat parameters.
|
|
322
|
+
* Transpose of the rotation block + direct translation column.
|
|
323
|
+
* Same inputs as mat4LookAt.
|
|
324
|
+
*
|
|
325
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
326
|
+
* @param {number} ex,ey,ez Eye (camera) position.
|
|
327
|
+
* @param {number} cx,cy,cz Center (look-at target).
|
|
328
|
+
* @param {number} ux,uy,uz World up hint (need not be unit).
|
|
329
|
+
* @returns {Float32Array|number[]} out
|
|
330
|
+
*/
|
|
331
|
+
function mat4EyeMatrix(out, ex,ey,ez, cx,cy,cz, ux,uy,uz) {
|
|
332
|
+
// Same basis computation as mat4LookAt.
|
|
333
|
+
let zx=ex-cx, zy=ey-cy, zz=ez-cz;
|
|
334
|
+
const zl=Math.sqrt(zx*zx+zy*zy+zz*zz)||1;
|
|
335
|
+
zx/=zl; zy/=zl; zz/=zl;
|
|
336
|
+
let xx=uy*zz-uz*zy, xy=uz*zx-ux*zz, xz=ux*zy-uy*zx;
|
|
337
|
+
const xl=Math.sqrt(xx*xx+xy*xy+xz*xz)||1;
|
|
338
|
+
xx/=xl; xy/=xl; xz/=xl;
|
|
339
|
+
const yx=zy*xz-zz*xy, yy=zz*xx-zx*xz, yz=zx*xy-zy*xx;
|
|
340
|
+
// Eye matrix = [R^T | t] (rotation transposed, translation = eye position)
|
|
341
|
+
out[0]=xx; out[1]=xy; out[2]=xz; out[3]=0;
|
|
342
|
+
out[4]=yx; out[5]=yy; out[6]=yz; out[7]=0;
|
|
343
|
+
out[8]=zx; out[9]=zy; out[10]=zz; out[11]=0;
|
|
344
|
+
out[12]=ex; out[13]=ey; out[14]=ez; out[15]=1;
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// =========================================================================
|
|
349
|
+
// TRS construction
|
|
350
|
+
// =========================================================================
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Column-major mat4 from flat TRS scalars.
|
|
354
|
+
* No struct allocation — all components passed as plain numbers.
|
|
355
|
+
*
|
|
356
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
357
|
+
* @param {number} tx,ty,tz Translation.
|
|
358
|
+
* @param {number} qx,qy,qz,qw Rotation quaternion [x,y,z,w].
|
|
359
|
+
* @param {number} sx,sy,sz Scale.
|
|
360
|
+
* @returns {Float32Array|number[]} out
|
|
361
|
+
*/
|
|
362
|
+
function mat4FromTRS(out, tx,ty,tz, qx,qy,qz,qw, sx,sy,sz) {
|
|
363
|
+
const x2=qx+qx,y2=qy+qy,z2=qz+qz;
|
|
364
|
+
const xx=qx*x2,xy=qx*y2,xz=qx*z2,yy=qy*y2,yz=qy*z2,zz=qz*z2;
|
|
365
|
+
const wx=qw*x2,wy=qw*y2,wz=qw*z2;
|
|
366
|
+
out[0]=(1-(yy+zz))*sx; out[1]=(xy+wz)*sx; out[2]=(xz-wy)*sx; out[3]=0;
|
|
367
|
+
out[4]=(xy-wz)*sy; out[5]=(1-(xx+zz))*sy; out[6]=(yz+wx)*sy; out[7]=0;
|
|
368
|
+
out[8]=(xz+wy)*sz; out[9]=(yz-wx)*sz; out[10]=(1-(xx+yy))*sz; out[11]=0;
|
|
369
|
+
out[12]=tx; out[13]=ty; out[14]=tz; out[15]=1;
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Translation-only mat4.
|
|
375
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
376
|
+
* @param {number} tx,ty,tz
|
|
377
|
+
* @returns {Float32Array|number[]} out
|
|
378
|
+
*/
|
|
379
|
+
function mat4FromTranslation(out, tx,ty,tz) {
|
|
380
|
+
out[0]=1; out[1]=0; out[2]=0; out[3]=0;
|
|
381
|
+
out[4]=0; out[5]=1; out[6]=0; out[7]=0;
|
|
382
|
+
out[8]=0; out[9]=0; out[10]=1; out[11]=0;
|
|
383
|
+
out[12]=tx; out[13]=ty; out[14]=tz; out[15]=1;
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Scale-only mat4.
|
|
389
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
390
|
+
* @param {number} sx,sy,sz
|
|
391
|
+
* @returns {Float32Array|number[]} out
|
|
392
|
+
*/
|
|
393
|
+
function mat4FromScale(out, sx,sy,sz) {
|
|
394
|
+
out[0]=sx; out[1]=0; out[2]=0; out[3]=0;
|
|
395
|
+
out[4]=0; out[5]=sy; out[6]=0; out[7]=0;
|
|
396
|
+
out[8]=0; out[9]=0; out[10]=sz; out[11]=0;
|
|
397
|
+
out[12]=0; out[13]=0; out[14]=0; out[15]=1;
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// =========================================================================
|
|
402
|
+
// Projection construction
|
|
403
|
+
// =========================================================================
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Perspective projection matrix.
|
|
407
|
+
*
|
|
408
|
+
* NDC convention: ndcZMin = WEBGL (−1) or WEBGPU (0).
|
|
409
|
+
* near maps to ndcZMin, far maps to +1.
|
|
410
|
+
*
|
|
411
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
412
|
+
* @param {number} fov Vertical field of view (radians).
|
|
413
|
+
* @param {number} aspect Width / height.
|
|
414
|
+
* @param {number} near Near plane distance (positive).
|
|
415
|
+
* @param {number} far Far plane distance (positive, > near).
|
|
416
|
+
* @param {number} ndcZMin -1 (WEBGL) or 0 (WEBGPU).
|
|
417
|
+
* @returns {Float32Array|number[]} out
|
|
418
|
+
*/
|
|
419
|
+
function mat4Perspective(out, fov, aspect, near, far, ndcZMin) {
|
|
420
|
+
const f = 1 / Math.tan(fov * 0.5);
|
|
421
|
+
out[0]=f/aspect; out[1]=0; out[2]=0; out[3]=0;
|
|
422
|
+
out[4]=0; out[5]=f; out[6]=0; out[7]=0;
|
|
423
|
+
out[8]=0; out[9]=0;
|
|
424
|
+
out[10]=(ndcZMin*near-far)/(far-near);
|
|
425
|
+
out[11]=-1;
|
|
426
|
+
out[12]=0; out[13]=0;
|
|
427
|
+
out[14]=(ndcZMin-1)*far*near/(far-near);
|
|
428
|
+
out[15]=0;
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Orthographic projection matrix.
|
|
434
|
+
*
|
|
435
|
+
* NDC convention: ndcZMin = WEBGL (−1) or WEBGPU (0).
|
|
436
|
+
*
|
|
437
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
438
|
+
* @param {number} left,right,bottom,top Frustum extents.
|
|
439
|
+
* @param {number} near,far Clip plane distances (positive).
|
|
440
|
+
* @param {number} ndcZMin -1 (WEBGL) or 0 (WEBGPU).
|
|
441
|
+
* @returns {Float32Array|number[]} out
|
|
442
|
+
*/
|
|
443
|
+
function mat4Ortho(out, left, right, bottom, top, near, far, ndcZMin) {
|
|
444
|
+
const rl=1/(right-left), tb=1/(top-bottom), fn=1/(far-near);
|
|
445
|
+
out[0]=2*rl; out[1]=0; out[2]=0; out[3]=0;
|
|
446
|
+
out[4]=0; out[5]=2*tb; out[6]=0; out[7]=0;
|
|
447
|
+
out[8]=0; out[9]=0;
|
|
448
|
+
out[10]=(ndcZMin-1)*fn;
|
|
449
|
+
out[11]=0;
|
|
450
|
+
out[12]=-(right+left)*rl; out[13]=-(top+bottom)*tb;
|
|
451
|
+
out[14]=(ndcZMin*far-near)*fn;
|
|
452
|
+
out[15]=1;
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Frustum (off-centre perspective) projection matrix.
|
|
458
|
+
*
|
|
459
|
+
* NDC convention: ndcZMin = WEBGL (−1) or WEBGPU (0).
|
|
460
|
+
*
|
|
461
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
462
|
+
* @param {number} left,right,bottom,top Near-plane extents.
|
|
463
|
+
* @param {number} near,far Clip plane distances (positive).
|
|
464
|
+
* @param {number} ndcZMin -1 (WEBGL) or 0 (WEBGPU).
|
|
465
|
+
* @returns {Float32Array|number[]} out
|
|
466
|
+
*/
|
|
467
|
+
function mat4Frustum(out, left, right, bottom, top, near, far, ndcZMin) {
|
|
468
|
+
const rl=1/(right-left), tb=1/(top-bottom);
|
|
469
|
+
out[0]=2*near*rl; out[1]=0; out[2]=0; out[3]=0;
|
|
470
|
+
out[4]=0; out[5]=2*near*tb; out[6]=0; out[7]=0;
|
|
471
|
+
out[8]=(right+left)*rl; out[9]=(top+bottom)*tb;
|
|
472
|
+
out[10]=(ndcZMin*near-far)/(far-near);
|
|
473
|
+
out[11]=-1;
|
|
474
|
+
out[12]=0; out[13]=0;
|
|
475
|
+
out[14]=(ndcZMin-1)*far*near/(far-near);
|
|
476
|
+
out[15]=0;
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// =========================================================================
|
|
481
|
+
// Special-purpose construction
|
|
482
|
+
// =========================================================================
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Bias matrix: remaps xyz from NDC to texture/UV space [0,1].
|
|
486
|
+
* xy always remap from [−1,1]; z remaps from [ndcZMin,1].
|
|
487
|
+
* Used to transform light-space NDC coordinates for shadow map sampling.
|
|
488
|
+
*
|
|
489
|
+
* Column-major (WebGL, ndcZMin=−1):
|
|
490
|
+
* [ 0.5 0 0 0.5 ]
|
|
491
|
+
* [ 0 0.5 0 0.5 ]
|
|
492
|
+
* [ 0 0 0.5 0.5 ]
|
|
493
|
+
* [ 0 0 0 1 ]
|
|
494
|
+
*
|
|
495
|
+
* Column-major (WebGPU, ndcZMin=0):
|
|
496
|
+
* [ 0.5 0 0 0.5 ]
|
|
497
|
+
* [ 0 0.5 0 0.5 ]
|
|
498
|
+
* [ 0 0 1 0 ]
|
|
499
|
+
* [ 0 0 0 1 ]
|
|
500
|
+
*
|
|
501
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
502
|
+
* @param {number} ndcZMin WEBGL (−1) or WEBGPU (0).
|
|
503
|
+
* @returns {Float32Array|number[]} out
|
|
504
|
+
*/
|
|
505
|
+
function mat4Bias(out, ndcZMin) {
|
|
506
|
+
const sz = 1 / (1 - ndcZMin);
|
|
507
|
+
const tz = -ndcZMin / (1 - ndcZMin);
|
|
508
|
+
out[0]=0.5; out[1]=0; out[2]=0; out[3]=0;
|
|
509
|
+
out[4]=0; out[5]=0.5; out[6]=0; out[7]=0;
|
|
510
|
+
out[8]=0; out[9]=0; out[10]=sz; out[11]=0;
|
|
511
|
+
out[12]=0.5; out[13]=0.5; out[14]=tz; out[15]=1;
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Reflection matrix across a plane ax + by + cz = d.
|
|
517
|
+
* [nx, ny, nz] must be a unit normal.
|
|
42
518
|
*
|
|
43
|
-
*
|
|
519
|
+
* @param {Float32Array|number[]} out 16-element destination.
|
|
520
|
+
* @param {number} nx,ny,nz Unit plane normal.
|
|
521
|
+
* @param {number} d Plane offset (dot(point_on_plane, normal)).
|
|
522
|
+
* @returns {Float32Array|number[]} out
|
|
523
|
+
*/
|
|
524
|
+
function mat4Reflect(out, nx,ny,nz,d) {
|
|
525
|
+
out[0]=1-2*nx*nx; out[1]=-2*ny*nx; out[2]=-2*nz*nx; out[3]=0;
|
|
526
|
+
out[4]=-2*nx*ny; out[5]=1-2*ny*ny; out[6]=-2*nz*ny; out[7]=0;
|
|
527
|
+
out[8]=-2*nx*nz; out[9]=-2*ny*nz; out[10]=1-2*nz*nz; out[11]=0;
|
|
528
|
+
out[12]=2*d*nx; out[13]=2*d*ny; out[14]=2*d*nz; out[15]=1;
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// =========================================================================
|
|
533
|
+
// Partial decomposition (mat4To___ mirrors mat4From___)
|
|
534
|
+
// =========================================================================
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Extract translation from a column-major mat4 (column 3).
|
|
538
|
+
* @param {Float32Array|number[]} out3 3-element destination.
|
|
539
|
+
* @param {Float32Array|number[]} m 16-element source.
|
|
540
|
+
* @returns {Float32Array|number[]} out3
|
|
541
|
+
*/
|
|
542
|
+
function mat4ToTranslation(out3, m) {
|
|
543
|
+
out3[0]=m[12]; out3[1]=m[13]; out3[2]=m[14];
|
|
544
|
+
return out3;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Extract scale from a column-major mat4 (column lengths of rotation block).
|
|
549
|
+
* Assumes no shear.
|
|
550
|
+
* @param {Float32Array|number[]} out3 3-element destination.
|
|
551
|
+
* @param {Float32Array|number[]} m 16-element source.
|
|
552
|
+
* @returns {Float32Array|number[]} out3
|
|
553
|
+
*/
|
|
554
|
+
function mat4ToScale(out3, m) {
|
|
555
|
+
out3[0]=Math.sqrt(m[0]*m[0]+m[1]*m[1]+m[2]*m[2]);
|
|
556
|
+
out3[1]=Math.sqrt(m[4]*m[4]+m[5]*m[5]+m[6]*m[6]);
|
|
557
|
+
out3[2]=Math.sqrt(m[8]*m[8]+m[9]*m[9]+m[10]*m[10]);
|
|
558
|
+
return out3;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Extract rotation as a unit quaternion from a column-major mat4.
|
|
563
|
+
* Scale is factored out from each column before extraction.
|
|
564
|
+
* Assumes no shear.
|
|
565
|
+
* @param {number[]} out4 4-element [x,y,z,w] destination.
|
|
566
|
+
* @param {Float32Array|number[]} m 16-element source.
|
|
567
|
+
* @returns {number[]} out4
|
|
568
|
+
*/
|
|
569
|
+
function mat4ToRotation(out4, m) {
|
|
570
|
+
const sx=Math.sqrt(m[0]*m[0]+m[1]*m[1]+m[2]*m[2])||1;
|
|
571
|
+
const sy=Math.sqrt(m[4]*m[4]+m[5]*m[5]+m[6]*m[6])||1;
|
|
572
|
+
const sz=Math.sqrt(m[8]*m[8]+m[9]*m[9]+m[10]*m[10])||1;
|
|
573
|
+
return qFromRotMat3x3(out4,
|
|
574
|
+
m[0]/sx, m[4]/sy, m[8]/sz,
|
|
575
|
+
m[1]/sx, m[5]/sy, m[9]/sz,
|
|
576
|
+
m[2]/sx, m[6]/sy, m[10]/sz);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* @file Matrix arithmetic, space-transform dispatch, and projection queries.
|
|
581
|
+
* @module tree/query
|
|
582
|
+
* @license AGPL-3.0-only
|
|
583
|
+
*
|
|
584
|
+
* The operative layer — receives existing matrices and extracts information.
|
|
585
|
+
* Contrast with form.js which constructs matrices from specs.
|
|
586
|
+
*
|
|
587
|
+
* form.js — you have specs, you want a matrix
|
|
588
|
+
* query.js — you have a matrix, you want information
|
|
44
589
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* V = view (world → eye)
|
|
48
|
-
* M = model (local → world)
|
|
590
|
+
* No dependency on form.js. Operating on matrices requires no knowledge
|
|
591
|
+
* of how they were constructed.
|
|
49
592
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
593
|
+
* Storage: column-major Float32Array / ArrayLike<number>.
|
|
594
|
+
* Element [col*4 + row] = M[row, col].
|
|
52
595
|
*
|
|
53
|
-
*
|
|
54
|
-
* Each call post-multiplies: M = M · T, so:
|
|
55
|
-
* translate(tx,ty,tz); rotateY(a); scale(s);
|
|
56
|
-
* yields M = T · R · S. A vertex v is transformed as M·v = T·R·S·v
|
|
57
|
-
* (scaled first, then rotated, then translated — last-written-first-applied).
|
|
596
|
+
* Multiply: mat4Mul(out, A, B) = A · B (standard math order).
|
|
58
597
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* The bridge extracts .mat4 (Float32Array) and feeds it directly,
|
|
64
|
-
* or uses mat4Mul(out, proj, view) for the non-cached path.
|
|
598
|
+
* Pipeline: clip = P · V · M · v
|
|
599
|
+
* P = projection (eye → clip)
|
|
600
|
+
* V = view (world → eye)
|
|
601
|
+
* M = model (local → world)
|
|
65
602
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
603
|
+
* NDC convention parameter (ndcZMin):
|
|
604
|
+
* WEBGL = -1 z ∈ [−1, 1]
|
|
605
|
+
* WEBGPU = 0 z ∈ [0, 1]
|
|
606
|
+
*
|
|
607
|
+
* All functions follow the out-first, zero-allocation contract.
|
|
68
608
|
* Returns null on degeneracy (singular matrix, etc.).
|
|
69
609
|
*/
|
|
70
610
|
|
|
@@ -193,6 +733,24 @@ function mat4MulPoint(out, m, x, y, z) {
|
|
|
193
733
|
return out;
|
|
194
734
|
}
|
|
195
735
|
|
|
736
|
+
/**
|
|
737
|
+
* Apply only the 3×3 linear block of a mat4 to a direction vector.
|
|
738
|
+
* No translation, no perspective divide. Suitable for directions and normals
|
|
739
|
+
* when the matrix is known to be orthogonal (use mat3NormalFromMat4 for normals
|
|
740
|
+
* under non-uniform scale).
|
|
741
|
+
*
|
|
742
|
+
* @param {Float32Array|number[]} out 3-element destination.
|
|
743
|
+
* @param {Float32Array|number[]} m 16-element mat4.
|
|
744
|
+
* @param {number} dx,dy,dz Input direction.
|
|
745
|
+
* @returns {Float32Array|number[]} out
|
|
746
|
+
*/
|
|
747
|
+
function mat4MulDir(out, m, dx, dy, dz) {
|
|
748
|
+
out[0] = m[0]*dx + m[4]*dy + m[8]*dz;
|
|
749
|
+
out[1] = m[1]*dx + m[5]*dy + m[9]*dz;
|
|
750
|
+
out[2] = m[2]*dx + m[6]*dy + m[10]*dz;
|
|
751
|
+
return out;
|
|
752
|
+
}
|
|
753
|
+
|
|
196
754
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
197
755
|
// Projection queries (read scalars from a projection mat4)
|
|
198
756
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -320,13 +878,13 @@ function mat3Direction(out, from, to) {
|
|
|
320
878
|
//
|
|
321
879
|
// Matrices bag m:
|
|
322
880
|
// {
|
|
323
|
-
// pMatrix:
|
|
324
|
-
// vMatrix:
|
|
325
|
-
// eMatrix?:
|
|
326
|
-
// pvMatrix?:
|
|
327
|
-
// ipvMatrix?:Float32Array(16) — inv(P · V); lazy
|
|
328
|
-
// fromFrame?:Float32Array(16) — MATRIX source frame (custom space)
|
|
329
|
-
// toFrameInv?:Float32Array(16)
|
|
881
|
+
// pMatrix: Float32Array(16) — projection (eye → clip)
|
|
882
|
+
// vMatrix: Float32Array(16) — view (world → eye)
|
|
883
|
+
// eMatrix?: Float32Array(16) — eye (eye → world, inv view); lazy
|
|
884
|
+
// pvMatrix?: Float32Array(16) — P · V; lazy
|
|
885
|
+
// ipvMatrix?: Float32Array(16) — inv(P · V); lazy
|
|
886
|
+
// fromFrame?: Float32Array(16) — MATRIX source frame (custom space)
|
|
887
|
+
// toFrameInv?:Float32Array(16) — inv(MATRIX dest frame)
|
|
330
888
|
// }
|
|
331
889
|
//
|
|
332
890
|
|
|
@@ -337,90 +895,62 @@ function _worldToScreen(out, px, py, pz, pv, vp, ndcZMin) {
|
|
|
337
895
|
const y = pv[1]*px+pv[5]*py+pv[9]*pz+pv[13];
|
|
338
896
|
const z = pv[2]*px+pv[6]*py+pv[10]*pz+pv[14];
|
|
339
897
|
const w = pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
|
|
340
|
-
const xi = (w !== 0 && w !== 1) ?
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
out[
|
|
345
|
-
out[
|
|
346
|
-
out[2] = (zi - ndcZMin) / ndcZRange;
|
|
898
|
+
const xi = (w !== 0 && w !== 1) ? 1/w : 1;
|
|
899
|
+
const nx = x*xi, ny = y*xi, nz = z*xi;
|
|
900
|
+
const vpX=vp[0], vpY=vp[1], vpW=Math.abs(vp[2]), vpH=Math.abs(vp[3]);
|
|
901
|
+
out[0] = vpX + vpW * (nx + 1) * 0.5;
|
|
902
|
+
out[1] = vpY + vpH * (1 - (ny + 1) * 0.5);
|
|
903
|
+
out[2] = (nz - ndcZMin) / (1 - ndcZMin);
|
|
347
904
|
return out;
|
|
348
905
|
}
|
|
349
906
|
|
|
350
|
-
function _screenToWorld(out,
|
|
351
|
-
const
|
|
352
|
-
const nx = (
|
|
353
|
-
const ny = (
|
|
354
|
-
const nz =
|
|
355
|
-
|
|
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];
|
|
359
|
-
out[0]=x/w; out[1]=y/w; out[2]=z/w;
|
|
360
|
-
return out;
|
|
907
|
+
function _screenToWorld(out, sx, sy, sz, ipv, vp, ndcZMin) {
|
|
908
|
+
const vpX=vp[0], vpY=vp[1], vpW=Math.abs(vp[2]), vpH=Math.abs(vp[3]);
|
|
909
|
+
const nx = (sx - vpX) / vpW * 2 - 1;
|
|
910
|
+
const ny = 1 - (sy - vpY) / vpH * 2;
|
|
911
|
+
const nz = sz * (1 - ndcZMin) + ndcZMin;
|
|
912
|
+
return mat4MulPoint(out, ipv, nx, ny, nz);
|
|
361
913
|
}
|
|
362
914
|
|
|
363
915
|
function _worldToNDC(out, px, py, pz, pv) {
|
|
364
|
-
const x
|
|
365
|
-
const y
|
|
366
|
-
const z
|
|
367
|
-
const w
|
|
368
|
-
|
|
916
|
+
const x=pv[0]*px+pv[4]*py+pv[8]*pz+pv[12];
|
|
917
|
+
const y=pv[1]*px+pv[5]*py+pv[9]*pz+pv[13];
|
|
918
|
+
const z=pv[2]*px+pv[6]*py+pv[10]*pz+pv[14];
|
|
919
|
+
const w=pv[3]*px+pv[7]*py+pv[11]*pz+pv[15];
|
|
920
|
+
const xi = (w !== 0 && w !== 1) ? 1/w : 1;
|
|
921
|
+
out[0]=x*xi; out[1]=y*xi; out[2]=z*xi;
|
|
369
922
|
return out;
|
|
370
923
|
}
|
|
371
924
|
|
|
372
|
-
function _ndcToWorld(out,
|
|
373
|
-
|
|
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];
|
|
377
|
-
out[0]=x/w; out[1]=y/w; out[2]=z/w;
|
|
378
|
-
return out;
|
|
925
|
+
function _ndcToWorld(out, nx, ny, nz, ipv) {
|
|
926
|
+
return mat4MulPoint(out, ipv, nx, ny, nz);
|
|
379
927
|
}
|
|
380
928
|
|
|
381
|
-
function _screenToNDC(out,
|
|
382
|
-
const
|
|
383
|
-
out[0] = (
|
|
384
|
-
out[1] = (
|
|
385
|
-
out[2] =
|
|
929
|
+
function _screenToNDC(out, sx, sy, sz, vp, ndcZMin) {
|
|
930
|
+
const vpX=vp[0], vpY=vp[1], vpW=Math.abs(vp[2]), vpH=Math.abs(vp[3]);
|
|
931
|
+
out[0] = (sx - vpX) / vpW * 2 - 1;
|
|
932
|
+
out[1] = 1 - (sy - vpY) / vpH * 2;
|
|
933
|
+
out[2] = sz * (1 - ndcZMin) + ndcZMin;
|
|
386
934
|
return out;
|
|
387
935
|
}
|
|
388
936
|
|
|
389
|
-
function _ndcToScreen(out,
|
|
390
|
-
const
|
|
391
|
-
out[0] = (
|
|
392
|
-
out[1] = (
|
|
393
|
-
out[2] = (
|
|
937
|
+
function _ndcToScreen(out, nx, ny, nz, vp, ndcZMin) {
|
|
938
|
+
const vpX=vp[0], vpY=vp[1], vpW=Math.abs(vp[2]), vpH=Math.abs(vp[3]);
|
|
939
|
+
out[0] = vpX + vpW * (nx + 1) * 0.5;
|
|
940
|
+
out[1] = vpY + vpH * (1 - (ny + 1) * 0.5);
|
|
941
|
+
out[2] = (nz - ndcZMin) / (1 - ndcZMin);
|
|
394
942
|
return out;
|
|
395
943
|
}
|
|
396
944
|
|
|
397
|
-
// ── _ensurePV — return pvMatrix from bag, computing inline if absent ──────
|
|
398
|
-
|
|
399
945
|
function _ensurePV(m) {
|
|
400
946
|
if (m.pvMatrix) return m.pvMatrix;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
p[1]*v[0]+p[5]*v[1]+p[9]*v[2]+p[13]*v[3],
|
|
405
|
-
p[2]*v[0]+p[6]*v[1]+p[10]*v[2]+p[14]*v[3],
|
|
406
|
-
p[3]*v[0]+p[7]*v[1]+p[11]*v[2]+p[15]*v[3],
|
|
407
|
-
p[0]*v[4]+p[4]*v[5]+p[8]*v[6]+p[12]*v[7],
|
|
408
|
-
p[1]*v[4]+p[5]*v[5]+p[9]*v[6]+p[13]*v[7],
|
|
409
|
-
p[2]*v[4]+p[6]*v[5]+p[10]*v[6]+p[14]*v[7],
|
|
410
|
-
p[3]*v[4]+p[7]*v[5]+p[11]*v[6]+p[15]*v[7],
|
|
411
|
-
p[0]*v[8]+p[4]*v[9]+p[8]*v[10]+p[12]*v[11],
|
|
412
|
-
p[1]*v[8]+p[5]*v[9]+p[9]*v[10]+p[13]*v[11],
|
|
413
|
-
p[2]*v[8]+p[6]*v[9]+p[10]*v[10]+p[14]*v[11],
|
|
414
|
-
p[3]*v[8]+p[7]*v[9]+p[11]*v[10]+p[15]*v[11],
|
|
415
|
-
p[0]*v[12]+p[4]*v[13]+p[8]*v[14]+p[12]*v[15],
|
|
416
|
-
p[1]*v[12]+p[5]*v[13]+p[9]*v[14]+p[13]*v[15],
|
|
417
|
-
p[2]*v[12]+p[6]*v[13]+p[10]*v[14]+p[14]*v[15],
|
|
418
|
-
p[3]*v[12]+p[7]*v[13]+p[11]*v[14]+p[15]*v[15],
|
|
419
|
-
];
|
|
947
|
+
m.pvMatrix = new Float32Array(16);
|
|
948
|
+
mat4Mul(m.pvMatrix, m.pMatrix, m.vMatrix);
|
|
949
|
+
return m.pvMatrix;
|
|
420
950
|
}
|
|
421
951
|
|
|
422
952
|
/**
|
|
423
|
-
* Map a point between coordinate spaces.
|
|
953
|
+
* Map a point between named coordinate spaces.
|
|
424
954
|
*
|
|
425
955
|
* @param {Vec3} out Result written here.
|
|
426
956
|
* @param {number} px,py,pz Input point.
|
|
@@ -548,90 +1078,55 @@ function mapLocation(out, px, py, pz, from, to, m, vp, ndcZMin) {
|
|
|
548
1078
|
return out;
|
|
549
1079
|
}
|
|
550
1080
|
|
|
551
|
-
// ── Direction helpers
|
|
1081
|
+
// ── Direction leaf helpers ───────────────────────────────────────────────
|
|
552
1082
|
|
|
553
1083
|
/** Apply the 3×3 linear part of a mat4 (rotation/scale, no translation). */
|
|
554
|
-
function _applyDir(out,
|
|
555
|
-
out[0]=
|
|
556
|
-
out[1]=
|
|
557
|
-
out[2]=
|
|
1084
|
+
function _applyDir(out, m, dx, dy, dz) {
|
|
1085
|
+
out[0]=m[0]*dx+m[4]*dy+m[8]*dz;
|
|
1086
|
+
out[1]=m[1]*dx+m[5]*dy+m[9]*dz;
|
|
1087
|
+
out[2]=m[2]*dx+m[6]*dy+m[10]*dz;
|
|
558
1088
|
return out;
|
|
559
1089
|
}
|
|
560
1090
|
|
|
561
1091
|
function _worldToScreenDir(out, dx, dy, dz, proj, view, vpW, vpH, ndcZMin) {
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
const
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
sdx *= pixPerUnit;
|
|
573
|
-
sdy *= pixPerUnit;
|
|
574
|
-
} else {
|
|
575
|
-
const orthoW = Math.abs(projRight(proj, ndcZMin) - projLeft(proj, ndcZMin));
|
|
576
|
-
sdx *= vpW / orthoW;
|
|
577
|
-
sdy *= vpH / Math.abs(projTop(proj, ndcZMin) - projBottom(proj, ndcZMin));
|
|
578
|
-
}
|
|
579
|
-
const near = projNear(proj, ndcZMin), far = projFar(proj);
|
|
580
|
-
const depthRange = near - far;
|
|
581
|
-
let sdz;
|
|
582
|
-
if (isPersp) {
|
|
583
|
-
sdz = edz / (depthRange / Math.tan(projFov(proj) / 2));
|
|
584
|
-
} else {
|
|
585
|
-
sdz = edz / (depthRange / (Math.abs(projRight(proj, ndcZMin) - projLeft(proj, ndcZMin)) / vpW));
|
|
586
|
-
}
|
|
587
|
-
out[0] = sdx; out[1] = sdy; out[2] = sdz;
|
|
1092
|
+
// Transform to clip space (no w divide for direction).
|
|
1093
|
+
const vx=view[0]*dx+view[4]*dy+view[8]*dz;
|
|
1094
|
+
const vy=view[1]*dx+view[5]*dy+view[9]*dz;
|
|
1095
|
+
const vz=view[2]*dx+view[6]*dy+view[10]*dz;
|
|
1096
|
+
const cx=proj[0]*vx+proj[4]*vy+proj[8]*vz;
|
|
1097
|
+
const cy=proj[1]*vx+proj[5]*vy+proj[9]*vz;
|
|
1098
|
+
const cz=proj[2]*vx+proj[6]*vy+proj[10]*vz;
|
|
1099
|
+
// NDC→screen scale (direction, no offset).
|
|
1100
|
+
out[0]=cx*vpW*0.5; out[1]=-cy*vpH*0.5;
|
|
1101
|
+
out[2]=cz*(1-ndcZMin)*0.5;
|
|
588
1102
|
return out;
|
|
589
1103
|
}
|
|
590
1104
|
|
|
591
|
-
function _screenToWorldDir(out, dx, dy, dz, proj,
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
edy *= 2 * k / vpH;
|
|
600
|
-
} else {
|
|
601
|
-
const orthoW = Math.abs(projRight(proj, ndcZMin) - projLeft(proj, ndcZMin));
|
|
602
|
-
edx *= orthoW / vpW;
|
|
603
|
-
edy *= Math.abs(projTop(proj, ndcZMin) - projBottom(proj, ndcZMin)) / vpH;
|
|
604
|
-
}
|
|
605
|
-
const near = projNear(proj, ndcZMin), far = projFar(proj);
|
|
606
|
-
const depthRange = near - far;
|
|
607
|
-
let edz;
|
|
608
|
-
if (isPersp) {
|
|
609
|
-
edz = dz * (depthRange / Math.tan(projFov(proj) / 2));
|
|
610
|
-
} else {
|
|
611
|
-
edz = dz * (depthRange / (Math.abs(projRight(proj, ndcZMin) - projLeft(proj, ndcZMin)) / vpW));
|
|
612
|
-
}
|
|
613
|
-
_applyDir(out, eye, edx, edy, edz);
|
|
1105
|
+
function _screenToWorldDir(out, dx, dy, dz, proj, eMatrix, vpW, vpH, ndcZMin) {
|
|
1106
|
+
// Screen direction → NDC direction.
|
|
1107
|
+
const nx=dx/(vpW*0.5), ny=-dy/(vpH*0.5);
|
|
1108
|
+
const nz=dz/((1-ndcZMin)*0.5);
|
|
1109
|
+
// NDC direction → eye direction (inverse projection, linear only).
|
|
1110
|
+
const ex=nx/proj[0], ey=ny/proj[5], ez=nz;
|
|
1111
|
+
// Eye direction → world direction.
|
|
1112
|
+
_applyDir(out, eMatrix, ex, ey, ez);
|
|
614
1113
|
return out;
|
|
615
1114
|
}
|
|
616
1115
|
|
|
617
1116
|
function _screenToNDCDir(out, dx, dy, dz, vpW, vpH, ndcZMin) {
|
|
618
|
-
|
|
619
|
-
out[
|
|
620
|
-
out[1] = 2 * dy / vpH;
|
|
621
|
-
out[2] = dz * ndcZRange;
|
|
1117
|
+
out[0]=dx/(vpW*0.5); out[1]=-dy/(vpH*0.5);
|
|
1118
|
+
out[2]=dz/((1-ndcZMin)*0.5);
|
|
622
1119
|
return out;
|
|
623
1120
|
}
|
|
624
1121
|
|
|
625
1122
|
function _ndcToScreenDir(out, dx, dy, dz, vpW, vpH, ndcZMin) {
|
|
626
|
-
|
|
627
|
-
out[
|
|
628
|
-
out[1] = vpH * dy / 2;
|
|
629
|
-
out[2] = dz / ndcZRange;
|
|
1123
|
+
out[0]=dx*vpW*0.5; out[1]=-dy*vpH*0.5;
|
|
1124
|
+
out[2]=dz*(1-ndcZMin)*0.5;
|
|
630
1125
|
return out;
|
|
631
1126
|
}
|
|
632
1127
|
|
|
633
1128
|
/**
|
|
634
|
-
* Map a direction
|
|
1129
|
+
* Map a direction between named coordinate spaces.
|
|
635
1130
|
* Same bag contract as mapLocation.
|
|
636
1131
|
*/
|
|
637
1132
|
function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
@@ -645,7 +1140,7 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
645
1140
|
if (from === WORLD && to === SCREEN)
|
|
646
1141
|
return _worldToScreenDir(out, dx,dy,dz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
|
|
647
1142
|
if (from === SCREEN && to === WORLD)
|
|
648
|
-
return _screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.
|
|
1143
|
+
return _screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
649
1144
|
|
|
650
1145
|
// SCREEN ↔ NDC
|
|
651
1146
|
if (from === SCREEN && to === NDC)
|
|
@@ -662,7 +1157,7 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
662
1157
|
if (from === NDC && to === WORLD) {
|
|
663
1158
|
_ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
664
1159
|
const sx=out[0],sy=out[1],sz=out[2];
|
|
665
|
-
return _screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.
|
|
1160
|
+
return _screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
666
1161
|
}
|
|
667
1162
|
|
|
668
1163
|
// EYE ↔ SCREEN
|
|
@@ -672,7 +1167,7 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
672
1167
|
return _worldToScreenDir(out, wx,wy,wz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
|
|
673
1168
|
}
|
|
674
1169
|
if (from === SCREEN && to === EYE) {
|
|
675
|
-
_screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.
|
|
1170
|
+
_screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
676
1171
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
677
1172
|
return _applyDir(out, m.vMatrix, wx,wy,wz);
|
|
678
1173
|
}
|
|
@@ -688,7 +1183,7 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
688
1183
|
if (from === NDC && to === EYE) {
|
|
689
1184
|
_ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
690
1185
|
const sx=out[0],sy=out[1],sz=out[2];
|
|
691
|
-
_screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.
|
|
1186
|
+
_screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
692
1187
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
693
1188
|
return _applyDir(out, m.vMatrix, wx,wy,wz);
|
|
694
1189
|
}
|
|
@@ -716,7 +1211,7 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
716
1211
|
return _worldToScreenDir(out, wx,wy,wz, m.pMatrix, m.vMatrix, vpW, vpH, ndcZMin);
|
|
717
1212
|
}
|
|
718
1213
|
if (from === SCREEN && to === MATRIX) {
|
|
719
|
-
_screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.
|
|
1214
|
+
_screenToWorldDir(out, dx,dy,dz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
720
1215
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
721
1216
|
return _applyDir(out, m.toFrameInv, wx,wy,wz);
|
|
722
1217
|
}
|
|
@@ -732,7 +1227,7 @@ function mapDirection(out, dx, dy, dz, from, to, m, vp, ndcZMin) {
|
|
|
732
1227
|
if (from === NDC && to === MATRIX) {
|
|
733
1228
|
_ndcToScreenDir(out, dx,dy,dz, vpW, vpH, ndcZMin);
|
|
734
1229
|
const sx=out[0],sy=out[1],sz=out[2];
|
|
735
|
-
_screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.
|
|
1230
|
+
_screenToWorldDir(out, sx,sy,sz, m.pMatrix, m.eMatrix, vpW, vpH, ndcZMin);
|
|
736
1231
|
const wx=out[0],wy=out[1],wz=out[2];
|
|
737
1232
|
return _applyDir(out, m.toFrameInv, wx,wy,wz);
|
|
738
1233
|
}
|
|
@@ -791,14 +1286,13 @@ function pixelRatio(proj, vpH, eyeZ, ndcZMin) {
|
|
|
791
1286
|
* @param {number} H Canvas height (CSS pixels).
|
|
792
1287
|
* @returns {Float32Array} proj (same reference)
|
|
793
1288
|
*/
|
|
794
|
-
function
|
|
1289
|
+
function mat4Pick(proj, px, py, W, H) {
|
|
795
1290
|
const cx = 2 * (px + 0.5) / W - 1;
|
|
796
|
-
const cy = -2 * (py + 0.5) / H + 1;
|
|
1291
|
+
const cy = -2 * (py + 0.5) / H + 1;
|
|
797
1292
|
const sx = W;
|
|
798
1293
|
const sy = H;
|
|
799
1294
|
const tx = -cx * W;
|
|
800
1295
|
const ty = -cy * H;
|
|
801
|
-
// P_pick = M_pick * P_orig (rows 2 and 3 are unchanged)
|
|
802
1296
|
for (let j = 0; j < 4; j++) {
|
|
803
1297
|
const a = proj[j * 4];
|
|
804
1298
|
const b = proj[j * 4 + 1];
|
|
@@ -810,14 +1304,19 @@ function applyPickMatrix(proj, px, py, W, H) {
|
|
|
810
1304
|
}
|
|
811
1305
|
|
|
812
1306
|
/**
|
|
813
|
-
* @file
|
|
1307
|
+
* @file Spline math and keyframe animation state machines.
|
|
814
1308
|
* @module tree/track
|
|
815
1309
|
* @license AGPL-3.0-only
|
|
816
1310
|
*
|
|
817
|
-
*
|
|
1311
|
+
* Quaternion algebra is provided by quat.js — this module imports and uses
|
|
1312
|
+
* it but does not define it. Spline helpers (hermiteVec3, lerpVec3) and
|
|
1313
|
+
* TRS↔mat4 conversions (transformToMat4, mat4ToTransform) remain here
|
|
1314
|
+
* because they are tightly coupled to the PoseTrack keyframe shape.
|
|
1315
|
+
*
|
|
1316
|
+
* Zero dependencies on p5, DOM, WebGL, or WebGPU.
|
|
818
1317
|
*
|
|
819
1318
|
* ── Exports ──────────────────────────────────────────────────────────────────
|
|
820
|
-
* Quaternion helpers
|
|
1319
|
+
* Quaternion helpers (re-exported from quat.js)
|
|
821
1320
|
* qSet qCopy qDot qNormalize qNegate qMul qSlerp qNlerp
|
|
822
1321
|
* qFromAxisAngle qFromLookDir qFromRotMat3x3 qFromMat4 qToMat4
|
|
823
1322
|
* quatToAxisAngle
|
|
@@ -856,7 +1355,15 @@ function applyPickMatrix(proj, px, py, W, H) {
|
|
|
856
1355
|
* stop() → onStop → _onStop → _onDeactivate
|
|
857
1356
|
* reset() → onStop → _onStop → _onDeactivate
|
|
858
1357
|
*
|
|
859
|
-
* ──
|
|
1358
|
+
* ── Loop modes ────────────────────────────────────────────────────────────────
|
|
1359
|
+
* loop:false, bounce:false — play once, stop at end (fires onEnd)
|
|
1360
|
+
* loop:true, bounce:false — repeat, wrap back to start
|
|
1361
|
+
* loop:true, bounce:true — bounce forever at boundaries
|
|
1362
|
+
* loop:false, bounce:true — bounce once: flip at far boundary, stop at origin
|
|
1363
|
+
*
|
|
1364
|
+
* bounce and loop are fully independent flags — no exclusivity enforced.
|
|
1365
|
+
*
|
|
1366
|
+
* ── Playback semantics (rate + _dir) ─────────────────────────────────────────
|
|
860
1367
|
* rate > 0 forward
|
|
861
1368
|
* rate < 0 backward
|
|
862
1369
|
* rate === 0 frozen: tick() no-op; playing unchanged
|
|
@@ -865,181 +1372,18 @@ function applyPickMatrix(proj, px, py, W, H) {
|
|
|
865
1372
|
* stop() is the sole setter of playing = false.
|
|
866
1373
|
* Assigning rate never starts or stops playback.
|
|
867
1374
|
*
|
|
1375
|
+
* _dir (internal, ±1) tracks the current bounce travel direction.
|
|
1376
|
+
* tick() advances by rate * _dir and flips _dir at boundaries.
|
|
1377
|
+
* rate always holds the user-set value — it is never mutated by bounce.
|
|
1378
|
+
* _dir is reset to 1 only in reset() (keyframes cleared) — stop/replay
|
|
1379
|
+
* preserves the current travel direction.
|
|
1380
|
+
*
|
|
868
1381
|
* ── One-keyframe behaviour ────────────────────────────────────────────────────
|
|
869
1382
|
* play() with exactly one keyframe snaps eval() to that keyframe without
|
|
870
1383
|
* setting playing = true and without firing hooks.
|
|
871
1384
|
*/
|
|
872
1385
|
|
|
873
1386
|
|
|
874
|
-
// =========================================================================
|
|
875
|
-
// S1 Quaternion helpers (flat [x, y, z, w], w-last)
|
|
876
|
-
// =========================================================================
|
|
877
|
-
|
|
878
|
-
/** Set all four components. @returns {number[]} out */
|
|
879
|
-
const qSet = (out, x, y, z, w) => {
|
|
880
|
-
out[0] = x; out[1] = y; out[2] = z; out[3] = w; return out;
|
|
881
|
-
};
|
|
882
|
-
|
|
883
|
-
/** Copy quaternion a into out. @returns {number[]} out */
|
|
884
|
-
const qCopy = (out, a) => {
|
|
885
|
-
out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; return out;
|
|
886
|
-
};
|
|
887
|
-
|
|
888
|
-
/** Dot product of two quaternions. */
|
|
889
|
-
const qDot = (a, b) => a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3];
|
|
890
|
-
|
|
891
|
-
/** Normalise quaternion in-place. @returns {number[]} out */
|
|
892
|
-
const qNormalize = (out) => {
|
|
893
|
-
const l = Math.sqrt(out[0]*out[0]+out[1]*out[1]+out[2]*out[2]+out[3]*out[3]) || 1;
|
|
894
|
-
out[0]/=l; out[1]/=l; out[2]/=l; out[3]/=l; return out;
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
/** Negate quaternion (same rotation, different hemisphere). @returns {number[]} out */
|
|
898
|
-
const qNegate = (out, a) => {
|
|
899
|
-
out[0]=-a[0]; out[1]=-a[1]; out[2]=-a[2]; out[3]=-a[3]; return out;
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
/** Hamilton product out = a * b. @returns {number[]} out */
|
|
903
|
-
const qMul = (out, a, b) => {
|
|
904
|
-
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];
|
|
905
|
-
out[0]=aw*bx+ax*bw+ay*bz-az*by;
|
|
906
|
-
out[1]=aw*by-ax*bz+ay*bw+az*bx;
|
|
907
|
-
out[2]=aw*bz+ax*by-ay*bx+az*bw;
|
|
908
|
-
out[3]=aw*bw-ax*bx-ay*by-az*bz;
|
|
909
|
-
return out;
|
|
910
|
-
};
|
|
911
|
-
|
|
912
|
-
/** Spherical linear interpolation. @returns {number[]} out */
|
|
913
|
-
const qSlerp = (out, a, b, t) => {
|
|
914
|
-
let bx=b[0],by=b[1],bz=b[2],bw=b[3];
|
|
915
|
-
let d = a[0]*bx+a[1]*by+a[2]*bz+a[3]*bw;
|
|
916
|
-
if (d < 0) { bx=-bx; by=-by; bz=-bz; bw=-bw; d=-d; }
|
|
917
|
-
let f0, f1;
|
|
918
|
-
if (1-d > 1e-10) {
|
|
919
|
-
const th=Math.acos(d), st=Math.sin(th);
|
|
920
|
-
f0=Math.sin((1-t)*th)/st; f1=Math.sin(t*th)/st;
|
|
921
|
-
} else {
|
|
922
|
-
f0=1-t; f1=t;
|
|
923
|
-
}
|
|
924
|
-
out[0]=a[0]*f0+bx*f1; out[1]=a[1]*f0+by*f1;
|
|
925
|
-
out[2]=a[2]*f0+bz*f1; out[3]=a[3]*f0+bw*f1;
|
|
926
|
-
return qNormalize(out);
|
|
927
|
-
};
|
|
928
|
-
|
|
929
|
-
/**
|
|
930
|
-
* Normalised linear interpolation (nlerp).
|
|
931
|
-
* Cheaper than slerp; slightly non-constant angular velocity.
|
|
932
|
-
* Handles antipodal quats by flipping b when dot < 0.
|
|
933
|
-
* @returns {number[]} out
|
|
934
|
-
*/
|
|
935
|
-
const qNlerp = (out, a, b, t) => {
|
|
936
|
-
let bx=b[0],by=b[1],bz=b[2],bw=b[3];
|
|
937
|
-
if (a[0]*bx+a[1]*by+a[2]*bz+a[3]*bw < 0) { bx=-bx; by=-by; bz=-bz; bw=-bw; }
|
|
938
|
-
out[0]=a[0]+t*(bx-a[0]); out[1]=a[1]+t*(by-a[1]);
|
|
939
|
-
out[2]=a[2]+t*(bz-a[2]); out[3]=a[3]+t*(bw-a[3]);
|
|
940
|
-
return qNormalize(out);
|
|
941
|
-
};
|
|
942
|
-
|
|
943
|
-
/**
|
|
944
|
-
* Build a quaternion from axis-angle.
|
|
945
|
-
* @param {number[]} out
|
|
946
|
-
* @param {number} ax @param {number} ay @param {number} az Axis (need not be unit).
|
|
947
|
-
* @param {number} angle Radians.
|
|
948
|
-
* @returns {number[]} out
|
|
949
|
-
*/
|
|
950
|
-
const qFromAxisAngle = (out, ax, ay, az, angle) => {
|
|
951
|
-
const half = angle * 0.5;
|
|
952
|
-
const s = Math.sin(half);
|
|
953
|
-
const len = Math.sqrt(ax*ax + ay*ay + az*az) || 1;
|
|
954
|
-
out[0] = s * ax / len; out[1] = s * ay / len; out[2] = s * az / len;
|
|
955
|
-
out[3] = Math.cos(half);
|
|
956
|
-
return out;
|
|
957
|
-
};
|
|
958
|
-
|
|
959
|
-
/**
|
|
960
|
-
* Build a quaternion from a look direction (−Z forward) and optional up (default +Y).
|
|
961
|
-
* @param {number[]} out
|
|
962
|
-
* @param {number[]} dir Forward direction [x,y,z].
|
|
963
|
-
* @param {number[]} [up] Up vector [x,y,z].
|
|
964
|
-
* @returns {number[]} out
|
|
965
|
-
*/
|
|
966
|
-
const qFromLookDir = (out, dir, up) => {
|
|
967
|
-
let fx=dir[0],fy=dir[1],fz=dir[2];
|
|
968
|
-
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
969
|
-
fx/=fl; fy/=fl; fz/=fl;
|
|
970
|
-
let ux=up?up[0]:0, uy=up?up[1]:1, uz=up?up[2]:0;
|
|
971
|
-
let rx=uy*fz-uz*fy, ry=uz*fx-ux*fz, rz=ux*fy-uy*fx;
|
|
972
|
-
const rl=Math.sqrt(rx*rx+ry*ry+rz*rz)||1;
|
|
973
|
-
rx/=rl; ry/=rl; rz/=rl;
|
|
974
|
-
ux=fy*rz-fz*ry; uy=fz*rx-fx*rz; uz=fx*ry-fy*rx;
|
|
975
|
-
return qFromRotMat3x3(out, rx,ry,rz, ux,uy,uz, -fx,-fy,-fz);
|
|
976
|
-
};
|
|
977
|
-
|
|
978
|
-
/**
|
|
979
|
-
* Build a quaternion from a 3×3 rotation matrix (9 row-major scalars).
|
|
980
|
-
* @returns {number[]} out (normalised)
|
|
981
|
-
*/
|
|
982
|
-
const qFromRotMat3x3 = (out, m00,m01,m02, m10,m11,m12, m20,m21,m22) => {
|
|
983
|
-
const tr = m00+m11+m22;
|
|
984
|
-
if (tr > 0) {
|
|
985
|
-
const s=0.5/Math.sqrt(tr+1);
|
|
986
|
-
out[3]=0.25/s; out[0]=(m21-m12)*s; out[1]=(m02-m20)*s; out[2]=(m10-m01)*s;
|
|
987
|
-
} else if (m00>m11 && m00>m22) {
|
|
988
|
-
const s=2*Math.sqrt(1+m00-m11-m22);
|
|
989
|
-
out[3]=(m21-m12)/s; out[0]=0.25*s; out[1]=(m01+m10)/s; out[2]=(m02+m20)/s;
|
|
990
|
-
} else if (m11>m22) {
|
|
991
|
-
const s=2*Math.sqrt(1+m11-m00-m22);
|
|
992
|
-
out[3]=(m02-m20)/s; out[0]=(m01+m10)/s; out[1]=0.25*s; out[2]=(m12+m21)/s;
|
|
993
|
-
} else {
|
|
994
|
-
const s=2*Math.sqrt(1+m22-m00-m11);
|
|
995
|
-
out[3]=(m10-m01)/s; out[0]=(m02+m20)/s; out[1]=(m12+m21)/s; out[2]=0.25*s;
|
|
996
|
-
}
|
|
997
|
-
return qNormalize(out);
|
|
998
|
-
};
|
|
999
|
-
|
|
1000
|
-
/**
|
|
1001
|
-
* Extract a unit quaternion from the upper-left 3×3 of a column-major mat4.
|
|
1002
|
-
* @param {number[]} out
|
|
1003
|
-
* @param {Float32Array|number[]} m Column-major mat4.
|
|
1004
|
-
* @returns {number[]} out
|
|
1005
|
-
*/
|
|
1006
|
-
const qFromMat4 = (out, m) =>
|
|
1007
|
-
qFromRotMat3x3(out, m[0],m[4],m[8], m[1],m[5],m[9], m[2],m[6],m[10]);
|
|
1008
|
-
|
|
1009
|
-
/**
|
|
1010
|
-
* Write a quaternion into the rotation block of a column-major mat4.
|
|
1011
|
-
* Translation and perspective rows/cols are set to identity values.
|
|
1012
|
-
* @param {Float32Array|number[]} out 16-element array.
|
|
1013
|
-
* @param {number[]} q [x,y,z,w].
|
|
1014
|
-
* @returns {Float32Array|number[]} out
|
|
1015
|
-
*/
|
|
1016
|
-
const qToMat4 = (out, q) => {
|
|
1017
|
-
const x=q[0],y=q[1],z=q[2],w=q[3];
|
|
1018
|
-
const x2=x+x,y2=y+y,z2=z+z;
|
|
1019
|
-
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;
|
|
1020
|
-
out[0]=1-(yy+zz); out[1]=xy+wz; out[2]=xz-wy; out[3]=0;
|
|
1021
|
-
out[4]=xy-wz; out[5]=1-(xx+zz); out[6]=yz+wx; out[7]=0;
|
|
1022
|
-
out[8]=xz+wy; out[9]=yz-wx; out[10]=1-(xx+yy); out[11]=0;
|
|
1023
|
-
out[12]=0; out[13]=0; out[14]=0; out[15]=1;
|
|
1024
|
-
return out;
|
|
1025
|
-
};
|
|
1026
|
-
|
|
1027
|
-
/**
|
|
1028
|
-
* Decompose a unit quaternion into { axis:[x,y,z], angle } (radians).
|
|
1029
|
-
* @param {number[]} q [x,y,z,w].
|
|
1030
|
-
* @param {Object} [out]
|
|
1031
|
-
* @returns {{ axis: number[], angle: number }}
|
|
1032
|
-
*/
|
|
1033
|
-
const quatToAxisAngle = (q, out) => {
|
|
1034
|
-
out = out || {};
|
|
1035
|
-
const x=q[0],y=q[1],z=q[2],w=q[3];
|
|
1036
|
-
const sinHalf = Math.sqrt(x*x+y*y+z*z);
|
|
1037
|
-
if (sinHalf < 1e-8) { out.axis=[0,1,0]; out.angle=0; return out; }
|
|
1038
|
-
out.angle = 2*Math.atan2(sinHalf, w);
|
|
1039
|
-
out.axis = [x/sinHalf, y/sinHalf, z/sinHalf];
|
|
1040
|
-
return out;
|
|
1041
|
-
};
|
|
1042
|
-
|
|
1043
1387
|
// =========================================================================
|
|
1044
1388
|
// S2 Spline / vector helpers
|
|
1045
1389
|
// =========================================================================
|
|
@@ -1071,14 +1415,13 @@ const hermiteVec3 = (out, p0, m0, p1, m1, t) => {
|
|
|
1071
1415
|
|
|
1072
1416
|
// Centripetal CR outgoing tangent at p1 for segment p1→p2, scaled by dt1.
|
|
1073
1417
|
const _crTanOut = (out, p0, p1, p2, p3) => {
|
|
1074
|
-
const dt0=Math.pow(_dist3(p0,p1),0.5)||1, dt1=Math.pow(_dist3(p1,p2),0.5)||1;
|
|
1418
|
+
const dt0=Math.pow(_dist3(p0,p1),0.5)||1, dt1=Math.pow(_dist3(p1,p2),0.5)||1;
|
|
1075
1419
|
for (let i=0;i<3;i++) out[i]=((p1[i]-p0[i])/dt0-(p2[i]-p0[i])/(dt0+dt1)+(p2[i]-p1[i])/dt1)*dt1;
|
|
1076
1420
|
return out;
|
|
1077
1421
|
};
|
|
1078
1422
|
|
|
1079
|
-
// Centripetal CR incoming tangent at p2 for segment p1→p2, scaled by dt1.
|
|
1080
1423
|
const _crTanIn = (out, p0, p1, p2, p3) => {
|
|
1081
|
-
|
|
1424
|
+
const dt1=Math.pow(_dist3(p1,p2),0.5)||1, dt2=Math.pow(_dist3(p2,p3),0.5)||1;
|
|
1082
1425
|
for (let i=0;i<3;i++) out[i]=((p2[i]-p1[i])/dt1-(p3[i]-p1[i])/(dt1+dt2)+(p3[i]-p2[i])/dt2)*dt1;
|
|
1083
1426
|
return out;
|
|
1084
1427
|
};
|
|
@@ -1166,33 +1509,13 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
|
1166
1509
|
*
|
|
1167
1510
|
* Accepted forms:
|
|
1168
1511
|
*
|
|
1169
|
-
* [x,y,z,w]
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1172
|
-
* {
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1175
|
-
* {
|
|
1176
|
-
* Object orientation — forward direction (−Z) with optional up hint.
|
|
1177
|
-
*
|
|
1178
|
-
* { eMatrix: mat4 }
|
|
1179
|
-
* Extract rotation block from an eye (eye→world) matrix.
|
|
1180
|
-
* Column-major Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1181
|
-
*
|
|
1182
|
-
* { mat3: mat3 }
|
|
1183
|
-
* Column-major 3×3 rotation matrix — Float32Array(9) or plain Array.
|
|
1184
|
-
*
|
|
1185
|
-
* { euler:[rx,ry,rz], order?:'YXZ' }
|
|
1186
|
-
* Intrinsic Euler angles (radians). Angles are indexed by order position:
|
|
1187
|
-
* e[0] rotates around order[0] axis, e[1] around order[1], e[2] around order[2].
|
|
1188
|
-
* Supported orders: YXZ (default), XYZ, ZYX, ZXY, XZY, YZX.
|
|
1189
|
-
* Note: intrinsic ABC = extrinsic CBA with the same angles — to use
|
|
1190
|
-
* extrinsic order ABC, reverse the string and use intrinsic CBA.
|
|
1191
|
-
*
|
|
1192
|
-
* { from:[x,y,z], to:[x,y,z] }
|
|
1193
|
-
* Shortest-arc rotation from one direction onto another.
|
|
1194
|
-
* Both vectors are normalised internally.
|
|
1195
|
-
* Antiparallel input: 180° rotation around a perpendicular axis.
|
|
1512
|
+
* [x,y,z,w] — raw quaternion array
|
|
1513
|
+
* { axis:[x,y,z], angle } — axis-angle
|
|
1514
|
+
* { dir:[x,y,z], up?:[x,y,z] } — forward direction (−Z) with optional up
|
|
1515
|
+
* { eMatrix: mat4 } — rotation block of an eye matrix
|
|
1516
|
+
* { mat3: mat3 } — column-major 3×3 rotation matrix
|
|
1517
|
+
* { euler:[rx,ry,rz], order? } — intrinsic Euler (default order: YXZ)
|
|
1518
|
+
* { from:[x,y,z], to:[x,y,z] } — shortest-arc rotation
|
|
1196
1519
|
*
|
|
1197
1520
|
* @param {*} v
|
|
1198
1521
|
* @returns {number[]|null} [x,y,z,w] or null if unparseable.
|
|
@@ -1200,46 +1523,48 @@ const _EULER_ORDERS = new Set(['XYZ','XZY','YXZ','YZX','ZXY','ZYX']);
|
|
|
1200
1523
|
function _parseQuat(v) {
|
|
1201
1524
|
if (!v) return null;
|
|
1202
1525
|
|
|
1203
|
-
//
|
|
1204
|
-
if (
|
|
1526
|
+
// Raw array [x,y,z,w]
|
|
1527
|
+
if (Array.isArray(v) && v.length === 4) return [v[0],v[1],v[2],v[3]];
|
|
1528
|
+
if (ArrayBuffer.isView(v) && v.length >= 4) return [v[0],v[1],v[2],v[3]];
|
|
1529
|
+
|
|
1530
|
+
if (typeof v !== 'object') return null;
|
|
1205
1531
|
|
|
1206
1532
|
// { axis, angle }
|
|
1207
|
-
if (v.axis &&
|
|
1208
|
-
const
|
|
1209
|
-
return qFromAxisAngle([0,0,0,1],
|
|
1533
|
+
if (v.axis != null && v.angle != null) {
|
|
1534
|
+
const ax = Array.isArray(v.axis) ? v.axis : [v.axis.x||0, v.axis.y||0, v.axis.z||0];
|
|
1535
|
+
return qFromAxisAngle([0,0,0,1], ax[0],ax[1],ax[2], v.angle);
|
|
1210
1536
|
}
|
|
1211
1537
|
|
|
1212
1538
|
// { dir, up? }
|
|
1213
|
-
if (v.dir) {
|
|
1539
|
+
if (v.dir != null) {
|
|
1214
1540
|
const d = Array.isArray(v.dir) ? v.dir : [v.dir.x||0, v.dir.y||0, v.dir.z||0];
|
|
1215
1541
|
const u = v.up ? (Array.isArray(v.up) ? v.up : [v.up.x||0, v.up.y||0, v.up.z||0]) : null;
|
|
1216
1542
|
return qFromLookDir([0,0,0,1], d, u);
|
|
1217
1543
|
}
|
|
1218
1544
|
|
|
1219
|
-
// { eMatrix }
|
|
1545
|
+
// { eMatrix }
|
|
1220
1546
|
if (v.eMatrix != null) {
|
|
1221
1547
|
const m = (ArrayBuffer.isView(v.eMatrix) || Array.isArray(v.eMatrix))
|
|
1222
1548
|
? v.eMatrix : (v.eMatrix.mat4 ?? null);
|
|
1223
|
-
if (m
|
|
1549
|
+
if (!m || m.length < 16) return null;
|
|
1550
|
+
return qFromRotMat3x3([0,0,0,1], m[0],m[4],m[8], m[1],m[5],m[9], m[2],m[6],m[10]);
|
|
1224
1551
|
}
|
|
1225
1552
|
|
|
1226
|
-
// { mat3 }
|
|
1227
|
-
// col0=[m0,m1,m2], col1=[m3,m4,m5], col2=[m6,m7,m8]
|
|
1228
|
-
// row-major for qFromRotMat3x3: row0=[m0,m3,m6], row1=[m1,m4,m7], row2=[m2,m5,m8]
|
|
1553
|
+
// { mat3 }
|
|
1229
1554
|
if (v.mat3 != null) {
|
|
1230
|
-
const m = v.mat3
|
|
1231
|
-
|
|
1232
|
-
|
|
1555
|
+
const m = (ArrayBuffer.isView(v.mat3) || Array.isArray(v.mat3))
|
|
1556
|
+
? v.mat3 : null;
|
|
1557
|
+
if (!m || m.length < 9) return null;
|
|
1558
|
+
return qFromRotMat3x3([0,0,0,1], m[0],m[3],m[6], m[1],m[4],m[7], m[2],m[5],m[8]);
|
|
1233
1559
|
}
|
|
1234
1560
|
|
|
1235
|
-
// { euler, order? }
|
|
1561
|
+
// { euler, order? }
|
|
1236
1562
|
if (v.euler != null) {
|
|
1237
1563
|
const e = v.euler;
|
|
1238
1564
|
if (!Array.isArray(e) || e.length < 3) return null;
|
|
1239
|
-
const order = (
|
|
1240
|
-
? v.order : 'YXZ';
|
|
1565
|
+
const order = (v.order && _EULER_ORDERS.has(v.order)) ? v.order : 'YXZ';
|
|
1241
1566
|
const q = [0,0,0,1];
|
|
1242
|
-
const s = [0,0,0,1];
|
|
1567
|
+
const s = [0,0,0,1];
|
|
1243
1568
|
for (let i = 0; i < 3; i++) {
|
|
1244
1569
|
const ax = _EULER_AXES[order[i]];
|
|
1245
1570
|
qMul(q, q, qFromAxisAngle(s, ax[0],ax[1],ax[2], e[i]));
|
|
@@ -1247,7 +1572,7 @@ function _parseQuat(v) {
|
|
|
1247
1572
|
return q;
|
|
1248
1573
|
}
|
|
1249
1574
|
|
|
1250
|
-
// { from, to }
|
|
1575
|
+
// { from, to }
|
|
1251
1576
|
if (v.from != null && v.to != null) {
|
|
1252
1577
|
const f = Array.isArray(v.from) ? v.from : [v.from.x||0, v.from.y||0, v.from.z||0];
|
|
1253
1578
|
const t = Array.isArray(v.to) ? v.to : [v.to.x||0, v.to.y||0, v.to.z||0];
|
|
@@ -1256,22 +1581,14 @@ function _parseQuat(v) {
|
|
|
1256
1581
|
const fx=f[0]/fl, fy=f[1]/fl, fz=f[2]/fl;
|
|
1257
1582
|
const tx=t[0]/tl, ty=t[1]/tl, tz=t[2]/tl;
|
|
1258
1583
|
const dot = fx*tx + fy*ty + fz*tz;
|
|
1259
|
-
// parallel — identity
|
|
1260
1584
|
if (dot >= 1 - 1e-8) return [0,0,0,1];
|
|
1261
|
-
// antiparallel — 180° around any perpendicular axis
|
|
1262
1585
|
if (dot <= -1 + 1e-8) {
|
|
1263
|
-
// cross(from, X=[1,0,0]) = [0, fz, -fy]
|
|
1264
1586
|
let px=0, py=fz, pz=-fy;
|
|
1265
1587
|
let pl = Math.sqrt(px*px+py*py+pz*pz);
|
|
1266
|
-
if (pl < 1e-8) {
|
|
1267
|
-
// from ≈ ±X; try cross(from, Z=[0,0,1]) = [fy, -fx, 0]
|
|
1268
|
-
px=fy; py=-fx; pz=0;
|
|
1269
|
-
pl = Math.sqrt(px*px+py*py+pz*pz);
|
|
1270
|
-
}
|
|
1588
|
+
if (pl < 1e-8) { px=fy; py=-fx; pz=0; pl = Math.sqrt(px*px+py*py+pz*pz); }
|
|
1271
1589
|
if (pl < 1e-8) return [0,0,0,1];
|
|
1272
1590
|
return qFromAxisAngle([0,0,0,1], px/pl,py/pl,pz/pl, Math.PI);
|
|
1273
1591
|
}
|
|
1274
|
-
// general case — axis = normalize(cross(from, to))
|
|
1275
1592
|
let ax=fy*tz-fz*ty, ay=fz*tx-fx*tz, az=fx*ty-fy*tx;
|
|
1276
1593
|
const al = Math.sqrt(ax*ax+ay*ay+az*az) || 1;
|
|
1277
1594
|
return qFromAxisAngle([0,0,0,1], ax/al,ay/al,az/al,
|
|
@@ -1289,14 +1606,12 @@ function _parseQuat(v) {
|
|
|
1289
1606
|
* { mMatrix }
|
|
1290
1607
|
* Decompose a column-major mat4 into TRS via mat4ToTransform.
|
|
1291
1608
|
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1292
|
-
* pos from col3, scl from column lengths, rot from normalised rotation block.
|
|
1293
1609
|
*
|
|
1294
1610
|
* { pos?, rot?, scl?, tanIn?, tanOut? }
|
|
1295
1611
|
* Explicit TRS. pos and scl are vec3, rot accepts any form from _parseQuat.
|
|
1296
1612
|
* All fields are optional — missing pos/scl default to [0,0,0] / [1,1,1],
|
|
1297
1613
|
* missing rot defaults to identity.
|
|
1298
1614
|
* tanIn/tanOut are optional vec3 tangents for Hermite interpolation.
|
|
1299
|
-
* When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
|
|
1300
1615
|
*
|
|
1301
1616
|
* @param {Object} spec
|
|
1302
1617
|
* @returns {{ pos:number[], rot:number[], scl:number[], tanIn:number[]|null, tanOut:number[]|null }|null}
|
|
@@ -1340,22 +1655,12 @@ function _sameTransform(a, b) {
|
|
|
1340
1655
|
* { eye, center?, up?, fov?, halfHeight?,
|
|
1341
1656
|
* eyeTanIn?, eyeTanOut?, centerTanIn?, centerTanOut? }
|
|
1342
1657
|
* Explicit lookat. center defaults to [0,0,0], up defaults to [0,1,0].
|
|
1343
|
-
* Both are normalised/stored as-is. eye must be a vec3.
|
|
1344
1658
|
* eyeTanIn/Out and centerTanIn/Out are optional vec3 tangents for Hermite.
|
|
1345
1659
|
* When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
|
|
1346
1660
|
*
|
|
1347
|
-
*
|
|
1348
|
-
*
|
|
1349
|
-
*
|
|
1350
|
-
* The matrix's up_ortho (col1) is intentionally NOT used as up —
|
|
1351
|
-
* passing it to cam.camera() shifts orbitControl's orbit reference.
|
|
1352
|
-
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1353
|
-
*
|
|
1354
|
-
* { eMatrix: mat4 }
|
|
1355
|
-
* Column-major eye matrix (eye→world, i.e. inverse view).
|
|
1356
|
-
* eye read directly from col3; center = eye + forward·1; up = [0,1,0].
|
|
1357
|
-
* Simpler extraction than vMatrix; prefer this form when eMatrix is available.
|
|
1358
|
-
* Float32Array(16), plain Array, or { mat4 } wrapper.
|
|
1661
|
+
* Removed forms (task 2):
|
|
1662
|
+
* { vMatrix } and { eMatrix } — use PoseTrack.add({ mMatrix: eMatrix }) for
|
|
1663
|
+
* full-fidelity capture including roll, or cam.capturePose() for lookat-style.
|
|
1359
1664
|
*
|
|
1360
1665
|
* @param {Object} spec
|
|
1361
1666
|
* @returns {{ eye:number[], center:number[], up:number[],
|
|
@@ -1366,36 +1671,8 @@ function _sameTransform(a, b) {
|
|
|
1366
1671
|
function _parseCameraSpec(spec) {
|
|
1367
1672
|
if (!spec || typeof spec !== 'object') return null;
|
|
1368
1673
|
|
|
1369
|
-
// {
|
|
1370
|
-
|
|
1371
|
-
const m = (ArrayBuffer.isView(spec.vMatrix) || Array.isArray(spec.vMatrix))
|
|
1372
|
-
? spec.vMatrix : (spec.vMatrix.mat4 ?? null);
|
|
1373
|
-
if (!m || m.length < 16) return null;
|
|
1374
|
-
const ex = -(m[0]*m[12] + m[4]*m[13] + m[8]*m[14]);
|
|
1375
|
-
const ey = -(m[1]*m[12] + m[5]*m[13] + m[9]*m[14]);
|
|
1376
|
-
const ez = -(m[2]*m[12] + m[6]*m[13] + m[10]*m[14]);
|
|
1377
|
-
const fx=-m[8], fy=-m[9], fz=-m[10];
|
|
1378
|
-
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
1379
|
-
return { eye:[ex,ey,ez], center:[ex+fx/fl,ey+fy/fl,ez+fz/fl], up:[0,1,0],
|
|
1380
|
-
fov:null, halfHeight:null,
|
|
1381
|
-
eyeTanIn:null, eyeTanOut:null, centerTanIn:null, centerTanOut:null };
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
// { eMatrix } — eye matrix (eye→world); eye = col3, forward = -col2
|
|
1385
|
-
if (spec.eMatrix != null) {
|
|
1386
|
-
const m = (ArrayBuffer.isView(spec.eMatrix) || Array.isArray(spec.eMatrix))
|
|
1387
|
-
? spec.eMatrix : (spec.eMatrix.mat4 ?? null);
|
|
1388
|
-
if (!m || m.length < 16) return null;
|
|
1389
|
-
const ex=m[12], ey=m[13], ez=m[14];
|
|
1390
|
-
const fx=-m[8], fy=-m[9], fz=-m[10];
|
|
1391
|
-
const fl=Math.sqrt(fx*fx+fy*fy+fz*fz)||1;
|
|
1392
|
-
return { eye:[ex,ey,ez], center:[ex+fx/fl,ey+fy/fl,ez+fz/fl], up:[0,1,0],
|
|
1393
|
-
fov:null, halfHeight:null,
|
|
1394
|
-
eyeTanIn:null, eyeTanOut:null, centerTanIn:null, centerTanOut:null };
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// { eye, center?, up? } — explicit lookat (eye is a vec3, not a mat4)
|
|
1398
|
-
const eye = _parseVec3(spec.eye);
|
|
1674
|
+
// { eye, center?, up? } — explicit lookat
|
|
1675
|
+
const eye = _parseVec3(spec.eye);
|
|
1399
1676
|
if (!eye) return null;
|
|
1400
1677
|
const center = _parseVec3(spec.center) || [0,0,0];
|
|
1401
1678
|
const upRaw = spec.up ? _parseVec3(spec.up) : null;
|
|
@@ -1437,7 +1714,7 @@ class Track {
|
|
|
1437
1714
|
/** Loop at boundaries. @type {boolean} */
|
|
1438
1715
|
this.loop = false;
|
|
1439
1716
|
/** Ping-pong bounce (takes precedence over loop). @type {boolean} */
|
|
1440
|
-
this.
|
|
1717
|
+
this.bounce = false;
|
|
1441
1718
|
/** Frames per segment (≥1). @type {number} */
|
|
1442
1719
|
this.duration = 30;
|
|
1443
1720
|
/** Current segment index. @type {number} */
|
|
@@ -1447,6 +1724,10 @@ class Track {
|
|
|
1447
1724
|
|
|
1448
1725
|
// Internal rate — never directly starts/stops playback
|
|
1449
1726
|
this._rate = 1;
|
|
1727
|
+
// Internal bounce direction: +1 forward, -1 backward.
|
|
1728
|
+
this._dir = 1;
|
|
1729
|
+
// Scratch: true once _dir has been flipped in bounce-once mode.
|
|
1730
|
+
this._bounced = false;
|
|
1450
1731
|
|
|
1451
1732
|
// User-space hooks
|
|
1452
1733
|
/** @type {Function|null} */ this.onPlay = null;
|
|
@@ -1472,7 +1753,7 @@ class Track {
|
|
|
1472
1753
|
/**
|
|
1473
1754
|
* Start or update playback.
|
|
1474
1755
|
* @param {number|Object} [rateOrOpts] Numeric rate or options object:
|
|
1475
|
-
* { rate, duration, loop,
|
|
1756
|
+
* { rate, duration, loop, bounce, onPlay, onEnd, onStop }
|
|
1476
1757
|
* @returns {Track} this
|
|
1477
1758
|
*/
|
|
1478
1759
|
play(rateOrOpts) {
|
|
@@ -1488,9 +1769,9 @@ class Track {
|
|
|
1488
1769
|
this._rate = rateOrOpts;
|
|
1489
1770
|
} else if (rateOrOpts && typeof rateOrOpts === 'object') {
|
|
1490
1771
|
const o = rateOrOpts;
|
|
1491
|
-
if (_isNum(o.duration)) this.duration
|
|
1492
|
-
if ('loop'
|
|
1493
|
-
if ('
|
|
1772
|
+
if (_isNum(o.duration)) this.duration = Math.max(1, o.duration | 0);
|
|
1773
|
+
if ('loop' in o) this.loop = !!o.loop;
|
|
1774
|
+
if ('bounce' in o) this.bounce = !!o.bounce;
|
|
1494
1775
|
if (typeof o.onPlay === 'function') this.onPlay = o.onPlay;
|
|
1495
1776
|
if (typeof o.onEnd === 'function') this.onEnd = o.onEnd;
|
|
1496
1777
|
if (typeof o.onStop === 'function') this.onStop = o.onStop;
|
|
@@ -1506,6 +1787,7 @@ class Track {
|
|
|
1506
1787
|
const wasPlaying = this.playing;
|
|
1507
1788
|
this.playing = true;
|
|
1508
1789
|
if (!wasPlaying) {
|
|
1790
|
+
this._bounced = false;
|
|
1509
1791
|
if (typeof this.onPlay === 'function') { try { this.onPlay(this); } catch (_) {} }
|
|
1510
1792
|
this._onPlay?.();
|
|
1511
1793
|
this._onActivate?.();
|
|
@@ -1522,10 +1804,11 @@ class Track {
|
|
|
1522
1804
|
const wasPlaying = this.playing;
|
|
1523
1805
|
this.playing = false;
|
|
1524
1806
|
if (wasPlaying) {
|
|
1807
|
+
this._bounced = false;
|
|
1525
1808
|
if (typeof this.onStop === 'function') { try { this.onStop(this); } catch (_) {} }
|
|
1526
1809
|
this._onStop?.();
|
|
1527
1810
|
this._onDeactivate?.();
|
|
1528
|
-
if (rewind && this.keyframes.length > 1) this.seek(this._rate < 0 ? 1 : 0);
|
|
1811
|
+
if (rewind && this.keyframes.length > 1) this.seek(this._rate * this._dir < 0 ? 1 : 0);
|
|
1529
1812
|
}
|
|
1530
1813
|
return this;
|
|
1531
1814
|
}
|
|
@@ -1543,7 +1826,7 @@ class Track {
|
|
|
1543
1826
|
this._onDeactivate?.();
|
|
1544
1827
|
}
|
|
1545
1828
|
this.keyframes.length = 0;
|
|
1546
|
-
this.seg = 0; this.f = 0;
|
|
1829
|
+
this.seg = 0; this.f = 0; this._dir = 1; this._bounced = false;
|
|
1547
1830
|
return this;
|
|
1548
1831
|
}
|
|
1549
1832
|
|
|
@@ -1605,7 +1888,7 @@ class Track {
|
|
|
1605
1888
|
f: this.f,
|
|
1606
1889
|
playing: this.playing,
|
|
1607
1890
|
loop: this.loop,
|
|
1608
|
-
|
|
1891
|
+
bounce: this.bounce,
|
|
1609
1892
|
rate: this._rate,
|
|
1610
1893
|
duration: this.duration,
|
|
1611
1894
|
time: this.segments > 0 ? this.time() : 0
|
|
@@ -1628,24 +1911,50 @@ class Track {
|
|
|
1628
1911
|
const dur = Math.max(1, this.duration | 0);
|
|
1629
1912
|
const total = nSeg * dur;
|
|
1630
1913
|
const s = _clampS(this.seg * dur + this.f, 0, total);
|
|
1631
|
-
const next = s + this._rate;
|
|
1914
|
+
const next = s + this._rate * this._dir;
|
|
1632
1915
|
|
|
1633
|
-
|
|
1916
|
+
// ── loop:true, bounce:true — bounce forever ───────────────────────────
|
|
1917
|
+
if (this.loop && this.bounce) {
|
|
1634
1918
|
let pos = next, flips = 0;
|
|
1635
1919
|
while (pos < 0 || pos > total) {
|
|
1636
1920
|
if (pos < 0) { pos = -pos; flips++; }
|
|
1637
1921
|
else { pos = 2 * total - pos; flips++; }
|
|
1638
1922
|
}
|
|
1639
|
-
if (flips & 1) this.
|
|
1923
|
+
if (flips & 1) this._dir = -this._dir;
|
|
1640
1924
|
this._setCursorFromScalar(pos);
|
|
1641
1925
|
return true;
|
|
1642
1926
|
}
|
|
1643
1927
|
|
|
1928
|
+
// ── loop:false, bounce:true — bounce once, stop at origin ────────────
|
|
1929
|
+
if (!this.loop && this.bounce) {
|
|
1930
|
+
if (next >= total) {
|
|
1931
|
+
// far boundary: reflect and flip direction once
|
|
1932
|
+
this._setCursorFromScalar(Math.min(total, 2 * total - next));
|
|
1933
|
+
this._dir = -this._dir;
|
|
1934
|
+
this._bounced = true;
|
|
1935
|
+
return true;
|
|
1936
|
+
}
|
|
1937
|
+
if (next <= 0) {
|
|
1938
|
+
// origin: stop (whether we bounced or started backward)
|
|
1939
|
+
this._setCursorFromScalar(0);
|
|
1940
|
+
this.playing = false;
|
|
1941
|
+
this._dir = 1; this._bounced = false;
|
|
1942
|
+
if (typeof this.onEnd === 'function') { try { this.onEnd(this); } catch (_) {} }
|
|
1943
|
+
this._onEnd?.();
|
|
1944
|
+
this._onDeactivate?.();
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1947
|
+
this._setCursorFromScalar(next);
|
|
1948
|
+
return true;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// ── loop:true, bounce:false — repeat forever ──────────────────────────
|
|
1644
1952
|
if (this.loop) {
|
|
1645
1953
|
this._setCursorFromScalar(((next % total) + total) % total);
|
|
1646
1954
|
return true;
|
|
1647
1955
|
}
|
|
1648
1956
|
|
|
1957
|
+
// ── loop:false, bounce:false — play once, stop at boundary ───────────
|
|
1649
1958
|
if (next <= 0) {
|
|
1650
1959
|
this._setCursorFromScalar(0);
|
|
1651
1960
|
this.playing = false;
|
|
@@ -1692,47 +2001,16 @@ class Track {
|
|
|
1692
2001
|
* tanOut — outgoing position tangent at this keyframe (Hermite mode).
|
|
1693
2002
|
* When only one is supplied, the other mirrors it.
|
|
1694
2003
|
* When neither is supplied, centripetal Catmull-Rom tangents are auto-computed.
|
|
1695
|
-
*
|
|
1696
|
-
* add() accepts individual specs or a bulk array of specs:
|
|
1697
|
-
*
|
|
1698
|
-
* { mMatrix } — full TRS from model matrix
|
|
1699
|
-
* { pos?, rot?, scl?, tanIn?, tanOut? } — direct TRS; all fields optional
|
|
1700
|
-
* { pos?, rot: [x,y,z,w] } — explicit quaternion
|
|
1701
|
-
* { pos?, rot: { axis, angle } } — axis-angle
|
|
1702
|
-
* { pos?, rot: { dir, up? } } — look direction
|
|
1703
|
-
* { pos?, rot: { eMatrix: mat4 } } — rotation from eye matrix
|
|
1704
|
-
* { pos?, rot: { mat3 } } — column-major 3×3 rotation matrix
|
|
1705
|
-
* { pos?, rot: { euler, order? } } — intrinsic Euler angles (default YXZ)
|
|
1706
|
-
* { pos?, rot: { from, to } } — shortest-arc between two directions
|
|
1707
|
-
* [ spec, spec, ... ] — bulk
|
|
1708
|
-
*
|
|
1709
|
-
* Missing fields default to: pos → [0,0,0], rot → [0,0,0,1], scl → [1,1,1].
|
|
1710
|
-
*
|
|
1711
|
-
* eval() writes { pos, rot, scl }:
|
|
1712
|
-
* pos — Hermite (tanIn/tanOut per keyframe; auto-CR when absent) or linear or step
|
|
1713
|
-
* rot — slerp (rotInterp='slerp') or nlerp or step
|
|
1714
|
-
* scl — lerp
|
|
1715
|
-
*
|
|
1716
|
-
* @example
|
|
1717
|
-
* const track = new PoseTrack()
|
|
1718
|
-
* track.add({ pos:[0,0,0] })
|
|
1719
|
-
* track.add({ pos:[100,0,0], tanOut:[0,50,0] }) // leave heading +Y
|
|
1720
|
-
* track.add({ pos:[200,0,0] })
|
|
1721
|
-
* track.play({ loop: true })
|
|
1722
|
-
* // per frame:
|
|
1723
|
-
* track.tick()
|
|
1724
|
-
* const out = { pos:[0,0,0], rot:[0,0,0,1], scl:[1,1,1] }
|
|
1725
|
-
* track.eval(out)
|
|
1726
2004
|
*/
|
|
1727
2005
|
class PoseTrack extends Track {
|
|
1728
2006
|
constructor() {
|
|
1729
2007
|
super();
|
|
1730
2008
|
/**
|
|
1731
2009
|
* Position interpolation mode.
|
|
1732
|
-
* - 'hermite' — cubic Hermite;
|
|
1733
|
-
*
|
|
2010
|
+
* - 'hermite' — cubic Hermite; auto-computes centripetal Catmull-Rom tangents
|
|
2011
|
+
* when none are stored (default)
|
|
1734
2012
|
* - 'linear' — lerp
|
|
1735
|
-
* - 'step' — snap to k0
|
|
2013
|
+
* - 'step' — snap to k0; useful for discrete state changes
|
|
1736
2014
|
* @type {'hermite'|'linear'|'step'}
|
|
1737
2015
|
*/
|
|
1738
2016
|
this.posInterp = 'hermite';
|
|
@@ -1817,11 +2095,9 @@ class PoseTrack extends Track {
|
|
|
1817
2095
|
} else {
|
|
1818
2096
|
const p0 = seg > 0 ? this.keyframes[seg - 1].pos : k0.pos;
|
|
1819
2097
|
const p3 = seg + 2 < n ? this.keyframes[seg + 2].pos : k1.pos;
|
|
1820
|
-
// tanOut on k0: use stored, else symmetric from tanIn, else auto-CR
|
|
1821
2098
|
const m0 = k0.tanOut != null ? k0.tanOut
|
|
1822
2099
|
: k0.tanIn != null ? k0.tanIn
|
|
1823
|
-
: _crTanOut(_m0, p0, k0.pos, k1.pos
|
|
1824
|
-
// tanIn on k1: use stored, else symmetric from tanOut, else auto-CR
|
|
2100
|
+
: _crTanOut(_m0, p0, k0.pos, k1.pos);
|
|
1825
2101
|
const m1 = k1.tanIn != null ? k1.tanIn
|
|
1826
2102
|
: k1.tanOut != null ? k1.tanOut
|
|
1827
2103
|
: _crTanIn(_m1, p0, k0.pos, k1.pos, p3);
|
|
@@ -1875,83 +2151,34 @@ class PoseTrack extends Track {
|
|
|
1875
2151
|
* interpolation of the eye and center paths respectively.
|
|
1876
2152
|
* When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
|
|
1877
2153
|
*
|
|
1878
|
-
* Each field is independently interpolated — eye and center along their
|
|
1879
|
-
* own paths, up nlerped on the unit sphere. This correctly handles cameras
|
|
1880
|
-
* that always look at a fixed target (center stays at origin throughout)
|
|
1881
|
-
* as well as free-fly paths where center moves independently.
|
|
1882
|
-
*
|
|
1883
2154
|
* Missing fields default to: center → [0,0,0], up → [0,1,0].
|
|
1884
2155
|
*
|
|
1885
2156
|
* add() accepts individual specs or a bulk array of specs:
|
|
1886
2157
|
*
|
|
1887
2158
|
* { eye, center?, up?, fov?, halfHeight?,
|
|
1888
2159
|
* eyeTanIn?, eyeTanOut?, centerTanIn?, centerTanOut? }
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
* { eMatrix: mat4 } eye matrix (eye→world); eye read from col3 directly
|
|
1893
|
-
* [ spec, spec, ... ] bulk
|
|
1894
|
-
*
|
|
1895
|
-
* Note on up for matrix forms:
|
|
1896
|
-
* up is always [0,1,0]. The matrix's col1 (up_ortho) is intentionally
|
|
1897
|
-
* not used — it differs from the hint [0,1,0] for upright cameras and
|
|
1898
|
-
* passing it to cam.camera() shifts orbitControl's orbit reference.
|
|
1899
|
-
* Use capturePose() (p5.tree bridge) when the real up hint is needed.
|
|
1900
|
-
*
|
|
1901
|
-
* eval() writes { eye, center, up, fov, halfHeight }:
|
|
1902
|
-
* eye — Hermite (auto-CR when no tangents stored) or linear or step
|
|
1903
|
-
* center — Hermite (auto-CR when no tangents stored) or linear or step
|
|
1904
|
-
* up — nlerp (normalize-after-lerp on unit sphere)
|
|
1905
|
-
* fov — lerp when both keyframes carry non-null fov; else null
|
|
1906
|
-
* halfHeight — lerp when both keyframes carry non-null halfHeight; else null
|
|
1907
|
-
*
|
|
1908
|
-
* @example
|
|
1909
|
-
* const track = new CameraTrack()
|
|
1910
|
-
* track.add({ eye:[0,0,500] }) // center defaults to [0,0,0]
|
|
1911
|
-
* track.add({ eye:[300,-150,0], center:[0,0,0] })
|
|
1912
|
-
* track.add({ eMatrix: myEyeMatrix })
|
|
1913
|
-
* track.add({ vMatrix: myViewMatrix })
|
|
1914
|
-
* track.play({ loop: true })
|
|
1915
|
-
* // per frame:
|
|
1916
|
-
* track.tick()
|
|
1917
|
-
* const out = { eye:[0,0,0], center:[0,0,0], up:[0,1,0] }
|
|
1918
|
-
* track.eval(out)
|
|
1919
|
-
* cam.camera(out.eye[0],out.eye[1],out.eye[2],
|
|
1920
|
-
* out.center[0],out.center[1],out.center[2],
|
|
1921
|
-
* out.up[0],out.up[1],out.up[2])
|
|
2160
|
+
*
|
|
2161
|
+
* To capture a matrix-based pose, use PoseTrack.add({ mMatrix: eMatrix })
|
|
2162
|
+
* for full-fidelity including roll, or cam.capturePose() for lookat-style.
|
|
1922
2163
|
*/
|
|
1923
2164
|
class CameraTrack extends Track {
|
|
1924
2165
|
constructor() {
|
|
1925
2166
|
super();
|
|
1926
2167
|
/**
|
|
1927
|
-
* Eye
|
|
1928
|
-
* - 'hermite' — cubic Hermite; auto-CR tangents when none stored (default)
|
|
1929
|
-
* - 'linear' — lerp
|
|
1930
|
-
* - 'step' — snap to k0 eye
|
|
2168
|
+
* Eye-path interpolation mode.
|
|
1931
2169
|
* @type {'hermite'|'linear'|'step'}
|
|
1932
2170
|
*/
|
|
1933
2171
|
this.eyeInterp = 'hermite';
|
|
1934
2172
|
/**
|
|
1935
|
-
* Center
|
|
1936
|
-
* 'linear' suits fixed or predictably moving targets (default).
|
|
1937
|
-
* 'hermite' gives smoother paths when center is also flying freely.
|
|
1938
|
-
* - 'hermite' — cubic Hermite; auto-CR tangents when none stored
|
|
1939
|
-
* - 'linear' — lerp
|
|
1940
|
-
* - 'step' — snap to k0 center
|
|
2173
|
+
* Center-path interpolation mode.
|
|
1941
2174
|
* @type {'hermite'|'linear'|'step'}
|
|
1942
2175
|
*/
|
|
1943
2176
|
this.centerInterp = 'linear';
|
|
1944
|
-
// Scratch for toCamera() — avoids hot-path allocations
|
|
1945
|
-
this._eye = [0,0,0];
|
|
1946
|
-
this._center = [0,0,0];
|
|
1947
|
-
this._up = [0,1,0];
|
|
1948
2177
|
}
|
|
1949
2178
|
|
|
1950
2179
|
/**
|
|
1951
2180
|
* Append one or more camera keyframes. Adjacent duplicates are skipped by default.
|
|
1952
|
-
*
|
|
1953
2181
|
* @param {Object|Object[]} spec
|
|
1954
|
-
* { eye, center?, up? } or { vMatrix: mat4 } or { eMatrix: mat4 } or an array of either.
|
|
1955
2182
|
* @param {{ deduplicate?: boolean }} [opts]
|
|
1956
2183
|
*/
|
|
1957
2184
|
add(spec, opts) {
|
|
@@ -1969,7 +2196,7 @@ class CameraTrack extends Track {
|
|
|
1969
2196
|
}
|
|
1970
2197
|
|
|
1971
2198
|
/**
|
|
1972
|
-
* Replace (or append at end) the keyframe at index.
|
|
2199
|
+
* Replace (or append at end) the camera keyframe at index.
|
|
1973
2200
|
* @param {number} index
|
|
1974
2201
|
* @param {Object} spec
|
|
1975
2202
|
* @returns {boolean}
|
|
@@ -2021,7 +2248,7 @@ class CameraTrack extends Track {
|
|
|
2021
2248
|
const p3 = seg + 2 < n ? this.keyframes[seg + 2].eye : k1.eye;
|
|
2022
2249
|
const m0 = k0.eyeTanOut != null ? k0.eyeTanOut
|
|
2023
2250
|
: k0.eyeTanIn != null ? k0.eyeTanIn
|
|
2024
|
-
: _crTanOut(_m0, p0, k0.eye, k1.eye
|
|
2251
|
+
: _crTanOut(_m0, p0, k0.eye, k1.eye);
|
|
2025
2252
|
const m1 = k1.eyeTanIn != null ? k1.eyeTanIn
|
|
2026
2253
|
: k1.eyeTanOut != null ? k1.eyeTanOut
|
|
2027
2254
|
: _crTanIn(_m1, p0, k0.eye, k1.eye, p3);
|
|
@@ -2031,33 +2258,31 @@ class CameraTrack extends Track {
|
|
|
2031
2258
|
// center — Hermite, linear, or step (independent lookat target)
|
|
2032
2259
|
if (this.centerInterp === 'step') {
|
|
2033
2260
|
out.center[0]=k0.center[0]; out.center[1]=k0.center[1]; out.center[2]=k0.center[2];
|
|
2034
|
-
} else if (this.centerInterp === '
|
|
2035
|
-
lerpVec3(out.center, k0.center, k1.center, t);
|
|
2036
|
-
} else {
|
|
2261
|
+
} else if (this.centerInterp === 'hermite') {
|
|
2037
2262
|
const c0 = seg > 0 ? this.keyframes[seg - 1].center : k0.center;
|
|
2038
2263
|
const c3 = seg + 2 < n ? this.keyframes[seg + 2].center : k1.center;
|
|
2039
2264
|
const m0 = k0.centerTanOut != null ? k0.centerTanOut
|
|
2040
2265
|
: k0.centerTanIn != null ? k0.centerTanIn
|
|
2041
|
-
: _crTanOut(_m0, c0, k0.center, k1.center
|
|
2266
|
+
: _crTanOut(_m0, c0, k0.center, k1.center);
|
|
2042
2267
|
const m1 = k1.centerTanIn != null ? k1.centerTanIn
|
|
2043
2268
|
: k1.centerTanOut != null ? k1.centerTanOut
|
|
2044
2269
|
: _crTanIn(_m1, c0, k0.center, k1.center, c3);
|
|
2045
2270
|
hermiteVec3(out.center, k0.center, m0, k1.center, m1, t);
|
|
2271
|
+
} else {
|
|
2272
|
+
lerpVec3(out.center, k0.center, k1.center, t);
|
|
2046
2273
|
}
|
|
2047
2274
|
|
|
2048
|
-
// up — nlerp
|
|
2049
|
-
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
const ul = Math.sqrt(ux*ux+uy*uy+uz*uz) || 1;
|
|
2053
|
-
out.up[0]=ux/ul; out.up[1]=uy/ul; out.up[2]=uz/ul;
|
|
2275
|
+
// up — nlerp on unit sphere
|
|
2276
|
+
lerpVec3(out.up, k0.up, k1.up, t);
|
|
2277
|
+
const ul=Math.sqrt(out.up[0]*out.up[0]+out.up[1]*out.up[1]+out.up[2]*out.up[2])||1;
|
|
2278
|
+
out.up[0]/=ul; out.up[1]/=ul; out.up[2]/=ul;
|
|
2054
2279
|
|
|
2055
|
-
// fov — lerp
|
|
2056
|
-
out.fov
|
|
2057
|
-
? k0.fov
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2280
|
+
// fov / halfHeight — lerp when both keyframes carry non-null values
|
|
2281
|
+
out.fov = (k0.fov != null && k1.fov != null)
|
|
2282
|
+
? k0.fov + t * (k1.fov - k0.fov) : (k0.fov ?? k1.fov ?? null);
|
|
2283
|
+
out.halfHeight = (k0.halfHeight != null && k1.halfHeight != null)
|
|
2284
|
+
? k0.halfHeight + t * (k1.halfHeight - k0.halfHeight)
|
|
2285
|
+
: (k0.halfHeight ?? k1.halfHeight ?? null);
|
|
2061
2286
|
|
|
2062
2287
|
return out;
|
|
2063
2288
|
}
|
|
@@ -2220,5 +2445,5 @@ function boxVisibility(planes, x0, y0, z0, x1, y1, z1) {
|
|
|
2220
2445
|
return allIn ? VISIBLE : SEMIVISIBLE;
|
|
2221
2446
|
}
|
|
2222
2447
|
|
|
2223
|
-
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,
|
|
2448
|
+
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, distanceToPlane, frustumPlanes, hermiteVec3, i, j, k, lerpVec3, mapDirection, mapLocation, mat3Direction, mat3NormalFromMat4, mat4Bias, mat4EyeMatrix, mat4FromBasis, mat4FromScale, mat4FromTRS, mat4FromTranslation, mat4Frustum, mat4Invert, mat4Location, mat4LookAt, mat4MV, mat4Mul, mat4MulDir, mat4MulPoint, mat4Ortho, mat4PV, mat4Perspective, mat4Pick, mat4Reflect, mat4ToRotation, mat4ToScale, mat4ToTransform, mat4ToTranslation, 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 };
|
|
2224
2449
|
//# sourceMappingURL=index.js.map
|