Browse Source

Improved Transforms.AffineTransform structure so it can represent a Matrix or a SRT transform chain.

vpenades 4 years ago
parent
commit
2681603bdf

+ 10 - 13
src/SharpGLTF.Core/Schema2/gltf.Node.cs

@@ -195,13 +195,14 @@ namespace SharpGLTF.Schema2
             set
             set
             {
             {
                 Guard.IsFalse(this._skin.HasValue, _NOTRANSFORMMESSAGE);
                 Guard.IsFalse(this._skin.HasValue, _NOTRANSFORMMESSAGE);
-
                 Guard.IsTrue(value.IsValid, nameof(value));
                 Guard.IsTrue(value.IsValid, nameof(value));
 
 
+                var decomposed = value.GetDecomposed();
+
                 _matrix = null;
                 _matrix = null;
-                _scale = value.Scale.AsNullable(Vector3.One);
-                _rotation = value.Rotation.Sanitized().AsNullable(Quaternion.Identity);
-                _translation = value.Translation.AsNullable(Vector3.Zero);
+                _scale = decomposed.Scale.AsNullable(Vector3.One);
+                _rotation = decomposed.Rotation.Sanitized().AsNullable(Quaternion.Identity);
+                _translation = decomposed.Translation.AsNullable(Vector3.Zero);
             }
             }
         }
         }
 
 
@@ -813,17 +814,13 @@ namespace SharpGLTF.Schema2
 
 
         public Transforms.AffineTransform GetLocalTransform(Single time)
         public Transforms.AffineTransform GetLocalTransform(Single time)
         {
         {
-            var xform = TargetNode.LocalTransform;
-
-            var sfunc = Scale?.CreateCurveSampler();
-            var rfunc = Rotation?.CreateCurveSampler();
-            var tfunc = Translation?.CreateCurveSampler();
+            var xform = TargetNode.LocalTransform.GetDecomposed();
 
 
-            if (sfunc != null) xform.Scale = sfunc.GetPoint(time);
-            if (rfunc != null) xform.Rotation = rfunc.GetPoint(time);
-            if (tfunc != null) xform.Translation = tfunc.GetPoint(time);
+            var s = Scale?.CreateCurveSampler()?.GetPoint(time) ?? xform.Scale;
+            var r = Rotation?.CreateCurveSampler()?.GetPoint(time) ?? xform.Rotation;
+            var t = Translation?.CreateCurveSampler()?.GetPoint(time) ?? xform.Translation;
 
 
-            return xform;
+            return new Transforms.AffineTransform(s, r, t);
         }
         }
 
 
         public IReadOnlyList<float> GetMorphingWeights(Single time)
         public IReadOnlyList<float> GetMorphingWeights(Single time)

+ 315 - 49
src/SharpGLTF.Core/Transforms/AffineTransform.cs

