polyhorn_ui/linalg/inter/
compose.rs

1use num_traits::Float;
2
3use super::algebra::{Combine, Cross, Dot, Vector};
4use super::LaplaceExpansion3D;
5use crate::linalg::{Quaternion3D, Transform3D};
6
7/// This represents the decomposition of a 3D transformation matrix into
8/// three-component translation, scale and skew vectors, a four-component
9/// perspective vector and a rotation quaternion.
10#[derive(Copy, Clone, Debug, Default, PartialEq)]
11pub struct Decomposition3D<T> {
12    /// This is the three-component translation vector of this decomposition.
13    pub translation: [T; 3],
14
15    /// This is the three-component scale vector of this decomposition.
16    pub scale: [T; 3],
17
18    /// This is the three-component skew vector of this decomposition.
19    pub skew: [T; 3],
20
21    /// This is the four-component perspective vector of this decomposition.
22    pub perspective: [T; 4],
23
24    /// This is the rotation quaternion of this decomposition.
25    pub quaternion: Quaternion3D<T>,
26}
27
28impl<T> Decomposition3D<T> {
29    /// Returns a new transformation matrix decomposition with the given
30    /// translation, scale, skew, perspective and rotation quaternion.
31    pub fn new(
32        translation: [T; 3],
33        scale: [T; 3],
34        skew: [T; 3],
35        perspective: [T; 4],
36        quaternion: Quaternion3D<T>,
37    ) -> Decomposition3D<T> {
38        Decomposition3D {
39            translation,
40            scale,
41            skew,
42            perspective,
43            quaternion,
44        }
45    }
46
47    /// Applies the given operation to each element in this decomposition and
48    /// returns the result. The operation may return a value of different type
49    /// than the decomposition's previous value type.
50    pub fn map<F, O>(self, mut op: F) -> Decomposition3D<O>
51    where
52        F: FnMut(T) -> O,
53    {
54        let [tx, ty, tz] = self.translation;
55        let [sx, sy, sz] = self.scale;
56        let [skx, sky, skz] = self.skew;
57        let [px, py, pz, pw] = self.perspective;
58
59        Decomposition3D {
60            translation: [op(tx), op(ty), op(tz)],
61            scale: [op(sx), op(sy), op(sz)],
62            skew: [op(skx), op(sky), op(skz)],
63            perspective: [op(px), op(py), op(pz), op(pw)],
64            quaternion: self.quaternion.map(op),
65        }
66    }
67
68    /// Returns a new decomposition with references to all elements of the
69    /// previous composition. This is particularly useful if `T` does not
70    /// implement `Copy`.
71    pub fn as_ref(&self) -> Decomposition3D<&T> {
72        let [tx, ty, tz] = &self.translation;
73        let [sx, sy, sz] = &self.scale;
74        let [skx, sky, skz] = &self.skew;
75        let [px, py, pz, pw] = &self.perspective;
76
77        Decomposition3D {
78            translation: [tx, ty, tz],
79            scale: [sx, sy, sz],
80            skew: [skx, sky, skz],
81            perspective: [px, py, pz, pw],
82            quaternion: self.quaternion.as_ref(),
83        }
84    }
85}
86
87impl<T> Decomposition3D<T>
88where
89    T: Default + Float,
90{
91    /// Decomposes a 3D transformation matrix into its three-component
92    /// translation, scale and skew vectors, its four-component perspective
93    /// vector and its rotation quaternion.
94    pub fn decompose(mut matrix: Transform3D<T>) -> Option<Decomposition3D<T>> {
95        let mut result = Decomposition3D::default();
96
97        // We start by normalizing the matrix. This is done by dividing each
98        // entry by the bottom-right entry (i.e. the bottom right entry must
99        // become 1). Of course, this isn't possible if the bottom right entry
100        // is zero.
101        if matrix.columns[3][3].is_zero() {
102            return None;
103        }
104
105        for i in 0..4 {
106            for j in 0..4 {
107                matrix.columns[i][j] = matrix.columns[i][j] / matrix.columns[3][3];
108            }
109        }
110
111        // perspectiveMatrix is used to solve for perspective, but it also
112        // provides an easy way to test for singularity of the upper 3x3
113        // component.
114        let mut perspective_matrix = matrix;
115
116        for i in 0..3 {
117            perspective_matrix.columns[i][3] = T::zero();
118        }
119
120        perspective_matrix.columns[3][3] = T::one();
121
122        let perspective_matrix = LaplaceExpansion3D::new(&perspective_matrix);
123
124        if perspective_matrix.determinant().is_zero() {
125            return None;
126        }
127
128        // First, isolate perspective.
129        if !matrix.columns[0][3].is_zero()
130            || !matrix.columns[1][3].is_zero()
131            || !matrix.columns[2][3].is_zero()
132        {
133            unimplemented!(
134                "Decomposing transformation matrices with a perspective is not yet implemented."
135            )
136        } else {
137            // No perspective.
138            result.perspective = [T::zero(), T::zero(), T::zero(), T::one()];
139        }
140
141        // Next, take care of translation.
142        for i in 0..3 {
143            result.translation[i] = matrix.columns[3][i];
144        }
145
146        // Now, get scale and shear.
147        let mut row = [
148            <[T; 3]>::default(),
149            <[T; 3]>::default(),
150            <[T; 3]>::default(),
151        ];
152
153        for i in 0..3 {
154            row[i][0] = matrix.columns[i][0];
155            row[i][1] = matrix.columns[i][1];
156            row[i][2] = matrix.columns[i][2];
157        }
158
159        // Compute X scale factor and normalize first row.
160        result.scale[0] = row[0].length();
161        row[0] = row[0].normalize();
162
163        // Compute XY shear factor and make 2nd row orthogonal to 1st.
164        result.skew[0] = row[0].dot(&row[1]);
165        row[1] = row[1].combine(T::one(), &row[0], -result.skew[0]);
166
167        // Now, compute Y scale and normalize 2nd row.
168        result.scale[1] = row[1].length();
169        row[1] = row[1].normalize();
170        result.skew[0] = result.skew[0] / result.scale[1];
171
172        // Compute XZ and YZ shears, orthogonalize 3rd row.
173        result.skew[1] = row[0].dot(&row[2]);
174        row[2] = row[2].combine(T::one(), &row[0], -result.skew[1]);
175        result.skew[2] = row[1].dot(&row[2]);
176        row[2] = row[2].combine(T::one(), &row[1], -result.skew[2]);
177
178        // Next, get Z scale and normalize 3rd row.
179        result.scale[2] = row[2].length();
180        row[2] = row[2].normalize();
181        result.skew[1] = result.skew[1] / result.scale[2];
182        result.skew[2] = result.skew[2] / result.scale[2];
183
184        // At this point, the matrix (in rows) is orthonormal. Check for a
185        // coordinate system flip. If the determinant is -1, then negate the
186        // matrix and the scaling factors.
187        let pdum3 = row[1].cross(&row[2]);
188
189        if row[0].dot(&pdum3) < T::zero() {
190            for i in 0..3 {
191                result.scale[i] = result.scale[i].neg();
192                row[i][0] = row[i][0].neg();
193                row[i][1] = row[i][1].neg();
194                row[i][2] = row[i][2].neg();
195            }
196        }
197
198        // Now, get the rotations out.
199        let half = T::from(0.5).unwrap();
200        result.quaternion.x = half
201            * (T::one() + row[0][0] - row[1][1] - row[2][2])
202                .max(T::zero())
203                .sqrt();
204        result.quaternion.y = half
205            * (T::one() - row[0][0] + row[1][1] - row[2][2])
206                .max(T::zero())
207                .sqrt();
208        result.quaternion.z = half
209            * (T::one() - row[0][0] - row[1][1] + row[2][2])
210                .max(T::zero())
211                .sqrt();
212        result.quaternion.w = half
213            * (T::one() + row[0][0] + row[1][1] + row[2][2])
214                .max(T::zero())
215                .sqrt();
216
217        if row[2][1] > row[1][2] {
218            result.quaternion.x = result.quaternion.x.neg();
219        }
220
221        if row[0][2] > row[2][0] {
222            result.quaternion.y = result.quaternion.y.neg();
223        }
224
225        if row[1][0] > row[0][1] {
226            result.quaternion.z = result.quaternion.z.neg();
227        }
228
229        Some(result)
230    }
231
232    /// This function interpolates between this decomposition and the other
233    /// given decomposition with the given progress value. If the progress value
234    /// is zero, it will return this decomposition. If the progress value is
235    /// one, it will return the other decomposition.
236    pub fn interpolate(&self, other: &Self, progress: T) -> Self {
237        let mut result = *self;
238
239        let alpha = T::one() - progress;
240        let beta = progress;
241
242        result.translation = result.translation.combine(alpha, &other.translation, beta);
243        result.scale = result.scale.combine(alpha, &other.scale, beta);
244        result.skew = result.skew.combine(alpha, &other.skew, beta);
245        result.perspective = result.perspective.combine(alpha, &other.perspective, beta);
246        result.quaternion = self.quaternion.mix(alpha, other.quaternion);
247
248        result
249    }
250
251    /// Reconstructs the 3D transformation matrix from this decomposition.
252    pub fn recompose(&self) -> Transform3D<T> {
253        let mut result = Transform3D::identity();
254
255        // Apply perspective.
256        for i in 0..4 {
257            result.columns[i][3] = self.perspective[i];
258        }
259
260        // Apply translation.
261        for i in 0..4 {
262            for j in 0..3 {
263                result.columns[3][i] =
264                    result.columns[3][i] + self.translation[j] * result.columns[j][i]
265            }
266        }
267
268        // Apply rotation.
269        result = result.rotate(self.quaternion);
270
271        if !self.skew[2].is_zero() {
272            let mut temp = Transform3D::identity();
273            temp.columns[2][1] = self.skew[2];
274            result = result.concat(temp);
275        }
276
277        if !self.skew[1].is_zero() {
278            let mut temp = Transform3D::identity();
279            temp.columns[2][0] = self.skew[1];
280            result = result.concat(temp);
281        }
282
283        if !self.skew[0].is_zero() {
284            let mut temp = Transform3D::identity();
285            temp.columns[1][0] = self.skew[0];
286            result = result.concat(temp);
287        }
288
289        // Apply scale.
290        for i in 0..3 {
291            for j in 0..4 {
292                result.columns[i][j] = result.columns[i][j] * self.scale[i];
293            }
294        }
295
296        result
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use std::f32::consts::PI;
304
305    fn interpolate<T>(a: Transform3D<T>, b: Transform3D<T>, t: T) -> Transform3D<T>
306    where
307        T: Default + Float,
308    {
309        let ad = Decomposition3D::decompose(a).unwrap();
310        let bd = Decomposition3D::decompose(b).unwrap();
311        let cd = ad.interpolate(&bd, t);
312        cd.recompose()
313    }
314
315    #[test]
316    fn test_halfway_rotation() {
317        assert_eq!(
318            interpolate(
319                Transform3D::identity(),
320                Transform3D::with_rotation(Quaternion3D::with_angle(
321                    90.0 / 180.0 * PI,
322                    0.0,
323                    0.0,
324                    1.0
325                )),
326                0.0
327            ),
328            Transform3D::with_rotation(Quaternion3D::with_angle(0.0 / 180.0 * PI, 0.0, 0.0, 1.0))
329        );
330
331        assert_eq!(
332            interpolate(
333                Transform3D::identity(),
334                Transform3D::with_rotation(Quaternion3D::with_angle(
335                    90.0 / 180.0 * PI,
336                    0.0,
337                    0.0,
338                    1.0
339                )),
340                1.0
341            ),
342            Transform3D::with_rotation(Quaternion3D::with_angle(90.0 / 180.0 * PI, 0.0, 0.0, 1.0))
343        );
344    }
345
346    #[test]
347    fn test_halfway_rotation_nonunit() {
348        assert_eq!(
349            interpolate(
350                Transform3D::identity(),
351                Transform3D::with_rotation(Quaternion3D::with_angle(
352                    90.0 / 180.0 * PI,
353                    0.5,
354                    0.3,
355                    0.8
356                )),
357                0.5
358            ),
359            Transform3D::with_rotation(Quaternion3D::with_angle(45.0 / 180.0 * PI, 0.5, 0.3, 0.8))
360        );
361    }
362
363    #[test]
364    fn test_halfway_translation() {
365        assert_eq!(
366            interpolate(
367                Transform3D::identity(),
368                Transform3D::with_translation(160.0, 20.0, 10.0),
369                0.5
370            ),
371            Transform3D::with_translation(80.0, 10.0, 5.0)
372        );
373
374        assert_eq!(
375            interpolate(
376                Transform3D::with_translation(160.0, 20.0, 10.0),
377                Transform3D::identity(),
378                0.5
379            ),
380            Transform3D::with_translation(80.0, 10.0, 5.0)
381        );
382
383        assert_eq!(
384            interpolate(
385                Transform3D::identity(),
386                Transform3D::with_translation(-160.0, -20.0, 10.0),
387                0.5
388            ),
389            Transform3D::with_translation(-80.0, -10.0, 5.0)
390        );
391
392        assert_eq!(
393            interpolate(
394                Transform3D::with_translation(-160.0, -20.0, 10.0),
395                Transform3D::identity(),
396                0.5
397            ),
398            Transform3D::with_translation(-80.0, -10.0, 5.0)
399        );
400    }
401
402    #[test]
403    fn test_decomposition_1() {
404        let transform = Transform3D::with_translation(160.0, 20.0, 10.0)
405            .rotate(Quaternion3D::with_angle(90.0 / 180.0 * PI, 0.0, 0.0, 1.0));
406
407        let decomposition = Decomposition3D::decompose(transform).unwrap();
408
409        let distance = decomposition.quaternion.subtract(&Quaternion3D::with_angle(
410            90.0 / 180.0 * PI,
411            0.0,
412            0.0,
413            1.0,
414        ));
415
416        assert!(distance.dot(&distance) <= 0.001);
417    }
418
419    #[test]
420    fn test_decomposition_2() {
421        let transform = Transform3D::with_translation(160.0, 20.0, 10.0)
422            .rotate(Quaternion3D::with_angle(90.0 / 180.0 * PI, 0.0, 0.0, 1.0));
423
424        let decomposition = Decomposition3D::decompose(transform).unwrap();
425
426        assert_eq!(decomposition.translation, [160.0, 20.0, 10.0]);
427
428        let distance = decomposition.quaternion.subtract(&Quaternion3D::with_angle(
429            90.0 / 180.0 * PI,
430            0.0,
431            0.0,
432            1.0,
433        ));
434
435        assert!(distance.dot(&distance) <= 0.001);
436    }
437
438    #[test]
439    fn test_interpolation() {
440        let start = Transform3D::identity();
441        let end = Transform3D::with_translation(160.0, 20.0, 10.0)
442            .rotate(Quaternion3D::with_angle(90.0 / 180.0 * PI, 0.0, 0.0, 1.0));
443
444        let start = Decomposition3D::decompose(start).unwrap();
445        let end = Decomposition3D::decompose(end).unwrap();
446
447        let mid = start.interpolate(&end, 0.5);
448
449        assert_eq!(mid.translation, [80.0, 10.0, 5.0]);
450
451        let distance =
452            mid.quaternion
453                .subtract(&Quaternion3D::with_angle(45.0 / 180.0 * PI, 0.0, 0.0, 1.0));
454
455        assert!(distance.dot(&distance) <= 0.001);
456    }
457
458    #[test]
459    fn test_complex() {
460        assert_eq!(
461            interpolate(
462                Transform3D::identity(),
463                Transform3D::with_translation(160.0, 20.0, 10.0).rotate(Quaternion3D::with_angle(
464                    90.0 / 180.0 * PI,
465                    0.0,
466                    0.0,
467                    1.0
468                )),
469                0.5
470            ),
471            Transform3D::with_translation(80.0, 10.0, 5.0).rotate(Quaternion3D::with_angle(
472                45.0 / 180.0 * PI,
473                0.0,
474                0.0,
475                1.0
476            ))
477        );
478    }
479}