浏览代码

improved skinning transform
improved numeric asserts

Vicente Penades 6 年之前
父节点
当前提交
64ad92cf3b

+ 1 - 1
SharpGLTF.ruleset

@@ -60,7 +60,7 @@
     <Rule Id="CA2213" Action="Error" />
     <Rule Id="CA2216" Action="Error" />
     <Rule Id="CA2242" Action="Error" />
-    <Rule Id="CA1303" Action="Info" />
+    <Rule Id="CA1303" Action="None" />
     <Rule Id="CA1308" Action="None" />
   </Rules>
 </RuleSet>

+ 17 - 17
src/Shared/_Extensions.cs

@@ -34,38 +34,38 @@ namespace SharpGLTF
             return length + (padding == 0 ? 0 : 4 - padding);
         }
 
-        internal static bool _IsReal(this float value)
+        internal static bool _IsFinite(this float value)
         {
             return !(float.IsNaN(value) | float.IsInfinity(value));
         }
 
-        internal static bool _IsReal(this Vector2 v)
+        internal static bool _IsFinite(this Vector2 v)
         {
-            return v.X._IsReal() & v.Y._IsReal();
+            return v.X._IsFinite() & v.Y._IsFinite();
         }
 
-        internal static bool _IsReal(this Vector3 v)
+        internal static bool _IsFinite(this Vector3 v)
         {
-            return v.X._IsReal() & v.Y._IsReal() & v.Z._IsReal();
+            return v.X._IsFinite() & v.Y._IsFinite() & v.Z._IsFinite();
         }
 
-        internal static bool _IsReal(this Vector4 v)
+        internal static bool _IsFinite(this Vector4 v)
         {
-            return v.X._IsReal() & v.Y._IsReal() & v.Z._IsReal() & v.W._IsReal();
+            return v.X._IsFinite() & v.Y._IsFinite() & v.Z._IsFinite() & v.W._IsFinite();
         }
 
-        internal static bool _IsReal(this Matrix4x4 v)
+        internal static bool _IsFinite(this Matrix4x4 v)
         {
-            if (!(v.M11._IsReal() & v.M12._IsReal() & v.M13._IsReal() & v.M14._IsReal())) return false;
-            if (!(v.M21._IsReal() & v.M22._IsReal() & v.M23._IsReal() & v.M24._IsReal())) return false;
-            if (!(v.M31._IsReal() & v.M32._IsReal() & v.M33._IsReal() & v.M34._IsReal())) return false;
-            if (!(v.M41._IsReal() & v.M42._IsReal() & v.M43._IsReal() & v.M44._IsReal())) return false;
+            if (!(v.M11._IsFinite() & v.M12._IsFinite() & v.M13._IsFinite() & v.M14._IsFinite())) return false;
+            if (!(v.M21._IsFinite() & v.M22._IsFinite() & v.M23._IsFinite() & v.M24._IsFinite())) return false;
+            if (!(v.M31._IsFinite() & v.M32._IsFinite() & v.M33._IsFinite() & v.M34._IsFinite())) return false;
+            if (!(v.M41._IsFinite() & v.M42._IsFinite() & v.M43._IsFinite() & v.M44._IsFinite())) return false;
             return true;
         }
 
-        internal static bool _IsReal(this Quaternion v)
+        internal static bool _IsFinite(this Quaternion v)
         {
-            return v.X._IsReal() & v.Y._IsReal() & v.Z._IsReal() & v.W._IsReal();
+            return v.X._IsFinite() & v.Y._IsFinite() & v.Z._IsFinite() & v.W._IsFinite();
         }
 
         internal static Vector3 WithLength(this Vector3 v, float len)
@@ -116,12 +116,12 @@ namespace SharpGLTF
 
         internal static void Validate(this Vector3 vector, string msg)
         {
-            if (!vector._IsReal()) throw new NotFiniteNumberException($"{msg} is invalid.");
+            if (!vector._IsFinite()) throw new NotFiniteNumberException($"{msg} is invalid.");
         }
 
         internal static void ValidateNormal(this Vector3 normal, string msg)
         {
-            if (!normal._IsReal()) throw new NotFiniteNumberException($"{msg} is invalid.");
+            if (!normal._IsFinite()) throw new NotFiniteNumberException($"{msg} is invalid.");
 
             var len = normal.Length();
 
@@ -137,7 +137,7 @@ namespace SharpGLTF
 
         internal static bool IsValidNormal(this Vector3 normal)
         {
-            if (!normal._IsReal()) return false;
+            if (!normal._IsFinite()) return false;
 
             var len = normal.Length();
 

+ 2 - 2
src/SharpGLTF.Core/Memory/FloatingArrays.cs

@@ -198,7 +198,7 @@ namespace SharpGLTF.Memory
             get => _Getter(index * _ByteStride);
             set
             {
-                if (!value._IsReal()) throw new NotFiniteNumberException(nameof(value), value);
+                if (!value._IsFinite()) throw new NotFiniteNumberException(nameof(value), value);
                 _Setter(index * _ByteStride, value);
             }
         }
@@ -208,7 +208,7 @@ namespace SharpGLTF.Memory
             get => _Getter((rowIndex * _ByteStride) + (subIndex * _EncodedLen));
             set
             {
-                if (!value._IsReal()) throw new NotFiniteNumberException(nameof(value), value);
+                if (!value._IsFinite()) throw new NotFiniteNumberException(nameof(value), value);
                 _Setter((rowIndex * _ByteStride) + (subIndex * _EncodedLen), value);
             }
         }

+ 5 - 5
src/SharpGLTF.Core/Schema2/gltf.Accessors.cs

@@ -384,7 +384,7 @@ namespace SharpGLTF.Schema2
                 {
                     var v = current[j];
 
-                    if (!v._IsReal()) result.AddError(this, $"Item[{j}][{i}] is not a finite number: {v}");
+                    if (!v._IsFinite()) result.AddError(this, $"Item[{j}][{i}] is not a finite number: {v}");
 
                     var min = minimum[j];
                     var max = maximum[j];
@@ -440,7 +440,7 @@ namespace SharpGLTF.Schema2
             {
                 var pos = positions[i];
 
-                if (!pos._IsReal()) result.AddError(this, $"POSITION[{i}] value {pos} has non finite values");
+                if (!pos._IsFinite()) result.AddError(this, $"POSITION[{i}] value {pos} has non finite values");
             }
         }
 
@@ -464,7 +464,7 @@ namespace SharpGLTF.Schema2
             {
                 var tgt = tangents[i];
 
-                if (!tgt._IsReal()) result.AddError(this, $"TANGENT[{i}] value {tgt} has non finite values");
+                if (!tgt._IsFinite()) result.AddError(this, $"TANGENT[{i}] value {tgt} has non finite values");
 
                 var len = new Vector3(tgt.X, tgt.Y, tgt.Z).Length();
 
@@ -480,7 +480,7 @@ namespace SharpGLTF.Schema2
 
             void _CheckJoint(Validation.ValidationContext r, float v, int idx, string n)
             {
-                if (!v._IsReal()) result.AddError(this, $"JOINTS_{jwset}[{idx}].{n} value {v} is not finite");
+                if (!v._IsFinite()) result.AddError(this, $"JOINTS_{jwset}[{idx}].{n} value {v} is not finite");
                 if ((v % 1) != 0) result.AddError(this, $"JOINTS_{jwset}[{idx}].{n} value {v} should be a round value");
                 if (v < 0 || v >= jointsCount) result.AddError(this, $"JOINTS_{jwset}[{idx}].{n} value {v} is out of range 0-{jointsCount}");
             }
@@ -501,7 +501,7 @@ namespace SharpGLTF.Schema2
 
             void _CheckWeight(Validation.ValidationContext r, float v, int idx, string n)
             {
-                if (!v._IsReal()) result.AddError(this, $"WEIGHTS_{jwset}[{idx}].{n} value {v} is not finite");
+                if (!v._IsFinite()) result.AddError(this, $"WEIGHTS_{jwset}[{idx}].{n} value {v} is not finite");
                 if (v < 0 || v > 1) result.AddError(this, $"WEIGHTS_{jwset}[{idx}].{n} value {v} is out of range 0-1");
             }
 

+ 12 - 17
src/SharpGLTF.Core/Schema2/gltf.Node.cs

@@ -159,22 +159,22 @@ namespace SharpGLTF.Schema2
         }
 
         /// <summary>