@@ -6,38 +6,55 @@ using System.Text;
 namespace SharpGLTF.Transforms
 namespace SharpGLTF.Transforms
 {
 {
     /// <summary>
     /// <summary>
-    /// Represents an affine transform in 3D space, defined by:
-    /// - A <see cref="Vector3"/> scale.
-    /// - A <see cref="Quaternion"/> rotation.
-    /// - A <see cref="Vector3"/> translation.
+    /// Represents an affine transform in 3D space, with two exclusive representantions:<br/>
+    /// <list type="bullet">
+    /// <item>
+    /// When <see cref="IsMatrix"/> is true, A 4x3 Matrix. which is publicly<br/>
+    /// exposed as a <see cref="Matrix4x4"/> matrix.
+    /// </item>
+    /// <item>
+    /// When <see cref="IsDecomposed"/> is true, A decomposed transform defined by:<br/>
+    /// <see cref="Vector3"/> Scale.<br/>
+    /// <see cref="Quaternion"/> Rotation.<br/>
+    /// <see cref="Vector3"/> Translation.
+    /// </item>
+    /// </list>
     /// </summary>
     /// </summary>
     /// <remarks>
     /// <remarks>
-    /// <see cref="AffineTransform"/> cannot represent skewed matrices. This means
-    /// that it can be used to represent <see cref="Schema2.Node"/> local transforms,
-    /// but since chained transforms can become skewed, a world transform cannot be
-    /// represented by a <see cref="AffineTransform"/>.
+    /// <para>
+    /// Depending on how <see cref="AffineTransform"/> structures are created, the underlaying<br/>
+    /// fields must be interprested as a Matrix4x3 or a decomposed sequence of Scale, Rotation and<br/>
+    /// Translation.
+    /// </para>
+    /// <para>
+    /// This approach allows <see cref="AffineTransform"/> to preserve the source data, avoiding loosing<br/>
+    /// precission when decomposing a matrix, or creating a matrix from a SRT transform.
+    /// </para>
+    /// <para>
+    /// Decomposing matrices is tricky because not all valid matrices can be decomposed; in particular<br/>
+    /// squewed matrices will fail to decompose. See <see href="https://github.com/vpenades/SharpGLTF/issues/41"/>.
+    /// </para>
     /// </remarks>
     /// </remarks>
-    /// <see href="https://github.com/vpenades/SharpGLTF/issues/41"/>
     [System.Diagnostics.DebuggerDisplay("AffineTransform 𝐒:{Scale} 𝐑:{Rotation} 𝚻:{Translation}")]
     [System.Diagnostics.DebuggerDisplay("AffineTransform 𝐒:{Scale} 𝐑:{Rotation} 𝚻:{Translation}")]
-    public struct AffineTransform
+    public readonly struct AffineTransform
     {
     {
-        #region lifecycle
+        #region constants
+
+        private const string _CannotDecomposeMessage = "Matrix is invalid or skewed.";
+
+        public static readonly AffineTransform Identity = new AffineTransform(null, null, null);
+
+        #endregion
+
+        #region factories
 
 
         public static implicit operator AffineTransform(Matrix4x4 matrix) { return new AffineTransform(matrix); }
         public static implicit operator AffineTransform(Matrix4x4 matrix) { return new AffineTransform(matrix); }
 
 
-        public AffineTransform(Matrix4x4 matrix)
+        public static AffineTransform CreateDecomposed(Matrix4x4 matrix)
         {
         {
-            if (!Matrix4x4.Decompose(matrix, out this.Scale, out this.Rotation, out this.Translation))
-            {
-                throw new ArgumentException("matrix is invalid or skewed.", nameof(matrix));
-            }
-        }
+            if (!Matrix4x4.Decompose(matrix, out var s, out var r, out var t)) throw new ArgumentException("Can't decompose", nameof(matrix));
 
 
-        public AffineTransform(Vector3? scale, Quaternion? rotation, Vector3? translation)
-        {
-            this.Scale = scale ?? Vector3.One;
-            this.Rotation = rotation ?? Quaternion.Identity;
-            this.Translation = translation ?? Vector3.Zero;
+            return new AffineTransform(s, r, t);
         }
         }
 
 
         public static AffineTransform CreateFromAny(Matrix4x4? matrix, Vector3? scale, Quaternion? rotation, Vector3? translation)
         public static AffineTransform CreateFromAny(Matrix4x4? matrix, Vector3? scale, Quaternion? rotation, Vector3? translation)
@@ -52,52 +69,199 @@ namespace SharpGLTF.Transforms
             }
             }
         }
         }
 
 
+        public AffineTransform WithScale(Vector3 scale)
+        {
+            return new AffineTransform(scale, this.Rotation, this.Translation);
+        }
+
+        public AffineTransform WithRotation(Quaternion rotation)
+        {
+            return new AffineTransform(this.Scale, rotation, this.Translation);
+        }
+
+        public AffineTransform WithTranslation(Vector3 translation)
+        {
+            return new AffineTransform(this.Scale, this.Rotation, translation);
+        }
+
+        #endregion
+
+        #region constructors
+
+        public AffineTransform(Matrix4x4 matrix)
+        {
+            if (matrix.M14 != 0) throw new ArgumentException(nameof(matrix));
+            if (matrix.M24 != 0) throw new ArgumentException(nameof(matrix));
+            if (matrix.M34 != 0) throw new ArgumentException(nameof(matrix));
+            if (matrix.M44 != 1) throw new ArgumentException(nameof(matrix));
+
+            _Representation = 0;
+
+            _M11 = matrix.M11;
+            _M12 = matrix.M12;
+            _M13 = matrix.M13;
+
+            _M21 = matrix.M21;
+            _M22 = matrix.M22;
+            _M23 = matrix.M23;
+
+            _M31 = matrix.M31;
+            _M32 = matrix.M32;
+            _M33 = matrix.M33;
+
+            _Translation = matrix.Translation;
+        }
+
+        public AffineTransform(Vector3? scale, Quaternion? rotation, Vector3? translation)
+            : this(scale ?? Vector3.One, rotation ?? Quaternion.Identity, translation ?? Vector3.Zero)
+        { }
+
+        public AffineTransform(Vector3 scale, Quaternion rotation, Vector3 translation)
+        {
+            _Representation = 1;
+
+            _M11 = scale.X;
+            _M12 = scale.Y;
+            _M13 = scale.Z;
+
+            _M21 = rotation.X;
+            _M22 = rotation.Y;
+            _M23 = rotation.Z;
+            _M31 = rotation.W;
+            _M32 = 0;
+            _M33 = 0;
+
+            this._Translation = translation;
+        }
+
         #endregion
         #endregion
 
 
         #region data
         #region data
 
 
         /// <summary>
         /// <summary>
