@forgeax/engine-math 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +294 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/_arbs.d.ts +36 -0
- package/dist/__tests__/_arbs.d.ts.map +1 -0
- package/dist/__tests__/_fixtures.d.ts +61 -0
- package/dist/__tests__/_fixtures.d.ts.map +1 -0
- package/dist/__tests__/bounds2.test.d.ts +2 -0
- package/dist/__tests__/bounds2.test.d.ts.map +1 -0
- package/dist/__tests__/box3.test.d.ts +2 -0
- package/dist/__tests__/box3.test.d.ts.map +1 -0
- package/dist/__tests__/easing.test.d.ts +2 -0
- package/dist/__tests__/easing.test.d.ts.map +1 -0
- package/dist/__tests__/euler.test-d.d.ts +2 -0
- package/dist/__tests__/euler.test-d.d.ts.map +1 -0
- package/dist/__tests__/mat3.test-d.d.ts +2 -0
- package/dist/__tests__/mat3.test-d.d.ts.map +1 -0
- package/dist/__tests__/mat4.property.test.d.ts +2 -0
- package/dist/__tests__/mat4.property.test.d.ts.map +1 -0
- package/dist/__tests__/mat4.test-d.d.ts +2 -0
- package/dist/__tests__/mat4.test-d.d.ts.map +1 -0
- package/dist/__tests__/mat4.test.d.ts +2 -0
- package/dist/__tests__/mat4.test.d.ts.map +1 -0
- package/dist/__tests__/noise.test.d.ts +2 -0
- package/dist/__tests__/noise.test.d.ts.map +1 -0
- package/dist/__tests__/quat.basis.test.d.ts +2 -0
- package/dist/__tests__/quat.basis.test.d.ts.map +1 -0
- package/dist/__tests__/quat.interpolation.test.d.ts +2 -0
- package/dist/__tests__/quat.interpolation.test.d.ts.map +1 -0
- package/dist/__tests__/quat.lookat.test.d.ts +2 -0
- package/dist/__tests__/quat.lookat.test.d.ts.map +1 -0
- package/dist/__tests__/quat.property.test.d.ts +2 -0
- package/dist/__tests__/quat.property.test.d.ts.map +1 -0
- package/dist/__tests__/quat.rotateaxis.test.d.ts +2 -0
- package/dist/__tests__/quat.rotateaxis.test.d.ts.map +1 -0
- package/dist/__tests__/quat.test-d.d.ts +2 -0
- package/dist/__tests__/quat.test-d.d.ts.map +1 -0
- package/dist/__tests__/ray.property.test.d.ts +2 -0
- package/dist/__tests__/ray.property.test.d.ts.map +1 -0
- package/dist/__tests__/ray.test.d.ts +2 -0
- package/dist/__tests__/ray.test.d.ts.map +1 -0
- package/dist/__tests__/types.test-d.d.ts +2 -0
- package/dist/__tests__/types.test-d.d.ts.map +1 -0
- package/dist/__tests__/vec-catmull-rom.test.d.ts +2 -0
- package/dist/__tests__/vec-catmull-rom.test.d.ts.map +1 -0
- package/dist/__tests__/vec-smooth-damp.test.d.ts +2 -0
- package/dist/__tests__/vec-smooth-damp.test.d.ts.map +1 -0
- package/dist/__tests__/vec2.test-d.d.ts +2 -0
- package/dist/__tests__/vec2.test-d.d.ts.map +1 -0
- package/dist/__tests__/vec3.property.test.d.ts +2 -0
- package/dist/__tests__/vec3.property.test.d.ts.map +1 -0
- package/dist/__tests__/vec3.test-d.d.ts +2 -0
- package/dist/__tests__/vec3.test-d.d.ts.map +1 -0
- package/dist/__tests__/vec4.test-d.d.ts +2 -0
- package/dist/__tests__/vec4.test-d.d.ts.map +1 -0
- package/dist/_internal/epsilon.d.ts +9 -0
- package/dist/_internal/epsilon.d.ts.map +1 -0
- package/dist/_internal/scalar.d.ts +40 -0
- package/dist/_internal/scalar.d.ts.map +1 -0
- package/dist/box2.d.ts +35 -0
- package/dist/box2.d.ts.map +1 -0
- package/dist/box3.d.ts +76 -0
- package/dist/box3.d.ts.map +1 -0
- package/dist/circle2.d.ts +29 -0
- package/dist/circle2.d.ts.map +1 -0
- package/dist/color.d.ts +64 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/easing.d.ts +18 -0
- package/dist/easing.d.ts.map +1 -0
- package/dist/euler.d.ts +51 -0
- package/dist/euler.d.ts.map +1 -0
- package/dist/f32-to-f16-bytes.d.ts +10 -0
- package/dist/f32-to-f16-bytes.d.ts.map +1 -0
- package/dist/frustum.d.ts +49 -0
- package/dist/frustum.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3688 -0
- package/dist/index.mjs.map +1 -0
- package/dist/mat3.d.ts +70 -0
- package/dist/mat3.d.ts.map +1 -0
- package/dist/mat4.d.ts +329 -0
- package/dist/mat4.d.ts.map +1 -0
- package/dist/noise.d.ts +12 -0
- package/dist/noise.d.ts.map +1 -0
- package/dist/quat.d.ts +312 -0
- package/dist/quat.d.ts.map +1 -0
- package/dist/ray.d.ts +144 -0
- package/dist/ray.d.ts.map +1 -0
- package/dist/ray2.d.ts +38 -0
- package/dist/ray2.d.ts.map +1 -0
- package/dist/sphere.d.ts +41 -0
- package/dist/sphere.d.ts.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/vec2.d.ts +76 -0
- package/dist/vec2.d.ts.map +1 -0
- package/dist/vec3.d.ts +82 -0
- package/dist/vec3.d.ts.map +1 -0
- package/dist/vec4.d.ts +61 -0
- package/dist/vec4.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/__tests__/_arbs.ts +149 -0
- package/src/__tests__/_fixtures.ts +118 -0
- package/src/__tests__/bounds2.test.ts +146 -0
- package/src/__tests__/box3.test.ts +277 -0
- package/src/__tests__/easing.test.ts +109 -0
- package/src/__tests__/euler.test-d.ts +63 -0
- package/src/__tests__/mat3.test-d.ts +47 -0
- package/src/__tests__/mat4.property.test.ts +256 -0
- package/src/__tests__/mat4.test-d.ts +130 -0
- package/src/__tests__/mat4.test.ts +162 -0
- package/src/__tests__/noise.test.ts +75 -0
- package/src/__tests__/quat.basis.test.ts +134 -0
- package/src/__tests__/quat.interpolation.test.ts +45 -0
- package/src/__tests__/quat.lookat.test.ts +103 -0
- package/src/__tests__/quat.property.test.ts +150 -0
- package/src/__tests__/quat.rotateaxis.test.ts +149 -0
- package/src/__tests__/quat.test-d.ts +138 -0
- package/src/__tests__/ray.property.test.ts +105 -0
- package/src/__tests__/ray.test.ts +539 -0
- package/src/__tests__/types.test-d.ts +64 -0
- package/src/__tests__/vec-catmull-rom.test.ts +125 -0
- package/src/__tests__/vec-smooth-damp.test.ts +167 -0
- package/src/__tests__/vec2.test-d.ts +59 -0
- package/src/__tests__/vec3.property.test.ts +72 -0
- package/src/__tests__/vec3.test-d.ts +65 -0
- package/src/__tests__/vec4.test-d.ts +61 -0
- package/src/_internal/epsilon.ts +29 -0
- package/src/_internal/scalar.ts +90 -0
- package/src/box2.ts +168 -0
- package/src/box3.ts +372 -0
- package/src/circle2.ts +134 -0
- package/src/color.ts +177 -0
- package/src/easing.ts +49 -0
- package/src/euler.ts +239 -0
- package/src/f32-to-f16-bytes.ts +71 -0
- package/src/frustum.ts +240 -0
- package/src/index.ts +65 -0
- package/src/mat3.ts +286 -0
- package/src/mat4.ts +1334 -0
- package/src/noise.ts +78 -0
- package/src/quat.ts +847 -0
- package/src/ray.ts +575 -0
- package/src/ray2.ts +198 -0
- package/src/sphere.ts +138 -0
- package/src/types.ts +78 -0
- package/src/vec2.ts +229 -0
- package/src/vec3.ts +294 -0
- package/src/vec4.ts +235 -0
package/src/quat.ts
ADDED
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
// quat.ts — quaternion namespace (M4 / T-027)
|
|
2
|
+
//
|
|
3
|
+
// 23-function surface (≥ 16 lower bound):
|
|
4
|
+
// create / clone / identity / fromAxisAngle / fromEuler / fromRotationMatrix /
|
|
5
|
+
// fromLookAt / fromUnitVectors / multiply / rotateAxis / slerp / nlerp / invert /
|
|
6
|
+
// conjugate / dot / length / lengthSq / transformVec3 / normalize / eulerY /
|
|
7
|
+
// right / up / forward
|
|
8
|
+
//
|
|
9
|
+
// Memory layout lock: Float32Array length 4 [x, y, z, w], **Hamilton convention**
|
|
10
|
+
// (graphics mainstream: glm / Three.js / wgpu-matrix / DirectXMath / glam are all Hamilton (x,y,z,w)).
|
|
11
|
+
// Identity = [0, 0, 0, 1].
|
|
12
|
+
//
|
|
13
|
+
// Degenerate convention (plan-strategy §appendix A degenerate registry #8-#13):
|
|
14
|
+
// - fromAxisAngle(0-axis, _) → identity (same convention as the M2 baseline)
|
|
15
|
+
// - fromEuler(x, y, z, 'unknown') → silent fallback to 'XYZ' (D-P2 + AC-06 no throw)
|
|
16
|
+
// - slerp(a, b, t) when dot(a,b) < -EPS_SLERP_DOT_LIMIT → negate b then slerp normally (D-P6)
|
|
17
|
+
// - slerp(a, b, t) when |dot| > 1 - EPS_SLERP_DOT_LIMIT → falls back to nlerp (avoids acos blow-up)
|
|
18
|
+
// - fromUnitVectors(v, -v) → pick perpendicular axis (prefer (0,1,0); fall back to (1,0,0) if
|
|
19
|
+
// collinear with v) and 180° rotation (D-P18)
|
|
20
|
+
// - fromUnitVectors(v, v) → identity
|
|
21
|
+
// - normalize(zero quat) → out = zero (same convention as vec.normalize)
|
|
22
|
+
//
|
|
23
|
+
// Four ironclad rules (gl-matrix wiki / research §F1):
|
|
24
|
+
// 1. Out-param first; 2. Aliasing-safe; 3. Module-as-namespace; 4. Float32Array by default.
|
|
25
|
+
//
|
|
26
|
+
// Related: requirements §Surface quat lower bound 16 + AC-06 no throw + AC-08 naming alignment +
|
|
27
|
+
// AC-01 brand + boundary-case quat row;
|
|
28
|
+
// plan-strategy §6 M4 + D-P2/D-P6/D-P18 + §appendix A degenerate registry #8-#13;
|
|
29
|
+
// research §Finding 3 glam Hamilton + §fact-correction 4 fromEuler unknown silenced;
|
|
30
|
+
// wiki/gl-matrix-overview §quat degenerate anchor + wiki/glam-rs-overview §Hamilton +
|
|
31
|
+
// wiki/wgpu-matrix-overview §quat namespace.
|
|
32
|
+
//
|
|
33
|
+
// Degenerate-semantics registry (plan-strategy.md §appendix A; shares numbering with mat;
|
|
34
|
+
// D-P16 dual promise: runtime silent + JSDoc `@degrade` + `@example` guard pattern landing together):
|
|
35
|
+
// #8 quat.fromAxisAngle(0-axis, _) → out = identity
|
|
36
|
+
// #9 quat.fromEuler(_, _, _, unknown) → silently computes as 'XYZ' (D-P2)
|
|
37
|
+
// #10 quat.slerp(a, b, t) dot < -1+ε → negate b then slerp (D-P6)
|
|
38
|
+
// #11 quat.slerp(a, b, t) |dot| > 1-ε → falls back to nlerp
|
|
39
|
+
// #12 quat.fromUnitVectors(v, -v) → perpendicular-axis 180° rotation (D-P18)
|
|
40
|
+
// #13 quat.fromUnitVectors(v, v) → out = identity
|
|
41
|
+
|
|
42
|
+
import { EPS_NORMALIZE, EPS_QUAT_PARALLEL, EPS_SLERP_DOT_LIMIT } from './_internal/epsilon';
|
|
43
|
+
import { lengthSq4, normalize4 } from './_internal/scalar';
|
|
44
|
+
import type { EulerOrder, Mat3Like, Quat, QuatLike, Vec3, Vec3Like } from './types';
|
|
45
|
+
|
|
46
|
+
export type { Quat, QuatLike };
|
|
47
|
+
|
|
48
|
+
// Canonical axes for the local-basis accessors (right / up / forward). −Z is the
|
|
49
|
+
// forward convention (RL-4), matching mat4.getForward and fromLookAt.
|
|
50
|
+
const UNIT_X = [1, 0, 0] as const;
|
|
51
|
+
const UNIT_Y = [0, 1, 0] as const;
|
|
52
|
+
const UNIT_NEG_Z = [0, 0, -1] as const;
|
|
53
|
+
|
|
54
|
+
/** Create a Quat (default all zero; callers usually call identity() right after). */
|
|
55
|
+
export function create(): Quat {
|
|
56
|
+
return new Float32Array(4) as Quat;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Allocate a new Quat copy. */
|
|
60
|
+
export function clone(a: QuatLike): Quat {
|
|
61
|
+
return Float32Array.of(a[0] as number, a[1] as number, a[2] as number, a[3] as number) as Quat;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** out = identity quaternion [0, 0, 0, 1]. Returns out. */
|
|
65
|
+
export function identity(out: Quat): Quat {
|
|
66
|
+
out[0] = 0;
|
|
67
|
+
out[1] = 0;
|
|
68
|
+
out[2] = 0;
|
|
69
|
+
out[3] = 1;
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* out = quaternion representing rotation by angleRadians around axis. Returns out.
|
|
75
|
+
*
|
|
76
|
+
* axis does not have to be pre-normalized; this function normalizes it internally.
|
|
77
|
+
*
|
|
78
|
+
* @degrade axis is the zero vector (lengthSq < EPS_NORMALIZE) → out = identity
|
|
79
|
+
* (no throw, registry #8).
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* quat.fromAxisAngle(out, [0, 1, 0], Math.PI / 2);
|
|
84
|
+
* // Guard: if (vec3.lengthSq(axis) < EPS_NORMALIZE) skip;
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function fromAxisAngle(out: Quat, axis: Vec3Like, angleRadians: number): Quat {
|
|
88
|
+
const ax = axis[0] as number;
|
|
89
|
+
const ay = axis[1] as number;
|
|
90
|
+
const az = axis[2] as number;
|
|
91
|
+
const lenSq = ax * ax + ay * ay + az * az;
|
|
92
|
+
if (lenSq < EPS_NORMALIZE) {
|
|
93
|
+
return identity(out);
|
|
94
|
+
}
|
|
95
|
+
const inv = 1 / Math.sqrt(lenSq);
|
|
96
|
+
const half = angleRadians / 2;
|
|
97
|
+
const s = Math.sin(half);
|
|
98
|
+
out[0] = ax * inv * s;
|
|
99
|
+
out[1] = ay * inv * s;
|
|
100
|
+
out[2] = az * inv * s;
|
|
101
|
+
out[3] = Math.cos(half);
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* out = quaternion from intrinsic Euler angles (x, y, z, order). Returns out.
|
|
107
|
+
*
|
|
108
|
+
* Angle unit = **radians** (aligned with wgpu-matrix / Three.js; research §fact-correction 4
|
|
109
|
+
* deviates from gl-matrix's degrees).
|
|
110
|
+
* 6 orders: XYZ / YXZ / ZXY / ZYX / YZX / XZY (intrinsic rotation: literal order x → y → z).
|
|
111
|
+
* Implementation: split each axis rotation into single-axis quaternions, then Hamilton-multiply
|
|
112
|
+
* in `order`.
|
|
113
|
+
*
|
|
114
|
+
* @degrade order outside the EulerOrder union (only possible via `as any` cast) →
|
|
115
|
+
* **silently computes as if 'XYZ'** (D-P2 + AC-06 no throw; registry #9).
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* quat.fromEuler(out, 0.5, 0.3, 0.1, 'XYZ');
|
|
120
|
+
* quat.fromEuler(out, 0.5, 0.3, 0.1, 'unknown' as any); // silently computed as 'XYZ'
|
|
121
|
+
* // Guard: pass an EulerOrder union literal; the TS layer already blocks unknown strings.
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export function fromEuler(out: Quat, x: number, y: number, z: number, order: EulerOrder): Quat {
|
|
125
|
+
// Single-axis half-angle quat coefficients
|
|
126
|
+
const c1 = Math.cos(x / 2);
|
|
127
|
+
const c2 = Math.cos(y / 2);
|
|
128
|
+
const c3 = Math.cos(z / 2);
|
|
129
|
+
const s1 = Math.sin(x / 2);
|
|
130
|
+
const s2 = Math.sin(y / 2);
|
|
131
|
+
const s3 = Math.sin(z / 2);
|
|
132
|
+
|
|
133
|
+
// Formulas from Three.js Quaternion.setFromEuler (intrinsic rotation, Hamilton convention).
|
|
134
|
+
// 6 orders dispatched via switch; unknown silently falls back to XYZ (D-P2).
|
|
135
|
+
switch (order) {
|
|
136
|
+
case 'XYZ':
|
|
137
|
+
out[0] = s1 * c2 * c3 + c1 * s2 * s3;
|
|
138
|
+
out[1] = c1 * s2 * c3 - s1 * c2 * s3;
|
|
139
|
+
out[2] = c1 * c2 * s3 + s1 * s2 * c3;
|
|
140
|
+
out[3] = c1 * c2 * c3 - s1 * s2 * s3;
|
|
141
|
+
break;
|
|
142
|
+
case 'YXZ':
|
|
143
|
+
out[0] = s1 * c2 * c3 + c1 * s2 * s3;
|
|
144
|
+
out[1] = c1 * s2 * c3 - s1 * c2 * s3;
|
|
145
|
+
out[2] = c1 * c2 * s3 - s1 * s2 * c3;
|
|
146
|
+
out[3] = c1 * c2 * c3 + s1 * s2 * s3;
|
|
147
|
+
break;
|
|
148
|
+
case 'ZXY':
|
|
149
|
+
out[0] = s1 * c2 * c3 - c1 * s2 * s3;
|
|
150
|
+
out[1] = c1 * s2 * c3 + s1 * c2 * s3;
|
|
151
|
+
out[2] = c1 * c2 * s3 + s1 * s2 * c3;
|
|
152
|
+
out[3] = c1 * c2 * c3 - s1 * s2 * s3;
|
|
153
|
+
break;
|
|
154
|
+
case 'ZYX':
|
|
155
|
+
out[0] = s1 * c2 * c3 - c1 * s2 * s3;
|
|
156
|
+
out[1] = c1 * s2 * c3 + s1 * c2 * s3;
|
|
157
|
+
out[2] = c1 * c2 * s3 - s1 * s2 * c3;
|
|
158
|
+
out[3] = c1 * c2 * c3 + s1 * s2 * s3;
|
|
159
|
+
break;
|
|
160
|
+
case 'YZX':
|
|
161
|
+
out[0] = s1 * c2 * c3 + c1 * s2 * s3;
|
|
162
|
+
out[1] = c1 * s2 * c3 + s1 * c2 * s3;
|
|
163
|
+
out[2] = c1 * c2 * s3 - s1 * s2 * c3;
|
|
164
|
+
out[3] = c1 * c2 * c3 - s1 * s2 * s3;
|
|
165
|
+
break;
|
|
166
|
+
case 'XZY':
|
|
167
|
+
out[0] = s1 * c2 * c3 - c1 * s2 * s3;
|
|
168
|
+
out[1] = c1 * s2 * c3 - s1 * c2 * s3;
|
|
169
|
+
out[2] = c1 * c2 * s3 + s1 * s2 * c3;
|
|
170
|
+
out[3] = c1 * c2 * c3 + s1 * s2 * s3;
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
// D-P2 + AC-06: unknown order silently falls back to 'XYZ' (no throw, no console.warn).
|
|
174
|
+
out[0] = s1 * c2 * c3 + c1 * s2 * s3;
|
|
175
|
+
out[1] = c1 * s2 * c3 - s1 * c2 * s3;
|
|
176
|
+
out[2] = c1 * c2 * s3 + s1 * s2 * c3;
|
|
177
|
+
out[3] = c1 * c2 * c3 - s1 * s2 * s3;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* out = quaternion from a 3x3 rotation matrix m (column-major, length 9). Returns out.
|
|
185
|
+
*
|
|
186
|
+
* Implementation: Shepperd's case-split (branch on trace / largest diagonal entry); numerically
|
|
187
|
+
* more stable than the direct trace method (avoids sqrt(0) when trace ≈ -1).
|
|
188
|
+
*
|
|
189
|
+
* Assumption: m is a pure rotation matrix (|det|=1, columns orthogonal). If not, the result is
|
|
190
|
+
* undefined but does not throw.
|
|
191
|
+
*
|
|
192
|
+
* @degrade m is not a pure rotation (contains shear / scale) → numerically undefined but does
|
|
193
|
+
* not throw (AC-06 no throw; callers should normalize column vectors first or avoid
|
|
194
|
+
* introducing non-pure-rotation matrices at the ECS-design layer).
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* quat.fromRotationMatrix(out, mat3InstanceColumnMajor);
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export function fromRotationMatrix(out: Quat, m: Mat3Like): Quat {
|
|
202
|
+
const m00 = m[0] as number;
|
|
203
|
+
const m01 = m[1] as number;
|
|
204
|
+
const m02 = m[2] as number;
|
|
205
|
+
const m10 = m[3] as number;
|
|
206
|
+
const m11 = m[4] as number;
|
|
207
|
+
const m12 = m[5] as number;
|
|
208
|
+
const m20 = m[6] as number;
|
|
209
|
+
const m21 = m[7] as number;
|
|
210
|
+
const m22 = m[8] as number;
|
|
211
|
+
|
|
212
|
+
// m is column-major: m[col*3+row] → m00 is col=0,row=0; m01 is col=0,row=1.
|
|
213
|
+
// Rotation matrix R[row][col]:
|
|
214
|
+
// R[0][0]=m00, R[1][0]=m01, R[2][0]=m02
|
|
215
|
+
// R[0][1]=m10, R[1][1]=m11, R[2][1]=m12
|
|
216
|
+
// R[0][2]=m20, R[1][2]=m21, R[2][2]=m22
|
|
217
|
+
// Shepperd's formulas use R[i][j]; here the column-major indices are equivalent:
|
|
218
|
+
// trace = R[0][0]+R[1][1]+R[2][2] = m00+m11+m22
|
|
219
|
+
const trace = m00 + m11 + m22;
|
|
220
|
+
if (trace > 0) {
|
|
221
|
+
const s = 0.5 / Math.sqrt(trace + 1);
|
|
222
|
+
out[0] = (m12 - m21) * s; // (R[2][1] - R[1][2])
|
|
223
|
+
out[1] = (m20 - m02) * s; // (R[0][2] - R[2][0])
|
|
224
|
+
out[2] = (m01 - m10) * s; // (R[1][0] - R[0][1])
|
|
225
|
+
out[3] = 0.25 / s;
|
|
226
|
+
} else if (m00 > m11 && m00 > m22) {
|
|
227
|
+
const s = 2 * Math.sqrt(1 + m00 - m11 - m22);
|
|
228
|
+
out[0] = 0.25 * s;
|
|
229
|
+
out[1] = (m10 + m01) / s;
|
|
230
|
+
out[2] = (m20 + m02) / s;
|
|
231
|
+
out[3] = (m12 - m21) / s;
|
|
232
|
+
} else if (m11 > m22) {
|
|
233
|
+
const s = 2 * Math.sqrt(1 + m11 - m00 - m22);
|
|
234
|
+
out[0] = (m10 + m01) / s;
|
|
235
|
+
out[1] = 0.25 * s;
|
|
236
|
+
out[2] = (m21 + m12) / s;
|
|
237
|
+
out[3] = (m20 - m02) / s;
|
|
238
|
+
} else {
|
|
239
|
+
const s = 2 * Math.sqrt(1 + m22 - m00 - m11);
|
|
240
|
+
out[0] = (m20 + m02) / s;
|
|
241
|
+
out[1] = (m21 + m12) / s;
|
|
242
|
+
out[2] = 0.25 * s;
|
|
243
|
+
out[3] = (m01 - m10) / s;
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* out = orientation quaternion for an object placed at `eye` and facing `target`. Returns out.
|
|
250
|
+
*
|
|
251
|
+
* This is the ergonomic camera/look-at helper: it yields the WORLD-space orientation an entity's
|
|
252
|
+
* Transform.rotation needs so that its local -z axis points from `eye` toward `target` (the camera
|
|
253
|
+
* convention, matching `mat4.lookAt`). Use it instead of hand-wiring
|
|
254
|
+
* `mat4.lookAt → mat4.invert → mat3.fromMat4 → quat.fromRotationMatrix`; that chain is easy to get
|
|
255
|
+
* wrong (notably `fromRotationMatrix` takes a mat3, but `Mat4Like`≡`Mat3Like`≡`ArrayLike<number>`,
|
|
256
|
+
* so passing a mat4 typechecks and silently reads garbage → NaN → nothing renders).
|
|
257
|
+
*
|
|
258
|
+
* Convenience composition (like `mat4.computeViewProj`), not a primitive: it builds the same
|
|
259
|
+
* right/newUp/forward basis as `mat4.lookAt` and reuses `fromRotationMatrix` for the extraction.
|
|
260
|
+
*
|
|
261
|
+
* @degrade eye ≈ target (|eye-target| < EPS_NORMALIZE) → out = identity (same convention as
|
|
262
|
+
* `mat4.lookAt` degenerate #4; no throw, AC-06).
|
|
263
|
+
* @degrade up collinear with the view direction → alternative up auto-selected (same as
|
|
264
|
+
* `mat4.lookAt` #5).
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```ts
|
|
268
|
+
* // aim a camera at the origin
|
|
269
|
+
* const q = quat.fromLookAt(quat.create(), [-2.5, 4.5, 9], [0, 0, 0], [0, 1, 0]);
|
|
270
|
+
* world.set(cameraEntity, Transform, { pos: [-2.5, 4.5, 9], rot: q });
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
export function fromLookAt(out: Quat, eye: Vec3Like, target: Vec3Like, up: Vec3Like): Quat {
|
|
274
|
+
const ex = eye[0] as number;
|
|
275
|
+
const ey = eye[1] as number;
|
|
276
|
+
const ez = eye[2] as number;
|
|
277
|
+
|
|
278
|
+
// forward = normalize(eye - target): the object's local -z points at the target, so +z = eye - target
|
|
279
|
+
// (right-handed camera convention, identical to mat4.lookAt).
|
|
280
|
+
let fx = ex - (target[0] as number);
|
|
281
|
+
let fy = ey - (target[1] as number);
|
|
282
|
+
let fz = ez - (target[2] as number);
|
|
283
|
+
const fLenSq = fx * fx + fy * fy + fz * fz;
|
|
284
|
+
if (fLenSq < EPS_NORMALIZE) {
|
|
285
|
+
return identity(out);
|
|
286
|
+
}
|
|
287
|
+
const fInv = 1 / Math.sqrt(fLenSq);
|
|
288
|
+
fx *= fInv;
|
|
289
|
+
fy *= fInv;
|
|
290
|
+
fz *= fInv;
|
|
291
|
+
|
|
292
|
+
const upx = up[0] as number;
|
|
293
|
+
const upy = up[1] as number;
|
|
294
|
+
const upz = up[2] as number;
|
|
295
|
+
|
|
296
|
+
// right = normalize(cross(up, forward)); degrade path mirrors mat4.lookAt (#5 alternative up).
|
|
297
|
+
let rx = upy * fz - upz * fy;
|
|
298
|
+
let ry = upz * fx - upx * fz;
|
|
299
|
+
let rz = upx * fy - upy * fx;
|
|
300
|
+
let rLenSq = rx * rx + ry * ry + rz * rz;
|
|
301
|
+
if (rLenSq < EPS_NORMALIZE) {
|
|
302
|
+
// up collinear with forward: pick alternative up = (0, 0, 1); if still collinear pick (0, 1, 0)
|
|
303
|
+
rx = -fy;
|
|
304
|
+
ry = fx;
|
|
305
|
+
rz = 0;
|
|
306
|
+
rLenSq = rx * rx + ry * ry + rz * rz;
|
|
307
|
+
if (rLenSq < EPS_NORMALIZE) {
|
|
308
|
+
rx = 0;
|
|
309
|
+
ry = -fz;
|
|
310
|
+
rz = fy;
|
|
311
|
+
rLenSq = rx * rx + ry * ry + rz * rz;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const rInv = 1 / Math.sqrt(rLenSq);
|
|
315
|
+
rx *= rInv;
|
|
316
|
+
ry *= rInv;
|
|
317
|
+
rz *= rInv;
|
|
318
|
+
|
|
319
|
+
// newUp = cross(forward, right)
|
|
320
|
+
const ux = fy * rz - fz * ry;
|
|
321
|
+
const uy = fz * rx - fx * rz;
|
|
322
|
+
const uz = fx * ry - fy * rx;
|
|
323
|
+
|
|
324
|
+
// World rotation columns are the basis vectors themselves (col0=right, col1=up, col2=forward) —
|
|
325
|
+
// this is the transpose of mat4.lookAt's view rotation, i.e. the camera's world orientation.
|
|
326
|
+
// Pack column-major into a length-9 mat3 for fromRotationMatrix (SSOT for the Shepperd extraction).
|
|
327
|
+
// Per-call alloc mirrors mat4.computeViewProj's convenience-composition grain (camera orientation
|
|
328
|
+
// is set rarely; no module-scoped mutable state to reason about).
|
|
329
|
+
const m3 = new Float32Array(9);
|
|
330
|
+
m3[0] = rx;
|
|
331
|
+
m3[1] = ry;
|
|
332
|
+
m3[2] = rz;
|
|
333
|
+
m3[3] = ux;
|
|
334
|
+
m3[4] = uy;
|
|
335
|
+
m3[5] = uz;
|
|
336
|
+
m3[6] = fx;
|
|
337
|
+
m3[7] = fy;
|
|
338
|
+
m3[8] = fz;
|
|
339
|
+
return fromRotationMatrix(out, m3);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* out = quaternion that rotates the unit vector v to w (shortest arc). Returns out.
|
|
344
|
+
*
|
|
345
|
+
* Assumes v and w are normalized; if not, the caller is responsible.
|
|
346
|
+
*
|
|
347
|
+
* @degrade v ≈ w (dot > 1 - EPS_QUAT_PARALLEL) → out = identity (registry #13).
|
|
348
|
+
* @degrade v ≈ -w (dot < -1 + EPS_QUAT_PARALLEL) → pick a perpendicular axis and do a 180°
|
|
349
|
+
* rotation (prefer (0,1,0); fall back to (1,0,0) when collinear with v;
|
|
350
|
+
* registry #12 + D-P18).
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```ts
|
|
354
|
+
* quat.fromUnitVectors(out, [1,0,0], [0,1,0]); // 90° around Z
|
|
355
|
+
* quat.fromUnitVectors(out, [1,0,0], [-1,0,0]); // 180° opposite: pick (0,1,0) axis
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
export function fromUnitVectors(out: Quat, v: Vec3Like, w: Vec3Like): Quat {
|
|
359
|
+
const vx = v[0] as number;
|
|
360
|
+
const vy = v[1] as number;
|
|
361
|
+
const vz = v[2] as number;
|
|
362
|
+
const wx = w[0] as number;
|
|
363
|
+
const wy = w[1] as number;
|
|
364
|
+
const wz = w[2] as number;
|
|
365
|
+
|
|
366
|
+
const d = vx * wx + vy * wy + vz * wz;
|
|
367
|
+
|
|
368
|
+
if (d > 1 - EPS_QUAT_PARALLEL) {
|
|
369
|
+
// same direction: identity
|
|
370
|
+
return identity(out);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (d < -1 + EPS_QUAT_PARALLEL) {
|
|
374
|
+
// opposite direction: pick a perpendicular axis (D-P18).
|
|
375
|
+
// Prefer (0, 1, 0); when v is collinear with (0, 1, 0) (|vy| ~ 1) fall back to (1, 0, 0).
|
|
376
|
+
let ax: number;
|
|
377
|
+
let ay: number;
|
|
378
|
+
let az: number;
|
|
379
|
+
if (Math.abs(vy) < 1 - EPS_QUAT_PARALLEL) {
|
|
380
|
+
// axis = normalize(cross(v, (0, 1, 0)))
|
|
381
|
+
ax = -vz;
|
|
382
|
+
ay = 0;
|
|
383
|
+
az = vx;
|
|
384
|
+
} else {
|
|
385
|
+
// v is collinear with (0, 1, 0) → use (1, 0, 0)
|
|
386
|
+
// axis = normalize(cross(v, (1, 0, 0)))
|
|
387
|
+
ax = 0;
|
|
388
|
+
ay = vz;
|
|
389
|
+
az = -vy;
|
|
390
|
+
}
|
|
391
|
+
const axLen = Math.sqrt(ax * ax + ay * ay + az * az);
|
|
392
|
+
if (axLen < EPS_NORMALIZE) {
|
|
393
|
+
// extreme safety net (theoretically unreachable): write identity, no throw
|
|
394
|
+
return identity(out);
|
|
395
|
+
}
|
|
396
|
+
const axInv = 1 / axLen;
|
|
397
|
+
out[0] = ax * axInv;
|
|
398
|
+
out[1] = ay * axInv;
|
|
399
|
+
out[2] = az * axInv;
|
|
400
|
+
out[3] = 0; // cos(PI/2) = 0
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// General case: half-vector method (numerically stable; consensus of wgpu-matrix / Three.js)
|
|
405
|
+
// q = (cross(v, w), 1 + dot(v, w)), then normalize
|
|
406
|
+
const cx = vy * wz - vz * wy;
|
|
407
|
+
const cy = vz * wx - vx * wz;
|
|
408
|
+
const cz = vx * wy - vy * wx;
|
|
409
|
+
const sw = 1 + d;
|
|
410
|
+
const len = Math.sqrt(cx * cx + cy * cy + cz * cz + sw * sw);
|
|
411
|
+
const inv = 1 / len;
|
|
412
|
+
out[0] = cx * inv;
|
|
413
|
+
out[1] = cy * inv;
|
|
414
|
+
out[2] = cz * inv;
|
|
415
|
+
out[3] = sw * inv;
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* out = a * b (Hamilton product). Returns out.
|
|
421
|
+
*
|
|
422
|
+
* Aliasing-safe: reads all 8 source elements into locals first.
|
|
423
|
+
*/
|
|
424
|
+
export function multiply(out: Quat, a: QuatLike, b: QuatLike): Quat {
|
|
425
|
+
const ax = a[0] as number;
|
|
426
|
+
const ay = a[1] as number;
|
|
427
|
+
const az = a[2] as number;
|
|
428
|
+
const aw = a[3] as number;
|
|
429
|
+
const bx = b[0] as number;
|
|
430
|
+
const by = b[1] as number;
|
|
431
|
+
const bz = b[2] as number;
|
|
432
|
+
const bw = b[3] as number;
|
|
433
|
+
out[0] = aw * bx + ax * bw + ay * bz - az * by;
|
|
434
|
+
out[1] = aw * by - ax * bz + ay * bw + az * bx;
|
|
435
|
+
out[2] = aw * bz + ax * by - ay * bx + az * bw;
|
|
436
|
+
out[3] = aw * bw - ax * bx - ay * by - az * bz;
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* out = the orientation `q` after rotating a further `angleRadians` about world-space `axis`,
|
|
442
|
+
* re-normalized. Returns out.
|
|
443
|
+
*
|
|
444
|
+
* This is the ergonomic *incremental rotate* helper — the per-frame spin/animation move. It folds
|
|
445
|
+
* the three steps every rotating demo otherwise hand-wires: build the delta quaternion
|
|
446
|
+
* (`fromAxisAngle`), **pre**-multiply it onto the current orientation (world-space axis, matching
|
|
447
|
+
* **Bevy `Transform::rotate(r)` = `r * self.rotation`** / `rotate_y(θ)`), and — the step that is
|
|
448
|
+
* silently omitted and makes the naive loop wrong — **normalize** to shed the floating-point error
|
|
449
|
+
* that accumulates over thousands of frames into a non-unit quaternion (skew / scale artefacts).
|
|
450
|
+
*
|
|
451
|
+
* Prefer this over hand-writing `quat.multiply(q, delta, q)` in an update system: that loop drifts,
|
|
452
|
+
* so demos work around it with an absolute-angle accumulator + `fromAxisAngle` (can't compose onto an
|
|
453
|
+
* existing orientation) or raw `sin/cos` quaternion literals. `rotateAxis` composes safely.
|
|
454
|
+
*
|
|
455
|
+
* Convenience composition (like `fromLookAt`), not a primitive: same result as
|
|
456
|
+
* `normalize(out, multiply(out, fromAxisAngle(tmp, axis, angleRadians), q))`, fused + aliasing-safe.
|
|
457
|
+
*
|
|
458
|
+
* @degrade `axis` zero-length → the delta is identity (degenerate registry #8), so out = normalize(q)
|
|
459
|
+
* (no rotation applied; no throw, consistent with the sibling helpers).
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```ts
|
|
463
|
+
* // in an Update system: spin a cube about +Y at `speed` rad/s using the frame delta
|
|
464
|
+
* const dt = world.getResource(Time).delta;
|
|
465
|
+
* const t = world.get(entity, Transform).unwrap();
|
|
466
|
+
* quat.rotateAxis(t.quat, t.quat, [0, 1, 0], speed * dt); // in-place accumulate, no drift
|
|
467
|
+
* world.set(entity, Transform, t);
|
|
468
|
+
* ```
|
|
469
|
+
*/
|
|
470
|
+
export function rotateAxis(out: Quat, q: QuatLike, axis: Vec3Like, angleRadians: number): Quat {
|
|
471
|
+
// Read q into locals first (aliasing-safe: rotateAxis(q, q, ...) is the common per-frame call).
|
|
472
|
+
const qx = q[0] as number;
|
|
473
|
+
const qy = q[1] as number;
|
|
474
|
+
const qz = q[2] as number;
|
|
475
|
+
const qw = q[3] as number;
|
|
476
|
+
|
|
477
|
+
// Delta quaternion for `angleRadians` about `axis` (fromAxisAngle inline; 0-axis → identity, #8).
|
|
478
|
+
const ax = axis[0] as number;
|
|
479
|
+
const ay = axis[1] as number;
|
|
480
|
+
const az = axis[2] as number;
|
|
481
|
+
const axisLen = Math.sqrt(ax * ax + ay * ay + az * az);
|
|
482
|
+
let dx = 0;
|
|
483
|
+
let dy = 0;
|
|
484
|
+
let dz = 0;
|
|
485
|
+
let dw = 1;
|
|
486
|
+
if (axisLen >= EPS_NORMALIZE) {
|
|
487
|
+
const half = angleRadians * 0.5;
|
|
488
|
+
const s = Math.sin(half) / axisLen;
|
|
489
|
+
dx = ax * s;
|
|
490
|
+
dy = ay * s;
|
|
491
|
+
dz = az * s;
|
|
492
|
+
dw = Math.cos(half);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Pre-multiply: out = delta * q (world-space axis; matches Bevy Transform::rotate order).
|
|
496
|
+
let rx = dw * qx + dx * qw + dy * qz - dz * qy;
|
|
497
|
+
let ry = dw * qy - dx * qz + dy * qw + dz * qx;
|
|
498
|
+
let rz = dw * qz + dx * qy - dy * qx + dz * qw;
|
|
499
|
+
let rw = dw * qw - dx * qx - dy * qy - dz * qz;
|
|
500
|
+
|
|
501
|
+
// Normalize to kill accumulation drift (the whole point of this helper).
|
|
502
|
+
const len = Math.sqrt(rx * rx + ry * ry + rz * rz + rw * rw);
|
|
503
|
+
if (len >= EPS_NORMALIZE) {
|
|
504
|
+
const inv = 1 / len;
|
|
505
|
+
rx *= inv;
|
|
506
|
+
ry *= inv;
|
|
507
|
+
rz *= inv;
|
|
508
|
+
rw *= inv;
|
|
509
|
+
}
|
|
510
|
+
out[0] = rx;
|
|
511
|
+
out[1] = ry;
|
|
512
|
+
out[2] = rz;
|
|
513
|
+
out[3] = rw;
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* out = spherical linear interpolation(a, b, t). Returns out.
|
|
519
|
+
*
|
|
520
|
+
* t is not clamped (extrapolation semantics matches vec.lerp).
|
|
521
|
+
*
|
|
522
|
+
* @degrade dot(a, b) < -EPS_SLERP_DOT_LIMIT (anti-parallel) → **negate b' = -b then slerp**
|
|
523
|
+
* (D-P6; q and -q represent the same rotation; registry #10).
|
|
524
|
+
* @degrade |dot(a, b)| > 1 - EPS_SLERP_DOT_LIMIT (endpoint coincidence or near-coincidence) →
|
|
525
|
+
* falls back to nlerp (avoids acos blow-up; registry #11).
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```ts
|
|
529
|
+
* quat.slerp(out, a, b, 0.5);
|
|
530
|
+
* // Anti-parallel (dot ≈ -1): caller does not need to pre-process; the negation fall-back
|
|
531
|
+
* // happens internally.
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
export function slerp(out: Quat, a: QuatLike, b: QuatLike, t: number): Quat {
|
|
535
|
+
const ax = a[0] as number;
|
|
536
|
+
const ay = a[1] as number;
|
|
537
|
+
const az = a[2] as number;
|
|
538
|
+
const aw = a[3] as number;
|
|
539
|
+
let bx = b[0] as number;
|
|
540
|
+
let by = b[1] as number;
|
|
541
|
+
let bz = b[2] as number;
|
|
542
|
+
let bw = b[3] as number;
|
|
543
|
+
|
|
544
|
+
let cosTheta = ax * bx + ay * by + az * bz + aw * bw;
|
|
545
|
+
|
|
546
|
+
// D-P6: anti-parallel → negate b then slerp (q and -q are the same rotation; shortest arc)
|
|
547
|
+
if (cosTheta < 0) {
|
|
548
|
+
bx = -bx;
|
|
549
|
+
by = -by;
|
|
550
|
+
bz = -bz;
|
|
551
|
+
bw = -bw;
|
|
552
|
+
cosTheta = -cosTheta;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Endpoint coincidence (|dot| → 1): nlerp fall-back avoids sin(0) division by zero
|
|
556
|
+
if (cosTheta > 1 - EPS_SLERP_DOT_LIMIT) {
|
|
557
|
+
return nlerp(out, a, b, t);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const theta = Math.acos(cosTheta);
|
|
561
|
+
const sinTheta = Math.sin(theta);
|
|
562
|
+
const wa = Math.sin((1 - t) * theta) / sinTheta;
|
|
563
|
+
const wb = Math.sin(t * theta) / sinTheta;
|
|
564
|
+
out[0] = wa * ax + wb * bx;
|
|
565
|
+
out[1] = wa * ay + wb * by;
|
|
566
|
+
out[2] = wa * az + wb * bz;
|
|
567
|
+
out[3] = wa * aw + wb * bw;
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* out = normalized lerp(a, b, t). Returns out.
|
|
573
|
+
*
|
|
574
|
+
* Faster than slerp but the angular velocity is non-uniform; use it as a slerp substitute near endpoints.
|
|
575
|
+
* Like slerp, automatically handles dot < 0 by negating b to keep the shortest arc.
|
|
576
|
+
*
|
|
577
|
+
* @degrade lerp result is near zero (very rare) → out = identity.
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* ```ts
|
|
581
|
+
* quat.nlerp(out, qA, qB, 0.5);
|
|
582
|
+
* // Very rare: lerp result lengthSq < EPS_NORMALIZE → out = (0, 0, 0, 1)
|
|
583
|
+
* ```
|
|
584
|
+
*/
|
|
585
|
+
export function nlerp(out: Quat, a: QuatLike, b: QuatLike, t: number): Quat {
|
|
586
|
+
const ax = a[0] as number;
|
|
587
|
+
const ay = a[1] as number;
|
|
588
|
+
const az = a[2] as number;
|
|
589
|
+
const aw = a[3] as number;
|
|
590
|
+
let bx = b[0] as number;
|
|
591
|
+
let by = b[1] as number;
|
|
592
|
+
let bz = b[2] as number;
|
|
593
|
+
let bw = b[3] as number;
|
|
594
|
+
|
|
595
|
+
// anti-parallel → negate b for the shortest arc (same convention as slerp)
|
|
596
|
+
const cosTheta = ax * bx + ay * by + az * bz + aw * bw;
|
|
597
|
+
if (cosTheta < 0) {
|
|
598
|
+
bx = -bx;
|
|
599
|
+
by = -by;
|
|
600
|
+
bz = -bz;
|
|
601
|
+
bw = -bw;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
out[0] = ax + t * (bx - ax);
|
|
605
|
+
out[1] = ay + t * (by - ay);
|
|
606
|
+
out[2] = az + t * (bz - az);
|
|
607
|
+
out[3] = aw + t * (bw - aw);
|
|
608
|
+
|
|
609
|
+
const lenSq =
|
|
610
|
+
(out[0] as number) * (out[0] as number) +
|
|
611
|
+
(out[1] as number) * (out[1] as number) +
|
|
612
|
+
(out[2] as number) * (out[2] as number) +
|
|
613
|
+
(out[3] as number) * (out[3] as number);
|
|
614
|
+
if (lenSq < EPS_NORMALIZE) {
|
|
615
|
+
return identity(out);
|
|
616
|
+
}
|
|
617
|
+
const inv = 1 / Math.sqrt(lenSq);
|
|
618
|
+
out[0] = (out[0] as number) * inv;
|
|
619
|
+
out[1] = (out[1] as number) * inv;
|
|
620
|
+
out[2] = (out[2] as number) * inv;
|
|
621
|
+
out[3] = (out[3] as number) * inv;
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* out = inverse of a (conjugate / lengthSq). Returns out.
|
|
627
|
+
*
|
|
628
|
+
* For unit quaternions, invert is equivalent to conjugate (and faster); this implementation uses
|
|
629
|
+
* the general a/|a|² form to support non-unit inputs.
|
|
630
|
+
*
|
|
631
|
+
* @degrade lengthSq(a) < EPS_NORMALIZE (zero quaternion) → out = identity (does not return NaN;
|
|
632
|
+
* same convention as mat invert).
|
|
633
|
+
*
|
|
634
|
+
* @example
|
|
635
|
+
* ```ts
|
|
636
|
+
* quat.invert(out, q);
|
|
637
|
+
* quat.invert(out, quat.create(0, 0, 0, 0)); // → out = identity (0, 0, 0, 1), AC-06 no throw
|
|
638
|
+
* ```
|
|
639
|
+
*/
|
|
640
|
+
export function invert(out: Quat, a: QuatLike): Quat {
|
|
641
|
+
const ax = a[0] as number;
|
|
642
|
+
const ay = a[1] as number;
|
|
643
|
+
const az = a[2] as number;
|
|
644
|
+
const aw = a[3] as number;
|
|
645
|
+
const lenSq = ax * ax + ay * ay + az * az + aw * aw;
|
|
646
|
+
if (lenSq < EPS_NORMALIZE) {
|
|
647
|
+
return identity(out);
|
|
648
|
+
}
|
|
649
|
+
const inv = 1 / lenSq;
|
|
650
|
+
out[0] = -ax * inv;
|
|
651
|
+
out[1] = -ay * inv;
|
|
652
|
+
out[2] = -az * inv;
|
|
653
|
+
out[3] = aw * inv;
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** out = conjugate quaternion [-x, -y, -z, w] (aliasing-safe). Returns out. */
|
|
658
|
+
export function conjugate(out: Quat, a: QuatLike): Quat {
|
|
659
|
+
out[0] = -(a[0] as number);
|
|
660
|
+
out[1] = -(a[1] as number);
|
|
661
|
+
out[2] = -(a[2] as number);
|
|
662
|
+
out[3] = a[3] as number;
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** dot(a, b) = ax*bx + ay*by + az*bz + aw*bw (returns scalar). */
|
|
667
|
+
export function dot(a: QuatLike, b: QuatLike): number {
|
|
668
|
+
return (
|
|
669
|
+
(a[0] as number) * (b[0] as number) +
|
|
670
|
+
(a[1] as number) * (b[1] as number) +
|
|
671
|
+
(a[2] as number) * (b[2] as number) +
|
|
672
|
+
(a[3] as number) * (b[3] as number)
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** sqrt(x² + y² + z² + w²). Delegates to lengthSq + Math.sqrt (canonical pattern; see clone-5-quat-audit.md). */
|
|
677
|
+
export function length(a: QuatLike): number {
|
|
678
|
+
return Math.sqrt(lengthSq(a));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** x² + y² + z² + w² (no sqrt). */
|
|
682
|
+
export function lengthSq(a: QuatLike): number {
|
|
683
|
+
return lengthSq4(a);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* out = q · v · q⁻¹ (rotate vec3 v by the unit quaternion q). Returns out.
|
|
688
|
+
*
|
|
689
|
+
* Rodrigues optimized form (research Finding 3, 18 mul + 12 add):
|
|
690
|
+
* t = 2 · cross(q.xyz, v)
|
|
691
|
+
* out = v + q.w · t + cross(q.xyz, t)
|
|
692
|
+
*
|
|
693
|
+
* 3-5× faster than going through mat4 (fromQuat → transformVec3); same shape as gl-matrix
|
|
694
|
+
* `vec3.transformQuat` / Three.js `Vector3.applyQuaternion` (industry consensus).
|
|
695
|
+
*
|
|
696
|
+
* Aliasing-safe: reads v.xyz / q.xyzw into locals before writing out.
|
|
697
|
+
*
|
|
698
|
+
* @degrade q must be a unit-length quaternion. A non-unit q introduces implicit scaling
|
|
699
|
+
* (D-4 silent convention; no throw; AI users always get unit-length q from
|
|
700
|
+
* quat.fromAxisAngle / fromEuler / normalize / slerp, so this branch is not hit).
|
|
701
|
+
* When q = (0,0,0,0), t = 0 → out = v (the natural result of the formula; matches
|
|
702
|
+
* gl-matrix / Three.js; non-NaN, no throw).
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```ts
|
|
706
|
+
* const q = quat.fromAxisAngle(quat.create(), [0, 1, 0], Math.PI / 2);
|
|
707
|
+
* const out = vec3.create();
|
|
708
|
+
* quat.transformVec3(out, q, [1, 0, 0]); // → out ≈ (0, 0, -1)
|
|
709
|
+
* // Guard: q derived from the quat surface is always unit-length; callers do not need to normalize.
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
export function transformVec3(out: Vec3, q: QuatLike, v: Vec3Like): Vec3 {
|
|
713
|
+
const qx = q[0] as number;
|
|
714
|
+
const qy = q[1] as number;
|
|
715
|
+
const qz = q[2] as number;
|
|
716
|
+
const qw = q[3] as number;
|
|
717
|
+
const vx = v[0] as number;
|
|
718
|
+
const vy = v[1] as number;
|
|
719
|
+
const vz = v[2] as number;
|
|
720
|
+
// t = 2 * cross(q.xyz, v)
|
|
721
|
+
const tx = 2 * (qy * vz - qz * vy);
|
|
722
|
+
const ty = 2 * (qz * vx - qx * vz);
|
|
723
|
+
const tz = 2 * (qx * vy - qy * vx);
|
|
724
|
+
// out = v + q.w * t + cross(q.xyz, t)
|
|
725
|
+
out[0] = vx + qw * tx + (qy * tz - qz * ty);
|
|
726
|
+
out[1] = vy + qw * ty + (qz * tx - qx * tz);
|
|
727
|
+
out[2] = vz + qw * tz + (qx * ty - qy * tx);
|
|
728
|
+
return out;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* out = a / length(a). Returns out.
|
|
733
|
+
*
|
|
734
|
+
* @degrade lengthSq(a) < EPS_NORMALIZE → out = [0, 0, 0, 0] (same convention as vec.normalize;
|
|
735
|
+
* does not write identity, preserving the "zero quaternion" semantics; if the caller
|
|
736
|
+
* needs identity it should use fromAxisAngle with 0-axis).
|
|
737
|
+
*
|
|
738
|
+
* @example
|
|
739
|
+
* ```ts
|
|
740
|
+
* quat.normalize(out, q);
|
|
741
|
+
* quat.normalize(out, quat.create(0, 0, 0, 0)); // → out = (0, 0, 0, 0); zero-quaternion semantics preserved
|
|
742
|
+
* ```
|
|
743
|
+
*/
|
|
744
|
+
export function normalize(out: Quat, a: QuatLike): Quat {
|
|
745
|
+
normalize4(out, a, EPS_NORMALIZE);
|
|
746
|
+
return out;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Convenience: quaternion representing rotation `theta` radians around the
|
|
751
|
+
* Y axis. Equivalent to `quat.fromEuler(out, 0, theta, 0, 'YXZ')` but
|
|
752
|
+
* allocates its own output `Quat`.
|
|
753
|
+
*
|
|
754
|
+
* Formula: `(0, sin(theta/2), 0, cos(theta/2))`.
|
|
755
|
+
*
|
|
756
|
+
* @degrade eulerY(0) → identity, eulerY(2pi) → identity within epsilon.
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* ```ts
|
|
760
|
+
* const yaw45 = quat.eulerY(Math.PI / 4);
|
|
761
|
+
* // yaw45 ≈ [0, 0.3826834, 0, 0.9238795]
|
|
762
|
+
* ```
|
|
763
|
+
*
|
|
764
|
+
* @example Equivalent to the verbose form:
|
|
765
|
+
* ```ts
|
|
766
|
+
* const q1 = quat.eulerY(theta);
|
|
767
|
+
* const q2 = quat.fromEuler(quat.create(), 0, theta, 0, 'YXZ');
|
|
768
|
+
* // q1 === q2 (within epsilon)
|
|
769
|
+
* ```
|
|
770
|
+
*/
|
|
771
|
+
export function eulerY(theta: number): Quat {
|
|
772
|
+
const out = create();
|
|
773
|
+
return fromEuler(out, 0, theta, 0, 'YXZ');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ── Local basis accessors ─────────────────────────────────────────────────
|
|
777
|
+
//
|
|
778
|
+
// A rotation's three local basis vectors — the world-space directions its own
|
|
779
|
+
// +X / +Y / −Z axes point after the rotation. They are exactly `q` applied to
|
|
780
|
+
// the canonical axes: right = q·(1,0,0), up = q·(0,1,0), forward = q·(0,0,−1).
|
|
781
|
+
//
|
|
782
|
+
// The −Z forward convention matches `mat4.getForward` (RL-4) and the look
|
|
783
|
+
// convention `fromLookAt` / `computeViewProj` use, so a camera/listener built
|
|
784
|
+
// with `fromLookAt(eye, target)` has `forward(q)` ≈ normalize(target − eye).
|
|
785
|
+
// These fold the transformVec3-with-a-magic-axis idiom (and the handedness a
|
|
786
|
+
// caller would otherwise have to know) into a named accessor, mirroring the
|
|
787
|
+
// mat4 getters so learning one basis form applies to both. Because `q` from the
|
|
788
|
+
// quat surface is always unit-length, `transformVec3` returns a unit vector — no
|
|
789
|
+
// separate normalize step (unlike the mat4 getters, which read possibly-scaled
|
|
790
|
+
// basis columns and must normalize).
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Local right axis: the world-space direction the rotation's own +X axis points.
|
|
794
|
+
* `right(out, q) = quat.transformVec3(out, q, [1, 0, 0])`. Mirrors
|
|
795
|
+
* `mat4.getRight`; matches Bevy `Transform::local_x` / `Transform::right`.
|
|
796
|
+
*
|
|
797
|
+
* @degrade q must be unit-length (guaranteed by the quat surface); a unit q
|
|
798
|
+
* yields a unit result. q = (0,0,0,0) → out = (1,0,0) (the natural
|
|
799
|
+
* transformVec3 result; non-NaN, no throw).
|
|
800
|
+
*
|
|
801
|
+
* @example
|
|
802
|
+
* ```ts
|
|
803
|
+
* const q = quat.eulerY(Math.PI / 2); // 90° about +Y
|
|
804
|
+
* quat.right(vec3.create(), q); // → (0, 0, -1): +X yawed a quarter-turn
|
|
805
|
+
* ```
|
|
806
|
+
*/
|
|
807
|
+
export function right(out: Vec3, q: QuatLike): Vec3 {
|
|
808
|
+
return transformVec3(out, q, UNIT_X);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Local up axis: the world-space direction the rotation's own +Y axis points.
|
|
813
|
+
* `up(out, q) = quat.transformVec3(out, q, [0, 1, 0])`. Mirrors `mat4.getUp`;
|
|
814
|
+
* matches Bevy `Transform::local_y` / `Transform::up`.
|
|
815
|
+
*
|
|
816
|
+
* @degrade q must be unit-length (guaranteed by the quat surface); a unit q
|
|
817
|
+
* yields a unit result. q = (0,0,0,0) → out = (0,1,0).
|
|
818
|
+
*
|
|
819
|
+
* @example
|
|
820
|
+
* ```ts
|
|
821
|
+
* const q = quat.fromAxisAngle(quat.create(), [1, 0, 0], Math.PI / 2); // pitch 90°
|
|
822
|
+
* quat.up(vec3.create(), q); // → (0, 0, 1): +Y pitched onto +Z
|
|
823
|
+
* ```
|
|
824
|
+
*/
|
|
825
|
+
export function up(out: Vec3, q: QuatLike): Vec3 {
|
|
826
|
+
return transformVec3(out, q, UNIT_Y);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Local forward axis: the world-space direction the rotation's own −Z axis
|
|
831
|
+
* points (−Z look convention, RL-4). `forward(out, q) =
|
|
832
|
+
* quat.transformVec3(out, q, [0, 0, -1])`. Mirrors `mat4.getForward`; matches
|
|
833
|
+
* Bevy `Transform::forward` (Bevy also uses −Z). A quat from `fromLookAt(eye,
|
|
834
|
+
* target, up)` has `forward(q)` ≈ normalize(target − eye).
|
|
835
|
+
*
|
|
836
|
+
* @degrade q must be unit-length (guaranteed by the quat surface); a unit q
|
|
837
|
+
* yields a unit result. q = (0,0,0,0) → out = (0,0,-1).
|
|
838
|
+
*
|
|
839
|
+
* @example
|
|
840
|
+
* ```ts
|
|
841
|
+
* const q = quat.eulerY(Math.PI / 2); // 90° about +Y
|
|
842
|
+
* quat.forward(vec3.create(), q); // → (-1, 0, 0): -Z yawed a quarter-turn
|
|
843
|
+
* ```
|
|
844
|
+
*/
|
|
845
|
+
export function forward(out: Vec3, q: QuatLike): Vec3 {
|
|
846
|
+
return transformVec3(out, q, UNIT_NEG_Z);
|
|
847
|
+
}
|