1use crate::components::Transform;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub enum Axis {
20 PosX,
21 NegX,
22 PosY,
23 NegY,
24 PosZ,
25 NegZ,
26}
27
28impl Axis {
29 pub const fn index(self) -> usize {
31 match self {
32 Axis::PosX | Axis::NegX => 0,
33 Axis::PosY | Axis::NegY => 1,
34 Axis::PosZ | Axis::NegZ => 2,
35 }
36 }
37
38 pub const fn is_negative(self) -> bool {
39 matches!(self, Axis::NegX | Axis::NegY | Axis::NegZ)
40 }
41
42 const fn of(index: usize, negative: bool) -> Axis {
43 match (index, negative) {
44 (0, false) => Axis::PosX,
45 (0, true) => Axis::NegX,
46 (1, false) => Axis::PosY,
47 (1, true) => Axis::NegY,
48 (2, false) => Axis::PosZ,
49 _ => Axis::NegZ,
50 }
51 }
52}
53
54#[derive(Clone, Debug, PartialEq, thiserror::Error)]
56pub enum FrameError {
57 #[error("axes {0:?} are not a permutation of x, y, z")]
58 NotAPermutation([Axis; 3]),
59 #[error("metres per unit {0} is not a finite positive number")]
60 BadScale(f64),
61}
62
63#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct SourceFrame {
67 axes: [Axis; 3],
68 metres_per_unit: f64,
69}
70
71impl SourceFrame {
72 pub const WORLD: SourceFrame = SourceFrame::new([Axis::PosX, Axis::PosY, Axis::PosZ], 1.0);
74
75 pub const ENU: SourceFrame = SourceFrame::new([Axis::PosX, Axis::PosZ, Axis::NegY], 1.0);
78
79 pub const fn new(axes: [Axis; 3], metres_per_unit: f64) -> SourceFrame {
83 match SourceFrame::try_new(axes, metres_per_unit) {
84 Ok(f) => f,
85 Err(_) => {
86 panic!("not a frame: axes must be a permutation of x, y, z and the scale finite and positive")
87 }
88 }
89 }
90
91 pub const fn try_new(axes: [Axis; 3], metres_per_unit: f64) -> Result<SourceFrame, FrameError> {
92 let (a, b, c) = (axes[0].index(), axes[1].index(), axes[2].index());
93 if a == b || a == c || b == c {
94 return Err(FrameError::NotAPermutation(axes));
95 }
96 if !(metres_per_unit.is_finite() && metres_per_unit > 0.0) {
97 return Err(FrameError::BadScale(metres_per_unit));
98 }
99 Ok(SourceFrame {
100 axes,
101 metres_per_unit,
102 })
103 }
104
105 pub const fn axes(&self) -> [Axis; 3] {
107 self.axes
108 }
109
110 pub const fn metres_per_unit(&self) -> f64 {
111 self.metres_per_unit
112 }
113
114 pub const fn determinant(&self) -> i32 {
117 let [a, b, c] = self.axes;
118 let even = (a.index() + 1) % 3 == b.index() && (b.index() + 1) % 3 == c.index();
120 let negs = a.is_negative() as u32 + b.is_negative() as u32 + c.is_negative() as u32;
121 if even == negs.is_multiple_of(2) { 1 } else { -1 }
122 }
123
124 pub const fn flips_winding(&self) -> bool {
126 self.determinant() == -1
127 }
128
129 pub const fn inverse(&self) -> SourceFrame {
131 let mut inv = [Axis::PosX; 3];
132 let mut i = 0;
133 while i < 3 {
134 let a = self.axes[i];
135 inv[a.index()] = Axis::of(i, a.is_negative());
136 i += 1;
137 }
138 SourceFrame::new(inv, 1.0 / self.metres_per_unit)
139 }
140
141 pub fn direction(&self, d: [f32; 3]) -> [f32; 3] {
143 self.axes.map(|a| signed(d[a.index()], a))
144 }
145
146 pub fn point(&self, p: [f32; 3]) -> [f32; 3] {
148 let d = self.direction(p);
149 if self.metres_per_unit == 1.0 {
150 return d;
151 }
152 d.map(|x| (f64::from(x) * self.metres_per_unit) as f32)
153 }
154
155 pub fn transform(&self, m: &Transform) -> Transform {
158 let mut out = Transform { cols: [[0.0; 3]; 4] };
159 for (j, a) in self.axes.iter().enumerate() {
160 out.cols[j] = self.direction(m.cols[a.index()]).map(|x| signed(x, *a));
161 }
162 out.cols[3] = self.point(m.cols[3]);
163 out
164 }
165
166 pub fn direction_to_source(&self, d: [f32; 3]) -> [f32; 3] {
168 let mut out = [0.0; 3];
169 for (i, a) in self.axes.iter().enumerate() {
170 out[a.index()] = signed(d[i], *a);
171 }
172 out
173 }
174
175 pub fn point_to_source(&self, p: [f32; 3]) -> [f32; 3] {
176 let d = self.direction_to_source(p);
177 if self.metres_per_unit == 1.0 {
178 return d;
179 }
180 d.map(|x| (f64::from(x) / self.metres_per_unit) as f32)
181 }
182
183 pub fn transform_to_source(&self, m: &Transform) -> Transform {
184 let mut out = Transform { cols: [[0.0; 3]; 4] };
185 for (i, a) in self.axes.iter().enumerate() {
186 out.cols[a.index()] = self.direction_to_source(m.cols[i]).map(|x| signed(x, *a));
187 }
188 out.cols[3] = self.point_to_source(m.cols[3]);
189 out
190 }
191
192 pub fn length_to_source(&self, l: f32) -> f32 {
194 if self.metres_per_unit == 1.0 {
195 return l;
196 }
197 (f64::from(l) / self.metres_per_unit) as f32
198 }
199
200 pub fn inverse_length_to_source(&self, l: f32) -> f32 {
202 scale(l, self.metres_per_unit)
203 }
204
205 pub fn tangent_to_source(&self, t: [f32; 4]) -> [f32; 4] {
207 let d = self.direction_to_source([t[0], t[1], t[2]]);
208 [d[0], d[1], d[2], if self.flips_winding() { -t[3] } else { t[3] }]
209 }
210
211 pub fn plane_to_source(&self, plane: [f32; 4]) -> [f32; 4] {
214 let n = self.direction_to_source([plane[0], plane[1], plane[2]]);
215 [n[0], n[1], n[2], self.length_to_source(plane[3])]
216 }
217
218 pub fn from_world_matrix_to_source(&self, m: [[f32; 4]; 4]) -> [[f32; 4]; 4] {
225 let mut out = m;
226 for (i, a) in self.axes.iter().enumerate() {
227 out[a.index()] = m[i].map(|x| scale(signed(x, *a), self.metres_per_unit));
228 }
229 out
230 }
231
232 pub fn to_world_matrix_to_source(&self, m: [[f32; 4]; 4]) -> [[f32; 4]; 4] {
235 m.map(|c| {
236 let p = self.point_to_source([c[0], c[1], c[2]]);
237 [p[0], p[1], p[2], c[3]]
238 })
239 }
240
241 pub fn world_matrix_to_source(&self, m: [[f32; 4]; 4]) -> [[f32; 4]; 4] {
244 self.to_world_matrix_to_source(self.from_world_matrix_to_source(m))
245 }
246}
247
248impl crate::VertexMap for SourceFrame {
251 fn point(&self, p: [f32; 3]) -> [f32; 3] {
252 SourceFrame::point(self, p)
253 }
254 fn normal(&self, n: [f32; 3]) -> Option<[f32; 3]> {
255 Some(self.direction(n))
256 }
257 fn tangent(&self, t: [f32; 4]) -> Option<[f32; 4]> {
258 let d = self.direction([t[0], t[1], t[2]]);
259 Some([d[0], d[1], d[2], if self.flips_winding() { -t[3] } else { t[3] }])
260 }
261}
262
263fn signed(x: f32, a: Axis) -> f32 {
265 if a.is_negative() { -x } else { x }
266}
267
268fn scale(x: f32, metres_per_unit: f64) -> f32 {
270 if metres_per_unit == 1.0 {
271 return x;
272 }
273 (f64::from(x) * metres_per_unit) as f32
274}
275
276pub fn ground(p: [f32; 3]) -> [f32; 2] {
278 [p[0], -p[2]]
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use Axis::*;
285
286 fn bits(v: [f32; 3]) -> [u32; 3] {
287 v.map(f32::to_bits)
288 }
289
290 fn mul(m: &Transform, p: [f32; 3]) -> [f32; 3] {
291 std::array::from_fn(|i| {
292 m.cols[0][i] * p[0] + m.cols[1][i] * p[1] + m.cols[2][i] * p[2] + m.cols[3][i]
293 })
294 }
295
296 const FROMSOFT: SourceFrame = SourceFrame::new([PosX, PosY, NegZ], 1.0);
299 const UNITY: SourceFrame = SourceFrame::new([PosX, PosY, NegZ], 1.0);
301 const FORZA: SourceFrame = SourceFrame::new([NegX, PosY, PosZ], 1.0);
304
305 #[test]
306 fn handedness_follows_the_determinant() {
307 assert_eq!(SourceFrame::WORLD.determinant(), 1);
308 assert_eq!(SourceFrame::ENU.determinant(), 1);
309 assert!(!SourceFrame::ENU.flips_winding());
310 for f in [FROMSOFT, UNITY, FORZA] {
311 assert_eq!(f.determinant(), -1, "{f:?}");
312 assert!(f.flips_winding());
313 }
314 let all = [PosX, NegX, PosY, NegY, PosZ, NegZ];
316 let mut n = 0;
317 for a in all {
318 for b in all {
319 for c in all {
320 let Ok(f) = SourceFrame::try_new([a, b, c], 1.0) else {
321 continue;
322 };
323 let m: [[i32; 3]; 3] = std::array::from_fn(|i| {
324 let ax = f.axes()[i];
325 std::array::from_fn(|j| {
326 if ax.index() == j {
327 if ax.is_negative() { -1 } else { 1 }
328 } else {
329 0
330 }
331 })
332 });
333 let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
334 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
335 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
336 assert_eq!(f.determinant(), det, "{f:?}");
337 n += 1;
338 }
339 }
340 }
341 assert_eq!(n, 48, "6 permutations x 8 sign choices");
342 }
343
344 #[test]
345 fn invalid_frames_are_errors() {
346 assert_eq!(
347 SourceFrame::try_new([PosX, NegX, PosZ], 1.0),
348 Err(FrameError::NotAPermutation([PosX, NegX, PosZ]))
349 );
350 assert!(matches!(
351 SourceFrame::try_new([PosX, PosY, PosZ], 0.0),
352 Err(FrameError::BadScale(_))
353 ));
354 assert!(matches!(
355 SourceFrame::try_new([PosX, PosY, PosZ], f64::NAN),
356 Err(FrameError::BadScale(_))
357 ));
358 }
359
360 const IDENTITY: [[f32; 4]; 4] = [
361 [1.0, 0.0, 0.0, 0.0],
362 [0.0, 1.0, 0.0, 0.0],
363 [0.0, 0.0, 1.0, 0.0],
364 [0.0, 0.0, 0.0, 1.0],
365 ];
366
367 fn mat_mul(a: &[[f32; 4]; 4], b: &[[f32; 4]; 4]) -> [[f32; 4]; 4] {
368 std::array::from_fn(|j| std::array::from_fn(|i| (0..4).map(|k| a[k][i] * b[j][k]).sum()))
369 }
370
371 fn mat_apply(m: &[[f32; 4]; 4], p: [f32; 3]) -> [f32; 4] {
372 let p = [p[0], p[1], p[2], 1.0];
373 std::array::from_fn(|i| (0..4).map(|k| m[k][i] * p[k]).sum())
374 }
375
376 fn xyz(v: [f32; 4]) -> [f32; 3] {
377 [v[0], v[1], v[2]]
378 }
379
380 #[test]
385 fn matrices_take_and_give_the_frames_points() {
386 let m: [[f32; 4]; 4] = [
387 [0.6, -0.8, 0.1, 0.0],
388 [0.8, 0.6, -0.3, 0.0],
389 [0.25, 0.5, 0.9, 0.0],
390 [3.0, -2.0, 7.5, 1.0],
391 ];
392 let p = [1.5, -2.25, 4.0];
393 let world = mat_apply(&m, p);
394 let all = [PosX, NegX, PosY, NegY, PosZ, NegZ];
395 let mut n = 0;
396 for a in all {
397 for b in all {
398 for c in all {
399 let Ok(f) = SourceFrame::try_new([a, b, c], 1.0) else {
400 continue;
401 };
402 let q = f.point_to_source(p);
403 let from = mat_apply(&f.from_world_matrix_to_source(m), q);
404 assert!(
405 from.iter().zip(world).all(|(x, y)| (x - y).abs() <= 1e-5),
406 "{f:?}"
407 );
408 let moved = f.point_to_source(xyz(world));
409 let within = xyz(mat_apply(&f.world_matrix_to_source(m), q));
410 assert!(
411 within.iter().zip(moved).all(|(x, y)| (x - y).abs() <= 1e-5),
412 "{f:?}"
413 );
414 let to = xyz(mat_apply(&f.to_world_matrix_to_source(m), p));
415 assert_eq!(bits(to), bits(moved), "{f:?}");
416 let entries = |m: [[f32; 4]; 4]| {
417 let mut e: Vec<u32> = m.iter().flatten().map(|x| x.abs().to_bits()).collect();
418 e.sort_unstable();
419 e
420 };
421 assert_eq!(entries(f.world_matrix_to_source(m)), entries(m), "{f:?}");
422 n += 1;
423 }
424 }
425 }
426 assert_eq!(n, 48);
427 let from = mat_apply(
429 &FROMSOFT.from_world_matrix_to_source(m),
430 FROMSOFT.point_to_source(p),
431 );
432 assert_eq!(from.map(f32::to_bits), world.map(f32::to_bits));
433 let back = mat_mul(
436 &FROMSOFT.from_world_matrix_to_source(m),
437 &FROMSOFT.to_world_matrix_to_source(IDENTITY),
438 );
439 assert_eq!(back, m);
440 let cm = SourceFrame::new([PosX, PosZ, NegY], 0.01);
442 let from = mat_apply(&cm.from_world_matrix_to_source(m), cm.point_to_source(p));
443 assert!(
444 from.iter().zip(world).all(|(x, y)| (x - y).abs() <= 1e-4),
445 "{from:?}"
446 );
447 assert_eq!(cm.length_to_source(2.0), 200.0);
448 assert_eq!(cm.inverse_length_to_source(0.5), 0.005);
449 let plane = cm.plane_to_source([0.0, 1.0, 0.0, -3.0]);
450 let on = cm.point_to_source([5.0, 3.0, -1.0]);
451 let d = plane[0] * on[0] + plane[1] * on[1] + plane[2] * on[2] + plane[3];
452 assert!(d.abs() < 1e-3, "{plane:?}");
453 assert_eq!(
454 FROMSOFT.tangent_to_source([1.0, 0.0, 0.5, 1.0]),
455 [1.0, 0.0, -0.5, -1.0]
456 );
457 assert_eq!(SourceFrame::ENU.tangent_to_source([1.0, 0.0, 0.5, 1.0])[3], 1.0);
458 }
459
460 #[test]
461 fn fromsoft_and_unity_mirror_z() {
462 assert_eq!(FROMSOFT.point([1.5, 2.0, 3.0]), [1.5, 2.0, -3.0]);
463 assert_eq!(UNITY.direction([0.0, 0.0, 1.0]), [0.0, 0.0, -1.0]);
464 let m = Transform {
466 cols: [
467 [1.0, 2.0, 3.0],
468 [4.0, 5.0, 6.0],
469 [7.0, 8.0, 9.0],
470 [10.0, 11.0, 12.0],
471 ],
472 };
473 let want = Transform {
474 cols: [
475 [1.0, 2.0, -3.0],
476 [4.0, 5.0, -6.0],
477 [-7.0, -8.0, 9.0],
478 [10.0, 11.0, -12.0],
479 ],
480 };
481 assert_eq!(FROMSOFT.transform(&m), want);
482 }
483
484 #[test]
485 fn enu_is_the_writer_frame_bit_exact_with_signed_zeros() {
486 let world = [0.0, -0.0, 0.0];
488 let enu = SourceFrame::ENU.point_to_source(world);
489 assert_eq!(bits(enu), bits([0.0, -0.0, -0.0]));
490 assert_eq!(bits(SourceFrame::ENU.point(enu)), bits(world));
491 let p = [1.25, -7.5, 3.0e-41]; assert_eq!(SourceFrame::ENU.point_to_source(p), [1.25, -3.0e-41, -7.5]);
493 assert_eq!(ground(p), [1.25, -3.0e-41]);
494 assert_eq!(
495 ground([0.0, 0.0, 0.0]).map(f32::to_bits),
496 [0.0f32.to_bits(), (-0.0f32).to_bits()]
497 );
498 assert_eq!(SourceFrame::ENU.point([10.0, 20.0, 30.0]), [10.0, 30.0, -20.0]);
500 }
501
502 #[test]
503 fn forza_through_blender_matches_forza_direct() {
504 let to_blender = |p: [f32; 3]| [-p[0], -p[2], p[1]]; for p in [[1.0, 2.0, 3.0], [-0.0, 0.5, -4.0], [7.25, -1.0, 0.0]] {
506 assert_eq!(bits(FORZA.point(p)), bits(SourceFrame::ENU.point(to_blender(p))));
507 }
508 }
509
510 #[test]
511 fn transforms_commute_with_points_and_invert() {
512 let all = [PosX, NegX, PosY, NegY, PosZ, NegZ];
513 let m = Transform {
515 cols: [
516 [2.0, 0.0, 1.0],
517 [-1.0, 3.0, 0.0],
518 [0.0, 1.0, -2.0],
519 [5.0, -6.0, 7.0],
520 ],
521 };
522 let locals = [[1.0, 2.0, 3.0], [-4.0, 0.0, 1.0], [0.5, -0.25, 8.0]];
523 for a in all {
524 for b in all {
525 for c in all {
526 for scale in [1.0, 0.5, 4.0] {
527 let Ok(f) = SourceFrame::try_new([a, b, c], scale) else {
528 continue;
529 };
530 let fm = f.transform(&m);
531 for l in locals {
532 assert_eq!(f.point(mul(&m, l)), mul(&fm, f.point(l)), "{f:?}");
533 }
534 assert_eq!(f.transform_to_source(&fm), m, "{f:?}");
535 assert_eq!(f.inverse().transform(&fm), m, "{f:?}");
536 assert_eq!(f.inverse().inverse(), f);
537 for l in locals {
538 assert_eq!(bits(f.point_to_source(f.point(l))), bits(l));
539 assert_eq!(bits(f.inverse().point(f.point(l))), bits(l));
540 assert_eq!(bits(f.direction_to_source(f.direction(l))), bits(l));
541 }
542 }
543 }
544 }
545 }
546 }
547
548 #[test]
549 fn scale_applies_to_points_and_translations_only() {
550 let cm = SourceFrame::new([Axis::PosX, Axis::PosY, Axis::PosZ], 0.01);
551 assert_eq!(cm.point([100.0, 250.0, -50.0]), [1.0, 2.5, -0.5]);
552 assert_eq!(cm.direction([0.0, 1.0, 0.0]), [0.0, 1.0, 0.0]);
553 let m = Transform {
554 cols: [
555 [1.0, 0.0, 0.0],
556 [0.0, 1.0, 0.0],
557 [0.0, 0.0, 1.0],
558 [300.0, 0.0, 0.0],
559 ],
560 };
561 assert_eq!(cm.transform(&m).cols[3], [3.0, 0.0, 0.0]);
562 assert_eq!(
563 cm.transform(&m).cols[0],
564 [1.0, 0.0, 0.0],
565 "the scale cancels in the linear part"
566 );
567 }
568}