-        /// Rotation
+        /// Determines the underlaying representation:<br/>
+        /// 0 - Fields must be interpreted as a Matrix4x3.<br/>
+        /// 1 - Fields must be interpreted as a Scale, Rotation and Translation sequence.
         /// </summary>
         /// </summary>
-        public Quaternion Rotation;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly Int32 _Representation;
 
 
         /// <summary>
         /// <summary>
-        /// Scale
+        /// Matrix:  M11<br/>
+        /// Decomposed: Scale.X
         /// </summary>
         /// </summary>
-        public Vector3 Scale;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly float _M11;
 
 
         /// <summary>
         /// <summary>
-        /// Translation
+        /// Matrix:  M12<br/>
+        /// Decomposed: Scale.Y
         /// </summary>
         /// </summary>
-        public  Vector3 Translation;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly float _M12;
+
+        /// <summary>
+        /// Matrix:  M13<br/>
+        /// Decomposed: Scale.Z
+        /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly float _M13;
+
+        /// <summary>
+        /// Matrix:  M21<br/>
+        /// Decomposed: Rotation.X
+        /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly float _M21;
+
+        /// <summary>
+        /// Matrix:  M22<br/>
+        /// Decomposed: Rotation.Y
+        /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly float _M22;
+
+        /// <summary>
+        /// Matrix:  M23<br/>
+        /// Decomposed: Rotation.Z
+        /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly float _M23;
+
+        /// <summary>
+        /// Matrix:  M31<br/>
+        /// Decomposed: Rotation.W
+        /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly float _M31;
+
+        /// <summary>
+        /// Matrix:  M32<br/>
+        /// Decomposed: unused
+        /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly float _M32;
+
+        /// <summary>
+        /// Matrix:  M32<br/>
+        /// Decomposed: unused
+        /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly float _M33;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly Vector3 _Translation;
 
 
         #endregion
         #endregion
 
 
         #region properties
         #region properties
+        public bool IsMatrix => _Representation == 0;
+        public bool IsDecomposed => _Representation == 1;
+
+        /// <summary>
+        /// Gets the scale.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// When the internal representation is a Matrix and cannot be decomposed.
+        /// </exception>
+        public Vector3 Scale => _GetScale();
+
+        /// <summary>
+        /// Gets the rotation.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// When the internal representation is a Matrix and cannot be decomposed.
+        /// </exception>
+        public Quaternion Rotation => _GetRotation();
 
 
-        public static AffineTransform Identity => new AffineTransform { Rotation = Quaternion.Identity, Scale = Vector3.One, Translation = Vector3.Zero };
+        /// <summary>
+        /// Gets the translation
+        /// </summary>
+        public Vector3 Translation => _Translation;
 
 
         /// <summary>
         /// <summary>
         /// Gets the <see cref="Matrix4x4"/> transform of the current <see cref="AffineTransform"/>
         /// Gets the <see cref="Matrix4x4"/> transform of the current <see cref="AffineTransform"/>
         /// </summary>
         /// </summary>
