Browse Source

refactoring animation morphing to use SparseWeights8

Vicente Penades 6 years ago
parent
commit
2c9b3bb086

+ 57 - 3
src/SharpGLTF.Core/Animations/CubicSamplers.cs

@@ -34,7 +34,7 @@ namespace SharpGLTF.Animations
         {
         {
             var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
             var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
 
 
-            return SamplerFactory.CubicLerp
+            return SamplerFactory.InterpolateCubic
                 (
                 (
                 segment.Item1.Item2, segment.Item1.Item3,   // start, startTangentOut
                 segment.Item1.Item2, segment.Item1.Item3,   // start, startTangentOut
                 segment.Item2.Item2, segment.Item2.Item1,   // end, endTangentIn
                 segment.Item2.Item2, segment.Item2.Item1,   // end, endTangentIn
@@ -88,7 +88,7 @@ namespace SharpGLTF.Animations
         {
         {
             var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
             var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
 
 
-            return SamplerFactory.CubicLerp
+            return SamplerFactory.InterpolateCubic
                 (
                 (
                 segment.Item1.Item2, segment.Item1.Item3,   // start, startTangentOut
                 segment.Item1.Item2, segment.Item1.Item3,   // start, startTangentOut
                 segment.Item2.Item2, segment.Item2.Item1,   // end, endTangentIn
                 segment.Item2.Item2, segment.Item2.Item1,   // end, endTangentIn
@@ -114,6 +114,60 @@ namespace SharpGLTF.Animations
         #endregion
         #endregion
     }
     }
 
 
+    /// <summary>
+    /// Defines a <see cref="Transforms.SparseWeight8"/> curve sampler that can be sampled with CUBIC interpolation.
+    /// </summary>
+    struct SparseCubicSampler : ICurveSampler<Transforms.SparseWeight8>, IConvertibleCurve<Transforms.SparseWeight8>
+    {
+        #region lifecycle
+
+        public SparseCubicSampler(IEnumerable<(float, (Transforms.SparseWeight8, Transforms.SparseWeight8, Transforms.SparseWeight8))> sequence)
+        {
+            _Sequence = sequence;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly IEnumerable<(float, (Transforms.SparseWeight8, Transforms.SparseWeight8, Transforms.SparseWeight8))> _Sequence;
+
+        #endregion
+
+        #region API
+
+        public int MaxDegree => 3;
+
+        public Transforms.SparseWeight8 GetPoint(float offset)
+        {
+            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+
+            return Transforms.SparseWeight8.InterpolateCubic
+                (
+                segment.Item1.Item2, segment.Item1.Item3,   // start, startTangentOut
+                segment.Item2.Item2, segment.Item2.Item1,   // end, endTangentIn
+                segment.Item3                               // amount
+                );
+        }
+
+        public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToStepCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToLinearCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        public IReadOnlyDictionary<float, (Transforms.SparseWeight8, Transforms.SparseWeight8, Transforms.SparseWeight8)> ToSplineCurve()
+        {
+            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+        }
+
+        #endregion
+    }
+
     /// <summary>
     /// <summary>
     /// Defines a <see cref="float"/>[] curve sampler that can be sampled with CUBIC interpolation.
     /// Defines a <see cref="float"/>[] curve sampler that can be sampled with CUBIC interpolation.
     /// </summary>
     /// </summary>
@@ -142,7 +196,7 @@ namespace SharpGLTF.Animations
         {
         {
             var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
             var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
 
 
-            return SamplerFactory.CubicLerp
+            return SamplerFactory.InterpolateCubic
                 (
                 (
                 segment.Item1.Item2, segment.Item1.Item3,   // start, startTangentOut
                 segment.Item1.Item2, segment.Item1.Item3,   // start, startTangentOut
                 segment.Item2.Item2, segment.Item2.Item1,   // end, endTangentIn
                 segment.Item2.Item2, segment.Item2.Item1,   // end, endTangentIn

+ 57 - 1
src/SharpGLTF.Core/Animations/LinearSamplers.cs

@@ -116,6 +116,62 @@ namespace SharpGLTF.Animations
         #endregion
         #endregion
     }
     }
 
 
+    /// <summary>
+    /// Defines a <see cref="Transforms.SparseWeight8"/> curve sampler that can be sampled with STEP or LINEAR interpolation.
+    /// </summary>
+    struct SparseLinearSampler : ICurveSampler<Transforms.SparseWeight8>, IConvertibleCurve<Transforms.SparseWeight8>
+    {
+        #region lifecycle
+
+        public SparseLinearSampler(IEnumerable<(float, Transforms.SparseWeight8)> sequence, bool isLinear)
+        {
+            _Sequence = sequence;
+            _Linear = isLinear;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly IEnumerable<(float, Transforms.SparseWeight8)> _Sequence;
+        private readonly Boolean _Linear;
+
+        #endregion
+
+        #region API
+
+        public int MaxDegree => _Linear ? 1 : 0;
+
+        public Transforms.SparseWeight8 GetPoint(float offset)
+        {
+            var segment = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+
+            if (!_Linear) return segment.Item1;
+
+            var weights = Transforms.SparseWeight8.InterpolateLinear(segment.Item1, segment.Item2, segment.Item3);
+
+            return weights;
+        }
+
+        public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToStepCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToLinearCurve()
+        {
+            Guard.IsTrue(_Linear, nameof(_Linear));
+            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
+        }
+
+        public IReadOnlyDictionary<float, (Transforms.SparseWeight8, Transforms.SparseWeight8, Transforms.SparseWeight8)> ToSplineCurve()
+        {
+            throw new NotImplementedException();
+        }
+
+        #endregion
+    }
+
     /// <summary>
     /// <summary>
     /// Defines a <see cref="float"/>[] curve sampler that can be sampled with STEP or LINEAR interpolations.
     /// Defines a <see cref="float"/>[] curve sampler that can be sampled with STEP or LINEAR interpolations.
     /// </summary>
     /// </summary>
@@ -148,7 +204,7 @@ namespace SharpGLTF.Animations
 
 
             if (!_Linear) return segment.Item1;
             if (!_Linear) return segment.Item1;
 
 
-            return SamplerFactory.Lerp(segment.Item1, segment.Item2, segment.Item3);
+            return SamplerFactory.InterpolateLinear(segment.Item1, segment.Item2, segment.Item3);
         }
         }
 
 
         public IReadOnlyDictionary<float, float[]> ToStepCurve()
         public IReadOnlyDictionary<float, float[]> ToStepCurve()

+ 19 - 5
src/SharpGLTF.Core/Animations/SamplerFactory.cs

@@ -222,7 +222,7 @@ namespace SharpGLTF.Animations
 
 
         #region interpolation utils
         #region interpolation utils
 
 
-        public static Single[] Lerp(Single[] start, Single[] end, Single amount)
+        public static Single[] InterpolateLinear(Single[] start, Single[] end, Single amount)
         {
         {
             var startW = 1 - amount;
             var startW = 1 - amount;
             var endW = amount;
             var endW = amount;
@@ -237,21 +237,21 @@ namespace SharpGLTF.Animations
             return result;
             return result;
         }
         }
 
 
-        public static Vector3 CubicLerp(Vector3 start, Vector3 outgoingTangent, Vector3 end, Vector3 incomingTangent, Single amount)
+        public static Vector3 InterpolateCubic(Vector3 start, Vector3 outgoingTangent, Vector3 end, Vector3 incomingTangent, Single amount)
         {
         {
             var hermite = SamplerFactory.CreateHermitePointWeights(amount);
             var hermite = SamplerFactory.CreateHermitePointWeights(amount);
 
 
             return (start * hermite.Item1) + (end * hermite.Item2) + (outgoingTangent * hermite.Item3) + (incomingTangent * hermite.Item4);
             return (start * hermite.Item1) + (end * hermite.Item2) + (outgoingTangent * hermite.Item3) + (incomingTangent * hermite.Item4);
         }
         }
 
 
-        public static Quaternion CubicLerp(Quaternion start, Quaternion outgoingTangent, Quaternion end, Quaternion incomingTangent, Single amount)
+        public static Quaternion InterpolateCubic(Quaternion start, Quaternion outgoingTangent, Quaternion end, Quaternion incomingTangent, Single amount)
         {
         {
             var hermite = CreateHermitePointWeights(amount);
             var hermite = CreateHermitePointWeights(amount);
 
 
             return Quaternion.Normalize((start * hermite.Item1) + (end * hermite.Item2) + (outgoingTangent * hermite.Item3) + (incomingTangent * hermite.Item4));
             return Quaternion.Normalize((start * hermite.Item1) + (end * hermite.Item2) + (outgoingTangent * hermite.Item3) + (incomingTangent * hermite.Item4));
         }
         }
 
 
-        public static Single[] CubicLerp(Single[] start, Single[] outgoingTangent, Single[] end, Single[] incomingTangent, Single amount)
+        public static Single[] InterpolateCubic(Single[] start, Single[] outgoingTangent, Single[] end, Single[] incomingTangent, Single amount)
         {
         {
             var hermite = CreateHermitePointWeights(amount);
             var hermite = CreateHermitePointWeights(amount);
 
 
@@ -283,6 +283,13 @@ namespace SharpGLTF.Animations
             return new QuaternionLinearSampler(collection, isLinear);
             return new QuaternionLinearSampler(collection, isLinear);
         }
         }
 
 
+        public static ICurveSampler<Transforms.SparseWeight8> CreateSampler(this IEnumerable<(Single, Transforms.SparseWeight8)> collection, bool isLinear = true)
+        {
+            if (collection == null) return null;
+
+            return new SparseLinearSampler(collection, isLinear);
+        }
+
         public static ICurveSampler<Single[]> CreateSampler(this IEnumerable<(Single, Single[])> collection, bool isLinear = true)
         public static ICurveSampler<Single[]> CreateSampler(this IEnumerable<(Single, Single[])> collection, bool isLinear = true)
         {
         {
             if (collection == null) return null;
             if (collection == null) return null;
@@ -304,13 +311,20 @@ namespace SharpGLTF.Animations
             return new QuaternionCubicSampler(collection);
             return new QuaternionCubicSampler(collection);
         }
         }
 
 
+        public static ICurveSampler<Transforms.SparseWeight8> CreateSampler(this IEnumerable<(Single, (Transforms.SparseWeight8, Transforms.SparseWeight8, Transforms.SparseWeight8))> collection)
+        {
+            if (collection == null) return null;
+
+            return new SparseCubicSampler(collection);
+        }
+
         public static ICurveSampler<Single[]> CreateSampler(this IEnumerable<(Single, (Single[], Single[], Single[]))> collection)
         public static ICurveSampler<Single[]> CreateSampler(this IEnumerable<(Single, (Single[], Single[], Single[]))> collection)
         {
         {
             if (collection == null) return null;
             if (collection == null) return null;
 
 
             return new ArrayCubicSampler(collection);
             return new ArrayCubicSampler(collection);
         }
         }
-
+        
         #endregion
         #endregion
     }
     }
 }
 }

+ 98 - 2
src/SharpGLTF.Core/Schema2/gltf.Animations.cs

@@ -132,9 +132,14 @@ namespace SharpGLTF.Schema2
                 .SetSampler(sampler);
                 .SetSampler(sampler);
         }
         }
 
 
-        public void CreateMorphChannel(Node node, AnimationInterpolationMode mode, IReadOnlyDictionary<Single, Single[]> keyframes)
+        public void CreateMorphChannel(Node node, AnimationInterpolationMode mode, IReadOnlyDictionary<Single, SparseWeight8> keyframes, int morphCount, bool linear = true)
         {
         {
-            throw new NotImplementedException();
+            var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
+
+            sampler.SetKeys(keyframes, morphCount);
+
+            this._UseChannel(node, PropertyPath.weights)
+                .SetSampler(sampler);
         }
         }
 
 
         private AnimationChannel FindChannel(Node node, PropertyPath path)
         private AnimationChannel FindChannel(Node node, PropertyPath path)
@@ -150,6 +155,8 @@ namespace SharpGLTF.Schema2
 
 
         public IAnimationSampler<Single[]> FindMorphSampler(Node node) { return FindChannel(node, PropertyPath.weights)?.Sampler; }
         public IAnimationSampler<Single[]> FindMorphSampler(Node node) { return FindChannel(node, PropertyPath.weights)?.Sampler; }
 
 
+        public IAnimationSampler<SparseWeight8> FindSparseMorphSampler(Node node) { return FindChannel(node, PropertyPath.weights)?.Sampler; }
+
         public AffineTransform GetLocalTransform(Node node, float time)
         public AffineTransform GetLocalTransform(Node node, float time)
         {
         {
             Guard.MustShareLogicalParent(this, node, nameof(node));
             Guard.MustShareLogicalParent(this, node, nameof(node));
@@ -169,6 +176,8 @@ namespace SharpGLTF.Schema2
 
 
         public IReadOnlyList<float> GetMorphWeights(Node node, float time)
         public IReadOnlyList<float> GetMorphWeights(Node node, float time)
         {
         {
+            Guard.NotNull(node, nameof(node));
+
             var morphWeights = node.MorphWeights;
             var morphWeights = node.MorphWeights;
             if (morphWeights == null || morphWeights.Count == 0) return morphWeights;
             if (morphWeights == null || morphWeights.Count == 0) return morphWeights;
 
 
@@ -180,6 +189,21 @@ namespace SharpGLTF.Schema2
             return mfunc.GetPoint(time);
             return mfunc.GetPoint(time);
         }
         }
 
 
+        public SparseWeight8 GetSparseMorphWeights(Node node, float time)
+        {
+            Guard.NotNull(node, nameof(node));
+
+            var morphWeights = node.MorphWeights;
+            if (morphWeights == null || morphWeights.Count == 0) return default;
+
+            Guard.MustShareLogicalParent(this, node, nameof(node));
+
+            var mfunc = FindSparseMorphSampler(node)?.CreateCurveSampler();
+            if (mfunc == null) return default;
+
+            return mfunc.GetPoint(time);
+        }
+
         #endregion
         #endregion
     }
     }
 
 
@@ -260,6 +284,7 @@ namespace SharpGLTF.Schema2
     sealed partial class AnimationSampler : IChildOf<Animation>,
     sealed partial class AnimationSampler : IChildOf<Animation>,
         IAnimationSampler<Vector3>,
         IAnimationSampler<Vector3>,
         IAnimationSampler<Quaternion>,
         IAnimationSampler<Quaternion>,
+        IAnimationSampler<SparseWeight8>,
         IAnimationSampler<Single[]>
         IAnimationSampler<Single[]>
     {
     {
         #region lifecycle
         #region lifecycle
@@ -354,6 +379,32 @@ namespace SharpGLTF.Schema2
             return accessor;
             return accessor;
         }
         }
 
 
+        private Accessor _CreateOutputAccessor(IReadOnlyList<SparseWeight8> output, int expandedCount)
+        {
+            var root = LogicalParent.LogicalParent;
+
+            var buffer = root.UseBufferView(new Byte[output.Count * 4 * expandedCount]);
+            var accessor = root.CreateAccessor("Animation.Output");
+
+            accessor.SetData(buffer, 0, output.Count * expandedCount, DimensionType.SCALAR, EncodingType.FLOAT, false);
+
+            var dst = accessor.AsScalarArray();
+
+            for (int i = 0; i < output.Count; ++i)
+            {
+                var src = output[i];
+
+                for (int j = 0; j < expandedCount; ++j)
+                {
+                    dst[(i * expandedCount) + j] = src[j];
+                }
+            }
+
+            accessor.UpdateBounds();
+
+            return accessor;
+        }
+
         private static (Single[], TValue[]) _Split<TValue>(IReadOnlyDictionary<Single, TValue> keyframes)
         private static (Single[], TValue[]) _Split<TValue>(IReadOnlyDictionary<Single, TValue> keyframes)
         {
         {
             var sorted = keyframes
             var sorted = keyframes
@@ -406,6 +457,13 @@ namespace SharpGLTF.Schema2
             _output = this._CreateOutputAccessor(kv.Item2).LogicalIndex;
             _output = this._CreateOutputAccessor(kv.Item2).LogicalIndex;
         }
         }
 
 
+        internal void SetKeys(IReadOnlyDictionary<Single, SparseWeight8> keyframes, int expandedCount)
+        {
+            var kv = _Split(keyframes);
+            _input = this._CreateInputAccessor(kv.Item1).LogicalIndex;
+            _output = this._CreateOutputAccessor(kv.Item2, expandedCount).LogicalIndex;
+        }
+
         internal void SetKeys(IReadOnlyDictionary<Single, (Vector3, Vector3, Vector3)> keyframes)
         internal void SetKeys(IReadOnlyDictionary<Single, (Vector3, Vector3, Vector3)> keyframes)
         {
         {
             var kv = _Split(keyframes);
             var kv = _Split(keyframes);
@@ -448,6 +506,18 @@ namespace SharpGLTF.Schema2
             return keys.Zip(frames, (key, val) => (key, val));
             return keys.Zip(frames, (key, val) => (key, val));
         }
         }
 
 
+        IEnumerable<(Single, SparseWeight8)> IAnimationSampler<SparseWeight8>.GetLinearKeys()
+        {
+            Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
+
+            var dimensions = this.Output.Count / this.Input.Count;
+
+            var keys = this.Input.AsScalarArray();
+            var frames = this.Output.AsMultiArray(dimensions);
+
+            return keys.Zip(frames, (key, val) => (key, SparseWeight8.Create(val)));
+        }
+
         IEnumerable<(Single, Single[])> IAnimationSampler<Single[]>.GetLinearKeys()
         IEnumerable<(Single, Single[])> IAnimationSampler<Single[]>.GetLinearKeys()
         {
         {
             Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
             Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
@@ -492,6 +562,18 @@ namespace SharpGLTF.Schema2
             return keys.Zip(frames, (key, val) => (key, val));
             return keys.Zip(frames, (key, val) => (key, val));
         }
         }
 
 
+        IEnumerable<(Single, (SparseWeight8, SparseWeight8, SparseWeight8))> IAnimationSampler<SparseWeight8>.GetCubicKeys()
+        {
+            Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
+
+            var dimensions = this.Output.Count / (this.Input.Count * 3);
+
+            var keys = this.Input.AsScalarArray();
+            var frames = _GroupByThree(this.Output.AsMultiArray(dimensions));
+
+            return keys.Zip(frames, (key, val) => (key, (SparseWeight8.Create(val.Item1), SparseWeight8.Create(val.Item2), SparseWeight8.Create(val.Item3))));
+        }
+
         private static IEnumerable<(T, T, T)> _GroupByThree<T>(IEnumerable<T> collection)
         private static IEnumerable<(T, T, T)> _GroupByThree<T>(IEnumerable<T> collection)
         {
         {
             using (var ptr = collection.GetEnumerator())
             using (var ptr = collection.GetEnumerator())
@@ -552,6 +634,20 @@ namespace SharpGLTF.Schema2
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
 
 
+        ICurveSampler<SparseWeight8> IAnimationSampler<SparseWeight8>.CreateCurveSampler()
+        {
+            var xsampler = this as IAnimationSampler<SparseWeight8>;
+
+            switch (this.InterpolationMode)
+            {
+                case AnimationInterpolationMode.STEP: return xsampler.GetLinearKeys().CreateSampler(false);
+                case AnimationInterpolationMode.LINEAR: return xsampler.GetLinearKeys().CreateSampler();
+                case AnimationInterpolationMode.CUBICSPLINE: return xsampler.GetCubicKeys().CreateSampler();
+            }
+
+            throw new NotImplementedException();
+        }
+
         #endregion
         #endregion
     }
     }
 
 

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

@@ -176,7 +176,7 @@ namespace SharpGLTF.Schema2
         /// <returns>A <see cref="Transforms.ITransform"/> object</returns>
         /// <returns>A <see cref="Transforms.ITransform"/> object</returns>
         public Transforms.ITransform GetMeshWorldTransform(Animation animation, float time)
         public Transforms.ITransform GetMeshWorldTransform(Animation animation, float time)
         {
         {
-            var weights = animation == null ? this.MorphWeights : animation.GetMorphWeights(this, time);
+            var weights = animation == null ? Transforms.SparseWeight8.Create(this.MorphWeights) : animation.GetSparseMorphWeights(this, time);
 
 
             if (this.Skin == null) return new Transforms.StaticTransform(this.GetWorldMatrix(animation, time), weights);
             if (this.Skin == null) return new Transforms.StaticTransform(this.GetWorldMatrix(animation, time), weights);
 
 

+ 105 - 50
src/SharpGLTF.Core/Transforms/MeshTransforms.cs

@@ -36,66 +36,91 @@ namespace SharpGLTF.Transforms
     {
     {
         #region constructor
         #region constructor
 
 
-        protected MorphTransform(IReadOnlyList<float> morphWeights)
+        protected MorphTransform(SparseWeight8 morphWeights)
         {
         {
-            if (morphWeights == null || morphWeights.Count == 0)
+            if (morphWeights.IsZero)
             {
             {
-                _InvWeight = 1;
+                _Weights = new SparseWeight8((0, 1));
                 return;
                 return;
             }
             }
 
 
-            var sum = morphWeights.Sum();
-
-            if (sum == 0)
-            {
-                _InvWeight = 1;
-                return;
-            }
-
-            _MorphWeights = new float[morphWeights.Count];
-            for (int i = 0; i < morphWeights.Count; ++i) _MorphWeights[i] = morphWeights[i];
-
-            if (sum <= 1)
-            {
-                _InvWeight = 1 - sum;
-            }
-            else
-            {
-                _InvWeight = 0;
-                for (int i = 0; i < morphWeights.Count; ++i) _MorphWeights[i] = _MorphWeights[i] / sum;
-            }
+            _Weights = Normalize(morphWeights);
         }
         }
 
 
         #endregion
         #endregion
 
 
         #region data
         #region data
 
 
-        private readonly float _InvWeight;
-        private readonly float[] _MorphWeights;
+        /// <summary>
+        /// Represents a normalized sparse collection of weights where:
+        /// - Indices with value zero point to the master mesh
+        /// - Indices with value over zero point to MorphTarget[index-1].
+        /// </summary>
+        private readonly SparseWeight8 _Weights;
 
 
         #endregion
         #endregion
 
 
-        #region properties
-
-        public float InverseWeight => _InvWeight;
-
-        public IReadOnlyList<float> MorphWeights => _MorphWeights;
+        #region API
 
 
-        #endregion
+        /// <summary>
+        /// Increments all indices and adds a new Index[0] with a weight that makes the sum of all weights equal to 1
+        /// </summary>
+        /// <param name="r"></param>
+        /// <returns></returns>
+        internal static SparseWeight8 Normalize(SparseWeight8 r)
+        {
+            int i = -1;
+            float w = float.MaxValue;
+            float ww = 0;
+
+            ww += r.Weight0; if (r.Weight0 < w) { i = 0; w = r.Weight0; }
+            ww += r.Weight1; if (r.Weight1 < w) { i = 1; w = r.Weight1; }
+            ww += r.Weight2; if (r.Weight2 < w) { i = 2; w = r.Weight2; }
+            ww += r.Weight3; if (r.Weight3 < w) { i = 3; w = r.Weight3; }
+            ww += r.Weight4; if (r.Weight4 < w) { i = 4; w = r.Weight4; }
+            ww += r.Weight5; if (r.Weight5 < w) { i = 5; w = r.Weight5; }
+            ww += r.Weight6; if (r.Weight6 < w) { i = 6; w = r.Weight6; }
+            ww += r.Weight7; if (r.Weight7 < w) { i = 7; w = r.Weight7; }
+
+            r.Index0 += 1;
+            r.Index1 += 1;
+            r.Index2 += 1;
+            r.Index3 += 1;
+            r.Index4 += 1;
+            r.Index5 += 1;
+            r.Index6 += 1;
+            r.Index7 += 1;
+
+            ww -= w;
+            var iw = 1 - ww;
+
+            switch (i)
+            {
+                case 0: r.Index0 = 0; r.Weight0 = iw; break;
+                case 1: r.Index1 = 0; r.Weight1 = iw; break;
+                case 2: r.Index2 = 0; r.Weight2 = iw; break;
+                case 3: r.Index3 = 0; r.Weight3 = iw; break;
+                case 4: r.Index4 = 0; r.Weight4 = iw; break;
+                case 5: r.Index5 = 0; r.Weight5 = iw; break;
+                case 6: r.Index6 = 0; r.Weight6 = iw; break;
+                case 7: r.Index7 = 0; r.Weight7 = iw; break;
+            }
 
 
-        #region API
+            return r;
+        }
 
 
         protected V3 MorphVectors(V3 value, V3[] morphTargets)
         protected V3 MorphVectors(V3 value, V3[] morphTargets)
         {
         {
-            if (_InvWeight == 1 || morphTargets == null || morphTargets.Length == 0) return value;
+            if (morphTargets == null) return value;
 
 
-            Guard.IsTrue(_MorphWeights.Length == morphTargets.Length, nameof(morphTargets));
+            if (_Weights.Index0 == 0 && _Weights.Weight0 == 1) return value;
 
 
-            var p = value * _InvWeight;
+            var p = V3.Zero;
 
 
-            for (int i = 0; i < _MorphWeights.Length; ++i)
+            foreach (var pair in _Weights.GetPairs())
             {
             {
-                p += morphTargets[i] * _MorphWeights[i];
+                var val = pair.Item1 == 0 ? value : morphTargets[pair.Item1 - 1];
+                p += val * pair.Item2;
             }
             }
 
 
             return p;
             return p;
@@ -103,15 +128,16 @@ namespace SharpGLTF.Transforms
 
 
         protected V4 MorphVectors(V4 value, V4[] morphTargets)
         protected V4 MorphVectors(V4 value, V4[] morphTargets)
         {
         {
-            if (_InvWeight == 1 || morphTargets == null || morphTargets.Length == 0) return value;
+            if (morphTargets == null) return value;
 
 
-            Guard.IsTrue(_MorphWeights.Length == morphTargets.Length, nameof(morphTargets));
+            if (_Weights.Index0 == 0 && _Weights.Weight0 == 1) return value;
 
 
-            var p = value * _InvWeight;
+            var p = V4.Zero;
 
 
-            for (int i = 0; i < _MorphWeights.Length; ++i)
+            foreach (var pair in _Weights.GetPairs())
             {
             {
-                p += morphTargets[i] * _MorphWeights[i];
+                var val = pair.Item1 == 0 ? value : morphTargets[pair.Item1 - 1];
+                p += val * pair.Item2;
             }
             }
 
 
             return p;
             return p;
@@ -124,32 +150,47 @@ namespace SharpGLTF.Transforms
 
 
     public class StaticTransform : MorphTransform, ITransform
     public class StaticTransform : MorphTransform, ITransform
     {
     {
-        public StaticTransform(TRANSFORM xform, IReadOnlyList<float> morphWeights = null)
+        #region constructor
+
+        public StaticTransform(TRANSFORM xform, SparseWeight8 morphWeights)
             : base(morphWeights)
             : base(morphWeights)
         {
         {
             _Transform = xform;
             _Transform = xform;
 
 
             // http://m-hikari.com/ija/ija-password-2009/ija-password5-8-2009/hajrizajIJA5-8-2009.pdf
             // http://m-hikari.com/ija/ija-password-2009/ija-password5-8-2009/hajrizajIJA5-8-2009.pdf
-            var determinant3x3 =
+
+            float determinant3x3 =
                 +(xform.M13 * xform.M21 * xform.M32)
                 +(xform.M13 * xform.M21 * xform.M32)
-                +(xform.M11 * xform.M22 * xform.M33)
-                +(xform.M12 * xform.M23 * xform.M31)
-                -(xform.M12 * xform.M21 * xform.M33)
-                -(xform.M13 * xform.M22 * xform.M31)
-                -(xform.M11 * xform.M23 * xform.M32);
+                + (xform.M11 * xform.M22 * xform.M33)
+                + (xform.M12 * xform.M23 * xform.M31)
+                - (xform.M12 * xform.M21 * xform.M33)
+                - (xform.M13 * xform.M22 * xform.M31)
+                - (xform.M11 * xform.M23 * xform.M32);
 
 
             _Visible = Math.Abs(determinant3x3) > float.Epsilon;
             _Visible = Math.Abs(determinant3x3) > float.Epsilon;
             _FlipFaces = determinant3x3 < 0;
             _FlipFaces = determinant3x3 < 0;
         }
         }
 
 
+        #endregion
+
+        #region data
+
         private readonly TRANSFORM _Transform;
         private readonly TRANSFORM _Transform;
         private readonly Boolean _Visible;
         private readonly Boolean _Visible;
         private readonly Boolean _FlipFaces;
         private readonly Boolean _FlipFaces;
 
 
+        #endregion
+
+        #region properties
+
         public Boolean Visible => _Visible;
         public Boolean Visible => _Visible;
 
 
         public Boolean FlipFaces => _FlipFaces;
         public Boolean FlipFaces => _FlipFaces;
 
 
+        #endregion
+
+        #region API
+
         public V3 TransformPosition(V3 position, V3[] morphTargets, (int, float)[] skinWeights)
         public V3 TransformPosition(V3 position, V3[] morphTargets, (int, float)[] skinWeights)
         {
         {
             position = MorphVectors(position, morphTargets);
             position = MorphVectors(position, morphTargets);
@@ -172,11 +213,15 @@ namespace SharpGLTF.Transforms
 
 
             return new V4(n, tangent.W);
             return new V4(n, tangent.W);
         }
         }
+
+        #endregion
     }
     }
 
 
     public class SkinTransform : MorphTransform, ITransform
     public class SkinTransform : MorphTransform, ITransform
     {
     {
-        public SkinTransform(TRANSFORM[] invBindings, TRANSFORM[] xforms, IReadOnlyList<float> morphWeights = null)
+        #region constructor
+
+        public SkinTransform(TRANSFORM[] invBindings, TRANSFORM[] xforms, SparseWeight8 morphWeights)
             : base(morphWeights)
             : base(morphWeights)
         {
         {
             Guard.NotNull(invBindings, nameof(invBindings));
             Guard.NotNull(invBindings, nameof(invBindings));
@@ -191,8 +236,16 @@ namespace SharpGLTF.Transforms
             }
             }
         }
         }
 
 
+        #endregion
+
+        #region data
+
         private readonly TRANSFORM[] _JointTransforms;
         private readonly TRANSFORM[] _JointTransforms;
 
 
+        #endregion
+
+        #region API
+
         public bool Visible => true;
         public bool Visible => true;
 
 
         public bool FlipFaces => false;
         public bool FlipFaces => false;
@@ -242,5 +295,7 @@ namespace SharpGLTF.Transforms
 
 
             return new V4(worldTangent, localTangent.W);
             return new V4(worldTangent, localTangent.W);
         }
         }
+
+        #endregion
     }
     }
 }
 }

+ 221 - 54
src/SharpGLTF.Core/Transforms/SparseWeight8.cs

@@ -1,5 +1,7 @@
 using System;
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using System.Text;
 
 
 namespace SharpGLTF.Transforms
 namespace SharpGLTF.Transforms
@@ -7,12 +9,57 @@ namespace SharpGLTF.Transforms
     /// <summary>
     /// <summary>
     /// Represents a sparse collection of weight values, with a maximum of 8 weights.
     /// Represents a sparse collection of weight values, with a maximum of 8 weights.
     /// </summary>
     /// </summary>
-    public struct SparseWeight8
+    [System.Diagnostics.DebuggerDisplay("[{Index0}]={Weight0}  [{Index1}]={Weight1}  [{Index2}]={Weight2}  [{Index3}]={Weight3}  [{Index4}]={Weight4}  [{Index5}]={Weight5}  [{Index6}]={Weight6}  [{Index7}]={Weight7}")]
+    public struct SparseWeight8 : IReadOnlyList<float>, IReadOnlyDictionary<int, float>
     {
     {
         #region lifecycle
         #region lifecycle
 
 
-        internal SparseWeight8(ReadOnlySpan<int> indices, ReadOnlySpan<float> weights)
+        public static SparseWeight8 Create(params float[] weights)
         {
         {
+            return Create(weights as IEnumerable<float>);
+        }
+
+        public static SparseWeight8 Create(IEnumerable<float> weights)
+        {
+            if (weights == null) return default;
+
+            var indexedWeights = weights
+                .Select((val, idx) => (idx, val))
+                .Where(item => item.val > 0)
+                .OrderByDescending(item => item.val)
+                .Take(8)
+                .ToArray();
+
+            return new SparseWeight8(indexedWeights);
+        }
+
+        public SparseWeight8(params (int, float)[] items)
+        {
+            Guard.NotNull(items, nameof(items));
+
+            this.Index0 = items.Length > 0 ? items[0].Item1 : 0;
+            this.Index1 = items.Length > 1 ? items[1].Item1 : 0;
+            this.Index2 = items.Length > 2 ? items[2].Item1 : 0;
+            this.Index3 = items.Length > 3 ? items[3].Item1 : 0;
+            this.Index4 = items.Length > 4 ? items[4].Item1 : 0;
+            this.Index5 = items.Length > 5 ? items[5].Item1 : 0;
+            this.Index6 = items.Length > 6 ? items[6].Item1 : 0;
+            this.Index7 = items.Length > 7 ? items[7].Item1 : 0;
+
+            this.Weight0 = items.Length > 0 ? items[0].Item2 : 0;
+            this.Weight1 = items.Length > 1 ? items[1].Item2 : 0;
+            this.Weight2 = items.Length > 2 ? items[2].Item2 : 0;
+            this.Weight3 = items.Length > 3 ? items[3].Item2 : 0;
+            this.Weight4 = items.Length > 4 ? items[4].Item2 : 0;
+            this.Weight5 = items.Length > 5 ? items[5].Item2 : 0;
+            this.Weight6 = items.Length > 6 ? items[6].Item2 : 0;
+            this.Weight7 = items.Length > 7 ? items[7].Item2 : 0;
+        }
+
+        private SparseWeight8(ReadOnlySpan<int> indices, ReadOnlySpan<float> weights)
+        {
+            System.Diagnostics.Debug.Assert(indices.Length == weights.Length);
+
             this.Index0 = indices.Length > 0 ? indices[0] : 0;
             this.Index0 = indices.Length > 0 ? indices[0] : 0;
             this.Index1 = indices.Length > 1 ? indices[1] : 0;
             this.Index1 = indices.Length > 1 ? indices[1] : 0;
             this.Index2 = indices.Length > 2 ? indices[2] : 0;
             this.Index2 = indices.Length > 2 ? indices[2] : 0;
@@ -58,110 +105,194 @@ namespace SharpGLTF.Transforms
 
 
         #region properties
         #region properties
 
 
-        public (int, int) IndexRange
-        {
-            get
-            {
-                int minRange = int.MaxValue;
-                int maxRange = int.MinValue;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        public int Count => GetExpandedCount();
 
 
-                foreach (var p in GetPairs())
-                {
-                    if (minRange > p.Item1) minRange = p.Item1;
-                    if (maxRange < p.Item1) maxRange = p.Item1;
-                }
+        public float this[int index] => GetExpandedAt(index);
 
 
-                return (minRange, maxRange);
-            }
-        }
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        public bool IsZero => Weight0 == 0 & Weight1 == 0 & Weight2 == 0 & Weight3 == 0 & Weight4 == 0 & Weight5 == 0 & Weight6 == 0 & Weight7 == 0;
+
+        public IEnumerable<int> Keys => GetPairs().Select(item => item.Item1);
+
+        public IEnumerable<float> Values => GetPairs().Select(item => item.Item2);
 
 
         #endregion
         #endregion
 
 
         #region API
         #region API
 
 
-        public static SparseWeight8 Lerp(SparseWeight8 a, SparseWeight8 b, float amount)
+        public static SparseWeight8 InterpolateLinear(SparseWeight8 x, SparseWeight8 y, float amount)
         {
         {
+            Span<int> indices = stackalloc int[16];
+            Span<float> xWeights = stackalloc float[16];
+            Span<float> yWeights = stackalloc float[16];
+
+            int offset = 0;
+            offset = CopyTo(x, indices, xWeights, offset);
+            offset = CopyTo(y, indices, yWeights, offset);
+
+            indices = indices.Slice(0, offset);
+            xWeights = xWeights.Slice(0, offset);
+            yWeights = yWeights.Slice(0, offset);
+
             var invAmount = 1.0f - amount;
             var invAmount = 1.0f - amount;
 
 
-            Span<int> ai = stackalloc int[8];
-            Span<float> aw = stackalloc float[8];
-            Span<int> bi = stackalloc int[8];
-            Span<float> bw = stackalloc float[8];
+            for (int i = 0; i < indices.Length; ++i)
+            {
+                xWeights[i] = (xWeights[i] * invAmount) + (yWeights[i] * amount);
+            }
+
+            BubbleSort(indices, xWeights);
+
+            return new SparseWeight8(indices, xWeights);
+        }
+
+        public static SparseWeight8 InterpolateCubic(SparseWeight8 x, SparseWeight8 xt, SparseWeight8 y, SparseWeight8 yt, float amount)
+        {
+            Span<int> indices = stackalloc int[16];
+            Span<float> xWeights = stackalloc float[16];
+            Span<float> xTangent = stackalloc float[16];
+            Span<float> yWeights = stackalloc float[16];
+            Span<float> yTangent = stackalloc float[16];
 
 
-            ai = a.CopyTo(ai, aw);
-            bi = a.CopyTo(bi, bw);
+            int offset = 0;
+            offset = CopyTo(x, xt, indices, xWeights, xTangent, offset);
+            offset = CopyTo(y, yt, indices, yWeights, yTangent, offset);
 
 
-            Span<int> indices = stackalloc int[ai.Length + bi.Length];
-            Span<float> weights = stackalloc float[ai.Length + bi.Length];
+            indices = indices.Slice(0, offset);
+            xWeights = xWeights.Slice(0, offset);
+            xTangent = xWeights.Slice(0, offset);
+            yWeights = yWeights.Slice(0, offset);
+            yTangent = yTangent.Slice(0, offset);
 
 
-            // copy first batch
-            var c = ai.Length;
-            ai.CopyTo(indices);
-            aw.Slice(0, c).CopyTo(weights);
+            var basis = Animations.SamplerFactory.CreateHermitePointWeights(amount);
 
 
-            for (int i = 0; i < bi.Length; ++i)
+            for (int i = 0; i < indices.Length; ++i)
             {
             {
-                var bidx = bi[i];
-                var bwht = bw[i];
+                xWeights[i]
+                    = (xWeights[i] * basis.Item1)
+                    + (yWeights[i] * basis.Item2)
+                    + (xTangent[i] * basis.Item3)
+                    + (yTangent[i] * basis.Item4);
+            }
 
 
-                var idx = indices
-                    .Slice(0, c)
-                    .IndexOf(bidx);
+            BubbleSort(indices, xWeights);
+
+            return new SparseWeight8(indices, xWeights);
+        }
+
+        private static int CopyTo(SparseWeight8 p, SparseWeight8 t, Span<int> indices, Span<float> weights, Span<float> tangent, int offset)
+        {
+            System.Diagnostics.Debug.Assert(indices.Length == weights.Length);
+            System.Diagnostics.Debug.Assert(indices.Length == tangent.Length);
+
+            for (int i = 0; i < 8; ++i)
+            {
+                var pair = p.GetPair(i);
+                if (pair.Item2 == 0) continue;
+                var idx = indices.Slice(0, offset).IndexOf(pair.Item1);
 
 
                 if (idx < 0)
                 if (idx < 0)
                 {
                 {
-                    indices[c] = bidx;
-                    weights[c] = bwht;
-                    ++c;
-                }
-                else
-                {
-                    weights[idx] = (weights[idx] * invAmount) + (bwht * amount);
+                    indices[offset] = pair.Item1;
+                    weights[offset] = pair.Item2;
+                    tangent[offset] = t[pair.Item1];
+                    ++offset;
                 }
                 }
             }
             }
 
 
-            return new SparseWeight8(indices, weights);
+            return offset;
         }
         }
 
 
-        private Span<int> CopyTo(Span<int> indices, Span<float> weights)
+        private static int CopyTo(SparseWeight8 p, Span<int> indices, Span<float> weights, int offset)
         {
         {
-            int count = 0;
+            System.Diagnostics.Debug.Assert(indices.Length == weights.Length);
 
 
-            foreach (var pair in GetPairs())
+            for (int i = 0; i < 8; ++i)
             {
             {
+                var pair = p.GetPair(i);
+                if (pair.Item2 == 0) continue;
                 var idx = indices
                 var idx = indices
-                    .Slice(0, count)
+                    .Slice(0, offset)
                     .IndexOf(pair.Item1);
                     .IndexOf(pair.Item1);
 
 
                 if (idx < 0)
                 if (idx < 0)
                 {
                 {
-                    indices[count] = pair.Item1;
-                    weights[count] = pair.Item2;
-                    ++count;
+                    indices[offset] = pair.Item1;
+                    weights[offset] = pair.Item2;
+                    ++offset;
                 }
                 }
                 else
                 else
                 {
                 {
-                    weights[idx] += pair.Item2;
+                    weights[idx] = pair.Item2;
                 }
                 }
             }
             }
 
 
-            return indices.Slice(0, count);
+            return offset;
+        }
+
+        private static void BubbleSort(Span<int> indices, Span<float> weights)
+        {
+            System.Diagnostics.Debug.Assert(indices.Length == weights.Length);
 
 
+            for (int i = 0; i < weights.Length - 1; ++i)
+            {
+                for (int j = i + 1; j < weights.Length; ++j)
+                {
+                    if (weights[j - 1] < weights[j])
+                    {
+                        var index = indices[j - 1];
+                        indices[j - 1] = indices[j];
+                        indices[j] = index;
+
+                        var weight = weights[j - 1];
+                        weights[j - 1] = weights[j];
+                        weights[j] = weight;
+                    }
+                }
+            }
         }
         }
 
 
-        private IEnumerable<(int, float)> GetPairs()
+        internal IEnumerable<(int, float)> GetPairs()
         {
         {
             if (Weight0 > 0) yield return (Index0, Weight0);
             if (Weight0 > 0) yield return (Index0, Weight0);
             if (Weight1 > 0) yield return (Index1, Weight1);
             if (Weight1 > 0) yield return (Index1, Weight1);
             if (Weight2 > 0) yield return (Index2, Weight2);
             if (Weight2 > 0) yield return (Index2, Weight2);
             if (Weight3 > 0) yield return (Index3, Weight3);
             if (Weight3 > 0) yield return (Index3, Weight3);
-
             if (Weight4 > 0) yield return (Index4, Weight4);
             if (Weight4 > 0) yield return (Index4, Weight4);
             if (Weight5 > 0) yield return (Index5, Weight5);
             if (Weight5 > 0) yield return (Index5, Weight5);
             if (Weight6 > 0) yield return (Index6, Weight6);
             if (Weight6 > 0) yield return (Index6, Weight6);
             if (Weight7 > 0) yield return (Index7, Weight7);
             if (Weight7 > 0) yield return (Index7, Weight7);
         }
         }
 
 
+        private float GetExpandedAt(int idx)
+        {
+            if (idx == Index0) return Weight0;
+            if (idx == Index1) return Weight1;
+            if (idx == Index2) return Weight2;
+            if (idx == Index3) return Weight3;
+            if (idx == Index4) return Weight4;
+            if (idx == Index5) return Weight5;
+            if (idx == Index6) return Weight6;
+            if (idx == Index7) return Weight7;
+            return 0;
+        }
+
+        private int GetExpandedCount()
+        {
+            var c = 0;
+            if (Weight0 > 0 && c <= Index0) c = Index0 + 1;
+            if (Weight1 > 0 && c <= Index1) c = Index1 + 1;
+            if (Weight2 > 0 && c <= Index2) c = Index2 + 1;
+            if (Weight3 > 0 && c <= Index3) c = Index3 + 1;
+            if (Weight4 > 0 && c <= Index4) c = Index4 + 1;
+            if (Weight5 > 0 && c <= Index5) c = Index5 + 1;
+            if (Weight6 > 0 && c <= Index6) c = Index6 + 1;
+            if (Weight7 > 0 && c <= Index7) c = Index7 + 1;
+
+            return c;
+        }
+
         private (int, float) GetPair(int idx)
         private (int, float) GetPair(int idx)
         {
         {
             switch (idx)
             switch (idx)
@@ -178,6 +309,42 @@ namespace SharpGLTF.Transforms
             }
             }
         }
         }
 
 
+        public IEnumerable<float> Expand(int count)
+        {
+            for (int i = 0; i < count; ++i)
+            {
+                yield return GetExpandedAt(i);
+            }
+        }
+
+        public IEnumerator<float> GetEnumerator() { return Expand(GetExpandedCount()).GetEnumerator(); }
+
+        IEnumerator IEnumerable.GetEnumerator() { return Expand(GetExpandedCount()).GetEnumerator(); }
+
+        public bool ContainsKey(int key)
+        {
+            return GetPairs().Select(item => item.Item1).Contains(key);
+        }
+
+        public bool TryGetValue(int key, out float value)
+        {
+            if (key == Index0) { value = Weight0; return true; }
+            if (key == Index1) { value = Weight1; return true; }
+            if (key == Index2) { value = Weight2; return true; }
+            if (key == Index3) { value = Weight3; return true; }
+            if (key == Index4) { value = Weight4; return true; }
+            if (key == Index5) { value = Weight5; return true; }
+            if (key == Index6) { value = Weight6; return true; }
+            if (key == Index7) { value = Weight7; return true; }
+            value = 0;
+            return false;
+        }
+
+        IEnumerator<KeyValuePair<int, float>> IEnumerable<KeyValuePair<int, float>>.GetEnumerator()
+        {
+            return GetPairs().Select(item => new KeyValuePair<int, float>(item.Item1, item.Item2)).GetEnumerator();
+        }
+
         #endregion
         #endregion
 
 
     }
     }

+ 4 - 4
src/SharpGLTF.Toolkit/Animations/CurveFactory.cs

@@ -62,7 +62,7 @@ namespace SharpGLTF.Animations
                     return Vector3.Lerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
                     return Vector3.Lerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
 
 
                 case 3:
                 case 3:
-                    return SamplerFactory.CubicLerp
+                    return SamplerFactory.InterpolateCubic
                             (
                             (
                             sample.Item1.Point, sample.Item1.OutgoingTangent,
                             sample.Item1.Point, sample.Item1.OutgoingTangent,
                             sample.Item2.Point, sample.Item2.IncomingTangent,
                             sample.Item2.Point, sample.Item2.IncomingTangent,
@@ -123,7 +123,7 @@ namespace SharpGLTF.Animations
                     return Quaternion.Slerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
                     return Quaternion.Slerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
 
 
                 case 3:
                 case 3:
-                    return SamplerFactory.CubicLerp
+                    return SamplerFactory.InterpolateCubic
                             (
                             (
                             sample.Item1.Point, sample.Item1.OutgoingTangent,
                             sample.Item1.Point, sample.Item1.OutgoingTangent,
                             sample.Item2.Point, sample.Item2.IncomingTangent,
                             sample.Item2.Point, sample.Item2.IncomingTangent,
@@ -199,10 +199,10 @@ namespace SharpGLTF.Animations
                     return sample.Item1.Point;
                     return sample.Item1.Point;
 
 
                 case 1:
                 case 1:
-                    return SamplerFactory.Lerp(sample.Item1.Point, sample.Item2.Point, sample.Item3);
+                    return SamplerFactory.InterpolateLinear(sample.Item1.Point, sample.Item2.Point, sample.Item3);
 
 
                 case 3:
                 case 3:
-                    return SamplerFactory.CubicLerp
+                    return SamplerFactory.InterpolateCubic
                             (
                             (
                             sample.Item1.Point, sample.Item1.OutgoingTangent,
                             sample.Item1.Point, sample.Item1.OutgoingTangent,
                             sample.Item2.Point, sample.Item2.IncomingTangent,
                             sample.Item2.Point, sample.Item2.IncomingTangent,

+ 71 - 0
tests/SharpGLTF.Tests/Transforms/SparseWeight8Tests.cs

@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+using NUnit.Framework;
+
+namespace SharpGLTF.Transforms
+{
+    [Category("Core.Transforms")]
+    public class SparseWeight8Tests
+    {
+        [Test]
+        public void CreateSparse()
+        {
+            var sparse1 = SparseWeight8.Create(0, 0, 0, 0, 0, 0.1f, 0.7f, 0, 0, 0, 0.1f);
+            Assert.AreEqual(6, sparse1.Index0);
+            Assert.AreEqual(5, sparse1.Index1);
+            Assert.AreEqual(10, sparse1.Index2);
+
+            Assert.AreEqual(0.7f, sparse1.Weight0);
+            Assert.AreEqual(0.1f, sparse1.Weight1);
+            Assert.AreEqual(0.1f, sparse1.Weight2);
+            Assert.AreEqual(0, sparse1.Weight3);
+
+            var sparse1Nrm = MorphTransform.Normalize(sparse1);
+            Assert.AreEqual(7, sparse1Nrm.Index0);
+            Assert.AreEqual(6, sparse1Nrm.Index1);
+            Assert.AreEqual(11, sparse1Nrm.Index2);
+            Assert.AreEqual(0, sparse1Nrm.Index3);
+
+            Assert.AreEqual(0.7f, sparse1Nrm.Weight0);
+            Assert.AreEqual(0.1f, sparse1Nrm.Weight1);
+            Assert.AreEqual(0.1f, sparse1Nrm.Weight2);
+            Assert.AreEqual(0.1f, sparse1Nrm.Weight3, 0.00001f); // funny enough, 0.8f + 0.1f = 0.90000036f
+            Assert.AreEqual(0, sparse1Nrm.Weight4);
+        }
+
+        [Test]
+        public void TestLinearInterpolation1()
+        {
+            var x = new SparseWeight8((0,0f));
+            var y = new SparseWeight8((0,1f));
+
+            var z = SparseWeight8.InterpolateLinear(x, y, 0.5f);
+        }
+
+        [Test]
+        public void TestLinearInterpolation2()
+        {
+            var ax = new float[] { 0, 0, 0, 0, 0, 0.1f, 0.7f, 0, 0, 0, 0.1f };
+            var ay = new float[] { 0, 0, 0.2f, 0, 0.1f, 0, 0, 0, 0, 0, 0 };
+
+            var x = SparseWeight8.Create(ax); CollectionAssert.AreEqual(ax, x.Expand(ax.Length));
+            var y = SparseWeight8.Create(ay); CollectionAssert.AreEqual(ay, y.Expand(ay.Length));
+
+            var z = SparseWeight8.InterpolateLinear(x, y, 0.5f);
+            Assert.AreEqual(6, z.Index0);
+            Assert.AreEqual(2, z.Index1);
+            Assert.AreEqual(5, z.Index2);
+            Assert.AreEqual(10, z.Index3);
+            Assert.AreEqual(4, z.Index4);
+
+            Assert.AreEqual(0.35f, z.Weight0);
+            Assert.AreEqual(0.1f, z.Weight1);
+            Assert.AreEqual(0.05f, z.Weight2);
+            Assert.AreEqual(0.05f, z.Weight3);
+            Assert.AreEqual(0.05f, z.Weight4);
+            Assert.AreEqual(0, z.Weight5);
+        }
+    }
+}