-        /// Creates a <see cref="Transforms.ITransform"/> object, based on the current
+        /// Creates a <see cref="Transforms.IGeometryTransform"/> object, based on the current
         /// transform state, that can be used to transform the <see cref="Mesh"/>
         /// vertices to world space.
         /// </summary>
-        /// <returns>A <see cref="Transforms.ITransform"/> object</returns>
-        public Transforms.ITransform GetMeshWorldTransform() { return GetMeshWorldTransform(null, 0); }
+        /// <returns>A <see cref="Transforms.IGeometryTransform"/> object</returns>
+        public Transforms.IGeometryTransform GetMeshWorldTransform() { return GetMeshWorldTransform(null, 0); }
 
         /// <summary>
-        /// Creates a <see cref="Transforms.ITransform"/> object, based on the current
+        /// Creates a <see cref="Transforms.IGeometryTransform"/> object, based on the current
         /// transform state, and the given <see cref="Animation"/>, that can be used
         /// to transform the <see cref="Mesh"/> vertices to world space.
         /// </summary>
         /// <param name="animation">The <see cref="Animation"/> to use.</param>
         /// <param name="time">The time within <paramref name="animation"/>.</param>
-        /// <returns>A <see cref="Transforms.ITransform"/> object</returns>
-        public Transforms.ITransform GetMeshWorldTransform(Animation animation, float time)
+        /// <returns>A <see cref="Transforms.IGeometryTransform"/> object</returns>
+        public Transforms.IGeometryTransform GetMeshWorldTransform(Animation animation, float time)
         {
             if (animation != null) Guard.MustShareLogicalParent(this, animation, nameof(animation));
 
@@ -182,17 +182,12 @@ namespace SharpGLTF.Schema2
 
             if (this.Skin == null) return new Transforms.StaticTransform(this.GetWorldMatrix(animation, time), weights, false);
 
-            var jointXforms = new Matrix4x4[this.Skin.JointsCount];
-            var invBindings = new Matrix4x4[this.Skin.JointsCount];
-
-            for (int i = 0; i < this.Skin.JointsCount; ++i)
-            {
-                var j = this.Skin.GetJoint(i);
-                jointXforms[i] = j.Item1.GetWorldMatrix(animation, time);
-                invBindings[i] = j.Item2;
-            }
-
-            return new Transforms.SkinTransform(invBindings, jointXforms, weights, false);
+            return new Transforms.SkinTransform
+                (
+                this.Skin.JointsCount,
+                idx => this.Skin.GetJoint(idx).Item2,
+                idx => this.Skin.GetJoint(idx).Item1.GetWorldMatrix(animation, time),
+                weights, false);
         }
 
         #endregion

+ 1 - 1
src/SharpGLTF.Core/Schema2/gltf.Skin.cs

@@ -125,7 +125,7 @@ namespace SharpGLTF.Schema2
 
             _FindCommonAncestor(joints.Select(item => item.Item1));
 
-            foreach (var j in joints) { Guard.IsTrue(j.Item2._IsReal(), nameof(joints)); }
+            foreach (var j in joints) { Guard.IsTrue(j.Item2._IsFinite(), nameof(joints)); }
 
             // inverse bind matrices accessor
 

+ 3 - 3
src/SharpGLTF.Core/Transforms/AffineTransform.cs

@@ -90,9 +90,9 @@ namespace SharpGLTF.Transforms
         {
             get
             {
-                if (!Scale._IsReal()) return false;
-                if (!Rotation._IsReal()) return false;
-                if (!Translation._IsReal()) return false;
+                if (!Scale._IsFinite()) return false;
+                if (!Rotation._IsFinite()) return false;
+                if (!Translation._IsFinite()) return false;
 
                 return true;
             }

+ 5 - 5
src/SharpGLTF.Core/Transforms/IndexWeight.cs

@@ -51,9 +51,9 @@ namespace SharpGLTF.Transforms
             return -1;
         }
 
-        private static IndexWeight GetSparseWeight(in SparseWeight8 src, int sparseIndex)
+        private static IndexWeight GetIndexedWeight(in SparseWeight8 src, int offset)
         {
-            switch (sparseIndex)
+            switch (offset)
             {
                 case 0: return new IndexWeight(src.Index0, src.Weight0);
                 case 1: return new IndexWeight(src.Index1, src.Weight1);
@@ -63,7 +63,7 @@ namespace SharpGLTF.Transforms
                 case 5: return new IndexWeight(src.Index5, src.Weight5);
                 case 6: return new IndexWeight(src.Index6, src.Weight6);
                 case 7: return new IndexWeight(src.Index7, src.Weight7);
-                default: throw new ArgumentOutOfRangeException(nameof(sparseIndex));
+                default: throw new ArgumentOutOfRangeException(nameof(offset));
             }
         }
 