-        public Matrix4x4 Matrix
-        {
-            get
-            {
-                var m = Matrix4x4.CreateScale(this.Scale) * Matrix4x4.CreateFromQuaternion(this.Rotation.Sanitized());
-                m.Translation = this.Translation;
-                return m;
-            }
-        }
+        public Matrix4x4 Matrix => _GetMatrix();
 
 
         public bool IsValid
         public bool IsValid
         {
         {
             get
             get
             {
             {
-                if (!Scale._IsFinite()) return false;
-                if (!Rotation._IsFinite()) return false;
                 if (!Translation._IsFinite()) return false;
                 if (!Translation._IsFinite()) return false;
 
 
+                if (!_M11._IsFinite()) return false;
+                if (!_M12._IsFinite()) return false;
+                if (!_M13._IsFinite()) return false;
+
+                if (!_M21._IsFinite()) return false;
+                if (!_M22._IsFinite()) return false;
+                if (!_M23._IsFinite()) return false;
+
+                if (!_M31._IsFinite()) return false;
+                if (!_M32._IsFinite()) return false;
+                if (!_M33._IsFinite()) return false;
+
                 return true;
                 return true;
             }
             }
         }
         }
@@ -106,9 +270,39 @@ namespace SharpGLTF.Transforms
         {
         {
             get
             get
             {
             {
-                if (Scale != Vector3.One) return false;
-                if (Rotation != Quaternion.Identity) return false;
                 if (Translation != Vector3.Zero) return false;
                 if (Translation != Vector3.Zero) return false;
+
+                if (IsDecomposed)
+                {
+                    // scale
+                    if (_M11 != 1) return false;
+                    if (_M12 != 1) return false;
+                    if (_M13 != 1) return false;
+
+                    // rotation
+                    if (_M21 != 0) return false;
+                    if (_M22 != 0) return false;
+                    if (_M23 != 0) return false;
+                    if (_M31 != 1) return false;
+                }
+                else
+                {
+                    // row 1
+                    if (_M11 != 1) return false;
+                    if (_M12 != 0) return false;
+                    if (_M13 != 0) return false;
+
+                    // row 2
+                    if (_M21 != 0) return false;
+                    if (_M22 != 1) return false;
+                    if (_M23 != 0) return false;
+
+                    // row 3
+                    if (_M31 != 0) return false;
+                    if (_M32 != 0) return false;
+                    if (_M33 != 1) return false;
+                }
+
                 return true;
                 return true;
             }
             }
         }
         }
@@ -117,6 +311,45 @@ namespace SharpGLTF.Transforms
 
 
         #region API
         #region API
 
 
+        private Matrix4x4 _GetMatrix()
+        {
+            if (IsMatrix)
+            {
+                return new Matrix4x4
+                (
+                    _M11, _M12, _M13, 0,
+                    _M21, _M22, _M23, 0,
+                    _M31, _M32, _M33, 0,
+                    _Translation.X, _Translation.Y, _Translation.Z, 1
+                );
+            }
+
+            var m = Matrix4x4.CreateScale(this.Scale) * Matrix4x4.CreateFromQuaternion(this.Rotation.Sanitized());
+            m.Translation = this.Translation;
+            return m;
+        }
+
+        private Vector3 _GetScale()
+        {
+            if (IsDecomposed) return new Vector3(_M11, _M12, _M13);
+            if (Matrix4x4.Decompose(_GetMatrix(), out var scale, out _, out _)) return scale;
+            throw new InvalidOperationException(_CannotDecomposeMessage);
+        }
+
+        private Quaternion _GetRotation()
+        {
+            if (IsDecomposed) return new Quaternion(_M21, _M22, _M23, _M31);
+            if (Matrix4x4.Decompose(_GetMatrix(), out _, out var rotation, out _)) return rotation;
+            throw new InvalidOperationException(_CannotDecomposeMessage);
+        }
+
+        public AffineTransform GetDecomposed()
+        {
+            if (IsDecomposed) return this;
+            if (!Matrix4x4.Decompose(Matrix, out var s, out var r, out var t)) throw new InvalidOperationException(_CannotDecomposeMessage);
+            return new AffineTransform(s, r, t);
+        }
+
         public static AffineTransform Blend(ReadOnlySpan<AffineTransform> transforms, ReadOnlySpan<float> weights)
         public static AffineTransform Blend(ReadOnlySpan<AffineTransform> transforms, ReadOnlySpan<float> weights)
         {
         {
             var s = Vector3.Zero;
             var s = Vector3.Zero;
@@ -142,19 +375,52 @@ namespace SharpGLTF.Transforms
             return Multiply(a, b);
             return Multiply(a, b);
         }
         }
 
 
