@solidrt/3d 0.0.47 → 0.0.48

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