@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.
Files changed (150) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +294 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/_arbs.d.ts +36 -0
  5. package/dist/__tests__/_arbs.d.ts.map +1 -0
  6. package/dist/__tests__/_fixtures.d.ts +61 -0
  7. package/dist/__tests__/_fixtures.d.ts.map +1 -0
  8. package/dist/__tests__/bounds2.test.d.ts +2 -0
  9. package/dist/__tests__/bounds2.test.d.ts.map +1 -0
  10. package/dist/__tests__/box3.test.d.ts +2 -0
  11. package/dist/__tests__/box3.test.d.ts.map +1 -0
  12. package/dist/__tests__/easing.test.d.ts +2 -0
  13. package/dist/__tests__/easing.test.d.ts.map +1 -0
  14. package/dist/__tests__/euler.test-d.d.ts +2 -0
  15. package/dist/__tests__/euler.test-d.d.ts.map +1 -0
  16. package/dist/__tests__/mat3.test-d.d.ts +2 -0
  17. package/dist/__tests__/mat3.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/mat4.property.test.d.ts +2 -0
  19. package/dist/__tests__/mat4.property.test.d.ts.map +1 -0
  20. package/dist/__tests__/mat4.test-d.d.ts +2 -0
  21. package/dist/__tests__/mat4.test-d.d.ts.map +1 -0
  22. package/dist/__tests__/mat4.test.d.ts +2 -0
  23. package/dist/__tests__/mat4.test.d.ts.map +1 -0
  24. package/dist/__tests__/noise.test.d.ts +2 -0
  25. package/dist/__tests__/noise.test.d.ts.map +1 -0
  26. package/dist/__tests__/quat.basis.test.d.ts +2 -0
  27. package/dist/__tests__/quat.basis.test.d.ts.map +1 -0
  28. package/dist/__tests__/quat.interpolation.test.d.ts +2 -0
  29. package/dist/__tests__/quat.interpolation.test.d.ts.map +1 -0
  30. package/dist/__tests__/quat.lookat.test.d.ts +2 -0
  31. package/dist/__tests__/quat.lookat.test.d.ts.map +1 -0
  32. package/dist/__tests__/quat.property.test.d.ts +2 -0
  33. package/dist/__tests__/quat.property.test.d.ts.map +1 -0
  34. package/dist/__tests__/quat.rotateaxis.test.d.ts +2 -0
  35. package/dist/__tests__/quat.rotateaxis.test.d.ts.map +1 -0
  36. package/dist/__tests__/quat.test-d.d.ts +2 -0
  37. package/dist/__tests__/quat.test-d.d.ts.map +1 -0
  38. package/dist/__tests__/ray.property.test.d.ts +2 -0
  39. package/dist/__tests__/ray.property.test.d.ts.map +1 -0
  40. package/dist/__tests__/ray.test.d.ts +2 -0
  41. package/dist/__tests__/ray.test.d.ts.map +1 -0
  42. package/dist/__tests__/types.test-d.d.ts +2 -0
  43. package/dist/__tests__/types.test-d.d.ts.map +1 -0
  44. package/dist/__tests__/vec-catmull-rom.test.d.ts +2 -0
  45. package/dist/__tests__/vec-catmull-rom.test.d.ts.map +1 -0
  46. package/dist/__tests__/vec-smooth-damp.test.d.ts +2 -0
  47. package/dist/__tests__/vec-smooth-damp.test.d.ts.map +1 -0
  48. package/dist/__tests__/vec2.test-d.d.ts +2 -0
  49. package/dist/__tests__/vec2.test-d.d.ts.map +1 -0
  50. package/dist/__tests__/vec3.property.test.d.ts +2 -0
  51. package/dist/__tests__/vec3.property.test.d.ts.map +1 -0
  52. package/dist/__tests__/vec3.test-d.d.ts +2 -0
  53. package/dist/__tests__/vec3.test-d.d.ts.map +1 -0
  54. package/dist/__tests__/vec4.test-d.d.ts +2 -0
  55. package/dist/__tests__/vec4.test-d.d.ts.map +1 -0
  56. package/dist/_internal/epsilon.d.ts +9 -0
  57. package/dist/_internal/epsilon.d.ts.map +1 -0
  58. package/dist/_internal/scalar.d.ts +40 -0
  59. package/dist/_internal/scalar.d.ts.map +1 -0
  60. package/dist/box2.d.ts +35 -0
  61. package/dist/box2.d.ts.map +1 -0
  62. package/dist/box3.d.ts +76 -0
  63. package/dist/box3.d.ts.map +1 -0
  64. package/dist/circle2.d.ts +29 -0
  65. package/dist/circle2.d.ts.map +1 -0
  66. package/dist/color.d.ts +64 -0
  67. package/dist/color.d.ts.map +1 -0
  68. package/dist/easing.d.ts +18 -0
  69. package/dist/easing.d.ts.map +1 -0
  70. package/dist/euler.d.ts +51 -0
  71. package/dist/euler.d.ts.map +1 -0
  72. package/dist/f32-to-f16-bytes.d.ts +10 -0
  73. package/dist/f32-to-f16-bytes.d.ts.map +1 -0
  74. package/dist/frustum.d.ts +49 -0
  75. package/dist/frustum.d.ts.map +1 -0
  76. package/dist/index.d.ts +20 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.mjs +3688 -0
  79. package/dist/index.mjs.map +1 -0
  80. package/dist/mat3.d.ts +70 -0
  81. package/dist/mat3.d.ts.map +1 -0
  82. package/dist/mat4.d.ts +329 -0
  83. package/dist/mat4.d.ts.map +1 -0
  84. package/dist/noise.d.ts +12 -0
  85. package/dist/noise.d.ts.map +1 -0
  86. package/dist/quat.d.ts +312 -0
  87. package/dist/quat.d.ts.map +1 -0
  88. package/dist/ray.d.ts +144 -0
  89. package/dist/ray.d.ts.map +1 -0
  90. package/dist/ray2.d.ts +38 -0
  91. package/dist/ray2.d.ts.map +1 -0
  92. package/dist/sphere.d.ts +41 -0
  93. package/dist/sphere.d.ts.map +1 -0
  94. package/dist/types.d.ts +57 -0
  95. package/dist/types.d.ts.map +1 -0
  96. package/dist/vec2.d.ts +76 -0
  97. package/dist/vec2.d.ts.map +1 -0
  98. package/dist/vec3.d.ts +82 -0
  99. package/dist/vec3.d.ts.map +1 -0
  100. package/dist/vec4.d.ts +61 -0
  101. package/dist/vec4.d.ts.map +1 -0
  102. package/package.json +57 -0
  103. package/src/__tests__/_arbs.ts +149 -0
  104. package/src/__tests__/_fixtures.ts +118 -0
  105. package/src/__tests__/bounds2.test.ts +146 -0
  106. package/src/__tests__/box3.test.ts +277 -0
  107. package/src/__tests__/easing.test.ts +109 -0
  108. package/src/__tests__/euler.test-d.ts +63 -0
  109. package/src/__tests__/mat3.test-d.ts +47 -0
  110. package/src/__tests__/mat4.property.test.ts +256 -0
  111. package/src/__tests__/mat4.test-d.ts +130 -0
  112. package/src/__tests__/mat4.test.ts +162 -0
  113. package/src/__tests__/noise.test.ts +75 -0
  114. package/src/__tests__/quat.basis.test.ts +134 -0
  115. package/src/__tests__/quat.interpolation.test.ts +45 -0
  116. package/src/__tests__/quat.lookat.test.ts +103 -0
  117. package/src/__tests__/quat.property.test.ts +150 -0
  118. package/src/__tests__/quat.rotateaxis.test.ts +149 -0
  119. package/src/__tests__/quat.test-d.ts +138 -0
  120. package/src/__tests__/ray.property.test.ts +105 -0
  121. package/src/__tests__/ray.test.ts +539 -0
  122. package/src/__tests__/types.test-d.ts +64 -0
  123. package/src/__tests__/vec-catmull-rom.test.ts +125 -0
  124. package/src/__tests__/vec-smooth-damp.test.ts +167 -0
  125. package/src/__tests__/vec2.test-d.ts +59 -0
  126. package/src/__tests__/vec3.property.test.ts +72 -0
  127. package/src/__tests__/vec3.test-d.ts +65 -0
  128. package/src/__tests__/vec4.test-d.ts +61 -0
  129. package/src/_internal/epsilon.ts +29 -0
  130. package/src/_internal/scalar.ts +90 -0
  131. package/src/box2.ts +168 -0
  132. package/src/box3.ts +372 -0
  133. package/src/circle2.ts +134 -0
  134. package/src/color.ts +177 -0
  135. package/src/easing.ts +49 -0
  136. package/src/euler.ts +239 -0
  137. package/src/f32-to-f16-bytes.ts +71 -0
  138. package/src/frustum.ts +240 -0
  139. package/src/index.ts +65 -0
  140. package/src/mat3.ts +286 -0
  141. package/src/mat4.ts +1334 -0
  142. package/src/noise.ts +78 -0
  143. package/src/quat.ts +847 -0
  144. package/src/ray.ts +575 -0
  145. package/src/ray2.ts +198 -0
  146. package/src/sphere.ts +138 -0
  147. package/src/types.ts +78 -0
  148. package/src/vec2.ts +229 -0
  149. package/src/vec3.ts +294 -0
  150. package/src/vec4.ts +235 -0