+        /// <summary>
+        /// Multiplies <paramref name="a"/> by <paramref name="b"/>.
+        /// </summary>
+        /// <param name="a">The left transform.</param>
+        /// <param name="b">The right transform.</param>
+        /// <returns>
+        /// <para>A new <see cref="AffineTransform"/> structure.</para>
+        /// <para>
+        /// The returned value will use a decomposed<br/>
+        /// representation it these two conditions are met:<br/>
+        /// <list type="number">
+        /// <item>Arguments <paramref name="a"/> and <paramref name="b"/> are also in decomposed form.</item>
+        /// <item>The result of the operation is decomposable.</item>
+        /// </list>
+        /// Otherwise the returned value will use a Matrix representation.
+        /// </para>
+        /// </returns>
         public static AffineTransform Multiply(in AffineTransform a, in AffineTransform b)
         public static AffineTransform Multiply(in AffineTransform a, in AffineTransform b)
         {
         {
-            AffineTransform r;
+            // if any of the two operators is a matrix, perform a matrix multiplication.
+            if (a.IsMatrix || b.IsMatrix)
+            {
+                return new AffineTransform(a.Matrix * b.Matrix);
+            }
+
+            // if the B operator has an uneven scale AND a rotation, performa a matrix multiplication
+            // which produces a squeezed matrix and cannot be decomposed.
+
+            var sb = b.Scale;
+
+            if (!(sb.X == sb.Y && sb.X == sb.Z) && b.Rotation != Quaternion.Identity)
+            {
+                return new AffineTransform(a.Matrix * b.Matrix);
+            }
+
+            // we're safe to make a decomposed multiplication
 
 
-            r.Scale = Vector3Transform(b.Scale * Vector3Transform(a.Scale, a.Rotation), Quaternion.Inverse(a.Rotation));
+            var s = _Vector3Transform(b.Scale * _Vector3Transform(a.Scale, a.Rotation), Quaternion.Inverse(a.Rotation));
 
 
-            r.Rotation = Quaternion.Multiply(b.Rotation, a.Rotation);
+            var r = Quaternion.Multiply(b.Rotation, a.Rotation);
 
 
-            r.Translation
+            var t
                 = b.Translation
                 = b.Translation
-                + Vector3Transform(a.Translation * b.Scale, b.Rotation);
+                + _Vector3Transform(a.Translation * b.Scale, b.Rotation);
 
 
-            return r;
+            return new AffineTransform(s, r, t);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -163,7 +429,7 @@ namespace SharpGLTF.Transforms
         /// <param name="v">The vector to transform</param>
         /// <param name="v">The vector to transform</param>
         /// <param name="q">The transform rotation</param>
         /// <param name="q">The transform rotation</param>
         /// <returns>The rotated vector</returns>
         /// <returns>The rotated vector</returns>
-        private static Vector3 Vector3Transform(Vector3 v, Quaternion q)
+        private static Vector3 _Vector3Transform(Vector3 v, Quaternion q)
         {
         {
             // Extract the vector part of the quaternion
             // Extract the vector part of the quaternion
             var u = new Vector3(q.X, q.Y, q.Z);
             var u = new Vector3(q.X, q.Y, q.Z);

+ 1 - 1
src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs

@@ -300,7 +300,7 @@ namespace SharpGLTF.Scenes
 
 
         private void _DecomposeMatrix(Matrix4x4 matrix)
         private void _DecomposeMatrix(Matrix4x4 matrix)
         {
         {
-            var affine = new Transforms.AffineTransform(matrix);
+            var affine = Transforms.AffineTransform.CreateDecomposed(matrix);
 
 
             UseScale().Value = affine.Scale;
             UseScale().Value = affine.Scale;
             UseRotation().Value = affine.Rotation;
             UseRotation().Value = affine.Rotation;

+ 3 - 9
src/SharpGLTF.Toolkit/Schema2/SceneExtensions.cs

@@ -25,9 +25,7 @@ namespace SharpGLTF.Schema2
         {
         {
             Guard.NotNull(node, nameof(node));
             Guard.NotNull(node, nameof(node));
 
 
-            var xform = node.LocalTransform;
-            xform.Translation = translation;
-            node.LocalTransform = xform;
+            node.LocalTransform = node.LocalTransform.WithTranslation(translation);
 
 
             return node;
             return node;
         }
         }
@@ -36,9 +34,7 @@ namespace SharpGLTF.Schema2
         {
         {
             Guard.NotNull(node, nameof(node));
             Guard.NotNull(node, nameof(node));
 
 
-            var xform = node.LocalTransform;
-            xform.Rotation = rotation;
-            node.LocalTransform = xform;
+            node.LocalTransform = node.LocalTransform.WithRotation(rotation);
 
 
             return node;
             return node;
         }
         }
@@ -47,9 +43,7 @@ namespace SharpGLTF.Schema2
         {
         {
             Guard.NotNull(node, nameof(node));
             Guard.NotNull(node, nameof(node));
 
 
-            var xform = node.LocalTransform;
-            xform.Scale = scale;
-            node.LocalTransform = xform;
+            node.LocalTransform = node.LocalTransform.WithScale(scale);
 
 
             return node;
             return node;
         }
         }

+ 129 - 105
tests/SharpGLTF.Tests/Transforms/InverseBindMatrixTest.cs → tests/SharpGLTF.Tests/Transforms/AffineTransformMatrixTests.cs

@@ -1,105 +1,129 @@
-using System;
-using System.Collections.Generic;
-using System.Numerics;
-using System.Text;
-
-using NUnit.Framework;
-
-namespace SharpGLTF.Transforms
-{
-    [Category("Core.Transforms")]
-    public class InverseBindMatrixTest
-    {
-        [TestCase(0, 0, 0, 0, 0, 0)]
-        [TestCase(1, 2, 4, 3, 2, 1)]
-        [TestCase(-1, 1, 3, 2, 0, 1)]
-        [TestCase(0, 0, 1, 0, 1, 0)]
-        [TestCase(0, -1, 1, -2, 1, 0)]
-        public void CalculateInverseBindMatrix(float mx, float my, float mz, float jx, float jy, float jz)
-        {
-            var model = Matrix4x4.CreateFromYawPitchRoll(mx, my, mz);            
-            var joint = Matrix4x4.CreateFromYawPitchRoll(jx, jy, jz);
-            joint.Translation = new Vector3(jx, jy, jz);
-
-            var invBindMatrix = SkinnedTransform.CalculateInverseBinding(model, joint);
-
-            Matrix4x4.Invert(model, out Matrix4x4 xform);            
-            Matrix4x4.Invert(joint * xform, out Matrix4x4 result);
-            NumericsAssert.AreEqual(result, invBindMatrix, 0.000001f);
-
-            Matrix4x4.Invert(joint, out Matrix4x4 invJoint);
-            result = model * invJoint;
-            NumericsAssert.AreEqual(result, invBindMatrix, 0.000001f);
-        }
-
-
-        [Test]
-        public void TestMatrixNormalization()
-        {
-            void testMatrix(Matrix4x4 m, float tolerance = 0)
-            {
-                var o = m;
-
-                Matrix4x4Factory.NormalizeMatrix(ref m);
-
-                NumericsAssert.AreEqual(o, m, tolerance);
-
-                Assert.IsTrue(Matrix4x4.Decompose(m, out _, out _, out _));
-                Assert.IsTrue(Matrix4x4.Invert(m, out _));
-            }
-
-            void testSkewed(Func<Matrix4x4, Matrix4x4> mf, float tolerance = 0)
-            {
-                var m = Matrix4x4.Identity;
-
-                var o = m = mf(m);
-
-                Assert.IsFalse(Matrix4x4.Decompose(m, out _, out _, out _));
-
-                Matrix4x4Factory.NormalizeMatrix(ref m);
-
-                NumericsAssert.AreEqual(o, m, tolerance);                
-
-                Assert.IsTrue(Matrix4x4.Decompose(m, out _, out _, out _));
-                Assert.IsTrue(Matrix4x4.Invert(m, out _));               
-            }
-            
-            testSkewed(m => { m.M12 += 0.34f; return m; }, 0.34f);
-            testSkewed(m => { m.M13 += 0.34f; return m; }, 0.34f);
-            testSkewed(m => { m.M21 += 0.34f; return m; }, 0.34f);
-            testSkewed(m => { m.M23 += 0.34f; return m; }, 0.34f);
-            testSkewed(m => { m.M31 += 0.34f; return m; }, 0.34f);
-            testSkewed(m => { m.M32 += 0.34f; return m; }, 0.34f);
-
-            testSkewed(m => { m.M12 += 0.1f; m.M23 -= 0.1f; m.M31 += 0.05f; return m; }, 0.20f);
-
-            // test normalization with uneven scaling
-
-            testMatrix(Matrix4x4.CreateScale(0.0001f) * Matrix4x4.CreateFromYawPitchRoll(1, 2, 3), 0.0001f);
-            testMatrix(Matrix4x4.CreateScale(1000) * Matrix4x4.CreateFromYawPitchRoll(1, 2, 3), 0.0001f);
-
-            var SxR = Matrix4x4.CreateScale(5, 1, 1) * Matrix4x4.CreateFromYawPitchRoll(1, 2, 3);   // Decomposable
-            var RxS = Matrix4x4.CreateFromYawPitchRoll(1, 2, 3) * Matrix4x4.CreateScale(5, 1, 1);   // Not Decomposable            
-
-            Assert.IsTrue(Matrix4x4.Decompose(SxR, out _, out _, out _));
-            testMatrix(SxR, 0.0001f);
-
-            Assert.IsFalse(Matrix4x4.Decompose(RxS, out _, out _, out _));
-            testMatrix(RxS, 100);           
-        }
-
-        [Test]
-        public void TestAffineTransformMult()
-        {
-            var a = Matrix4x4.CreateScale(1,2,4) * Matrix4x4.CreateFromYawPitchRoll(1, 2, 3);
-            var b = Matrix4x4.CreateFromYawPitchRoll(1, 0, 2) * Matrix4x4.CreateTranslation(3, -4, 2);
-            var r = Matrix4x4.Multiply(a, b);
-
-            var aa = new AffineTransform(a);
-            var bb = new AffineTransform(b);
-            var rr = AffineTransform.Multiply(aa, bb);
-
-            NumericsAssert.AreEqual(r, rr.Matrix, 0.0001f);
-        }
-    }    
-}
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+
+using NUnit.Framework;
+
+namespace SharpGLTF.Transforms
+{
+    [Category("Core.Transforms")]
+    public class AffineTransformMatrixTests
+    {
+        [TestCase(0, 0, 0, 0, 0, 0)]
+        [TestCase(1, 2, 4, 3, 2, 1)]
+        [TestCase(-1, 1, 3, 2, 0, 1)]
+        [TestCase(0, 0, 1, 0, 1, 0)]
+        [TestCase(0, -1, 1, -2, 1, 0)]
+        public void CalculateInverseBindMatrix(float mx, float my, float mz, float jx, float jy, float jz)
+        {
+            var model = Matrix4x4.CreateFromYawPitchRoll(mx, my, mz);            
+            var joint = Matrix4x4.CreateFromYawPitchRoll(jx, jy, jz);
+            joint.Translation = new Vector3(jx, jy, jz);
+
+            var invBindMatrix = SkinnedTransform.CalculateInverseBinding(model, joint);
+
+            Matrix4x4.Invert(model, out Matrix4x4 xform);            
+            Matrix4x4.Invert(joint * xform, out Matrix4x4 result);
+            NumericsAssert.AreEqual(result, invBindMatrix, 0.000001f);
+
+            Matrix4x4.Invert(joint, out Matrix4x4 invJoint);
+            result = model * invJoint;
+            NumericsAssert.AreEqual(result, invBindMatrix, 0.000001f);
+        }
+
+
+        [Test]
+        public void TestMatrixNormalization()
+        {
+            void testMatrix(Matrix4x4 m, float tolerance = 0)
+            {
+                var o = m;
+
+                Matrix4x4Factory.NormalizeMatrix(ref m);
+
+                NumericsAssert.AreEqual(o, m, tolerance);
+
+                Assert.IsTrue(Matrix4x4.Decompose(m, out _, out _, out _));
+                Assert.IsTrue(Matrix4x4.Invert(m, out _));
+            }
+
+            void testSkewed(Func<Matrix4x4, Matrix4x4> mf, float tolerance = 0)
+            {
+                var m = Matrix4x4.Identity;
+
+                var o = m = mf(m);
+
+                Assert.IsFalse(Matrix4x4.Decompose(m, out _, out _, out _));
+
+                Matrix4x4Factory.NormalizeMatrix(ref m);
+
+                NumericsAssert.AreEqual(o, m, tolerance);                
+
+                Assert.IsTrue(Matrix4x4.Decompose(m, out _, out _, out _));
+                Assert.IsTrue(Matrix4x4.Invert(m, out _));               
+            }
+            
+            testSkewed(m => { m.M12 += 0.34f; return m; }, 0.34f);
+            testSkewed(m => { m.M13 += 0.34f; return m; }, 0.34f);
+            testSkewed(m => { m.M21 += 0.34f; return m; }, 0.34f);
+            testSkewed(m => { m.M23 += 0.34f; return m; }, 0.34f);
+            testSkewed(m => { m.M31 += 0.34f; return m; }, 0.34f);
+            testSkewed(m => { m.M32 += 0.34f; return m; }, 0.34f);
+
+            testSkewed(m => { m.M12 += 0.1f; m.M23 -= 0.1f; m.M31 += 0.05f; return m; }, 0.20f);
+
+            // test normalization with uneven scaling
+
+            testMatrix(Matrix4x4.CreateScale(0.0001f) * Matrix4x4.CreateFromYawPitchRoll(1, 2, 3), 0.0001f);
+            testMatrix(Matrix4x4.CreateScale(1000) * Matrix4x4.CreateFromYawPitchRoll(1, 2, 3), 0.0001f);
+
+            var SxR = Matrix4x4.CreateScale(5, 1, 1) * Matrix4x4.CreateFromYawPitchRoll(1, 2, 3);   // Decomposable
+            var RxS = Matrix4x4.CreateFromYawPitchRoll(1, 2, 3) * Matrix4x4.CreateScale(5, 1, 1);   // Not Decomposable            
+
+            Assert.IsTrue(Matrix4x4.Decompose(SxR, out _, out _, out _));
+            testMatrix(SxR, 0.0001f);
+
+            Assert.IsFalse(Matrix4x4.Decompose(RxS, out _, out _, out _));
+            testMatrix(RxS, 100);           
+        }
+
+        [Test]
+        public void TestAffineTransformIdentity()
+        {
+            var asMatrix = new AffineTransform(Matrix4x4.Identity);
+            var asDecomposed = new AffineTransform(null, null, null);
+
+            NumericsAssert.AreEqual(Matrix4x4.Identity, asMatrix.Matrix);
+            NumericsAssert.AreEqual(Matrix4x4.Identity, asDecomposed.Matrix);
+
+            Assert.IsTrue(asMatrix.IsIdentity);
+            Assert.IsTrue(asDecomposed.IsIdentity);
+        }
+
+        [Test]
+        public void TestAffineTransformMult()
+        {
+            var s_a = new Vector3(1, 2, 4);
+            var r_a = Quaternion.CreateFromYawPitchRoll(1, 2, 3);
+            var t_a = Vector3.Zero;
+
+            var s_b = new Vector3(1, 1, 1);
+            var r_b = Quaternion.CreateFromYawPitchRoll(1, 0, 2);
+            var t_b = new Vector3(3, -4, 2);
+
+            var mat_a = Matrix4x4.CreateScale(s_a) * Matrix4x4.CreateFromQuaternion(r_a) * Matrix4x4.CreateTranslation(t_a);
+            var mat_b = Matrix4x4.CreateScale(s_b) * Matrix4x4.CreateFromQuaternion(r_b) * Matrix4x4.CreateTranslation(t_b);
+            var mat_ab = Matrix4x4.Multiply(mat_a, mat_b);
+            var mat_ba = Matrix4x4.Multiply(mat_b, mat_a);
+
+            var srt_a = new AffineTransform(s_a, r_a, t_a);
+            var srt_b = new AffineTransform(s_b, r_b, t_b);
+            var srt_ab = AffineTransform.Multiply(srt_a, srt_b);
+            var srt_ba = AffineTransform.Multiply(srt_b, srt_a);
+
+            NumericsAssert.AreEqual(mat_ab, srt_ab.Matrix, 0.0001f);
+            NumericsAssert.AreEqual(mat_ba, srt_ba.Matrix, 0.0001f);
+        }
+    }    
+}