@@ -75,7 +75,7 @@ namespace SharpGLTF.Transforms
 
             for (int i = 0; i < 8; ++i)
             {
-                var pair = GetSparseWeight(src, i);
+                var pair = GetIndexedWeight(src, i);
                 if (pair.Weight == 0) continue;
 
                 var idx = IndexOf(dst.Slice(0, offset), pair.Index);
@@ -104,7 +104,7 @@ namespace SharpGLTF.Transforms
 
             for (int i = 0; i < 8; ++i)
             {
-                var pair = GetSparseWeight(src, i);
+                var pair = GetIndexedWeight(src, i);
                 if (pair.Weight == 0) continue;
 
                 var idx = dstIndices

+ 84 - 38
src/SharpGLTF.Core/Transforms/MeshTransforms.cs

@@ -13,21 +13,28 @@ namespace SharpGLTF.Transforms
     /// <summary>
     /// Interface for a mesh transform object
     /// </summary>
-    public interface ITransform
+    public interface IGeometryTransform
     {
         /// <summary>
-        /// Gets a value indicating whether the current <see cref="ITransform"/> will render visible geometry.
+        /// Gets a value indicating whether the current <see cref="IGeometryTransform"/> will render visible geometry.
         /// </summary>
+        /// <remarks>
+        /// When this value is false, a runtime should skip rendering any geometry using
+        /// this <see cref="IGeometryTransform"/> instance, since it will not be visible anyway.
+        /// </remarks>
         bool Visible { get; }
 
         /// <summary>
         /// Gets a value indicating whether the triangles need to be flipped to render correctly.
         /// </summary>
+        /// <remarks>
+        /// When this value is true, a runtime rendering triangles should inverse the face culling.
+        /// </remarks>
         bool FlipFaces { get; }
 
-        V3 TransformPosition(V3 position, V3[] morphTargets, (int, float)[] skinWeights);
-        V3 TransformNormal(V3 normal, V3[] morphTargets, (int, float)[] skinWeights);
-        V4 TransformTangent(V4 tangent, V3[] morphTargets, (int, float)[] skinWeights);
+        V3 TransformPosition(V3 position, V3[] morphTargets, in SparseWeight8 skinWeights);
+        V3 TransformNormal(V3 normal, V3[] morphTargets, in SparseWeight8 skinWeights);
+        V4 TransformTangent(V4 tangent, V3[] morphTargets, in SparseWeight8 skinWeights);
 
         V4 MorphColors(V4 color, V4[] morphTargets);
     }
@@ -38,7 +45,7 @@ namespace SharpGLTF.Transforms
 
         protected MorphTransform()
         {
-            Update(SparseWeight8.Create((0, 1)), false);
+            Update(default, false);
         }
 
         protected MorphTransform(SparseWeight8 morphWeights, bool useAbsoluteMorphTargets)
@@ -67,6 +74,15 @@ namespace SharpGLTF.Transforms
 
         #endregion
 
+        #region properties
+
+        /// <summary>
+        /// Gets a value indicating whether morph target values are absolute, and not relative to the master value.
+        /// </summary>
+        public bool AbsoluteMorphTargets => _AbsoluteMorphTargets;
+
+        #endregion
+
         #region API
 
         public void Update(SparseWeight8 morphWeights, bool useAbsoluteMorphTargets = false)
@@ -75,7 +91,7 @@ namespace SharpGLTF.Transforms
 
             if (morphWeights.IsWeightless)
             {
-                _Weights = SparseWeight8.Create((0, 1));
+                _Weights = SparseWeight8.Create((_COMPLEMENT_INDEX, 1));
                 return;
             }
 
@@ -84,10 +100,10 @@ namespace SharpGLTF.Transforms
 
         protected V3 MorphVectors(V3 value, V3[] morphTargets)
         {
-            if (morphTargets == null) return value;
-
             if (_Weights.Index0 == _COMPLEMENT_INDEX && _Weights.Weight0 == 1) return value;
 
+            if (morphTargets == null) return value;
+
             var p = V3.Zero;
 
             if (_AbsoluteMorphTargets)
@@ -112,10 +128,10 @@ namespace SharpGLTF.Transforms
 
         protected V4 MorphVectors(V4 value, V4[] morphTargets)
         {
-            if (morphTargets == null) return value;
-
             if (_Weights.Index0 == _COMPLEMENT_INDEX && _Weights.Weight0 == 1) return value;
 
+            if (morphTargets == null) return value;
+
             var p = V4.Zero;
 
             if (_AbsoluteMorphTargets)
@@ -146,13 +162,25 @@ namespace SharpGLTF.Transforms
         #endregion
     }
 
-    public class StaticTransform : MorphTransform, ITransform
+    public class StaticTransform : MorphTransform, IGeometryTransform
     {
         #region constructor
 
+        public StaticTransform()
+        {
+            Update(TRANSFORM.Identity);
+        }
+
+        public StaticTransform(TRANSFORM xform)
+        {
+            Update(default, false);
+            Update(xform);
+        }
+
         public StaticTransform(TRANSFORM xform, SparseWeight8 morphWeights, bool useAbsoluteMorphs)
         {
-            Update(xform, morphWeights, useAbsoluteMorphs);
+            Update(morphWeights, useAbsoluteMorphs);
+            Update(xform);
         }
 
         #endregion
@@ -175,10 +203,8 @@ namespace SharpGLTF.Transforms
 
         #region API
 
-        public void Update(TRANSFORM xform, SparseWeight8 morphWeights, bool useAbsoluteMorphs)
+        public void Update(TRANSFORM xform)
         {
-            Update(morphWeights, useAbsoluteMorphs);
-
             _Transform = xform;
 
             // http://m-hikari.com/ija/ija-password-2009/ija-password5-8-2009/hajrizajIJA5-8-2009.pdf
@@ -195,21 +221,21 @@ namespace SharpGLTF.Transforms
             _FlipFaces = determinant3x3 < 0;
         }
 
-        public V3 TransformPosition(V3 position, V3[] morphTargets, (int, float)[] skinWeights)
+        public V3 TransformPosition(V3 position, V3[] morphTargets, in SparseWeight8 skinWeights)
         {
             position = MorphVectors(position, morphTargets);
 
             return V3.Transform(position, _Transform);
         }
 
-        public V3 TransformNormal(V3 normal, V3[] morphTargets, (int, float)[] skinWeights)
+        public V3 TransformNormal(V3 normal, V3[] morphTargets, in SparseWeight8 skinWeights)
         {
             normal = MorphVectors(normal, morphTargets);
 
             return V3.Normalize(V3.Transform(normal, _Transform));
         }
 
-        public V4 TransformTangent(V4 tangent, V3[] morphTargets, (int, float)[] skinWeights)
+        public V4 TransformTangent(V4 tangent, V3[] morphTargets, in SparseWeight8 skinWeights)
         {
             var n = MorphVectors(new V3(tangent.X, tangent.Y, tangent.Z), morphTargets);
 
@@ -221,38 +247,58 @@ namespace SharpGLTF.Transforms
         #endregion
     }
 
-    public class SkinTransform : MorphTransform, ITransform
+    public class SkinTransform : MorphTransform, IGeometryTransform
     {
         #region constructor
 
-        public SkinTransform(TRANSFORM[] invBindings, TRANSFORM[] worldXforms, SparseWeight8 morphWeights, bool useAbsoluteMorphTargets)
+        public SkinTransform() { }
+
+        public SkinTransform(TRANSFORM[] invBindMatrix, TRANSFORM[] currWorldMatrix, SparseWeight8 morphWeights, bool useAbsoluteMorphTargets)
+        {
+            Update(morphWeights, useAbsoluteMorphTargets);
+            Update(invBindMatrix, currWorldMatrix);
+        }
+
+        public SkinTransform(int count, Func<int, TRANSFORM> invBindMatrix, Func<int, TRANSFORM> currWorldMatrix, SparseWeight8 morphWeights, bool useAbsoluteMorphTargets)
         {
-            Update(invBindings, worldXforms, morphWeights, useAbsoluteMorphTargets);
+            Update(morphWeights, useAbsoluteMorphTargets);
+            Update(count, invBindMatrix, currWorldMatrix);
         }
 
         #endregion
 
         #region data
 
-        private TRANSFORM[] _JointTransforms;
+        private IList<TRANSFORM> _JointTransforms;
 
         #endregion
 
         #region API
 
-        public void Update(TRANSFORM[] invBindings, TRANSFORM[] worldXforms, SparseWeight8 morphWeights, bool useAbsoluteMorphTargets)
+        public void Update(TRANSFORM[] invBindMatrix, TRANSFORM[] currWorldMatrix)
         {
-            Guard.NotNull(invBindings, nameof(invBindings));
-            Guard.NotNull(worldXforms, nameof(worldXforms));
-            Guard.IsTrue(invBindings.Length == worldXforms.Length, nameof(worldXforms), $"{invBindings} and {worldXforms} length mismatch.");
+            Guard.NotNull(invBindMatrix, nameof(invBindMatrix));
+            Guard.NotNull(currWorldMatrix, nameof(currWorldMatrix));
+            Guard.IsTrue(invBindMatrix.Length == currWorldMatrix.Length, nameof(currWorldMatrix), $"{invBindMatrix} and {currWorldMatrix} length mismatch.");
 
-            Update(morphWeights, useAbsoluteMorphTargets);
+            if (_JointTransforms == null || _JointTransforms.Count != invBindMatrix.Length) _JointTransforms = new TRANSFORM[invBindMatrix.Length];
+
+            for (int i = 0; i < _JointTransforms.Count; ++i)
+            {
+                _JointTransforms[i] = invBindMatrix[i] * currWorldMatrix[i];
+            }
+        }
+
+        public void Update(int count, Func<int, TRANSFORM> invBindMatrix, Func<int, TRANSFORM> currWorldMatrix)
+        {
+            Guard.NotNull(invBindMatrix, nameof(invBindMatrix));
+            Guard.NotNull(currWorldMatrix, nameof(currWorldMatrix));
 
-            if (_JointTransforms == null || _JointTransforms.Length != invBindings.Length) _JointTransforms = new TRANSFORM[invBindings.Length];
+            if (_JointTransforms == null || _JointTransforms.Count != count) _JointTransforms = new TRANSFORM[count];
 
-            for (int i = 0; i < _JointTransforms.Length; ++i)
+            for (int i = 0; i < _JointTransforms.Count; ++i)
             {
-                _JointTransforms[i] = invBindings[i] * worldXforms[i];
+                _JointTransforms[i] = invBindMatrix(i) * currWorldMatrix(i);
             }
         }
 
@@ -260,7 +306,7 @@ namespace SharpGLTF.Transforms
 
         public bool FlipFaces => false;
 
-        public V3 TransformPosition(V3 localPosition, V3[] morphTargets, (int, float)[] skinWeights)
+        public V3 TransformPosition(V3 localPosition, V3[] morphTargets, in SparseWeight8 skinWeights)
         {
             Guard.NotNull(skinWeights, nameof(skinWeights));
 
@@ -268,9 +314,9 @@ namespace SharpGLTF.Transforms
 
             var worldPosition = V3.Zero;
 
-            var wnrm = 1.0f / skinWeights.Sum(item => item.Item2);
+            var wnrm = 1.0f / skinWeights.WeightSum;
 
-            foreach (var jw in skinWeights)
+            foreach (var jw in skinWeights.GetIndexedWeights())
             {
                 worldPosition += V3.Transform(localPosition, _JointTransforms[jw.Item1]) * jw.Item2 * wnrm;
             }
@@ -278,7 +324,7 @@ namespace SharpGLTF.Transforms
             return worldPosition;
         }
 
-        public V3 TransformNormal(V3 localNormal, V3[] morphTargets, (int, float)[] skinWeights)
+        public V3 TransformNormal(V3 localNormal, V3[] morphTargets, in SparseWeight8 skinWeights)
         {
             Guard.NotNull(skinWeights, nameof(skinWeights));
 
@@ -286,7 +332,7 @@ namespace SharpGLTF.Transforms
 
             var worldNormal = V3.Zero;
 
-            foreach (var jw in skinWeights)
+            foreach (var jw in skinWeights.GetIndexedWeights())
             {
                 worldNormal += V3.TransformNormal(localNormal, _JointTransforms[jw.Item1]) * jw.Item2;
             }
@@ -294,7 +340,7 @@ namespace SharpGLTF.Transforms
             return V3.Normalize(localNormal);
         }
 
-        public V4 TransformTangent(V4 localTangent, V3[] morphTargets, (int, float)[] skinWeights)
+        public V4 TransformTangent(V4 localTangent, V3[] morphTargets, in SparseWeight8 skinWeights)
         {
             Guard.NotNull(skinWeights, nameof(skinWeights));
 
@@ -302,7 +348,7 @@ namespace SharpGLTF.Transforms
 
             var worldTangent = V3.Zero;
 
-            foreach (var jw in skinWeights)
+            foreach (var jw in skinWeights.GetIndexedWeights())
             {
                 worldTangent += V3.TransformNormal(localTangentV, _JointTransforms[jw.Item1]) * jw.Item2;
             }

+ 50 - 43
src/SharpGLTF.Core/Transforms/SparseWeight8.cs

@@ -16,7 +16,7 @@ namespace SharpGLTF.Transforms
     /// - As an animation key in morph targets; a mesh can have many morph targets, but realistically and due to GPU limitations, only up to 8 morph targets can be blended at the same time.
     /// </remarks>
     [System.Diagnostics.DebuggerDisplay("[{Index0}]={Weight0}  [{Index1}]={Weight1}  [{Index2}]={Weight2}  [{Index3}]={Weight3}  [{Index4}]={Weight4}  [{Index5}]={Weight5}  [{Index6}]={Weight6}  [{Index7}]={Weight7}")]
-    public readonly struct SparseWeight8 : IReadOnlyList<float>
+    public readonly struct SparseWeight8
     {
         #region constructors
 
@@ -55,29 +55,29 @@ namespace SharpGLTF.Transforms
         /// Creates a new <see cref="SparseWeight8"/> from an indexed weight collection.
         /// If there's more than 8 non zero values, the 8 most representative values are taken
         /// </summary>
-        /// <param name="pairs">A sequence of indexed weight values.</param>
+        /// <param name="indexedWeights">A sequence of indexed weight values.</param>
         /// <returns>A <see cref="SparseWeight8"/> instance.</returns>
-        public static SparseWeight8 Create(params (int, float)[] pairs)
+        public static SparseWeight8 Create(params (int, float)[] indexedWeights)
         {
-            if (pairs == null) return default;
+            if (indexedWeights == null) return default;
 
-            Span<IndexWeight> sparse = stackalloc IndexWeight[pairs.Length];
+            Span<IndexWeight> sparse = stackalloc IndexWeight[indexedWeights.Length];
 
             int o = 0;
 
-            for (int i = 0; i < pairs.Length; ++i)
+            for (int i = 0; i < indexedWeights.Length; ++i)
             {
-                var p = pairs[i];
+                var p = indexedWeights[i];
                 if (p.Item2 == 0) continue;
 
-                Guard.MustBeGreaterThanOrEqualTo(p.Item1, 0, nameof(pairs));
+                Guard.MustBeGreaterThanOrEqualTo(p.Item1, 0, nameof(indexedWeights));
 
                 sparse[o++] = p;
             }
 
             sparse = sparse.Slice(0, o);
 
-            if (pairs.Length > 8)
+            if (indexedWeights.Length > 8)
             {
                 IndexWeight.BubbleSortByWeight(sparse);
                 sparse = sparse.Slice(0, 8);
@@ -144,43 +144,43 @@ namespace SharpGLTF.Transforms
             Weight7 = wgt4567.W;
         }
 
-        private SparseWeight8(ReadOnlySpan<IndexWeight> pairs)
+        private SparseWeight8(ReadOnlySpan<IndexWeight> iw)
         {
-            System.Diagnostics.Debug.Assert(pairs.Length <= 8, nameof(pairs));
+            System.Diagnostics.Debug.Assert(iw.Length <= 8, nameof(iw));
 
             this = default;
 
-            if (pairs.Length < 1) return;
-            this.Index0 = pairs[0].Index;
-            this.Weight0 = pairs[0].Weight;
+            if (iw.Length < 1) return;
+            this.Index0 = iw[0].Index;
+            this.Weight0 = iw[0].Weight;
 
-            if (pairs.Length < 2) return;
-            this.Index1 = pairs[1].Index;
-            this.Weight1 = pairs[1].Weight;
+            if (iw.Length < 2) return;
+            this.Index1 = iw[1].Index;
+            this.Weight1 = iw[1].Weight;
 
-            if (pairs.Length < 3) return;
-            this.Index2 = pairs[2].Index;
-            this.Weight2 = pairs[2].Weight;
+            if (iw.Length < 3) return;
+            this.Index2 = iw[2].Index;
+            this.Weight2 = iw[2].Weight;
 
-            if (pairs.Length < 4) return;
-            this.Index3 = pairs[3].Index;
-            this.Weight3 = pairs[3].Weight;
+            if (iw.Length < 4) return;
+            this.Index3 = iw[3].Index;
+            this.Weight3 = iw[3].Weight;
 
-            if (pairs.Length < 5) return;
-            this.Index4 = pairs[4].Index;
-            this.Weight4 = pairs[4].Weight;
+            if (iw.Length < 5) return;
+            this.Index4 = iw[4].Index;
+            this.Weight4 = iw[4].Weight;
 
-            if (pairs.Length < 6) return;
-            this.Index5 = pairs[5].Index;
-            this.Weight5 = pairs[5].Weight;
+            if (iw.Length < 6) return;
+            this.Index5 = iw[5].Index;
+            this.Weight5 = iw[5].Weight;
 
-            if (pairs.Length < 7) return;
-            this.Index6 = pairs[6].Index;
-            this.Weight6 = pairs[6].Weight;
+            if (iw.Length < 7) return;
+            this.Index6 = iw[6].Index;
+            this.Weight6 = iw[6].Weight;
 
-            if (pairs.Length < 8) return;
-            this.Index7 = pairs[7].Index;
-            this.Weight7 = pairs[7].Weight;
+            if (iw.Length < 8) return;
+            this.Index7 = iw[7].Index;
+            this.Weight7 = iw[7].Weight;
         }
 
         private SparseWeight8(in SparseWeight8 sparse, float scale)
@@ -412,9 +412,17 @@ namespace SharpGLTF.Transforms
             }
         }
 
-        public IEnumerator<float> GetEnumerator() { return Expand(GetExpandedCount()).GetEnumerator(); }
-
-        IEnumerator IEnumerable.GetEnumerator() { return Expand(GetExpandedCount()).GetEnumerator(); }
+        public IEnumerable<(int, float)> GetIndexedWeights()
+        {
+            yield return (Index0, Weight0);
+            yield return (Index1, Weight1);
+            yield return (Index2, Weight2);
+            yield return (Index3, Weight3);
+            yield return (Index4, Weight4);
+            yield return (Index5, Weight5);
+            yield return (Index6, Weight6);
+            yield return (Index7, Weight7);
+        }
 
         public override string ToString()
         {
@@ -587,14 +595,13 @@ namespace SharpGLTF.Transforms
         /// <returns>A new <see cref="SparseWeight8"/> with a complementary weight.</returns>
         internal SparseWeight8 GetNormalizedWithComplement(int complementIndex)
         {
+            var sum = this.WeightSum;
+            if (sum >= 1) return this;
+
             Span<IndexWeight> weights = stackalloc IndexWeight[8 + 1];
 
             var offset = IndexWeight.CopyTo(this, weights);
-
-            float ww = this.WeightSum;
-
-            if (ww < 1) weights[offset++] = new IndexWeight(complementIndex, 1 - ww);
-
+            weights[offset++] = new IndexWeight(complementIndex, 1 - sum);
             weights = weights.Slice(0, offset);
 
             if (offset > 8)

+ 39 - 3
src/SharpGLTF.Toolkit/Geometry/MeshBuilderToolkit.cs

@@ -1,8 +1,11 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Numerics;
 using System.Text;
 
+using SharpGLTF.Geometry.VertexTypes;
+
 namespace SharpGLTF.Geometry
 {
     public interface IMeshBuilder<TMaterial>
@@ -31,14 +34,47 @@ namespace SharpGLTF.Geometry
 
         public static Type GetMeshBuilderType(Type materialType, string[] vertexAttributes)
         {
-            var tvg = VertexTypes.VertexUtils.GetVertexGeometryType(vertexAttributes);
-            var tvm = VertexTypes.VertexUtils.GetVertexMaterialType(vertexAttributes);
-            var tvs = VertexTypes.VertexUtils.GetVertexSkinningType(vertexAttributes);
+            var tvg = VertexUtils.GetVertexGeometryType(vertexAttributes);
+            var tvm = VertexUtils.GetVertexMaterialType(vertexAttributes);
+            var tvs = VertexUtils.GetVertexSkinningType(vertexAttributes);
 
             var meshType = typeof(MeshBuilder<,,,>);
 
             meshType = meshType.MakeGenericType(materialType, tvg, tvm, tvs);
             return meshType;
         }
+
+        public static IReadOnlyDictionary<Vector3, Vector3> CalculateSmoothNormals<TMaterial>(this IMeshBuilder<TMaterial> srcMesh)
+        {
+            var posnrm = new Dictionary<Vector3, Vector3>();
+
+            void addDirection(Dictionary<Vector3, Vector3> dict, Vector3 pos, Vector3 dir)
+            {
+                if (!dir._IsFinite()) return;
+                if (!dict.TryGetValue(pos, out Vector3 n)) n = Vector3.Zero;
+                dict[pos] = n + dir;
+            }
+
+            foreach (var prim in srcMesh.Primitives)
+            {
+                foreach (var tri in prim.Triangles)
+                {
+                    var a = prim.Vertices[tri.Item1].GetGeometry().GetPosition();
+                    var b = prim.Vertices[tri.Item1].GetGeometry().GetPosition();
+                    var c = prim.Vertices[tri.Item1].GetGeometry().GetPosition();
+                    var d = Vector3.Cross(b - a, c - a);
+                    addDirection(posnrm, a, d);
+                    addDirection(posnrm, b, d);
+                    addDirection(posnrm, c, d);
+                }
+            }
+
+            foreach (var pos in posnrm.Keys.ToList())
+            {
+                posnrm[pos] = Vector3.Normalize(posnrm[pos]);
+            }
+
+            return posnrm;
+        }
     }
 }

+ 30 - 42
src/SharpGLTF.Toolkit/Geometry/VertexBufferColumns.cs

@@ -19,6 +19,12 @@ namespace SharpGLTF.Geometry
     /// </remarks>
     public class VertexBufferColumns
     {
+        #region constants
+
+        private const string ERR_COLUMNLEN = "Column length mismatch.";
+
+        #endregion
+
         #region Data Columns
 
         #pragma warning disable CA2227 // Collection properties should be read only
@@ -97,19 +103,19 @@ namespace SharpGLTF.Geometry
         /// Once it's applied, skinning and morphing columns are removed, since they're baked
         /// into the position, normal and tangent columns.
         /// </remarks>
-        public void ApplyTransform(Transforms.ITransform transform)
+        public void ApplyTransform(Transforms.IGeometryTransform transform)
         {
             Guard.NotNull(this.Positions, nameof(this.Positions), "Missing Positions column");
-            if (this.Normals != null) Guard.IsTrue(this.Positions.Count == this.Normals.Count, nameof(this.Normals), "Column length mismatch.");
-            if (this.Tangents != null) Guard.IsTrue(this.Positions.Count == this.Tangents.Count, nameof(this.Tangents), "Column length mismatch.");
-            if (this.Colors0 != null) Guard.IsTrue(this.Positions.Count == this.Colors0.Count, nameof(this.Colors0), "Column length mismatch.");
-            if (this.Colors1 != null) Guard.IsTrue(this.Positions.Count == this.Colors1.Count, nameof(this.Colors1), "Column length mismatch.");
-            if (this.TexCoords0 != null) Guard.IsTrue(this.Positions.Count == this.TexCoords0.Count, nameof(this.TexCoords0), "Column length mismatch.");
-            if (this.TexCoords1 != null) Guard.IsTrue(this.Positions.Count == this.TexCoords1.Count, nameof(this.TexCoords1), "Column length mismatch.");
-            if (this.Joints0 != null) Guard.IsTrue(this.Positions.Count == this.Joints0.Count, nameof(this.Joints0), "Column length mismatch.");
-            if (this.Joints1 != null) Guard.IsTrue(this.Positions.Count == this.Joints1.Count, nameof(this.Joints1), "Column length mismatch.");
-            if (this.Weights0 != null) Guard.IsTrue(this.Positions.Count == this.Weights0.Count, nameof(this.Weights0), "Column length mismatch.");
-            if (this.Weights1 != null) Guard.IsTrue(this.Positions.Count == this.Weights1.Count, nameof(this.Weights1), "Column length mismatch.");
+            if (this.Normals != null) Guard.IsTrue(this.Positions.Count == this.Normals.Count, nameof(this.Normals), ERR_COLUMNLEN);
+            if (this.Tangents != null) Guard.IsTrue(this.Positions.Count == this.Tangents.Count, nameof(this.Tangents), ERR_COLUMNLEN);
+            if (this.Colors0 != null) Guard.IsTrue(this.Positions.Count == this.Colors0.Count, nameof(this.Colors0), ERR_COLUMNLEN);
+            if (this.Colors1 != null) Guard.IsTrue(this.Positions.Count == this.Colors1.Count, nameof(this.Colors1), ERR_COLUMNLEN);
+            if (this.TexCoords0 != null) Guard.IsTrue(this.Positions.Count == this.TexCoords0.Count, nameof(this.TexCoords0), ERR_COLUMNLEN);
+            if (this.TexCoords1 != null) Guard.IsTrue(this.Positions.Count == this.TexCoords1.Count, nameof(this.TexCoords1), ERR_COLUMNLEN);
+            if (this.Joints0 != null) Guard.IsTrue(this.Positions.Count == this.Joints0.Count, nameof(this.Joints0), ERR_COLUMNLEN);
+            if (this.Joints1 != null) Guard.IsTrue(this.Positions.Count == this.Joints1.Count, nameof(this.Joints1), ERR_COLUMNLEN);
+            if (this.Weights0 != null) Guard.IsTrue(this.Positions.Count == this.Weights0.Count, nameof(this.Weights0), ERR_COLUMNLEN);
+            if (this.Weights1 != null) Guard.IsTrue(this.Positions.Count == this.Weights1.Count, nameof(this.Weights1), ERR_COLUMNLEN);
 
             // since the attributes we want to overwrite might be binded directly to the model's buffer
             // data, and we don't want to modify the source data, we isolate the columns to be overwritten.
@@ -119,7 +125,9 @@ namespace SharpGLTF.Geometry
             this.Tangents = _IsolateColumn(this.Tangents);
             this.Colors0 = _IsolateColumn(this.Colors0);
 
-            // prepare morph data, if available
+            // prepare animation data, if available
+
+            var skinning = default(Transforms.SparseWeight8);
 
             Vector3[] morphPositions = null;
             Vector3[] morphNormals = null;
@@ -134,58 +142,37 @@ namespace SharpGLTF.Geometry
                 if (_MorphTargets.All(item => item.Colors0 != null)) morphColors0 = new Vector4[this.MorphTargets.Count];
             }
 
-            // prepare skinning data, if available
-
-            var jw0 = Joints0 != null && Weights0 != null;
-            var jw1 = Joints1 != null && Weights1 != null;
-
-            var skinning = new (int, float)[(jw0 ? 4 : 0) + (jw1 ? 4 : 0)];
-
             // loop over every vertex
 
-            int vcount = Positions.Count;
+            int vcount = this.Positions.Count;
 
             for (int i = 0; i < vcount; ++i)
             {
-                if (jw0)
-                {
-                    var j = Joints0[i];
-                    var w = Weights0[i];
-                    skinning[0] = ((int)j.X, w.X);
-                    skinning[1] = ((int)j.Y, w.Y);
-                    skinning[2] = ((int)j.Z, w.Z);
-                    skinning[3] = ((int)j.W, w.W);
-                }
-
-                if (jw1)
+                if (this.Joints0 != null)
                 {
-                    var j = Joints1[i];
-                    var w = Weights1[i];
-                    skinning[4] = ((int)j.X, w.X);
-                    skinning[5] = ((int)j.Y, w.Y);
-                    skinning[6] = ((int)j.Z, w.Z);
-                    skinning[7] = ((int)j.W, w.W);
+                    if (this.Joints1 != null) skinning = new Transforms.SparseWeight8(Joints0[i], Joints1[i], Weights0[i], Weights1[i]);
+                    else skinning = new Transforms.SparseWeight8(Joints0[i], Weights0[i]);
                 }
 
-                if (Positions != null)
+                if (this.Positions != null)
                 {
                     _FillMorphData(morphPositions, vc => vc.Positions[i]);
                     Positions[i] = transform.TransformPosition(Positions[i], morphPositions, skinning);
                 }
 
-                if (Normals != null)
+                if (this.Normals != null)
                 {
                     _FillMorphData(morphNormals, vc => vc.Normals[i]);
                     Normals[i] = transform.TransformNormal(Normals[i], morphNormals, skinning);
                 }
 
-                if (Tangents != null)
+                if (this.Tangents != null)
                 {
                     _FillMorphData(morphTangents, vc => vc.Tangents[i]);
                     Tangents[i] = transform.TransformTangent(Tangents[i], morphTangents, skinning);
                 }
 
-                if (Colors0 != null)
+                if (this.Colors0 != null)
                 {
                     _FillMorphData(morphColors0, vc => vc.Colors0[i]);
                     Colors0[i] = transform.MorphColors(Colors0[i], morphColors0);
@@ -193,7 +180,8 @@ namespace SharpGLTF.Geometry
             }
 
             // we've just applied the transform,
-            // so we no longer need these columns.
+            // so we clear animation columns since
+            // they're irrelevant now.
 
             _MorphTargets = null;
 

+ 3 - 3
src/SharpGLTF.Toolkit/Geometry/VertexBuilder.cs

@@ -325,19 +325,19 @@ namespace SharpGLTF.Geometry
             for (int i = 0; i < Material.MaxColors; ++i)
             {
                 var c = Material.GetColor(i);
-                if (!c._IsReal() | !c.IsInRange(Vector4.Zero, Vector4.One)) sb.Append($" ❌𝐂{i}:{c}");
+                if (!c._IsFinite() | !c.IsInRange(Vector4.Zero, Vector4.One)) sb.Append($" ❌𝐂{i}:{c}");
             }
 
             for (int i = 0; i < Material.MaxTextCoords; ++i)
             {
                 var uv = Material.GetTexCoord(i);
-                if (!uv._IsReal()) sb.Append($" ❌𝐔𝐕{i}:{uv}");
+                if (!uv._IsFinite()) sb.Append($" ❌𝐔𝐕{i}:{uv}");
             }
 
             for (int i = 0; i < Skinning.MaxBindings; ++i)
             {
                 var jw = Skinning.GetJointBinding(i);
-                if (!jw.Item2._IsReal() || jw.Item2 < 0 || jw.Item1 < 0) sb.Append($" ❌𝐉𝐖{i} {jw.Item1}:{jw.Item2}");
+                if (!jw.Item2._IsFinite() || jw.Item2 < 0 || jw.Item1 < 0) sb.Append($" ❌𝐉𝐖{i} {jw.Item1}:{jw.Item2}");
             }
 
             return sb.ToString();

+ 11 - 11
src/SharpGLTF.Toolkit/Geometry/VertexTypes/FragmentPreprocessors.cs

@@ -27,17 +27,17 @@ namespace SharpGLTF.Geometry.VertexTypes
             where TvG : struct, IVertexGeometry
         {
             var p = vertex.GetPosition();
-            Guard.IsTrue(p._IsReal(), "Position", "Values are not finite.");
+            Guard.IsTrue(p._IsFinite(), "Position", "Values are not finite.");
 
             if (vertex.TryGetNormal(out Vector3 n))
             {
-                Guard.IsTrue(n._IsReal(), "Normal", "Values are not finite.");
+                Guard.IsTrue(n._IsFinite(), "Normal", "Values are not finite.");
                 Guard.MustBeBetweenOrEqualTo(n.Length(), 0.99f, 1.01f, "Normal.Length");
             }
 
             if (vertex.TryGetTangent(out Vector4 t))
             {
-                Guard.IsTrue(t._IsReal(), "Tangent", "Values are not finite.");
+                Guard.IsTrue(t._IsFinite(), "Tangent", "Values are not finite.");
                 Guard.IsTrue(t.W == 1 || t.W == -1, "Tangent.W", "Invalid value");
                 Guard.MustBeBetweenOrEqualTo(new Vector3(t.X, t.Y, t.Z).Length(), 0.99f, 1.01f, "Tangent.XYZ.Length");
             }
@@ -64,7 +64,7 @@ namespace SharpGLTF.Geometry.VertexTypes
             for (int i = 0; i < vertex.MaxColors; ++i)
             {
                 var c = vertex.GetColor(i);
-                Guard.IsTrue(c._IsReal(), $"Color{i}", "Values are not finite.");
+                Guard.IsTrue(c._IsFinite(), $"Color{i}", "Values are not finite.");
                 Guard.MustBeBetweenOrEqualTo(c.X, 0, 1, $"Color{i}.R");
                 Guard.MustBeBetweenOrEqualTo(c.Y, 0, 1, $"Color{i}.G");
                 Guard.MustBeBetweenOrEqualTo(c.Z, 0, 1, $"Color{i}.B");
@@ -74,7 +74,7 @@ namespace SharpGLTF.Geometry.VertexTypes
             for (int i = 0; i < vertex.MaxTextCoords; ++i)
             {
                 var t = vertex.GetTexCoord(i);
-                Guard.IsTrue(t._IsReal(), $"TexCoord{i}", "Values are not finite.");
+                Guard.IsTrue(t._IsFinite(), $"TexCoord{i}", "Values are not finite.");
             }
 
             return vertex;
@@ -115,7 +115,7 @@ namespace SharpGLTF.Geometry.VertexTypes
                 var pair = vertex.GetJointBinding(i);
 
                 Guard.MustBeGreaterThanOrEqualTo(pair.Item1, 0, $"Joint{i}");
-                Guard.IsTrue(pair.Item2._IsReal(), $"Weight{i}", "Values are not finite.");
+                Guard.IsTrue(pair.Item2._IsFinite(), $"Weight{i}", "Values are not finite.");
                 if (pair.Item2 == 0) Guard.IsTrue(pair.Item1 == 0, "joints with weight zero must be set to zero");
 
                 weightsSum += pair.Item2;
@@ -145,11 +145,11 @@ namespace SharpGLTF.Geometry.VertexTypes
         {
             var p = vertex.GetPosition();
 
-            if (!p._IsReal()) return null;
+            if (!p._IsFinite()) return null;
 
             if (vertex.TryGetNormal(out Vector3 n))
             {
-                if (!n._IsReal()) n = p;
+                if (!n._IsFinite()) n = p;
                 if (n == Vector3.Zero) n = p;
                 if (n == Vector3.Zero) return null;
 
@@ -159,7 +159,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
             if (vertex.TryGetTangent(out Vector4 tw))
             {
-                if (!tw._IsReal()) return null;
+                if (!tw._IsFinite()) return null;
 
                 var t = new Vector3(tw.X, tw.Y, tw.Z);
                 if (t == Vector3.Zero) return null;
@@ -195,7 +195,7 @@ namespace SharpGLTF.Geometry.VertexTypes
             for (int i = 0; i < vertex.MaxColors; ++i)
             {
                 var c = vertex.GetColor(i);
-                if (!c._IsReal()) c = Vector4.Zero;
+                if (!c._IsFinite()) c = Vector4.Zero;
                 c = Vector4.Min(Vector4.One, c);
                 c = Vector4.Max(Vector4.Zero, c);
                 vertex.SetColor(i, c);
@@ -204,7 +204,7 @@ namespace SharpGLTF.Geometry.VertexTypes
             for (int i = 0; i < vertex.MaxTextCoords; ++i)
             {
                 var t = vertex.GetTexCoord(i);
-                if (!t._IsReal()) vertex.SetTexCoord(i, Vector2.Zero);
+                if (!t._IsFinite()) vertex.SetTexCoord(i, Vector2.Zero);
             }
 
             return vertex;

+ 3 - 3
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexUtils.cs

@@ -106,11 +106,11 @@ namespace SharpGLTF.Geometry.VertexTypes
 
             var p = inVertex.GetPosition();
 
-            if (!p._IsReal()) return false;
+            if (!p._IsFinite()) return false;
 
             if (inVertex.TryGetNormal(out Vector3 n))
             {
-                if (!n._IsReal()) return false;
+                if (!n._IsFinite()) return false;
                 if (n == Vector3.Zero) n = p;
                 if (n == Vector3.Zero) return false;
 
@@ -120,7 +120,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
             if (inVertex.TryGetTangent(out Vector4 tw))
             {
-                if (!tw._IsReal()) return false;
+                if (!tw._IsFinite()) return false;
 
                 var t = new Vector3(tw.X, tw.Y, tw.Z);
                 if (t == Vector3.Zero) return false;

+ 8 - 5
src/SharpGLTF.Toolkit/Scenes/Transformers.cs

@@ -205,14 +205,17 @@ namespace SharpGLTF.Scenes
                 .FirstOrDefault();
         }
 
-        public Transforms.ITransform GetWorldTransformer(string animationTrack, float time)
+        public Transforms.IGeometryTransform GetWorldTransformer(string animationTrack, float time)
         {
             var jb = GetJointBindings();
 
-            var ww = jb.Select(item => item.Item1.GetWorldMatrix(animationTrack, time)).ToArray();
-            var bb = jb.Select(item => item.Item2).ToArray();
-
-            return new Transforms.SkinTransform(bb, ww, default, false);
+            return new Transforms.SkinTransform
+                (
+                jb.Length,
+                idx => jb[idx].Item2,
+                idx => jb[idx].Item1.GetWorldMatrix(animationTrack, time),
+                default, false
+                );
         }
 
         #endregion

+ 2 - 2
src/SharpGLTF.Toolkit/Schema2/MeshExtensions.cs

@@ -8,7 +8,7 @@ using SharpGLTF.Memory;
 using SharpGLTF.Geometry;
 using SharpGLTF.Geometry.VertexTypes;
 
-using MESHXFORM = SharpGLTF.Transforms.ITransform;
+using MESHXFORM = SharpGLTF.Transforms.IGeometryTransform;
 
 namespace SharpGLTF.Schema2
 {
@@ -517,7 +517,7 @@ namespace SharpGLTF.Schema2
 
             void addDirection(Dictionary<Vector3, Vector3> dict, Vector3 pos, Vector3 dir)
             {
-                if (!dir._IsReal()) return;
+                if (!dir._IsFinite()) return;
                 if (!dict.TryGetValue(pos, out Vector3 n)) n = Vector3.Zero;
                 dict[pos] = n + dir;
             }

+ 110 - 46
tests/SharpGLTF.Tests/NumericsAssert.cs

@@ -7,58 +7,81 @@ using NUnit.Framework;
 
 namespace SharpGLTF
 {
-    static class NumericsAssert
+    public static class NumericsAssert
     {
+        public static void IsFinite(Single value, string message = null)
+        {
+            Assert.IsTrue(float.IsFinite(value), message);
+        }
+
+        public static void IsFinite(Double value, string message = null)
+        {
+            Assert.IsTrue(double.IsFinite(value), message);
+        }
+
         public static void IsFinite(Vector2 vector)
         {
-            Assert.IsTrue(float.IsFinite(vector.X), "X");
-            Assert.IsTrue(float.IsFinite(vector.Y), "Y");
+            IsFinite(vector.X, "X");
+            IsFinite(vector.Y, "Y");
         }
 
         public static void IsFinite(Vector3 vector)
         {
-            Assert.IsTrue(float.IsFinite(vector.X), "X");
-            Assert.IsTrue(float.IsFinite(vector.Y), "Y");
-            Assert.IsTrue(float.IsFinite(vector.Z), "Z");
+            IsFinite(vector.X, "X");
+            IsFinite(vector.Y, "Y");
+            IsFinite(vector.Z, "Z");
         }
 
         public static void IsFinite(Vector4 vector)
         {
-            Assert.IsTrue(float.IsFinite(vector.X), "X");
-            Assert.IsTrue(float.IsFinite(vector.Y), "Y");
-            Assert.IsTrue(float.IsFinite(vector.Z), "Z");
-            Assert.IsTrue(float.IsFinite(vector.W), "W");
+            IsFinite(vector.X, "X");
+            IsFinite(vector.Y, "Y");
+            IsFinite(vector.Z, "Z");
+            IsFinite(vector.W, "W");
         }
 
         public static void IsFinite(Quaternion quaternion)
         {
-            Assert.IsTrue(float.IsFinite(quaternion.X), "X");
-            Assert.IsTrue(float.IsFinite(quaternion.Y), "Y");
-            Assert.IsTrue(float.IsFinite(quaternion.Z), "Z");
-            Assert.IsTrue(float.IsFinite(quaternion.W), "W");
+            IsFinite(quaternion.X, "X");
+            IsFinite(quaternion.Y, "Y");
+            IsFinite(quaternion.Z, "Z");
+            IsFinite(quaternion.W, "W");
+        }
+
+        public static void IsFinite(Plane plane)
+        {
+            IsFinite(plane.Normal.X, "Normal.X");
+            IsFinite(plane.Normal.Y, "Normal.Y");
+            IsFinite(plane.Normal.Z, "Normal.Z");
+            IsFinite(plane.D, "D");
         }
 
         public static void IsFinite(Matrix4x4 matrix)
         {
-            Assert.IsTrue(float.IsFinite(matrix.M11), "M11");
-            Assert.IsTrue(float.IsFinite(matrix.M12), "M12");
-            Assert.IsTrue(float.IsFinite(matrix.M13), "M13");
-            Assert.IsTrue(float.IsFinite(matrix.M14), "M14");
+            IsFinite(matrix.M11, "M11");
+            IsFinite(matrix.M12, "M12");
+            IsFinite(matrix.M13, "M13");
+            IsFinite(matrix.M14, "M14");
 
-            Assert.IsTrue(float.IsFinite(matrix.M21), "M21");
-            Assert.IsTrue(float.IsFinite(matrix.M22), "M22");
-            Assert.IsTrue(float.IsFinite(matrix.M23), "M23");
-            Assert.IsTrue(float.IsFinite(matrix.M24), "M24");
+            IsFinite(matrix.M21, "M21");
+            IsFinite(matrix.M22, "M22");
+            IsFinite(matrix.M23, "M23");
+            IsFinite(matrix.M24, "M24");
 
-            Assert.IsTrue(float.IsFinite(matrix.M31), "M31");
-            Assert.IsTrue(float.IsFinite(matrix.M32), "M32");
-            Assert.IsTrue(float.IsFinite(matrix.M33), "M33");
-            Assert.IsTrue(float.IsFinite(matrix.M34), "M34");
+            IsFinite(matrix.M31, "M31");
+            IsFinite(matrix.M32, "M32");
+            IsFinite(matrix.M33, "M33");
+            IsFinite(matrix.M34, "M34");
 
-            Assert.IsTrue(float.IsFinite(matrix.M41), "M41");
-            Assert.IsTrue(float.IsFinite(matrix.M42), "M42");
-            Assert.IsTrue(float.IsFinite(matrix.M43), "M43");
-            Assert.IsTrue(float.IsFinite(matrix.M44), "M44");
+            IsFinite(matrix.M41, "M41");
+            IsFinite(matrix.M42, "M42");
+            IsFinite(matrix.M43, "M43");
+            IsFinite(matrix.M44, "M44");
+        }
+
+        public static void AreEqual(BigInteger expected, BigInteger actual, double tolerance = 0)
+        {
+            Assert.AreEqual(0, (double)BigInteger.Abs(actual - expected), tolerance);
         }
 
         public static void AreEqual(Vector2 expected, Vector2 actual, double tolerance = 0)
@@ -115,35 +138,56 @@ namespace SharpGLTF
 
         public static void IsInvertible(Matrix4x4 matrix)
         {
+            IsFinite(matrix);
             Assert.IsTrue(Matrix4x4.Invert(matrix, out Matrix4x4 inverted));
         }
 
-        public static void IsNormalized(Vector2 vector, double delta = 0)
+        public static void IsOrthogonal3x3(Matrix4x4 matrix, double tolerance = 0)
         {
-            var lenSquared = vector.X * vector.X + vector.Y * vector.Y;
+            IsFinite(matrix);
+
+            Assert.AreEqual(0, matrix.M41);
+            Assert.AreEqual(0, matrix.M42);
+            Assert.AreEqual(0, matrix.M43);
+            Assert.AreEqual(1, matrix.M44);
 
-            Assert.AreEqual(1, lenSquared, delta * delta, "Length");
+            var cx = new Vector3(matrix.M11, matrix.M21, matrix.M31);
+            var cy = new Vector3(matrix.M12, matrix.M22, matrix.M32);
+            var cz = new Vector3(matrix.M13, matrix.M23, matrix.M33);            
+
+            Assert.AreEqual(0, Vector3.Dot(cx, cy), tolerance);
+            Assert.AreEqual(0, Vector3.Dot(cx, cz), tolerance);
+            Assert.AreEqual(0, Vector3.Dot(cy, cz), tolerance);
         }
 
-        public static void IsNormalized(Vector3 vector, double delta = 0)
+        public static void IsNormalized(Vector2 actual, double delta = 0)
         {
-            var lenSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z;
-
-            Assert.AreEqual(1, lenSquared, delta * delta, "Length");
+            IsFinite(actual);
+            AreEqual(Vector2.Normalize(actual), actual, delta);
         }
 
-        public static void IsNormalized(Vector4 vector, double delta = 0)
+        public static void IsNormalized(Vector3 actual, double delta = 0)
         {
-            var lenSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z + vector.W * vector.W;
+            IsFinite(actual);
+            AreEqual(Vector3.Normalize(actual), actual, delta);
+        }
 
-            Assert.AreEqual(1, lenSquared, delta * delta, "Length");
+        public static void IsNormalized(Vector4 actual, double delta = 0)
+        {
+            IsFinite(actual);
+            AreEqual(Vector4.Normalize(actual), actual, delta);
         }
 
-        public static void IsNormalized(Quaternion vector, double delta = 0)
+        public static void IsNormalized(Quaternion actual, double delta = 0)
         {
-            var lenSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z + vector.W * vector.W;
+            IsFinite(actual);
+            AreEqual(Quaternion.Normalize(actual), actual, delta);
+        }
 
-            Assert.AreEqual(1, lenSquared, delta * delta, "Length");
+        public static void InRange(BigInteger value, BigInteger min, BigInteger max)
+        {
+            GreaterOrEqual(value, min);
+            LessOrEqual(value, max);
         }
 
         public static void InRange(Vector2 value, Vector2 min, Vector2 max)
@@ -164,6 +208,11 @@ namespace SharpGLTF
             LessOrEqual(value, max);
         }
 
+        public static void Less(BigInteger arg1, BigInteger arg2)
+        {
+            Assert.Less(arg1.CompareTo(arg2), 0);
+        }
+
         public static void Less(Vector2 arg1, Vector2 arg2)
         {
             Assert.Less(arg1.X, arg2.X, "X");
@@ -185,6 +234,11 @@ namespace SharpGLTF
             Assert.Less(arg1.W, arg2.W, "W");
         }
 
+        public static void LessOrEqual(BigInteger arg1, BigInteger arg2)
+        {
+            Assert.LessOrEqual(arg1.CompareTo(arg2), 0);
+        }
+
         public static void LessOrEqual(Vector2 arg1, Vector2 arg2)
         {
             Assert.LessOrEqual(arg1.X, arg2.X, "X");
@@ -206,6 +260,11 @@ namespace SharpGLTF
             Assert.LessOrEqual(arg1.W, arg2.W, "W");
         }
 
+        public static void Greater(BigInteger arg1, BigInteger arg2)
+        {
+            Assert.Greater(arg1.CompareTo(arg2), 0);
+        }
+
         public static void Greater(Vector2 arg1, Vector2 arg2)
         {
             Assert.Greater(arg1.X, arg2.X, "X");
@@ -227,6 +286,11 @@ namespace SharpGLTF
             Assert.Greater(arg1.W, arg2.W, "W");
         }
 
+        public static void GreaterOrEqual(BigInteger arg1, BigInteger arg2)
+        {
+            Assert.GreaterOrEqual(arg1.CompareTo(arg2), 0);
+        }
+
         public static void GreaterOrEqual(Vector2 arg1, Vector2 arg2)
         {
             Assert.GreaterOrEqual(arg1.X, arg2.X, "X");
@@ -248,21 +312,21 @@ namespace SharpGLTF
             Assert.GreaterOrEqual(arg1.W, arg2.W, "W");
         }
 
-        public static void AngleLessOrEqual(Vector2 a, Vector2 b, float radians)
+        public static void AngleLessOrEqual(Vector2 a, Vector2 b, double radians)
         {
             var angle = VectorsUtils.GetAngle(a, b);
 
             Assert.LessOrEqual(angle, radians, "Angle");
         }
 
-        public static void AngleLessOrEqual(Vector3 a, Vector3 b, float radians)
+        public static void AngleLessOrEqual(Vector3 a, Vector3 b, double radians)
         {
             var angle = VectorsUtils.GetAngle(a, b);
 
             Assert.LessOrEqual(angle, radians, "Angle");
         }
 
-        public static void AngleLessOrEqual(Quaternion a, Quaternion b, float radians)
+        public static void AngleLessOrEqual(Quaternion a, Quaternion b, double radians)
         {
             var angle = VectorsUtils.GetAngle(a, b);