@solidrt/3d 0.0.47 → 0.0.49

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/src/math.ts CHANGED
@@ -12,6 +12,8 @@
12
12
  export type Vec2 = [number, number]
13
13
  export type Vec3 = [number, number, number]
14
14
  export type Vec4 = [number, number, number, number]
15
+ /** A rotation as [x, y, z, w] - glTF's and Three's component order. */
16
+ export type Quat = [number, number, number, number]
15
17
  // prettier-ignore
16
18
  export type Mat4 = [
17
19
  number, number, number, number,
@@ -55,6 +57,48 @@ export function transformPoint(out: Vec4, m: Mat4, p: Vec3): Vec4 {
55
57
  return out
56
58
  }
57
59
 
60
+ /**
61
+ * Transform a DIRECTION by m's upper 3x3 (w = 0: rotation and scale apply,
62
+ * translation does not). The ray-direction counterpart of transformPoint.
63
+ */
64
+ export function transformVector(out: Vec3, m: Mat4, v: Vec3): Vec3 {
65
+ let x = v[0], y = v[1], z = v[2]
66
+ out[0] = m[0] * x + m[4] * y + m[8] * z
67
+ out[1] = m[1] * x + m[5] * y + m[9] * z
68
+ out[2] = m[2] * x + m[6] * y + m[10] * z
69
+ return out
70
+ }
71
+
72
+ /**
73
+ * Invert an AFFINE matrix - a world or local matrix whose bottom row is
74
+ * 0,0,0,1, NOT a projection: the upper 3x3 inverts by cofactors and the
75
+ * translation is pulled back through it. Picking's world-to-local step.
76
+ * `out` may alias `m`. A degenerate (zero-scale) matrix yields the raw
77
+ * cofactors instead of NaNs, the same policy as normalMatrix.
78
+ */
79
+ export function invertAffine(out: Mat4, m: Mat4): Mat4 {
80
+ let a = m[0], b = m[4], c = m[8]
81
+ let d = m[1], e = m[5], f = m[9]
82
+ let g = m[2], h = m[6], i = m[10]
83
+ let tx = m[12], ty = m[13], tz = m[14]
84
+ let c00 = e * i - f * h
85
+ let c01 = f * g - d * i
86
+ let c02 = d * h - e * g
87
+ let det = a * c00 + b * c01 + c * c02
88
+ let s = 1 / (det || 1)
89
+ let r00 = c00 * s, r01 = (c * h - b * i) * s, r02 = (b * f - c * e) * s
90
+ let r10 = c01 * s, r11 = (a * i - c * g) * s, r12 = (c * d - a * f) * s
91
+ let r20 = c02 * s, r21 = (b * g - a * h) * s, r22 = (a * e - b * d) * s
92
+ out[0] = r00; out[1] = r10; out[2] = r20; out[3] = 0
93
+ out[4] = r01; out[5] = r11; out[6] = r21; out[7] = 0
94
+ out[8] = r02; out[9] = r12; out[10] = r22; out[11] = 0
95
+ out[12] = -(r00 * tx + r01 * ty + r02 * tz)
96
+ out[13] = -(r10 * tx + r11 * ty + r12 * tz)
97
+ out[14] = -(r20 * tx + r21 * ty + r22 * tz)
98
+ out[15] = 1
99
+ return out
100
+ }
101
+
58
102
  /** out = a * b (column vectors: b applies first). out may alias a or b. */
59
103
  export function multiply(out: Mat4, a: Mat4, b: Mat4): Mat4 {
60
104
  let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]
@@ -85,26 +129,23 @@ export function multiply(out: Mat4, a: Mat4, b: Mat4): Mat4 {
85
129
  }
86
130
 
87
131
  /**
88
- * Compose translation + rotation + scale into a local matrix. Rotation is
89
- * Euler angles in radians applied x, then y, then z (R = Rz * Ry * Rx on
90
- * column vectors) - the common "XYZ" order.
132
+ * Compose translation + rotation + scale into a local matrix - Three's
133
+ * `Matrix4.compose` signature, rotation as a quaternion.
134
+ *
135
+ * `rotation` must be a UNIT quaternion: a non-unit one scales the geometry
136
+ * by |q|^2, silently. The scene closes that trap by normalizing on write
137
+ * (setTransform) rather than paying for a check on every compose.
91
138
  */
92
- export function compose(out: Mat4, position: Vec3, rotation: Vec3, scale: Vec3): Mat4 {
93
- let cx = Math.cos(rotation[0]), sx = Math.sin(rotation[0])
94
- let cy = Math.cos(rotation[1]), sy = Math.sin(rotation[1])
95
- let cz = Math.cos(rotation[2]), sz = Math.sin(rotation[2])
96
- let r00 = cz * cy
97
- let r01 = cz * sy * sx - sz * cx
98
- let r02 = cz * sy * cx + sz * sx
99
- let r10 = sz * cy
100
- let r11 = sz * sy * sx + cz * cx
101
- let r12 = sz * sy * cx - cz * sx
102
- let r20 = -sy
103
- let r21 = cy * sx
104
- let r22 = cy * cx
105
- out[0] = r00 * scale[0]; out[1] = r10 * scale[0]; out[2] = r20 * scale[0]; out[3] = 0
106
- out[4] = r01 * scale[1]; out[5] = r11 * scale[1]; out[6] = r21 * scale[1]; out[7] = 0
107
- out[8] = r02 * scale[2]; out[9] = r12 * scale[2]; out[10] = r22 * scale[2]; out[11] = 0
139
+ export function compose(out: Mat4, position: Vec3, rotation: Quat, scale: Vec3): Mat4 {
140
+ let x = rotation[0], y = rotation[1], z = rotation[2], w = rotation[3]
141
+ let x2 = x + x, y2 = y + y, z2 = z + z
142
+ let xx = x * x2, xy = x * y2, xz = x * z2
143
+ let yy = y * y2, yz = y * z2, zz = z * z2
144
+ let wx = w * x2, wy = w * y2, wz = w * z2
145
+ let sx = scale[0], sy = scale[1], sz = scale[2]
146
+ out[0] = (1 - (yy + zz)) * sx; out[1] = (xy + wz) * sx; out[2] = (xz - wy) * sx; out[3] = 0
147
+ out[4] = (xy - wz) * sy; out[5] = (1 - (xx + zz)) * sy; out[6] = (yz + wx) * sy; out[7] = 0
148
+ out[8] = (xz + wy) * sz; out[9] = (yz - wx) * sz; out[10] = (1 - (xx + yy)) * sz; out[11] = 0
108
149
  out[12] = position[0]; out[13] = position[1]; out[14] = position[2]; out[15] = 1
109
150
  return out
110
151
  }
@@ -150,6 +191,278 @@ export function perspective(out: Mat4, fovy: number, aspect: number, near: numbe
150
191
  return out
151
192
  }
152
193
 
194
+ // Quaternions, the rotation the scene actually stores. Euler triples are a
195
+ // boundary format only - authoring (setTransform's `rotation`, the
196
+ // components' `rotation` prop) and reading back (getRotation) - so the order
197
+ // convention and gimbal lock live at that boundary and nowhere else.
198
+
199
+ /** A fresh identity rotation. */
200
+ export function quat(): Quat {
201
+ return [0, 0, 0, 1]
202
+ }
203
+
204
+ /** Unit quaternion; a zero-length input comes back as the identity. `out`
205
+ * may alias `q`. */
206
+ export function quatNormalize(out: Quat, q: Quat): Quat {
207
+ let len = Math.hypot(q[0], q[1], q[2], q[3])
208
+ if (len === 0) {
209
+ out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1
210
+ return out
211
+ }
212
+ out[0] = q[0] / len; out[1] = q[1] / len; out[2] = q[2] / len; out[3] = q[3] / len
213
+ return out
214
+ }
215
+
216
+ /**
217
+ * Euler radians to a quaternion, in XYZ order: x applied first, then y,
218
+ * then z (R = Rx * Ry * Rz on column vectors), Three's `Euler` default - a
219
+ * triple copied from a Three scene means the same thing here.
220
+ *
221
+ * ONE order exists, deliberately: a per-call order argument is how the same
222
+ * triple ends up meaning two different things in two places, and the
223
+ * quaternion is right there for anything an order was going to express.
224
+ */
225
+ export function quatFromEuler(out: Quat, euler: Vec3): Quat {
226
+ let c1 = Math.cos(euler[0] / 2), s1 = Math.sin(euler[0] / 2)
227
+ let c2 = Math.cos(euler[1] / 2), s2 = Math.sin(euler[1] / 2)
228
+ let c3 = Math.cos(euler[2] / 2), s3 = Math.sin(euler[2] / 2)
229
+ out[0] = s1 * c2 * c3 + c1 * s2 * s3
230
+ out[1] = c1 * s2 * c3 - s1 * c2 * s3
231
+ out[2] = c1 * c2 * s3 + s1 * s2 * c3
232
+ out[3] = c1 * c2 * c3 - s1 * s2 * s3
233
+ return out
234
+ }
235
+
236
+ /**
237
+ * A quaternion back to Euler radians in the same XYZ order - the inverse of
238
+ * quatFromEuler, a convenience for reading and debugging rather than a peer
239
+ * of the quaternion: the mapping is many-to-one (a triple and that triple
240
+ * plus a full turn agree), and at the poles (local +z straight up or down)
241
+ * only the sum of x and z is determined, so this pins z to 0 and folds the
242
+ * roll into x. Round-tripping the result reproduces the rotation exactly;
243
+ * it need not reproduce the triple you started from.
244
+ */
245
+ export function eulerFromQuat(out: Vec3, q: Quat): Vec3 {
246
+ let x = q[0], y = q[1], z = q[2], w = q[3]
247
+ let x2 = x + x, y2 = y + y, z2 = z + z
248
+ let xx = x * x2, xy = x * y2, xz = x * z2
249
+ let yy = y * y2, yz = y * z2, zz = z * z2
250
+ let wx = w * x2, wy = w * y2, wz = w * z2
251
+ // The same matrix entries compose() writes: m02 = sin(y) alone, and the
252
+ // x/z pair reads off the rest unless cos(y) is 0 (the pole).
253
+ let m00 = 1 - (yy + zz)
254
+ let m01 = xy - wz
255
+ let m02 = xz + wy
256
+ // cos(y), which both remaining pairs scale with. atan2 against it beats
257
+ // asin(m02) - Three's form - near the poles, where asin's derivative
258
+ // blows up and a 1e-16 error in m02 becomes 1e-8 in the angle. It also
259
+ // lets the pole branch start three orders of magnitude later: the pairs
260
+ // stay well-conditioned until cos(y) approaches the noise floor.
261
+ let cy = Math.hypot(m00, m01)
262
+ out[1] = Math.atan2(m02, cy)
263
+ if (cy > 1e-7) {
264
+ out[0] = Math.atan2(wx - yz, 1 - (xx + yy))
265
+ out[2] = Math.atan2(-m01, m00)
266
+ } else {
267
+ // Only x + z (at +y) or x - z (at -y) is determined; pin z and fold
268
+ // the whole roll into x.
269
+ out[0] = Math.atan2(yz + wx, 1 - (xx + zz))
270
+ out[2] = 0
271
+ }
272
+ return out
273
+ }
274
+
275
+ /**
276
+ * The rotation of `angle` RADIANS about `axis` - Three's `setFromAxisAngle`,
277
+ * Unity's `AngleAxis` (which takes degrees; this takes radians like
278
+ * everything else here). The axis need not be normalized - the named
279
+ * engines all require a unit axis and silently corrupt the rotation
280
+ * otherwise, the same precondition trap `quatFromTo` closes. A zero axis
281
+ * yields the identity.
282
+ */
283
+ export function quatFromAxisAngle(out: Quat, axis: Vec3, angle: number): Quat {
284
+ let x = axis[0], y = axis[1], z = axis[2]
285
+ let len = Math.hypot(x, y, z)
286
+ if (len === 0) {
287
+ out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1
288
+ return out
289
+ }
290
+ let s = Math.sin(angle / 2) / len
291
+ out[0] = x * s
292
+ out[1] = y * s
293
+ out[2] = z * s
294
+ out[3] = Math.cos(angle / 2)
295
+ return out
296
+ }
297
+
298
+ /**
299
+ * out = a * b - the same order contract as the mat4 `multiply` above: on
300
+ * column vectors b applies first, so `quatMultiply(q, spin, q)` composes a
301
+ * further world-frame spin onto q while `quatMultiply(q, q, spin)` spins
302
+ * about q's own local frame. `out` may alias `a` or `b`.
303
+ *
304
+ * The product of unit quaternions is unit up to float drift, so this does
305
+ * not renormalize; an accumulator composed every frame drifts slowly, and
306
+ * the scene's setTransform renormalizes on write anyway.
307
+ */
308
+ export function quatMultiply(out: Quat, a: Quat, b: Quat): Quat {
309
+ let ax = a[0], ay = a[1], az = a[2], aw = a[3]
310
+ let bx = b[0], by = b[1], bz = b[2], bw = b[3]
311
+ out[0] = aw * bx + ax * bw + ay * bz - az * by
312
+ out[1] = aw * by - ax * bz + ay * bw + az * bx
313
+ out[2] = aw * bz + ax * by - ay * bx + az * bw
314
+ out[3] = aw * bw - ax * bx - ay * by - az * bz
315
+ return out
316
+ }
317
+
318
+ /**
319
+ * Spherical interpolation from `a` to `b`: constant angular velocity along
320
+ * the shortest path (the sign of `b` is flipped when the pair straddles the
321
+ * quaternion double cover, so it never takes the long way round). t = 0 is
322
+ * `a`, t = 1 is `b`'s rotation; inputs must be unit and the result is unit.
323
+ * `out` may alias `a` or `b`.
324
+ *
325
+ * The canonical damped follow is
326
+ * `quatSlerp(q, q, target, 1 - Math.exp(-k * dt))` - frame-rate
327
+ * independent, k is the tracking speed.
328
+ */
329
+ export function quatSlerp(out: Quat, a: Quat, b: Quat, t: number): Quat {
330
+ let ax = a[0], ay = a[1], az = a[2], aw = a[3]
331
+ let bx = b[0], by = b[1], bz = b[2], bw = b[3]
332
+ let cos = ax * bx + ay * by + az * bz + aw * bw
333
+ if (cos < 0) {
334
+ cos = -cos
335
+ bx = -bx; by = -by; bz = -bz; bw = -bw
336
+ }
337
+ let wa: number
338
+ let wb: number
339
+ if (cos < 0.9995) {
340
+ let theta = Math.acos(cos > 1 ? 1 : cos)
341
+ let sin = Math.sin(theta)
342
+ wa = Math.sin((1 - t) * theta) / sin
343
+ wb = Math.sin(t * theta) / sin
344
+ } else {
345
+ // Nearly identical: sin(theta) is noise, and a straight lerp is within
346
+ // float precision of the arc - normalized below like any other result.
347
+ wa = 1 - t
348
+ wb = t
349
+ }
350
+ out[0] = wa * ax + wb * bx
351
+ out[1] = wa * ay + wb * by
352
+ out[2] = wa * az + wb * bz
353
+ out[3] = wa * aw + wb * bw
354
+ return quatNormalize(out, out)
355
+ }
356
+
357
+ /**
358
+ * The shortest-arc rotation taking `from` to `to`: Unity's
359
+ * `Quaternion.FromToRotation`, glam's `Quat::from_rotation_arc`. Three
360
+ * calls this `setFromUnitVectors`; renamed because that name states a
361
+ * precondition instead of the operation, and this one has no such
362
+ * precondition - neither input need be normalized.
363
+ *
364
+ * This is how a y-axis solid gets aimed - `quatFromTo(q, [0, 1, 0], dir)`
365
+ * for a cylinder or cone - where lookAt's +z convention would need a
366
+ * correction. Opposite vectors have no shortest arc (every half turn is
367
+ * equally short); a stable perpendicular axis is picked. A zero-length
368
+ * input yields the identity.
369
+ */
370
+ export function quatFromTo(out: Quat, from: Vec3, to: Vec3): Quat {
371
+ let ax = from[0], ay = from[1], az = from[2]
372
+ let bx = to[0], by = to[1], bz = to[2]
373
+ let la = Math.hypot(ax, ay, az)
374
+ let lb = Math.hypot(bx, by, bz)
375
+ if (la === 0 || lb === 0) {
376
+ out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1
377
+ return out
378
+ }
379
+ ax /= la; ay /= la; az /= la
380
+ bx /= lb; by /= lb; bz /= lb
381
+ let r = ax * bx + ay * by + az * bz + 1
382
+ if (r < 1e-6) {
383
+ // Antiparallel: the cross product vanishes, so take any perpendicular
384
+ // axis - crossing with the smaller of from's x/z components cannot
385
+ // vanish too, and picking off from alone keeps the choice stable.
386
+ r = 0
387
+ if (Math.abs(ax) > Math.abs(az)) {
388
+ out[0] = -ay; out[1] = ax; out[2] = 0
389
+ } else {
390
+ out[0] = 0; out[1] = -az; out[2] = ay
391
+ }
392
+ } else {
393
+ out[0] = ay * bz - az * by
394
+ out[1] = az * bx - ax * bz
395
+ out[2] = ax * by - ay * bx
396
+ }
397
+ out[3] = r
398
+ return quatNormalize(out, out)
399
+ }
400
+
401
+ /**
402
+ * The rotation that points the local +z axis along `forward`, with `up`
403
+ * choosing the roll about it - the object-aiming counterpart of lookAt(),
404
+ * which builds the camera's inverse frame. Neither input need be
405
+ * normalized. Degenerate inputs (zero forward, up parallel to forward) fall
406
+ * back to a stable perpendicular instead of producing NaNs.
407
+ */
408
+ export function quatFromFrame(out: Quat, forward: Vec3, up: Vec3): Quat {
409
+ let zx = forward[0], zy = forward[1], zz = forward[2]
410
+ let len = Math.hypot(zx, zy, zz)
411
+ if (len === 0) {
412
+ zx = 0; zy = 0; zz = 1
413
+ } else {
414
+ zx /= len; zy /= len; zz /= len
415
+ }
416
+ let xx = up[1] * zz - up[2] * zy
417
+ let xy = up[2] * zx - up[0] * zz
418
+ let xz = up[0] * zy - up[1] * zx
419
+ len = Math.hypot(xx, xy, xz)
420
+ if (len === 0) {
421
+ // up is parallel to forward: cross with a world axis that cannot be,
422
+ // picked off z's own components so the choice is stable per direction.
423
+ let ax = Math.abs(zx) < 0.9 ? 1 : 0
424
+ let ay = ax === 1 ? 0 : 1
425
+ xx = ay * zz
426
+ xy = -ax * zz
427
+ xz = ax * zy - ay * zx
428
+ len = Math.hypot(xx, xy, xz)
429
+ }
430
+ xx /= len; xy /= len; xz /= len
431
+ let yx = zy * xz - zz * xy
432
+ let yy = zz * xx - zx * xz
433
+ let yz = zx * xy - zy * xx
434
+ // X | Y | Z are the rotation's columns, so its diagonal is xx, yy, zz.
435
+ // Branching on the largest diagonal entry keeps the divisor away from
436
+ // zero; the basis is orthonormal, so the result is already unit.
437
+ let trace = xx + yy + zz
438
+ if (trace > 0) {
439
+ let s = 0.5 / Math.sqrt(trace + 1)
440
+ out[0] = (yz - zy) * s
441
+ out[1] = (zx - xz) * s
442
+ out[2] = (xy - yx) * s
443
+ out[3] = 0.25 / s
444
+ } else if (xx > yy && xx > zz) {
445
+ let s = 2 * Math.sqrt(1 + xx - yy - zz)
446
+ out[0] = 0.25 * s
447
+ out[1] = (yx + xy) / s
448
+ out[2] = (zx + xz) / s
449
+ out[3] = (yz - zy) / s
450
+ } else if (yy > zz) {
451
+ let s = 2 * Math.sqrt(1 + yy - xx - zz)
452
+ out[0] = (yx + xy) / s
453
+ out[1] = 0.25 * s
454
+ out[2] = (zy + yz) / s
455
+ out[3] = (zx - xz) / s
456
+ } else {
457
+ let s = 2 * Math.sqrt(1 + zz - xx - yy)
458
+ out[0] = (zx + xz) / s
459
+ out[1] = (zy + yz) / s
460
+ out[2] = 0.25 * s
461
+ out[3] = (xy - yx) / s
462
+ }
463
+ return out
464
+ }
465
+
153
466
  // Vec3 helpers for geometry construction. These allocate (unlike the matrix
154
467
  // functions above): they serve generation-time code - curve frames, normals -
155
468
  // not the per-frame path. Exposed on the /math subpath only, so `add` does
@@ -185,6 +498,10 @@ export function normalize(v: Vec3): Vec3 {
185
498
  * View matrix (world -> camera) for a camera at `eye` looking at `target`
186
499
  * with the given `up`. Degenerate inputs (eye == target, up parallel to the
187
500
  * view direction) fall back to axis defaults instead of producing NaNs.
501
+ *
502
+ * On the /math subpath ONLY - the package root's `lookAt` is the scene verb
503
+ * that aims a node (the Matrix4/Object3D split Three makes under the same
504
+ * name), the same collision rule the Vec3 helpers follow.
188
505
  */
189
506
  export function lookAt(out: Mat4, eye: Vec3, target: Vec3, up: Vec3): Mat4 {
190
507
  let zx = eye[0] - target[0]