package/src/mat4.ts ADDED
@@ -0,0 +1,1334 @@
1
+ // mat4.ts — 4x4 matrix namespace (M3 / T-021 base + T-022 projection)
2
+ //
3
+ // Base functions (T-021): create / clone / identity / equals / multiply / transpose /
4
+ // invert / scale / translate / rotate / lookAt / compose / decompose / fromQuat /
5
+ // fromTranslation / fromScaling / fromRotation
6
+ // Projection functions (T-022): perspective / perspectiveNO / perspectiveReverseZ /
7
+ // orthographic / orthographicNO / orthographicReverseZ
8
+ //
9
+ // Surface: 23+ functions (≥ 22 lower bound).
10
+ //
11
+ // Memory layout: column-major 16 floats (compatible with WebGL/WebGPU shader uniforms);
12
+ // index mapping m[col*4 + row]. translation lives in col 3 → indices 12, 13, 14.
13
+ //
14
+ // Naming convention (D-3):
15
+ // - perspective / orthographic = WebGPU [0, 1] NDC (short names go to the primary target)
16
+ // - perspectiveNO / orthographicNO = WebGL/OpenGL [-1, 1] NDC
17
+ // - perspectiveReverseZ / orthographicReverseZ = reversed-Z (far→0, near→1)
18
+ //
19
+ // Degenerate convention:
20
+ // - invert(singular) → out = identity (D-P1; returns out, not null)
21
+ // - lookAt(eye=target) → out = identity (D-P17; same convention as D-P1)
22
+ // - perspective(near>=far) / fovy<=0 / aspect<=0 → numerically undefined but does not throw
23
+ //
24
+ // Four ironclad rules (gl-matrix wiki / research §F1):
25
+ // 1. Out-param first; 2. Aliasing-safe; 3. Module-as-namespace; 4. Float32Array by default.
26
+ //
27
+ // Related: requirements §Surface mat4 lower bound 22 + AC-04 three projection tiers complete +
28
+ // AC-05 reversed-Z fixture + AC-06 never raises (silent degrade) + AC-08 invert returns out, not null;
29
+ // plan-strategy §6 M3 + D-P1/D-P3/D-P17 + R-P1 aliasing-singular;
30
+ // wiki/wgpu-matrix-overview.md / gl-matrix-overview.md / reversed-z-projection.md.
31
+ //
32
+ // Degenerate-semantics registry (plan-strategy.md §appendix A numbering; mat portion has 5 of the
33
+ // AC-07 ≥ 8 hard lower-bound entries):
34
+ // #3 mat4.invert(singular) → out = identity (D-P1)
35
+ // #4 mat4.lookAt(eye=target) → out = identity (D-P17)
36
+ // #5 mat4.lookAt(up // forward) → auto-select alternative up
37
+ // #6 mat4.decompose(m with shear) → silent best-effort decomposition
38
+ // #7 mat4.perspective(near>=far) etc. → numerically undefined but does not throw
39
+
40
+ import { EPS_DET, EPS_NORMALIZE } from './_internal/epsilon';
41
+ import type { Mat4, Mat4Like, Quat, QuatLike, Vec3, Vec3Like } from './types';
42
+ import * as vec3 from './vec3';
43
+
44
+ export type { Mat4, Mat4Like };
45
+
46
+ /** Create a Mat4 (default all zero; callers usually call identity() right after). */
47
+ export function create(): Mat4 {
48
+ return new Float32Array(16) as Mat4;
49
+ }
50
+
51
+ /** Allocate a new Mat4 copy. */
52
+ export function clone(a: Mat4Like): Mat4 {
53
+ const r = new Float32Array(16) as Mat4;
54
+ for (let i = 0; i < 16; i++) r[i] = a[i] as number;
55
+ return r;
56
+ }
57
+
58
+ /** out = 4x4 identity. Returns out. */
59
+ export function identity(out: Mat4): Mat4 {
60
+ out[0] = 1;
61
+ out[1] = 0;
62
+ out[2] = 0;
63
+ out[3] = 0;
64
+ out[4] = 0;
65
+ out[5] = 1;
66
+ out[6] = 0;
67
+ out[7] = 0;
68
+ out[8] = 0;
69
+ out[9] = 0;
70
+ out[10] = 1;
71
+ out[11] = 0;
72
+ out[12] = 0;
73
+ out[13] = 0;
74
+ out[14] = 0;
75
+ out[15] = 1;
76
+ return out;
77
+ }
78
+
79
+ /** Approximate equality: each element differs by ≤ epsilon. NaN inputs always return false. */
80
+ export function equals(a: Mat4Like, b: Mat4Like, epsilon = 1e-6): boolean {
81
+ for (let i = 0; i < 16; i++) {
82
+ const av = a[i] as number;
83
+ const bv = b[i] as number;
84
+ if (Number.isNaN(av) || Number.isNaN(bv)) return false;
85
+ if (Math.abs(av - bv) > epsilon) return false;
86
+ }
87
+ return true;
88
+ }
89
+
90
+ /**
91
+ * out = a * b (column-major matrix multiply). Returns out.
92
+ *
93
+ * Aliasing-safe: out may equal a or b; reads all 32 source elements into locals first.
94
+ */
95
+ export function multiply(out: Mat4, a: Mat4Like, b: Mat4Like): Mat4 {
96
+ const a00 = a[0] as number;
97
+ const a01 = a[1] as number;
98
+ const a02 = a[2] as number;
99
+ const a03 = a[3] as number;
100
+ const a10 = a[4] as number;
101
+ const a11 = a[5] as number;
102
+ const a12 = a[6] as number;
103
+ const a13 = a[7] as number;
104
+ const a20 = a[8] as number;
105
+ const a21 = a[9] as number;
106
+ const a22 = a[10] as number;
107
+ const a23 = a[11] as number;
108
+ const a30 = a[12] as number;
109
+ const a31 = a[13] as number;
110
+ const a32 = a[14] as number;
111
+ const a33 = a[15] as number;
112
+ const b00 = b[0] as number;
113
+ const b01 = b[1] as number;
114
+ const b02 = b[2] as number;
115
+ const b03 = b[3] as number;
116
+ const b10 = b[4] as number;
117
+ const b11 = b[5] as number;
118
+ const b12 = b[6] as number;
119
+ const b13 = b[7] as number;
120
+ const b20 = b[8] as number;
121
+ const b21 = b[9] as number;
122
+ const b22 = b[10] as number;
123
+ const b23 = b[11] as number;
124
+ const b30 = b[12] as number;
125
+ const b31 = b[13] as number;
126
+ const b32 = b[14] as number;
127
+ const b33 = b[15] as number;
128
+ out[0] = a00 * b00 + a10 * b01 + a20 * b02 + a30 * b03;
129
+ out[1] = a01 * b00 + a11 * b01 + a21 * b02 + a31 * b03;
130
+ out[2] = a02 * b00 + a12 * b01 + a22 * b02 + a32 * b03;
131
+ out[3] = a03 * b00 + a13 * b01 + a23 * b02 + a33 * b03;
132
+ out[4] = a00 * b10 + a10 * b11 + a20 * b12 + a30 * b13;
133
+ out[5] = a01 * b10 + a11 * b11 + a21 * b12 + a31 * b13;
134
+ out[6] = a02 * b10 + a12 * b11 + a22 * b12 + a32 * b13;
135
+ out[7] = a03 * b10 + a13 * b11 + a23 * b12 + a33 * b13;
136
+ out[8] = a00 * b20 + a10 * b21 + a20 * b22 + a30 * b23;
137
+ out[9] = a01 * b20 + a11 * b21 + a21 * b22 + a31 * b23;
138
+ out[10] = a02 * b20 + a12 * b21 + a22 * b22 + a32 * b23;
139
+ out[11] = a03 * b20 + a13 * b21 + a23 * b22 + a33 * b23;
140
+ out[12] = a00 * b30 + a10 * b31 + a20 * b32 + a30 * b33;
141
+ out[13] = a01 * b30 + a11 * b31 + a21 * b32 + a31 * b33;
142
+ out[14] = a02 * b30 + a12 * b31 + a22 * b32 + a32 * b33;
143
+ out[15] = a03 * b30 + a13 * b31 + a23 * b32 + a33 * b33;
144
+ return out;
145
+ }
146
+
147
+ /** out = transpose(a). Returns out. Aliasing-safe (transpose(m, m) is legal). */
148
+ export function transpose(out: Mat4, a: Mat4Like): Mat4 {
149
+ const a01 = a[1] as number;
150
+ const a02 = a[2] as number;
151
+ const a03 = a[3] as number;
152
+ const a12 = a[6] as number;
153
+ const a13 = a[7] as number;
154
+ const a23 = a[11] as number;
155
+ const a10 = a[4] as number;
156
+ const a20 = a[8] as number;
157
+ const a30 = a[12] as number;
158
+ const a21 = a[9] as number;
159
+ const a31 = a[13] as number;
160
+ const a32 = a[14] as number;
161
+ out[0] = a[0] as number;
162
+ out[1] = a10;
163
+ out[2] = a20;
164
+ out[3] = a30;
165
+ out[4] = a01;
166
+ out[5] = a[5] as number;
167
+ out[6] = a21;
168
+ out[7] = a31;
169
+ out[8] = a02;
170
+ out[9] = a12;
171
+ out[10] = a[10] as number;
172
+ out[11] = a32;
173
+ out[12] = a03;
174
+ out[13] = a13;
175
+ out[14] = a23;
176
+ out[15] = a[15] as number;
177
+ return out;
178
+ }
179
+
180
+ /**
181
+ * out = invert(a). Returns out.
182
+ *
183
+ * @degrade a singular (|det| < EPS_DET) → out = identity (D-P1; AC-08: returns out, not null).
184
+ * @degrade aliasing invert(out, out) where out is singular → reads 16 elements into locals before
185
+ * falling back; R-P1 pins the behavior so that out is overwritten with identity.
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * import { mat4 } from '@forgeax/engine-math';
190
+ *
191
+ * const inv = mat4.invert(mat4.create(), m);
192
+ * // If m is singular, inv === identity (no NaN, no null); callers may keep using it.
193
+ * // For explicit diagnostics, the caller builds its own guard (the library never console.warns):
194
+ * // const EPS = 1e-12;
195
+ * // if (mat4.equals(inv, mat4.identity(mat4.create()))) console.warn('mat4.invert: singular input');
196
+ * ```
197
+ */
198
+ export function invert(out: Mat4, a: Mat4Like): Mat4 {
199
+ const a00 = a[0] as number;
200
+ const a01 = a[1] as number;
201
+ const a02 = a[2] as number;
202
+ const a03 = a[3] as number;
203
+ const a10 = a[4] as number;
204
+ const a11 = a[5] as number;
205
+ const a12 = a[6] as number;
206
+ const a13 = a[7] as number;
207
+ const a20 = a[8] as number;
208
+ const a21 = a[9] as number;
209
+ const a22 = a[10] as number;
210
+ const a23 = a[11] as number;
211
+ const a30 = a[12] as number;
212
+ const a31 = a[13] as number;
213
+ const a32 = a[14] as number;
214
+ const a33 = a[15] as number;
215
+
216
+ const b00 = a00 * a11 - a01 * a10;
217
+ const b01 = a00 * a12 - a02 * a10;
218
+ const b02 = a00 * a13 - a03 * a10;
219
+ const b03 = a01 * a12 - a02 * a11;
220
+ const b04 = a01 * a13 - a03 * a11;
221
+ const b05 = a02 * a13 - a03 * a12;
222
+ const b06 = a20 * a31 - a21 * a30;
223
+ const b07 = a20 * a32 - a22 * a30;
224
+ const b08 = a20 * a33 - a23 * a30;
225
+ const b09 = a21 * a32 - a22 * a31;
226
+ const b10 = a21 * a33 - a23 * a31;
227
+ const b11 = a22 * a33 - a23 * a32;
228
+
229
+ const det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
230
+
231
+ if (Math.abs(det) < EPS_DET) {
232
+ return identity(out);
233
+ }
234
+
235
+ const invDet = 1 / det;
236
+ out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet;
237
+ out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * invDet;
238
+ out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * invDet;
239
+ out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * invDet;
240
+ out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * invDet;
241
+ out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * invDet;
242
+ out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * invDet;
243
+ out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * invDet;
244
+ out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * invDet;
245
+ out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * invDet;
246
+ out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * invDet;
247
+ out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * invDet;
248
+ out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * invDet;
249
+ out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * invDet;
250
+ out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * invDet;
251
+ out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * invDet;
252
+ return out;
253
+ }
254
+
255
+ /**
256
+ * out = a * Scale(v). Returns out. Aliasing-safe (reads the source diagonal columns into locals first).
257
+ */
258
+ export function scale(out: Mat4, a: Mat4Like, v: Vec3Like): Mat4 {
259
+ const x = v[0] as number;
260
+ const y = v[1] as number;
261
+ const z = v[2] as number;
262
+ out[0] = (a[0] as number) * x;
263
+ out[1] = (a[1] as number) * x;
264
+ out[2] = (a[2] as number) * x;
265
+ out[3] = (a[3] as number) * x;
266
+ out[4] = (a[4] as number) * y;
267
+ out[5] = (a[5] as number) * y;
268
+ out[6] = (a[6] as number) * y;
269
+ out[7] = (a[7] as number) * y;
270
+ out[8] = (a[8] as number) * z;
271
+ out[9] = (a[9] as number) * z;
272
+ out[10] = (a[10] as number) * z;
273
+ out[11] = (a[11] as number) * z;
274
+ out[12] = a[12] as number;
275
+ out[13] = a[13] as number;
276
+ out[14] = a[14] as number;
277
+ out[15] = a[15] as number;
278
+ return out;
279
+ }
280
+
281
+ /**
282
+ * out = a * Translate(v) (translate by v in a's local coordinates). Returns out. Aliasing-safe.
283
+ */
284
+ export function translate(out: Mat4, a: Mat4Like, v: Vec3Like): Mat4 {
285
+ const x = v[0] as number;
286
+ const y = v[1] as number;
287
+ const z = v[2] as number;
288
+ if (a === out) {
289
+ out[12] =
290
+ (a[0] as number) * x + (a[4] as number) * y + (a[8] as number) * z + (a[12] as number);
291
+ out[13] =
292
+ (a[1] as number) * x + (a[5] as number) * y + (a[9] as number) * z + (a[13] as number);
293
+ out[14] =
294
+ (a[2] as number) * x + (a[6] as number) * y + (a[10] as number) * z + (a[14] as number);
295
+ out[15] =
296
+ (a[3] as number) * x + (a[7] as number) * y + (a[11] as number) * z + (a[15] as number);
297
+ return out;
298
+ }
299
+ const a00 = a[0] as number;
300
+ const a01 = a[1] as number;
301
+ const a02 = a[2] as number;
302
+ const a03 = a[3] as number;
303
+ const a10 = a[4] as number;
304
+ const a11 = a[5] as number;
305
+ const a12 = a[6] as number;
306
+ const a13 = a[7] as number;
307
+ const a20 = a[8] as number;
308
+ const a21 = a[9] as number;
309
+ const a22 = a[10] as number;
310
+ const a23 = a[11] as number;
311
+ out[0] = a00;
312
+ out[1] = a01;
313
+ out[2] = a02;
314
+ out[3] = a03;
315
+ out[4] = a10;
316
+ out[5] = a11;
317
+ out[6] = a12;
318
+ out[7] = a13;
319
+ out[8] = a20;
320
+ out[9] = a21;
321
+ out[10] = a22;
322
+ out[11] = a23;
323
+ out[12] = a00 * x + a10 * y + a20 * z + (a[12] as number);
324
+ out[13] = a01 * x + a11 * y + a21 * z + (a[13] as number);
325
+ out[14] = a02 * x + a12 * y + a22 * z + (a[14] as number);
326
+ out[15] = a03 * x + a13 * y + a23 * z + (a[15] as number);
327
+ return out;
328
+ }
329
+
330
+ /**
331
+ * out = a * Rotate(axis, rad) (build the rotation via the Rodrigues formula then right-multiply a).
332
+ * Returns out.
333
+ *
334
+ * @degrade axis is the zero vector (lengthSq < EPS_NORMALIZE) → out = a (no rotation; identity behavior).
335
+ *
336
+ * @example
337
+ * ```ts
338
+ * mat4.rotate(out, mat4.identity(mat4.create()), [0, 1, 0], Math.PI / 2);
339
+ * // Guard: if (vec3.lengthSq(axis) < EPS_NORMALIZE) skip;
340
+ * ```
341
+ */
342
+ export function rotate(out: Mat4, a: Mat4Like, axis: Vec3Like, rad: number): Mat4 {
343
+ let x = axis[0] as number;
344
+ let y = axis[1] as number;
345
+ let z = axis[2] as number;
346
+ const lenSq = x * x + y * y + z * z;
347
+ if (lenSq < EPS_NORMALIZE) {
348
+ // zero-axis degenerate: copy a to out (no rotation)
349
+ if (out !== a) {
350
+ for (let i = 0; i < 16; i++) out[i] = a[i] as number;
351
+ }
352
+ return out;
353
+ }
354
+ const invLen = 1 / Math.sqrt(lenSq);
355
+ x *= invLen;
356
+ y *= invLen;
357
+ z *= invLen;
358
+ const s = Math.sin(rad);
359
+ const c = Math.cos(rad);
360
+ const t = 1 - c;
361
+
362
+ // rotation matrix R column-major (3x3 embedded into the upper-left of 4x4)
363
+ const r00 = x * x * t + c;
364
+ const r01 = y * x * t + z * s;
365
+ const r02 = z * x * t - y * s;
366
+ const r10 = x * y * t - z * s;
367
+ const r11 = y * y * t + c;
368
+ const r12 = z * y * t + x * s;
369
+ const r20 = x * z * t + y * s;
370
+ const r21 = y * z * t - x * s;
371
+ const r22 = z * z * t + c;
372
+
373
+ // out = a * R (read a then write out; aliasing-safe)
374
+ const a00 = a[0] as number;
375
+ const a01 = a[1] as number;
376
+ const a02 = a[2] as number;
377
+ const a03 = a[3] as number;
378
+ const a10 = a[4] as number;
379
+ const a11 = a[5] as number;
380
+ const a12 = a[6] as number;
381
+ const a13 = a[7] as number;
382
+ const a20 = a[8] as number;
383
+ const a21 = a[9] as number;
384
+ const a22 = a[10] as number;
385
+ const a23 = a[11] as number;
386
+
387
+ out[0] = a00 * r00 + a10 * r01 + a20 * r02;
388
+ out[1] = a01 * r00 + a11 * r01 + a21 * r02;
389
+ out[2] = a02 * r00 + a12 * r01 + a22 * r02;
390
+ out[3] = a03 * r00 + a13 * r01 + a23 * r02;
391
+ out[4] = a00 * r10 + a10 * r11 + a20 * r12;
392
+ out[5] = a01 * r10 + a11 * r11 + a21 * r12;
393
+ out[6] = a02 * r10 + a12 * r11 + a22 * r12;
394
+ out[7] = a03 * r10 + a13 * r11 + a23 * r12;
395
+ out[8] = a00 * r20 + a10 * r21 + a20 * r22;
396
+ out[9] = a01 * r20 + a11 * r21 + a21 * r22;
397
+ out[10] = a02 * r20 + a12 * r21 + a22 * r22;
398
+ out[11] = a03 * r20 + a13 * r21 + a23 * r22;
399
+ out[12] = a[12] as number;
400
+ out[13] = a[13] as number;
401
+ out[14] = a[14] as number;
402
+ out[15] = a[15] as number;
403
+ return out;
404
+ }
405
+
406
+ /**
407
+ * out = view matrix lookAt(eye, target, up) (right-handed; camera looks toward -z at target).
408
+ * Returns out.
409
+ *
410
+ * @degrade eye === target (distanceSq < EPS_NORMALIZE) → out = identity (D-P17, same convention as D-P1).
411
+ * @degrade up collinear with the view direction (cross degenerate) → auto-select an alternative up
412
+ * (first (0,0,1), then (0,1,0)).
413
+ *
414
+ * @example
415
+ * ```ts
416
+ * mat4.lookAt(out, [0, 0, 5], [0, 0, 0], [0, 1, 0]);
417
+ * // Guard: if (vec3.distanceSq(eye, target) < EPS_NORMALIZE) skip;
418
+ * ```
419
+ */
420
+ export function lookAt(out: Mat4, eye: Vec3Like, target: Vec3Like, up: Vec3Like): Mat4 {
421
+ const ex = eye[0] as number;
422
+ const ey = eye[1] as number;
423
+ const ez = eye[2] as number;
424
+ const tx = target[0] as number;
425
+ const ty = target[1] as number;
426
+ const tz = target[2] as number;
427
+ const upx = up[0] as number;
428
+ const upy = up[1] as number;
429
+ const upz = up[2] as number;
430
+
431
+ // forward = normalize(eye - target) (right-handed: camera looks toward -z, so the z axis = eye - target)
432
+ let fx = ex - tx;
433
+ let fy = ey - ty;
434
+ let fz = ez - tz;
435
+ const fLenSq = fx * fx + fy * fy + fz * fz;
436
+ if (fLenSq < EPS_NORMALIZE) {
437
+ return identity(out);
438
+ }
439
+ const fInv = 1 / Math.sqrt(fLenSq);
440
+ fx *= fInv;
441
+ fy *= fInv;
442
+ fz *= fInv;
443
+
444
+ // right = normalize(cross(up, forward))
445
+ let rx = upy * fz - upz * fy;
446
+ let ry = upz * fx - upx * fz;
447
+ let rz = upx * fy - upy * fx;
448
+ let rLenSq = rx * rx + ry * ry + rz * rz;
449
+ if (rLenSq < EPS_NORMALIZE) {
450
+ // up collinear with forward: pick alternative up = (0, 0, 1); if still collinear pick (0, 1, 0)
451
+ rx = 0 * fz - 1 * fy;
452
+ ry = 1 * fx - 0 * fz;
453
+ rz = 0 * fy - 0 * fx;
454
+ rLenSq = rx * rx + ry * ry + rz * rz;
455
+ if (rLenSq < EPS_NORMALIZE) {
456
+ rx = 0 * fz - 0 * fy;
457
+ ry = 0 * fx - 1 * fz;
458
+ rz = 1 * fy - 0 * fx;
459
+ rLenSq = rx * rx + ry * ry + rz * rz;
460
+ }
461
+ }
462
+ const rInv = 1 / Math.sqrt(rLenSq);
463
+ rx *= rInv;
464
+ ry *= rInv;
465
+ rz *= rInv;
466
+
467
+ // newUp = cross(forward, right)
468
+ const ux = fy * rz - fz * ry;
469
+ const uy = fz * rx - fx * rz;
470
+ const uz = fx * ry - fy * rx;
471
+
472
+ // column-major: col0 = right, col1 = newUp, col2 = forward, col3 = -view * eye
473
+ out[0] = rx;
474
+ out[1] = ux;
475
+ out[2] = fx;
476
+ out[3] = 0;
477
+ out[4] = ry;
478
+ out[5] = uy;
479
+ out[6] = fy;
480
+ out[7] = 0;
481
+ out[8] = rz;
482
+ out[9] = uz;
483
+ out[10] = fz;
484
+ out[11] = 0;
485
+ out[12] = -(rx * ex + ry * ey + rz * ez);
486
+ out[13] = -(ux * ex + uy * ey + uz * ez);
487
+ out[14] = -(fx * ex + fy * ey + fz * ez);
488
+ out[15] = 1;
489
+ return out;
490
+ }
491
+
492
+ /**
493
+ * out = T(translation) * R(rotation) * S(scale) (TRS affine composition). Returns out.
494
+ */
495
+ export function compose(out: Mat4, t: Vec3Like, r: QuatLike, s: Vec3Like): Mat4 {
496
+ // build R (from quat) first, then scale each column, finally write translation
497
+ const x = r[0] as number;
498
+ const y = r[1] as number;
499
+ const z = r[2] as number;
500
+ const w = r[3] as number;
501
+ const x2 = x + x;
502
+ const y2 = y + y;
503
+ const z2 = z + z;
504
+ const xx = x * x2;
505
+ const xy = x * y2;
506
+ const xz = x * z2;
507
+ const yy = y * y2;
508
+ const yz = y * z2;
509
+ const zz = z * z2;
510
+ const wx = w * x2;
511
+ const wy = w * y2;
512
+ const wz = w * z2;
513
+ const sx = s[0] as number;
514
+ const sy = s[1] as number;
515
+ const sz = s[2] as number;
516
+
517
+ out[0] = (1 - (yy + zz)) * sx;
518
+ out[1] = (xy + wz) * sx;
519
+ out[2] = (xz - wy) * sx;
520
+ out[3] = 0;
521
+ out[4] = (xy - wz) * sy;
522
+ out[5] = (1 - (xx + zz)) * sy;
523
+ out[6] = (yz + wx) * sy;
524
+ out[7] = 0;
525
+ out[8] = (xz + wy) * sz;
526
+ out[9] = (yz - wx) * sz;
527
+ out[10] = (1 - (xx + yy)) * sz;
528
+ out[11] = 0;
529
+ out[12] = t[0] as number;
530
+ out[13] = t[1] as number;
531
+ out[14] = t[2] as number;
532
+ out[15] = 1;
533
+ return out;
534
+ }
535
+
536
+ /**
537
+ * Decompose m into (translation, rotation quat, scale).
538
+ *
539
+ * @degrade m contains shear (non-pure affine) → silent best-effort decomposition (matches
540
+ * Three.js behavior; does not throw).
541
+ *
542
+ * @example
543
+ * ```ts
544
+ * const t = vec3.create(); const r = quat.create(); const s = vec3.create();
545
+ * mat4.decompose(t, r, s, m);
546
+ * // Guard: callers should avoid introducing shear matrices at the ECS-design layer.
547
+ * ```
548
+ */
549
+ export function decompose(out_t: Vec3, out_r: Quat, out_s: Vec3, m: Mat4Like): void {
550
+ // translation
551
+ out_t[0] = m[12] as number;
552
+ out_t[1] = m[13] as number;
553
+ out_t[2] = m[14] as number;
554
+
555
+ // length of each column = scale
556
+ const sx = Math.hypot(m[0] as number, m[1] as number, m[2] as number);
557
+ const sy = Math.hypot(m[4] as number, m[5] as number, m[6] as number);
558
+ const sz = Math.hypot(m[8] as number, m[9] as number, m[10] as number);
559
+
560
+ // handle the determinant sign: negative det → flip sx
561
+ // simplified det check: use the upper-left 3x3 determinant
562
+ const det =
563
+ (m[0] as number) *
564
+ ((m[5] as number) * (m[10] as number) - (m[6] as number) * (m[9] as number)) -
565
+ (m[1] as number) *
566
+ ((m[4] as number) * (m[10] as number) - (m[6] as number) * (m[8] as number)) +
567
+ (m[2] as number) * ((m[4] as number) * (m[9] as number) - (m[5] as number) * (m[8] as number));
568
+ const sxFinal = det < 0 ? -sx : sx;
569
+
570
+ out_s[0] = sxFinal;
571
+ out_s[1] = sy;
572
+ out_s[2] = sz;
573
+
574
+ // extract rotation: divide each column of m's upper-left 3x3 by scale to get the pure rotation matrix → convert to quat
575
+ const invSx = sxFinal === 0 ? 0 : 1 / sxFinal;
576
+ const invSy = sy === 0 ? 0 : 1 / sy;
577
+ const invSz = sz === 0 ? 0 : 1 / sz;
578
+ const r00 = (m[0] as number) * invSx;
579
+ const r01 = (m[1] as number) * invSx;
580
+ const r02 = (m[2] as number) * invSx;
581
+ const r10 = (m[4] as number) * invSy;
582
+ const r11 = (m[5] as number) * invSy;
583
+ const r12 = (m[6] as number) * invSy;
584
+ const r20 = (m[8] as number) * invSz;
585
+ const r21 = (m[9] as number) * invSz;
586
+ const r22 = (m[10] as number) * invSz;
587
+
588
+ // rotation matrix → quat (Shoemake's method; case-split on the sign of trace)
589
+ const trace = r00 + r11 + r22;
590
+ if (trace > 0) {
591
+ const s = 0.5 / Math.sqrt(trace + 1);
592
+ out_r[0] = (r12 - r21) * s;
593
+ out_r[1] = (r20 - r02) * s;
594
+ out_r[2] = (r01 - r10) * s;
595
+ out_r[3] = 0.25 / s;
596
+ } else if (r00 > r11 && r00 > r22) {
597
+ const s = 2 * Math.sqrt(1 + r00 - r11 - r22);
598
+ out_r[0] = 0.25 * s;
599
+ out_r[1] = (r10 + r01) / s;
600
+ out_r[2] = (r20 + r02) / s;
601
+ out_r[3] = (r12 - r21) / s;
602
+ } else if (r11 > r22) {
603
+ const s = 2 * Math.sqrt(1 + r11 - r00 - r22);
604
+ out_r[0] = (r10 + r01) / s;
605
+ out_r[1] = 0.25 * s;
606
+ out_r[2] = (r21 + r12) / s;
607
+ out_r[3] = (r20 - r02) / s;
608
+ } else {
609
+ const s = 2 * Math.sqrt(1 + r22 - r00 - r11);
610
+ out_r[0] = (r20 + r02) / s;
611
+ out_r[1] = (r21 + r12) / s;
612
+ out_r[2] = 0.25 * s;
613
+ out_r[3] = (r01 - r10) / s;
614
+ }
615
+ }
616
+
617
+ /** out = 4x4 rotation matrix from quaternion (no translation, no scale). Returns out. */
618
+ export function fromQuat(out: Mat4, q: QuatLike): Mat4 {
619
+ const x = q[0] as number;
620
+ const y = q[1] as number;
621
+ const z = q[2] as number;
622
+ const w = q[3] as number;
623
+ const x2 = x + x;
624
+ const y2 = y + y;
625
+ const z2 = z + z;
626
+ const xx = x * x2;
627
+ const xy = x * y2;
628
+ const xz = x * z2;
629
+ const yy = y * y2;
630
+ const yz = y * z2;
631
+ const zz = z * z2;
632
+ const wx = w * x2;
633
+ const wy = w * y2;
634
+ const wz = w * z2;
635
+ out[0] = 1 - (yy + zz);
636
+ out[1] = xy + wz;
637
+ out[2] = xz - wy;
638
+ out[3] = 0;
639
+ out[4] = xy - wz;
640
+ out[5] = 1 - (xx + zz);
641
+ out[6] = yz + wx;
642
+ out[7] = 0;
643
+ out[8] = xz + wy;
644
+ out[9] = yz - wx;
645
+ out[10] = 1 - (xx + yy);
646
+ out[11] = 0;
647
+ out[12] = 0;
648
+ out[13] = 0;
649
+ out[14] = 0;
650
+ out[15] = 1;
651
+ return out;
652
+ }
653
+
654
+ /** out = pure translation matrix (translation added to identity). Returns out. */
655
+ export function fromTranslation(out: Mat4, v: Vec3Like): Mat4 {
656
+ out[0] = 1;
657
+ out[1] = 0;
658
+ out[2] = 0;
659
+ out[3] = 0;
660
+ out[4] = 0;
661
+ out[5] = 1;
662
+ out[6] = 0;
663
+ out[7] = 0;
664
+ out[8] = 0;
665
+ out[9] = 0;
666
+ out[10] = 1;
667
+ out[11] = 0;
668
+ out[12] = v[0] as number;
669
+ out[13] = v[1] as number;
670
+ out[14] = v[2] as number;
671
+ out[15] = 1;
672
+ return out;
673
+ }
674
+
675
+ /** out = pure scaling matrix diag(v.x, v.y, v.z, 1). Returns out. */
676
+ export function fromScaling(out: Mat4, v: Vec3Like): Mat4 {
677
+ out[0] = v[0] as number;
678
+ out[1] = 0;
679
+ out[2] = 0;
680
+ out[3] = 0;
681
+ out[4] = 0;
682
+ out[5] = v[1] as number;
683
+ out[6] = 0;
684
+ out[7] = 0;
685
+ out[8] = 0;
686
+ out[9] = 0;
687
+ out[10] = v[2] as number;
688
+ out[11] = 0;
689
+ out[12] = 0;
690
+ out[13] = 0;
691
+ out[14] = 0;
692
+ out[15] = 1;
693
+ return out;
694
+ }
695
+
696
+ /**
697
+ * out = pure rotation matrix from axis-angle. Returns out.
698
+ *
699
+ * @degrade axis is the zero vector → out = identity (same convention as rotate).
700
+ *
701
+ * @example
702
+ * ```ts
703
+ * mat4.fromRotation(out, [0, 1, 0], Math.PI / 2);
704
+ * mat4.fromRotation(out, [0, 0, 0], 1); // → identity (zero-axis degenerate, AC-06 no throw)
705
+ * ```
706
+ */
707
+ export function fromRotation(out: Mat4, axis: Vec3Like, rad: number): Mat4 {
708
+ let x = axis[0] as number;
709
+ let y = axis[1] as number;
710
+ let z = axis[2] as number;
711
+ const lenSq = x * x + y * y + z * z;
712
+ if (lenSq < EPS_NORMALIZE) {
713
+ return identity(out);
714
+ }
715
+ const invLen = 1 / Math.sqrt(lenSq);
716
+ x *= invLen;
717
+ y *= invLen;
718
+ z *= invLen;
719
+ const s = Math.sin(rad);
720
+ const c = Math.cos(rad);
721
+ const t = 1 - c;
722
+ out[0] = x * x * t + c;
723
+ out[1] = y * x * t + z * s;
724
+ out[2] = z * x * t - y * s;
725
+ out[3] = 0;
726
+ out[4] = x * y * t - z * s;
727
+ out[5] = y * y * t + c;
728
+ out[6] = z * y * t + x * s;
729
+ out[7] = 0;
730
+ out[8] = x * z * t + y * s;
731
+ out[9] = y * z * t - x * s;
732
+ out[10] = z * z * t + c;
733
+ out[11] = 0;
734
+ out[12] = 0;
735
+ out[13] = 0;
736
+ out[14] = 0;
737
+ out[15] = 1;
738
+ return out;
739
+ }
740
+
741
+ // ============================================================
742
+ // Projection family (T-022) — three perspective tiers + three orthographic tiers
743
+ // ============================================================
744
+ //
745
+ // Naming convention (D-3 + wiki/wgpu-matrix-overview / wiki/gl-matrix-overview):
746
+ // `perspective` / `orthographic` = WebGPU [0, 1] NDC (short names borrowed from wgpu-matrix)
747
+ // `perspectiveNO` / `orthographicNO` = WebGL/OpenGL [-1, 1] NDC (NO=Negative-One, borrowed from gl-matrix)
748
+ // `perspectiveReverseZ` / `orthographicReverseZ` = reversed-Z (far→0, near→1; covers finite + infinite)
749
+ //
750
+ // AC-04 three projection tiers complete + AC-05 reversed-Z numeric fixture (error ≤ 1e-5)
751
+ // + AC-06 never raises (silent degrade).
752
+ // reversed-Z numeric correctness: see wiki/reversed-z-projection.md §7.2 / §7.3 / §7.4.
753
+
754
+ /**
755
+ * out = perspective projection (**WebGPU [0, 1] NDC** short name, D-3). Returns out.
756
+ *
757
+ * Right-handed; z_eye negative values lie in the frustum; near → ndc_z=0, far → ndc_z=1.
758
+ * Naming borrowed from wgpu-matrix (short name for WebGPU), unlike gl-matrix (short name for WebGL).
759
+ * Supports both finite far and infinite far (far=Infinity, aligned with wgpu-matrix
760
+ * `m[10]=-1, m[14]=-near`).
761
+ *
762
+ * @degrade near >= far or fovy <= 0 or aspect <= 0 → numerically undefined but does not throw
763
+ * (plan §appendix A #7).
764
+ *
765
+ * @example
766
+ * ```ts
767
+ * mat4.perspective(out, Math.PI / 4, canvas.width / canvas.height, 0.1, 1000);
768
+ * mat4.perspective(out, Math.PI / 4, aspect, 0.1, Infinity); // infinite-far trick
769
+ * // Guard: if (!(near < far && fovy > 0 && aspect > 0)) handleInvalid();
770
+ * ```
771
+ */
772
+ export function perspective(
773
+ out: Mat4,
774
+ fovYRadians: number,
775
+ aspect: number,
776
+ near: number,
777
+ far: number,
778
+ ): Mat4 {
779
+ const f = 1 / Math.tan(fovYRadians / 2);
780
+ out[0] = f / aspect;
781
+ out[1] = 0;
782
+ out[2] = 0;
783
+ out[3] = 0;
784
+ out[4] = 0;
785
+ out[5] = f;
786
+ out[6] = 0;
787
+ out[7] = 0;
788
+ out[8] = 0;
789
+ out[9] = 0;
790
+ out[11] = -1;
791
+ out[12] = 0;
792
+ out[13] = 0;
793
+ out[15] = 0;
794
+
795
+ if (far === Number.POSITIVE_INFINITY) {
796
+ // infinite-far trick (aligned with wgpu-matrix): m[10]=-1, m[14]=-near
797
+ out[10] = -1;
798
+ out[14] = -near;
799
+ } else {
800
+ const nf = 1 / (near - far);
801
+ out[10] = far * nf;
802
+ out[14] = far * near * nf;
803
+ }
804
+ return out;
805
+ }
806
+
807
+ /**
808
+ * out = perspective projection (**WebGL/OpenGL [-1, 1] NDC**; *NO* = Negative-One, borrowed from gl-matrix).
809
+ *
810
+ * Right-handed, near → ndc_z=-1, far → ndc_z=+1.
811
+ *
812
+ * @degrade Same as perspective.
813
+ *
814
+ * @example
815
+ * ```ts
816
+ * mat4.perspectiveNO(out, Math.PI / 4, canvas.width / canvas.height, 0.1, 1000);
817
+ * mat4.perspectiveNO(out, Math.PI / 4, aspect, 0.1, Infinity); // infinite-far
818
+ * // Guard: if (!(near < far && fovy > 0 && aspect > 0)) handleInvalid();
819
+ * ```
820
+ */
821
+ export function perspectiveNO(
822
+ out: Mat4,
823
+ fovYRadians: number,
824
+ aspect: number,
825
+ near: number,
826
+ far: number,
827
+ ): Mat4 {
828
+ const f = 1 / Math.tan(fovYRadians / 2);
829
+ out[0] = f / aspect;
830
+ out[1] = 0;
831
+ out[2] = 0;
832
+ out[3] = 0;
833
+ out[4] = 0;
834
+ out[5] = f;
835
+ out[6] = 0;
836
+ out[7] = 0;
837
+ out[8] = 0;
838
+ out[9] = 0;
839
+ out[11] = -1;
840
+ out[12] = 0;
841
+ out[13] = 0;
842
+ out[15] = 0;
843
+
844
+ if (far === Number.POSITIVE_INFINITY) {
845
+ out[10] = -1;
846
+ out[14] = -2 * near;
847
+ } else {
848
+ const nf = 1 / (near - far);
849
+ out[10] = (far + near) * nf;
850
+ out[14] = 2 * far * near * nf;
851
+ }
852
+ return out;
853
+ }
854
+
855
+ /**
856
+ * out = reversed-Z perspective projection (**WebGPU [0, 1] NDC, near→1 far→0**). Returns out.
857
+ *
858
+ * Precision gain: matches the float dense region ([0, 0.01]) to the slowly-varying far end of 1/z.
859
+ * Under the standard test conditions `near=0.1, far=10000`, NVIDIA reports a reversed-Z + float32
860
+ * depth error rate of 0% (vs. high error rate for the standard mapping). See
861
+ * `.forgeax-harness/knowledge-base/wiki/reversed-z-projection.md` §5.
862
+ *
863
+ * GPU-side companion switches (5 places must be flipped together):
864
+ * 1. depthCompare: 'greater' (vs 'less')
865
+ * 2. depthClearValue: 0.0 (vs 1.0)
866
+ * 3. depth format: 'depth32float' (mandatory; fixed-point loses most of the gain)
867
+ * 4. depth bias: inverted (avoid worsening z-fighting)
868
+ * 5. multiple passes must share the reversed-Z convention
869
+ *
870
+ * Supports finite and infinite far (from wiki §3.3 / §4.2 derivations):
871
+ * - finite: m[10]=near/(far-near), m[14]=near*far/(far-near)
872
+ * - infinite (far=Infinity): m[10]=0, m[14]=near (lim f→∞ limit values)
873
+ *
874
+ * Numeric fixture (AC-05 error ≤ 1e-5): see __tests__/_fixtures.ts.
875
+ *
876
+ * @degrade Same as perspective.
877
+ *
878
+ * @example
879
+ * ```ts
880
+ * mat4.perspectiveReverseZ(out, Math.PI / 4, aspect, 0.1, 100);
881
+ * mat4.perspectiveReverseZ(out, Math.PI / 4, aspect, 0.1, Infinity); // infinite far
882
+ * // GPU companion: pipeline.depthCompare = 'greater'; passDesc.depthClearValue = 0.0;
883
+ * ```
884
+ */
885
+ export function perspectiveReverseZ(
886
+ out: Mat4,
887
+ fovYRadians: number,
888
+ aspect: number,
889
+ near: number,
890
+ far: number,
891
+ ): Mat4 {
892
+ const f = 1 / Math.tan(fovYRadians / 2);
893
+ out[0] = f / aspect;
894
+ out[1] = 0;
895
+ out[2] = 0;
896
+ out[3] = 0;
897
+ out[4] = 0;
898
+ out[5] = f;
899
+ out[6] = 0;
900
+ out[7] = 0;
901
+ out[8] = 0;
902
+ out[9] = 0;
903
+ out[11] = -1;
904
+ out[12] = 0;
905
+ out[13] = 0;
906
+ out[15] = 0;
907
+
908
+ if (far === Number.POSITIVE_INFINITY) {
909
+ out[10] = 0;
910
+ out[14] = near;
911
+ } else {
912
+ const fn = 1 / (far - near);
913
+ out[10] = near * fn;
914
+ out[14] = near * far * fn;
915
+ }
916
+ return out;
917
+ }
918
+
919
+ /**
920
+ * out = orthographic projection (**WebGPU [0, 1] NDC** short name, D-3). Returns out.
921
+ *
922
+ * @degrade near >= far or left >= right or bottom >= top → numerically undefined but does not throw.
923
+ *
924
+ * @example
925
+ * ```ts
926
+ * mat4.orthographic(out, -10, 10, -10, 10, 0.1, 100);
927
+ * // Guard: if (!(left < right && bottom < top && near < far)) handleInvalid();
928
+ * ```
929
+ */
930
+ export function orthographic(
931
+ out: Mat4,
932
+ left: number,
933
+ right: number,
934
+ bottom: number,
935
+ top: number,
936
+ near: number,
937
+ far: number,
938
+ ): Mat4 {
939
+ const lr = 1 / (left - right);
940
+ const bt = 1 / (bottom - top);
941
+ const nf = 1 / (near - far);
942
+ out[0] = -2 * lr;
943
+ out[1] = 0;
944
+ out[2] = 0;
945
+ out[3] = 0;
946
+ out[4] = 0;
947
+ out[5] = -2 * bt;
948
+ out[6] = 0;
949
+ out[7] = 0;
950
+ out[8] = 0;
951
+ out[9] = 0;
952
+ out[10] = nf; // [0,1] NDC: near→0, far→1 → m[10] = 1/(near-far), m[14] = near/(near-far)
953
+ out[11] = 0;
954
+ out[12] = (left + right) * lr;
955
+ out[13] = (top + bottom) * bt;
956
+ out[14] = near * nf;
957
+ out[15] = 1;
958
+ return out;
959
+ }
960
+
961
+ /** out = orthographic projection (**WebGL/OpenGL [-1, 1] NDC**, *NO*). Returns out. */
962
+ export function orthographicNO(
963
+ out: Mat4,
964
+ left: number,
965
+ right: number,
966
+ bottom: number,
967
+ top: number,
968
+ near: number,
969
+ far: number,
970
+ ): Mat4 {
971
+ const lr = 1 / (left - right);
972
+ const bt = 1 / (bottom - top);
973
+ const nf = 1 / (near - far);
974
+ out[0] = -2 * lr;
975
+ out[1] = 0;
976
+ out[2] = 0;
977
+ out[3] = 0;
978
+ out[4] = 0;
979
+ out[5] = -2 * bt;
980
+ out[6] = 0;
981
+ out[7] = 0;
982
+ out[8] = 0;
983
+ out[9] = 0;
984
+ out[10] = 2 * nf; // [-1,1] NDC: near→-1, far→1
985
+ out[11] = 0;
986
+ out[12] = (left + right) * lr;
987
+ out[13] = (top + bottom) * bt;
988
+ out[14] = (near + far) * nf;
989
+ out[15] = 1;
990
+ return out;
991
+ }
992
+
993
+ /**
994
+ * out = reversed-Z orthographic projection (**WebGPU [0, 1] NDC, near→1 far→0**;
995
+ * D-P3 self-extension).
996
+ *
997
+ * Pairs with perspectiveReverseZ to form a symmetric three-tier surface; in orthographic
998
+ * projection the reversed-Z precision gain is small (ortho is linear in z), but it is kept to
999
+ * avoid LLM single-pass-scan cognitive load (charter proposition 1).
1000
+ * Note: this function is an @forgeax/engine-math self-extension; wgpu-matrix / gl-matrix have no matching
1001
+ * name (see plan-strategy D-P3 + README quick-ref table footnote).
1002
+ */
1003
+ export function orthographicReverseZ(
1004
+ out: Mat4,
1005
+ left: number,
1006
+ right: number,
1007
+ bottom: number,
1008
+ top: number,
1009
+ near: number,
1010
+ far: number,
1011
+ ): Mat4 {
1012
+ const lr = 1 / (left - right);
1013
+ const bt = 1 / (bottom - top);
1014
+ const fn = 1 / (far - near); // reversed-Z [0,1]: near→1, far→0
1015
+ out[0] = -2 * lr;
1016
+ out[1] = 0;
1017
+ out[2] = 0;
1018
+ out[3] = 0;
1019
+ out[4] = 0;
1020
+ out[5] = -2 * bt;
1021
+ out[6] = 0;
1022
+ out[7] = 0;
1023
+ out[8] = 0;
1024
+ out[9] = 0;
1025
+ // Derivation: -m[10]*near + m[14] = 1, -m[10]*far + m[14] = 0
1026
+ // → m[10] = 1/(far-near), m[14] = far/(far-near)
1027
+ out[10] = fn;
1028
+ out[11] = 0;
1029
+ out[12] = (left + right) * lr;
1030
+ out[13] = (top + bottom) * bt;
1031
+ out[14] = far * fn;
1032
+ out[15] = 1;
1033
+ return out;
1034
+ }
1035
+
1036
+ // ============================================================
1037
+ // Transform reverse surface (M1 / t6) — mat4 → vec3 cross-type transforms
1038
+ // ============================================================
1039
+ //
1040
+ // 3 functions / +3 surface (mat4 23 → 26 / total 115 → 119, AC-03):
1041
+ // - transformVec3: (x, y, z, 1) multiplied by 4×4 + perspective divide + w'=0 explicit guard (D-4 silent)
1042
+ // - transformPoint: ES alias of transformVec3 (OQ-1 / S-1, saves 16 LOC)
1043
+ // - transformDirection: takes m's upper-left 3×3 (no translation column) + vec3.normalize (OQ-2 / S-2)
1044
+ //
1045
+ // Degenerate convention (D-4 library-wide silent fall-back):
1046
+ // - transformVec3 / transformPoint: w' = 0 (perspective divide by zero) → out = (0, 0, 0)
1047
+ // - transformDirection: |out| = 0 → vec3.normalize silently falls back to (0, 0, 0)
1048
+ //
1049
+ // Related: requirements §3.1 mat4 rows 1/2/3 + §11 surface full table mat4 23→26;
1050
+ // research Finding 1 (alias evidence) + Finding 2 (take 3×3 + normalize) +
1051
+ // Finding 4 (w'=0 explicit guard stricter than upstreams);
1052
+ // plan-strategy §2 S-1 / S-2 + §3 R-1 countermeasure + §6 M1 scope section.
1053
+
1054
+ /**
1055
+ * out = M * (v.x, v.y, v.z, 1) (with perspective divide by w'). Returns out.
1056
+ *
1057
+ * For affine m this is equivalent to transformPoint (treats v as a "position" that participates
1058
+ * in the transform, including translation).
1059
+ * Aliasing-safe: reads v.xyz into locals before writing out.
1060
+ *
1061
+ * @degrade w' = 0 (perspective divide by zero) → out = (0, 0, 0) (D-4 silent convention; avoids
1062
+ * NaN/Infinity propagation; stricter than the implicit 1/0 = Infinity behavior of
1063
+ * Three.js / gl-matrix).
1064
+ *
1065
+ * @example
1066
+ * ```ts
1067
+ * mat4.transformVec3(out, projViewModel, v);
1068
+ * // Guard: if (out[0] === 0 && out[1] === 0 && out[2] === 0 && wasNonZero(v)) {
1069
+ * // // w'=0 degenerate branch; the caller diagnoses as needed
1070
+ * // }
1071
+ * ```
1072
+ */
1073
+ export function transformVec3(out: Vec3, m: Mat4Like, v: Vec3Like): Vec3 {
1074
+ const x = v[0] as number;
1075
+ const y = v[1] as number;
1076
+ const z = v[2] as number;
1077
+ const m00 = m[0] as number;
1078
+ const m01 = m[1] as number;
1079
+ const m02 = m[2] as number;
1080
+ const m03 = m[3] as number;
1081
+ const m10 = m[4] as number;
1082
+ const m11 = m[5] as number;
1083
+ const m12 = m[6] as number;
1084
+ const m13 = m[7] as number;
1085
+ const m20 = m[8] as number;
1086
+ const m21 = m[9] as number;
1087
+ const m22 = m[10] as number;
1088
+ const m23 = m[11] as number;
1089
+ const m30 = m[12] as number;
1090
+ const m31 = m[13] as number;
1091
+ const m32 = m[14] as number;
1092
+ const m33 = m[15] as number;
1093
+
1094
+ const w = m03 * x + m13 * y + m23 * z + m33;
1095
+ if (w === 0) {
1096
+ // D-4 silent convention: perspective divide by zero → (0, 0, 0)
1097
+ out[0] = 0;
1098
+ out[1] = 0;
1099
+ out[2] = 0;
1100
+ return out;
1101
+ }
1102
+ const invW = 1 / w;
1103
+ out[0] = (m00 * x + m10 * y + m20 * z + m30) * invW;
1104
+ out[1] = (m01 * x + m11 * y + m21 * z + m31) * invW;
1105
+ out[2] = (m02 * x + m12 * y + m22 * z + m32) * invW;
1106
+ return out;
1107
+ }
1108
+
1109
+ /**
1110
+ * out = M * (v.x, v.y, v.z, 1) (with perspective divide by w'). Returns out.
1111
+ *
1112
+ * Treats v as a "position", including the translation column (shares the same function body as
1113
+ * transformVec3; OQ-1 / S-1).
1114
+ * Aliasing-safe; reference-equal with transformVec3: `mat4.transformPoint === mat4.transformVec3`.
1115
+ *
1116
+ * @degrade w' = 0 (perspective divide by zero) → out = (0, 0, 0) (D-4 silent convention;
1117
+ * same as transformVec3).
1118
+ *
1119
+ * @example
1120
+ * ```ts
1121
+ * mat4.transformPoint(out, modelMatrix, [1, 2, 3]);
1122
+ * // (1,2,3) → world-space position, including translation
1123
+ * ```
1124
+ */
1125
+ export const transformPoint = transformVec3;
1126
+
1127
+ /**
1128
+ * out = normalize(M_3x3 * v) (uses m's upper-left 3×3, no translation column). Returns out.
1129
+ *
1130
+ * Treats v as a "direction" and does **not** apply translation; the result is unit-normalized via
1131
+ * `vec3.normalize` (OQ-2 / S-2).
1132
+ * Aliasing-safe: reads v.xyz into locals before writing out.
1133
+ *
1134
+ * @degrade |out| = 0 (singular m + arbitrary v / m_3x3 maps v to the zero vector) → vec3.normalize
1135
+ * silently falls back to (0, 0, 0) (library-wide D-4 convention).
1136
+ *
1137
+ * @example
1138
+ * ```ts
1139
+ * const worldNormal = vec3.create();
1140
+ * mat4.transformDirection(worldNormal, modelMatrix, localNormal);
1141
+ * // Normal-vector transform: ignores translation; a unit-length input + orthogonal matrix
1142
+ * // guarantees a unit-length output.
1143
+ * ```
1144
+ */
1145
+ export function transformDirection(out: Vec3, m: Mat4Like, v: Vec3Like): Vec3 {
1146
+ const x = v[0] as number;
1147
+ const y = v[1] as number;
1148
+ const z = v[2] as number;
1149
+ const m00 = m[0] as number;
1150
+ const m01 = m[1] as number;
1151
+ const m02 = m[2] as number;
1152
+ const m10 = m[4] as number;
1153
+ const m11 = m[5] as number;
1154
+ const m12 = m[6] as number;
1155
+ const m20 = m[8] as number;
1156
+ const m21 = m[9] as number;
1157
+ const m22 = m[10] as number;
1158
+ out[0] = m00 * x + m10 * y + m20 * z;
1159
+ out[1] = m01 * x + m11 * y + m21 * z;
1160
+ out[2] = m02 * x + m12 * y + m22 * z;
1161
+ // S-2: reuse vec3.normalize's silent convention (|out|=0 → out=(0,0,0)); do not inline copy.
1162
+ return vec3.normalize(out, out);
1163
+ }
1164
+
1165
+ // ============================================================
1166
+ // world-mat4 basis / translation accessors
1167
+ // (feat-20260601-unify-transform-local-global-mat4-drop-globaltrans M1 w6)
1168
+ // ============================================================
1169
+ //
1170
+ // Decompose a world-space mat4 into its translation + the three orthonormal
1171
+ // basis directions. Placed adjacent to transformDirection because they share
1172
+ // the same normalization convention (vec3.normalize's |out|=0 → (0,0,0) D-4
1173
+ // fallback). The Transform world column (array<f32, 16>) is the producer; the
1174
+ // audio listener / camera-orientation consumers read forward/up via these.
1175
+ //
1176
+ // Layout (column-major, m[col*4 + row]):
1177
+ // col0 = m[0..2] = right col1 = m[4..6] = up
1178
+ // col2 = m[8..10] = forward col3 = m[12..14] = translation
1179
+ //
1180
+ // Sign (RL-4): getForward returns -normalize(col2) so it aligns with the
1181
+ // historical single-quaternion oracle quat.transformVec3(q, (0, 0, -1)) -- a
1182
+ // camera/listener looking down -Z in its local frame.
1183
+
1184
+ /**
1185
+ * World-space translation = col 3 (m[12], m[13], m[14]). Read verbatim, not
1186
+ * normalized (translation has magnitude).
1187
+ */
1188
+ export function getTranslation(out: Vec3, m: Mat4Like): Vec3 {
1189
+ out[0] = m[12] as number;
1190
+ out[1] = m[13] as number;
1191
+ out[2] = m[14] as number;
1192
+ return out;
1193
+ }
1194
+
1195
+ /**
1196
+ * World-space forward = `-normalize(col2)` (RL-4: -Z look convention; matches
1197
+ * `quat.transformVec3(q, [0, 0, -1])`). Degenerate zero column → (0,0,0).
1198
+ */
1199
+ export function getForward(out: Vec3, m: Mat4Like): Vec3 {
1200
+ out[0] = -(m[8] as number);
1201
+ out[1] = -(m[9] as number);
1202
+ out[2] = -(m[10] as number);
1203
+ return vec3.normalize(out, out);
1204
+ }
1205
+
1206
+ /** World-space up = `normalize(col1)`. Degenerate zero column → (0,0,0). */
1207
+ export function getUp(out: Vec3, m: Mat4Like): Vec3 {
1208
+ out[0] = m[4] as number;
1209
+ out[1] = m[5] as number;
1210
+ out[2] = m[6] as number;
1211
+ return vec3.normalize(out, out);
1212
+ }
1213
+
1214
+ /** World-space right = `normalize(col0)`. Degenerate zero column → (0,0,0). */
1215
+ export function getRight(out: Vec3, m: Mat4Like): Vec3 {
1216
+ out[0] = m[0] as number;
1217
+ out[1] = m[1] as number;
1218
+ out[2] = m[2] as number;
1219
+ return vec3.normalize(out, out);
1220
+ }
1221
+
1222
+ // ============================================================
1223
+ // unproject (feat-20260529-picking-raycasting-screen-to-entity M2 w7)
1224
+ // ============================================================
1225
+ //
1226
+ // Map an NDC point back to world space via the inverse view-projection matrix.
1227
+ // Internally constructs vec4 (ndc.x, ndc.y, ndc.z, 1) and delegates to transformVec3,
1228
+ // which performs the 4×4 multiply + perspective divide by w'.
1229
+ //
1230
+ // WebGPU [0,1] NDC z convention (D-NDC / research Finding 6):
1231
+ // near plane → z=0, far plane → z=1
1232
+ //
1233
+ // Related: plan-tasks.json w7; requirements in-scope #3;
1234
+ // research Finding 3 (transformVec3 built-in w-divide).
1235
+
1236
+ /**
1237
+ * Unproject an NDC point to world space.
1238
+ *
1239
+ * `ndcPoint` is in NDC space: x,y ∈ [-1,1], z ∈ [0,1] (WebGPU convention).
1240
+ * `invVP` is the inverse of the view-projection matrix.
1241
+ *
1242
+ * @example
1243
+ * ```ts
1244
+ * const worldPoint = vec3.create();
1245
+ * mat4.unproject(worldPoint, [0, 0, 0], invVP); // near-plane centre → world
1246
+ * mat4.unproject(worldPoint, [0, 0, 1], invVP); // far-plane centre → world
1247
+ * ```
1248
+ */
1249
+ export function unproject(out: Vec3, ndcPoint: Vec3Like, invVP: Mat4Like): Vec3 {
1250
+ return transformVec3(out, invVP, ndcPoint);
1251
+ }
1252
+
1253
+ // ============================================================
1254
+ // projectPoint — world → NDC dual of unproject
1255
+ // (roadmap 2026-06-15 game-demo-engine-gaps G-4: HUD anchor / worldToScreen)
1256
+ // ============================================================
1257
+ //
1258
+ // Map a world-space point to NDC via the view-projection matrix. Caller maps
1259
+ // NDC.xy ∈ [-1,1] to viewport pixels (DOM y-down → flip y), and inspects NDC.z
1260
+ // for "behind camera" / "outside far plane" culling.
1261
+ //
1262
+ // Pairs with unproject (NDC → world); both are intent-revealing aliases of
1263
+ // transformVec3 + perspective divide (D-4 silent w'=0 → out=(0,0,0) inherited).
1264
+
1265
+ /**
1266
+ * Project a world-space point to NDC space through a view-projection matrix.
1267
+ *
1268
+ * `worldPos` is a position (treated with translation column, not a direction).
1269
+ * `viewProj` is `proj × view` (eye→clip composed with world→eye).
1270
+ * Output NDC is WebGPU convention: x,y ∈ [-1,1], z ∈ [0,1] (near=0 / far=1).
1271
+ *
1272
+ * To map NDC to viewport pixels (DOM y-down):
1273
+ * px = (ndc.x * 0.5 + 0.5) * vpWidth
1274
+ * py = (1 - (ndc.y * 0.5 + 0.5)) * vpHeight
1275
+ * `ndc.z < 0` or `ndc.z > 1` means the point is outside the depth range
1276
+ * (behind near or beyond far); callers typically skip drawing the HUD anchor.
1277
+ *
1278
+ * @degrade w' = 0 (point lies on the camera plane) → out = (0, 0, 0)
1279
+ * (D-4 silent convention; inherited from transformVec3).
1280
+ *
1281
+ * @example
1282
+ * ```ts
1283
+ * const ndc = vec3.create();
1284
+ * mat4.projectPoint(ndc, worldPos, viewProj);
1285
+ * if (ndc[2] >= 0 && ndc[2] <= 1) {
1286
+ * const px = (ndc[0] * 0.5 + 0.5) * canvas.width;
1287
+ * const py = (1 - (ndc[1] * 0.5 + 0.5)) * canvas.height;
1288
+ * // anchor DOM tooltip at (px, py)
1289
+ * }
1290
+ * ```
1291
+ */
1292
+ export function projectPoint(out: Vec3, worldPos: Vec3Like, viewProj: Mat4Like): Vec3 {
1293
+ return transformVec3(out, viewProj, worldPos);
1294
+ }
1295
+
1296
+ // ============================================================
1297
+ // computeViewProj (feat-20260617-host-engine-contract-and-video-cutscene M2 w6)
1298
+ // ============================================================
1299
+ //
1300
+ // Convenience composition: out = perspective(fov,aspect,near,far) * lookAt(eye,target,up).
1301
+ // Plain numeric / Vec3Like params only — keeps math zero-dep (plan-strategy D-1).
1302
+ //
1303
+ // Related: requirements AC-03; plan-strategy D-1; research Finding 4.
1304
+
1305
+ /**
1306
+ * Compute a combined view-projection matrix from camera parameters.
1307
+ *
1308
+ * This is a convenience composition of `mat4.perspective * mat4.lookAt`, not a primitive.
1309
+ * All parameters are plain numbers / Vec3Like — no runtime POD types (math zero-dep).
1310
+ *
1311
+ * @param out Mat4 to write the result into.
1312
+ * @param eye Camera position in world space.
1313
+ * @param target Point the camera looks at.
1314
+ * @param up Approximate up direction.
1315
+ * @param fovYRadians Vertical field of view in radians.
1316
+ * @param aspect Aspect ratio (width / height).
1317
+ * @param near Near clip distance (positive).
1318
+ * @param far Far clip distance (positive, or Infinity for infinite-far).
1319
+ * @returns `out` (same Mat4 instance).
1320
+ */
1321
+ export function computeViewProj(
1322
+ out: Mat4,
1323
+ eye: Vec3Like,
1324
+ target: Vec3Like,
1325
+ up: Vec3Like,
1326
+ fovYRadians: number,
1327
+ aspect: number,
1328
+ near: number,
1329
+ far: number,
1330
+ ): Mat4 {
1331
+ const view = lookAt(create(), eye, target, up);
1332
+ const proj = perspective(create(), fovYRadians, aspect, near, far);
1333
+ return multiply(out, proj, view);
1334
+ }