Browse Source

Improved morphing animation support [WIP]:
Added support to define more than 8 keys, although few clients actually support it.

Vicente Penades 4 years ago
parent
commit
6b4dd2f5c3

+ 111 - 56
src/SharpGLTF.Core/Animations/CurveSampler.cs

@@ -3,6 +3,10 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Numerics;
 using System.Numerics;
 
 
+using ROLIST = System.Collections.Generic.IReadOnlyList<float>;
+using SEGMENT = System.ArraySegment<float>;
+using SPARSE = SharpGLTF.Transforms.SparseWeight8;
+
 namespace SharpGLTF.Animations
 namespace SharpGLTF.Animations
 {
 {
     /// <summary>
     /// <summary>
@@ -310,7 +314,21 @@ namespace SharpGLTF.Animations
 
 
         #region interpolation utils
         #region interpolation utils
 
 
-        public static Single[] InterpolateLinear(Single[] start, Single[] end, Single amount)
+        public static Single[] Subtract(ROLIST left, ROLIST right)
+        {
+            Guard.MustBeEqualTo(right.Count, left.Count, nameof(right));
+
+            var dst = new Single[left.Count];
+
+            for (int i = 0; i < dst.Length; ++i)
+            {
+                dst[i] = left[i] - right[i];
+            }
+
+            return dst;
+        }
+
+        public static Single[] InterpolateLinear(ROLIST start, ROLIST end, Single amount)
         {
         {
             Guard.NotNull(start, nameof(start));
             Guard.NotNull(start, nameof(start));
             Guard.NotNull(end, nameof(end));
             Guard.NotNull(end, nameof(end));
@@ -318,7 +336,7 @@ namespace SharpGLTF.Animations
             var startW = 1 - amount;
             var startW = 1 - amount;
             var endW = amount;
             var endW = amount;
 
 
-            var result = new float[start.Length];
+            var result = new float[start.Count];
 
 
             for (int i = 0; i < result.Length; ++i)
             for (int i = 0; i < result.Length; ++i)
             {
             {
@@ -342,7 +360,7 @@ namespace SharpGLTF.Animations
             return Quaternion.Normalize((start * hermite.StartPosition) + (end * hermite.EndPosition) + (outgoingTangent * hermite.StartTangent) + (incomingTangent * hermite.EndTangent));
             return Quaternion.Normalize((start * hermite.StartPosition) + (end * hermite.EndPosition) + (outgoingTangent * hermite.StartTangent) + (incomingTangent * hermite.EndTangent));
         }
         }
 
 
-        public static Single[] InterpolateCubic(Single[] start, Single[] outgoingTangent, Single[] end, Single[] incomingTangent, Single amount)
+        public static Single[] InterpolateCubic(ROLIST start, ROLIST outgoingTangent, ROLIST end, ROLIST incomingTangent, Single amount)
         {
         {
             Guard.NotNull(start, nameof(start));
             Guard.NotNull(start, nameof(start));
             Guard.NotNull(outgoingTangent, nameof(outgoingTangent));
             Guard.NotNull(outgoingTangent, nameof(outgoingTangent));
@@ -351,7 +369,7 @@ namespace SharpGLTF.Animations
 
 
             var hermite = CreateHermitePointWeights(amount);
             var hermite = CreateHermitePointWeights(amount);
 
 
-            var result = new float[start.Length];
+            var result = new float[start.Count];
 
 
             for (int i = 0; i < result.Length; ++i)
             for (int i = 0; i < result.Length; ++i)
             {
             {
@@ -365,99 +383,136 @@ namespace SharpGLTF.Animations
 
 
         #region sampler creation
         #region sampler creation
 
 
+        private static bool _HasZero<T>(this IEnumerable<T> collection) { return collection == null || !collection.Any(); }
+        private static bool _HasOne<T>(this IEnumerable<T> collection) { return !collection.Skip(1).Any(); }
+
         public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, Vector3)> collection, bool isLinear = true, bool optimize = false)
         public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, Vector3)> collection, bool isLinear = true, bool optimize = false)
         {
         {
-            if (collection == null) return null;
-
-            var single = SingleValueSampler<Vector3>.CreateForSingle(collection);
-            if (single != null) return single;
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Vector3>.Create(collection);
 
 
-            var sampler = new Vector3LinearSampler(collection, isLinear);
-
-            return optimize ? sampler.ToFastSampler() : sampler;
+            if (isLinear)
+            {
+                var sampler = new LinearSampler<Vector3>(collection, SamplerTraits.Vector3);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
+            else
+            {
+                var sampler = new StepSampler<Vector3>(collection, SamplerTraits.Vector3);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
         }
         }
 
 
         public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(Single, Quaternion)> collection, bool isLinear = true, bool optimize = false)
         public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(Single, Quaternion)> collection, bool isLinear = true, bool optimize = false)
         {
         {
-            if (collection == null) return null;
-
-            var single = SingleValueSampler<Quaternion>.CreateForSingle(collection);
-            if (single != null) return single;
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Quaternion>.Create(collection);
 
 
-            var sampler = new QuaternionLinearSampler(collection, isLinear);
-
-            return optimize ? sampler.ToFastSampler() : sampler;
+            if (isLinear)
+            {
+                var sampler = new LinearSampler<Quaternion>(collection, SamplerTraits.Quaternion);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
+            else
+            {
+                var sampler = new StepSampler<Quaternion>(collection, SamplerTraits.Quaternion);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
         }
         }
 
 
-        public static ICurveSampler<Transforms.SparseWeight8> CreateSampler(this IEnumerable<(Single, Transforms.SparseWeight8)> collection, bool isLinear = true, bool optimize = false)
+        public static ICurveSampler<Single[]> CreateSampler(this IEnumerable<(Single, Single[])> collection, bool isLinear = true, bool optimize = false)
         {
         {
-            if (collection == null) return null;
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Single[]>.Create(collection);
 
 
-            var single = SingleValueSampler<Transforms.SparseWeight8>.CreateForSingle(collection);
-            if (single != null) return single;
-
-            var sampler = new SparseLinearSampler(collection, isLinear);
-
-            return optimize ? sampler.ToFastSampler() : sampler;
+            if (isLinear)
+            {
+                var sampler = new LinearSampler<Single[]>(collection, SamplerTraits.Array);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
+            else
+            {
+                var sampler = new StepSampler<Single[]>(collection, SamplerTraits.Array);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
         }
         }
 
 
-        public static ICurveSampler<Single[]> CreateSampler(this IEnumerable<(Single, Single[])> collection, bool isLinear = true, bool optimize = false)
+        public static ICurveSampler<SEGMENT> CreateSampler(this IEnumerable<(Single, SEGMENT)> collection, bool isLinear = true, bool optimize = false)
         {
         {
-            if (collection == null) return null;
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<SEGMENT>.Create(collection);
 
 
-            var single = SingleValueSampler<Single[]>.CreateForSingle(collection);
-            if (single != null) return single;
+            if (isLinear)
+            {
+                var sampler = new LinearSampler<SEGMENT>(collection, SamplerTraits.Segment);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
+            else
+            {
+                var sampler = new StepSampler<SEGMENT>(collection, SamplerTraits.Segment);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
+        }
 
 
-            var sampler = new ArrayLinearSampler(collection, isLinear);
+        public static ICurveSampler<SPARSE> CreateSampler(this IEnumerable<(Single, SPARSE)> collection, bool isLinear = true, bool optimize = false)
+        {
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<SPARSE>.Create(collection);
 
 
-            return optimize ? sampler.ToFastSampler() : sampler;
+            if (isLinear)
+            {
+                var sampler = new LinearSampler<SPARSE>(collection, SamplerTraits.Sparse);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
+            else
+            {
+                var sampler = new StepSampler<SPARSE>(collection, SamplerTraits.Sparse);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
         }
         }
 
 
         public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, (Vector3, Vector3, Vector3))> collection, bool optimize = false)
         public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, (Vector3, Vector3, Vector3))> collection, bool optimize = false)
         {
         {
-            if (collection == null) return null;
-
-            var single = SingleValueSampler<Vector3>.CreateForSingle(collection);
-            if (single != null) return single;
-
-            var sampler = new Vector3CubicSampler(collection);
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Vector3>.Create(collection);
 
 
+            var sampler = new CubicSampler<Vector3>(collection, SamplerTraits.Vector3);
             return optimize ? sampler.ToFastSampler() : sampler;
             return optimize ? sampler.ToFastSampler() : sampler;
         }
         }
 
 
         public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(Single, (Quaternion, Quaternion, Quaternion))> collection, bool optimize = false)
         public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(Single, (Quaternion, Quaternion, Quaternion))> collection, bool optimize = false)
         {
         {
-            if (collection == null) return null;
-
-            var single = SingleValueSampler<Quaternion>.CreateForSingle(collection);
-            if (single != null) return single;
-
-            var sampler = new QuaternionCubicSampler(collection);
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Quaternion>.Create(collection);
 
 
+            var sampler = new CubicSampler<Quaternion>(collection, SamplerTraits.Quaternion);
             return optimize ? sampler.ToFastSampler() : sampler;
             return optimize ? sampler.ToFastSampler() : sampler;
         }
         }
 
 
-        public static ICurveSampler<Transforms.SparseWeight8> CreateSampler(this IEnumerable<(Single, (Transforms.SparseWeight8, Transforms.SparseWeight8, Transforms.SparseWeight8))> collection, bool optimize = false)
+        public static ICurveSampler<Single[]> CreateSampler(this IEnumerable<(Single, (Single[], Single[], Single[]))> collection, bool optimize = false)
         {
         {
-            if (collection == null) return null;
-
-            var single = SingleValueSampler<Transforms.SparseWeight8>.CreateForSingle(collection);
-            if (single != null) return single;
-
-            var sampler = new SparseCubicSampler(collection);
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Single[]>.Create(collection);
 
 
+            var sampler = new CubicSampler<Single[]>(collection, SamplerTraits.Array);
             return optimize ? sampler.ToFastSampler() : sampler;
             return optimize ? sampler.ToFastSampler() : sampler;
         }
         }
 
 
-        public static ICurveSampler<Single[]> CreateSampler(this IEnumerable<(Single, (Single[], Single[], Single[]))> collection, bool optimize = false)
+        public static ICurveSampler<SEGMENT> CreateSampler(this IEnumerable<(Single, (SEGMENT, SEGMENT, SEGMENT))> collection, bool optimize = false)
         {
         {
-            if (collection == null) return null;
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<SEGMENT>.Create(collection);
 
 
-            var single = SingleValueSampler<Single[]>.CreateForSingle(collection);
-            if (single != null) return single;
+            var sampler = new CubicSampler<SEGMENT>(collection, SamplerTraits.Segment);
+            return optimize ? sampler.ToFastSampler() : sampler;
+        }
 
 
-            var sampler = new ArrayCubicSampler(collection);
+        public static ICurveSampler<SPARSE> CreateSampler(this IEnumerable<(Single, (SPARSE, SPARSE, SPARSE))> collection, bool optimize = false)
+        {
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<SPARSE>.Create(collection);
 
 
+            var sampler = new CubicSampler<SPARSE>(collection, SamplerTraits.Sparse);
             return optimize ? sampler.ToFastSampler() : sampler;
             return optimize ? sampler.ToFastSampler() : sampler;
         }
         }
 
 

+ 24 - 192
src/SharpGLTF.Core/Animations/CurveSamplers.Cubic.cs

@@ -1,146 +1,30 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
-using System.Numerics;
-using System.Text;
 
 
 namespace SharpGLTF.Animations
 namespace SharpGLTF.Animations
 {
 {
-    /// <summary>
-    /// Defines a <see cref="Vector3"/> curve sampler that can be sampled with CUBIC interpolation.
-    /// </summary>
-    readonly struct Vector3CubicSampler : ICurveSampler<Vector3>, IConvertibleCurve<Vector3>
+    readonly struct CubicSampler<T> :
+        ICurveSampler<T>,
+        IConvertibleCurve<T>
     {
     {
         #region lifecycle
         #region lifecycle
 
 
-        public Vector3CubicSampler(IEnumerable<(float, (Vector3, Vector3, Vector3))> sequence)
+        public CubicSampler(IEnumerable<(float, (T, T, T))> sequence, ISamplerTraits<T> traits)
         {
         {
             _Sequence = sequence;
             _Sequence = sequence;
+            _Traits = traits;
         }
         }
 
 
         #endregion
         #endregion
 
 
         #region data
         #region data
 
 
-        private readonly IEnumerable<(float, (Vector3, Vector3, Vector3))> _Sequence;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly ISamplerTraits<T> _Traits;
 
 
-        #endregion
-
-        #region API
-
-        public int MaxDegree => 3;
-
-        public Vector3 GetPoint(float offset)
-        {
-            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
-
-            return CurveSampler.InterpolateCubic
-                (
-                valA.Item2, valA.Item3,   // start, startTangentOut
-                valB.Item2, valB.Item1,   // end, endTangentIn
-                amount                               // amount
-                );
-        }
-
-        IReadOnlyDictionary<float, Vector3> IConvertibleCurve<Vector3>.ToStepCurve()
-        {
-            throw new NotSupportedException(CurveSampler.SplineCurveError);
-        }
-
-        IReadOnlyDictionary<float, Vector3> IConvertibleCurve<Vector3>.ToLinearCurve()
-        {
-            throw new NotSupportedException(CurveSampler.SplineCurveError);
-        }
-
-        public IReadOnlyDictionary<float, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> ToSplineCurve()
-        {
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
-
-        public ICurveSampler<Vector3> ToFastSampler()
-        {
-            return FastCurveSampler<Vector3>.CreateFrom(_Sequence, chunk => new Vector3CubicSampler(chunk)) ?? this;
-        }
-
-        #endregion
-    }
-
-    /// <summary>
-    /// Defines a <see cref="Quaternion"/> curve sampler that can be sampled with CUBIC interpolation.
-    /// </summary>
-    readonly struct QuaternionCubicSampler : ICurveSampler<Quaternion>, IConvertibleCurve<Quaternion>
-    {
-        #region lifecycle
-
-        public QuaternionCubicSampler(IEnumerable<(float, (Quaternion, Quaternion, Quaternion))> sequence)
-        {
-            _Sequence = sequence;
-        }
-
-        #endregion
-
-        #region data
-
-        private readonly IEnumerable<(float, (Quaternion, Quaternion, Quaternion))> _Sequence;
-
-        #endregion
-
-        #region API
-
-        public int MaxDegree => 3;
-
-        public Quaternion GetPoint(float offset)
-        {
-            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
-
-            return CurveSampler.InterpolateCubic
-                (
-                valA.Item2, valA.Item3,   // start, startTangentOut
-                valB.Item2, valB.Item1,   // end, endTangentIn
-                amount                               // amount
-                );
-        }
-
-        IReadOnlyDictionary<float, Quaternion> IConvertibleCurve<Quaternion>.ToStepCurve()
-        {
-            throw new NotSupportedException(CurveSampler.SplineCurveError);
-        }
-
-        IReadOnlyDictionary<float, Quaternion> IConvertibleCurve<Quaternion>.ToLinearCurve()
-        {
-            throw new NotSupportedException(CurveSampler.SplineCurveError);
-        }
-
-        public IReadOnlyDictionary<float, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> ToSplineCurve()
-        {
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
-
-        public ICurveSampler<Quaternion> ToFastSampler()
-        {
-            return FastCurveSampler<Quaternion>.CreateFrom(_Sequence, chunk => new QuaternionCubicSampler(chunk)) ?? this;
-        }
-
-        #endregion
-    }
-
-    /// <summary>
-    /// Defines a <see cref="Transforms.SparseWeight8"/> curve sampler that can be sampled with CUBIC interpolation.
-    /// </summary>
-    readonly 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;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+        private readonly IEnumerable<(float Key, (T TangentIn, T Value, T TangentOut))> _Sequence;
 
 
         #endregion
         #endregion
 
 
@@ -148,95 +32,43 @@ namespace SharpGLTF.Animations
 
 
         public int MaxDegree => 3;
         public int MaxDegree => 3;
 
 
-        public Transforms.SparseWeight8 GetPoint(float offset)
+        public T GetPoint(float offset)
         {
         {
             var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
             var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
 
-            return Transforms.SparseWeight8.InterpolateCubic
+            return _Traits.InterpolateCubic
                 (
                 (
-                valA.Item2, valA.Item3,   // start, startTangentOut
-                valB.Item2, valB.Item1,   // end, endTangentIn
-                amount                               // amount
+                valA.Value, valA.TangentOut,   // start, startTangentOut
+                valB.Value, valB.TangentIn,   // end, endTangentIn
+                amount                    // amount
                 );
                 );
         }
         }
 
 
-        IReadOnlyDictionary<float, Transforms.SparseWeight8> IConvertibleCurve<Transforms.SparseWeight8>.ToStepCurve()
+        IReadOnlyDictionary<float, T> IConvertibleCurve<T>.ToStepCurve()
         {
         {
             throw new NotSupportedException(CurveSampler.SplineCurveError);
             throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
         }
 
 
-        IReadOnlyDictionary<float, Transforms.SparseWeight8> IConvertibleCurve<Transforms.SparseWeight8>.ToLinearCurve()
+        IReadOnlyDictionary<float, T> IConvertibleCurve<T>.ToLinearCurve()
         {
         {
             throw new NotSupportedException(CurveSampler.SplineCurveError);
             throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
         }
 
 
-        public IReadOnlyDictionary<float, (Transforms.SparseWeight8 TangentIn, Transforms.SparseWeight8 Value, Transforms.SparseWeight8 TangentOut)> ToSplineCurve()
+        IReadOnlyDictionary<float, (T TangentIn, T Value, T TangentOut)> IConvertibleCurve<T>.ToSplineCurve()
         {
         {
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
+            var traits = _Traits;
 
 
-        public ICurveSampler<Transforms.SparseWeight8> ToFastSampler()
-        {
-            return FastCurveSampler<Transforms.SparseWeight8>.CreateFrom(_Sequence, chunk => new SparseCubicSampler(chunk)) ?? this;
-        }
-
-        #endregion
-    }
-
-    /// <summary>
-    /// Defines a <see cref="Single"/>[] curve sampler that can be sampled with CUBIC interpolation.
-    /// </summary>
-    readonly struct ArrayCubicSampler : ICurveSampler<Single[]>, IConvertibleCurve<Single[]>
-    {
-        #region lifecycle
-
-        public ArrayCubicSampler(IEnumerable<(float, (float[], float[], float[]))> sequence)
-        {
-            _Sequence = sequence;
-        }
-
-        #endregion
-
-        #region data
-
-        private readonly IEnumerable<(float, (float[], float[], float[]))> _Sequence;
-
-        #endregion
-
-        #region API
-
-        public int MaxDegree => 3;
-
-        public float[] GetPoint(float offset)
-        {
-            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
-
-            return CurveSampler.InterpolateCubic
+            return _Sequence.ToDictionary
                 (
                 (
-                valA.Item2, valA.Item3,   // start, startTangentOut
-                valB.Item2, valB.Item1,   // end, endTangentIn
-                amount                               // amount
+                pair => pair.Key,
+                pair => (traits.Clone(pair.Item2.TangentIn), traits.Clone(pair.Item2.Value), traits.Clone(pair.Item2.TangentOut))
                 );
                 );
         }
         }
 
 
-        IReadOnlyDictionary<float, float[]> IConvertibleCurve<Single[]>.ToStepCurve()
-        {
-            throw new NotSupportedException(CurveSampler.SplineCurveError);
-        }
-
-        IReadOnlyDictionary<float, float[]> IConvertibleCurve<Single[]>.ToLinearCurve()
-        {
-            throw new NotSupportedException(CurveSampler.SplineCurveError);
-        }
-
-        public IReadOnlyDictionary<float, (float[] TangentIn, float[] Value, float[] TangentOut)> ToSplineCurve()
-        {
-            return _Sequence.ToDictionary(pair => pair.Item1, pair => pair.Item2);
-        }
-
-        public ICurveSampler<float[]> ToFastSampler()
+        public ICurveSampler<T> ToFastSampler()
         {
         {
-            return FastCurveSampler<float[]>.CreateFrom(_Sequence, chunk => new ArrayCubicSampler(chunk)) ?? this;
+            var traits = _Traits;
+            return FastCurveSampler<T>.CreateFrom(_Sequence, chunk => new CubicSampler<T>(chunk, traits)) ?? this;
         }
         }
 
 
         #endregion
         #endregion

+ 66 - 0
src/SharpGLTF.Core/Animations/CurveSamplers.Fixed.cs

@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace SharpGLTF.Animations
+{
+    /// <summary>
+    /// Represents a special sampler for single values.
+    /// </summary>
+    /// <typeparam name="T">The sample type.</typeparam>
+    readonly struct FixedSampler<T> :
+        ICurveSampler<T>,
+        IConvertibleCurve<T>
+    {
+        #region lifecycle
+
+        public static ICurveSampler<T> Create(IEnumerable<(float Key, T Value)> sequence)
+        {
+            System.Diagnostics.Debug.Assert(!sequence.Skip(1).Any());
+            return new FixedSampler<T>(sequence.First().Value);
+        }
+
+        public static ICurveSampler<T> Create(IEnumerable<(float Key, (T, T, T) Value)> sequence)
+        {
+            System.Diagnostics.Debug.Assert(!sequence.Skip(1).Any());
+            return new FixedSampler<T>(sequence.First().Value.Item2);
+        }
+
+        private FixedSampler(T value)
+        {
+            _Value = value;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly T _Value;
+
+        #endregion
+
+        #region API
+
+        public int MaxDegree => 0;
+
+        public T GetPoint(float offset) { return _Value; }
+
+        public IReadOnlyDictionary<float, T> ToStepCurve()
+        {
+            return new Dictionary<float, T> { [0] = _Value };
+        }
+
+        public IReadOnlyDictionary<float, T> ToLinearCurve()
+        {
+            return new Dictionary<float, T> { [0] = _Value };
+        }
+
+        public IReadOnlyDictionary<float, (T TangentIn, T Value, T TangentOut)> ToSplineCurve()
+        {
+            return new Dictionary<float, (T TangentIn, T Value, T TangentOut)> { [0] = (default, _Value, default) };
+        }
+
+        #endregion
+    }
+}

+ 20 - 263
src/SharpGLTF.Core/Animations/CurveSamplers.Linear.cs

@@ -1,306 +1,63 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
-using System.Numerics;
-using System.Text;
 
 
 namespace SharpGLTF.Animations
 namespace SharpGLTF.Animations
 {
 {
-    readonly struct SingleValueSampler<T> : ICurveSampler<T>, IConvertibleCurve<T>
+    readonly struct LinearSampler<T> :
+        ICurveSampler<T>,
+        IConvertibleCurve<T>
     {
     {
         #region lifecycle
         #region lifecycle
 
 
-        public static ICurveSampler<T> CreateForSingle(IEnumerable<(float Key, T Value)> sequence)
-        {
-            if (sequence.Skip(1).Any()) return null;
-
-            return new SingleValueSampler<T>(sequence.First().Value);
-        }
-
-        public static ICurveSampler<T> CreateForSingle(IEnumerable<(float Key, (T, T, T) Value)> sequence)
-        {
-            if (sequence.Skip(1).Any()) return null;
-
-            return new SingleValueSampler<T>(sequence.First().Value.Item2);
-        }
-
-        private SingleValueSampler(T value)
-        {
-            _Value = value;
-        }
-
-        #endregion
-
-        #region data
-
-        private readonly T _Value;
-
-        #endregion
-
-        #region API
-
-        public int MaxDegree => 0;
-
-        public T GetPoint(float offset) { return _Value; }
-
-        public IReadOnlyDictionary<float, T> ToStepCurve()
-        {
-            return new Dictionary<float, T> { [0] = _Value };
-        }
-
-        public IReadOnlyDictionary<float, T> ToLinearCurve()
-        {
-            return new Dictionary<float, T> { [0] = _Value };
-        }
-
-        public IReadOnlyDictionary<float, (T TangentIn, T Value, T TangentOut)> ToSplineCurve()
-        {
-            return new Dictionary<float, (T TangentIn, T Value, T TangentOut)> { [0] = (default, _Value, default) };
-        }
-
-        #endregion
-    }
-
-    /// <summary>
-    /// Defines a <see cref="Vector3"/> curve sampler that can be sampled with STEP or LINEAR interpolations.
-    /// </summary>
-    readonly struct Vector3LinearSampler : ICurveSampler<Vector3>, IConvertibleCurve<Vector3>
-    {
-        #region lifecycle
-
-        public Vector3LinearSampler(IEnumerable<(float Key, Vector3 Value)> sequence, bool isLinear)
+        public LinearSampler(IEnumerable<(float, T)> sequence, ISamplerTraits<T> traits)
         {
         {
             _Sequence = sequence;
             _Sequence = sequence;
-            _Linear = isLinear;
+            _Traits = traits;
         }
         }
 
 
         #endregion
         #endregion
 
 
         #region data
         #region data
 
 
-        private readonly IEnumerable<(float Key, Vector3 Value)> _Sequence;
-        private readonly Boolean _Linear;
-
-        #endregion
-
-        #region API
-
-        public int MaxDegree => _Linear ? 1 : 0;
-
-        public Vector3 GetPoint(float offset)
-        {
-            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
-
-            if (!_Linear) return valA;
-
-            return Vector3.Lerp(valA, valB, amount);
-        }
-
-        public IReadOnlyDictionary<float, Vector3> ToStepCurve()
-        {
-            Guard.IsFalse(_Linear, nameof(MaxDegree), CurveSampler.StepCurveError);
-            return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
-        }
-
-        public IReadOnlyDictionary<float, Vector3> ToLinearCurve()
-        {
-            Guard.IsTrue(_Linear, nameof(MaxDegree), CurveSampler.LinearCurveError);
-            return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
-        }
-
-        public IReadOnlyDictionary<float, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> ToSplineCurve()
-        {
-            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
-        }
-
-        public ICurveSampler<Vector3> ToFastSampler()
-        {
-            var linear = _Linear;
-            return FastCurveSampler<Vector3>.CreateFrom(_Sequence, chunk => new Vector3LinearSampler(chunk, linear)) ?? this;
-        }
-
-        #endregion
-    }
-
-    /// <summary>
-    /// Defines a <see cref="Quaternion"/> curve sampler that can be sampled with STEP or LINEAR interpolations.
-    /// </summary>
-    readonly struct QuaternionLinearSampler : ICurveSampler<Quaternion>, IConvertibleCurve<Quaternion>
-    {
-        #region lifecycle
-
-        public QuaternionLinearSampler(IEnumerable<(float, Quaternion)> sequence, bool isLinear)
-        {
-            _Sequence = sequence;
-            _Linear = isLinear;
-        }
-
-        #endregion
-
-        #region data
-
-        private readonly IEnumerable<(float Key, Quaternion Value)> _Sequence;
-        private readonly Boolean _Linear;
-
-        #endregion
-
-        #region API
-
-        public int MaxDegree => _Linear ? 1 : 0;
-
-        public Quaternion GetPoint(float offset)
-        {
-            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly ISamplerTraits<T> _Traits;
 
 
-            if (!_Linear) return valA;
-
-            return Quaternion.Slerp(valA, valB, amount);
-        }
-
-        public IReadOnlyDictionary<float, Quaternion> ToStepCurve()
-        {
-            Guard.IsFalse(_Linear, nameof(MaxDegree), CurveSampler.StepCurveError);
-            return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
-        }
-
-        public IReadOnlyDictionary<float, Quaternion> ToLinearCurve()
-        {
-            Guard.IsTrue(_Linear, nameof(MaxDegree), CurveSampler.LinearCurveError);
-            return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
-        }
-
-        public IReadOnlyDictionary<float, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> ToSplineCurve()
-        {
-            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
-        }
-
-        public ICurveSampler<Quaternion> ToFastSampler()
-        {
-            var linear = _Linear;
-            return FastCurveSampler<Quaternion>.CreateFrom(_Sequence, chunk => new QuaternionLinearSampler(chunk, linear)) ?? this;
-        }
-
-        #endregion
-    }
-
-    /// <summary>
-    /// Defines a <see cref="Transforms.SparseWeight8"/> curve sampler that can be sampled with STEP or LINEAR interpolation.
-    /// </summary>
-    readonly struct SparseLinearSampler : ICurveSampler<Transforms.SparseWeight8>, IConvertibleCurve<Transforms.SparseWeight8>
-    {
-        #region lifecycle
-
-        public SparseLinearSampler(IEnumerable<(float Key, Transforms.SparseWeight8 Value)> sequence, bool isLinear)
-        {
-            _Sequence = sequence;
-            _Linear = isLinear;
-        }
-
-        #endregion
-
-        #region data
-
-        private readonly IEnumerable<(float Key, Transforms.SparseWeight8 Value)> _Sequence;
-        private readonly Boolean _Linear;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+        private readonly IEnumerable<(float Key, T Value)> _Sequence;
+        public int MaxDegree => 1;
 
 
         #endregion
         #endregion
 
 
         #region API
         #region API
 
 
-        public int MaxDegree => _Linear ? 1 : 0;
-
-        public Transforms.SparseWeight8 GetPoint(float offset)
+        public T GetPoint(float offset)
         {
         {
             var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
             var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
 
-            if (!_Linear) return valA;
-
-            var weights = Transforms.SparseWeight8.InterpolateLinear(valA, valB, amount);
-
-            return weights;
+            return _Traits.InterpolateLinear(valA, valB, amount);
         }
         }
 
 
-        public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToStepCurve()
-        {
-            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
-        }
-
-        public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToLinearCurve()
-        {
-            Guard.IsTrue(_Linear, nameof(MaxDegree), CurveSampler.LinearCurveError);
-            return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
-        }
-
-        public IReadOnlyDictionary<float, (Transforms.SparseWeight8 TangentIn, Transforms.SparseWeight8 Value, Transforms.SparseWeight8 TangentOut)> ToSplineCurve()
+        public IReadOnlyDictionary<float, T> ToStepCurve()
         {
         {
             throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
             throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
         }
 
 
-        public ICurveSampler<Transforms.SparseWeight8> ToFastSampler()
-        {
-            var linear = _Linear;
-            return FastCurveSampler<Transforms.SparseWeight8>.CreateFrom(_Sequence, chunk => new SparseLinearSampler(chunk, linear)) ?? this;
-        }
-
-        #endregion
-    }
-
-    /// <summary>
-    /// Defines a <see cref="float"/>[] curve sampler that can be sampled with STEP or LINEAR interpolations.
-    /// </summary>
-    readonly struct ArrayLinearSampler : ICurveSampler<float[]>, IConvertibleCurve<float[]>
-    {
-        #region lifecycle
-
-        public ArrayLinearSampler(IEnumerable<(float, float[])> sequence, bool isLinear)
-        {
-            _Sequence = sequence;
-            _Linear = isLinear;
-        }
-
-        #endregion
-
-        #region data
-
-        private readonly IEnumerable<(float Key, float[] Value)> _Sequence;
-        private readonly Boolean _Linear;
-
-        #endregion
-
-        #region API
-
-        public int MaxDegree => _Linear ? 1 : 0;
-
-        public float[] GetPoint(float offset)
-        {
-            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
-
-            if (!_Linear) return valA;
-
-            return CurveSampler.InterpolateLinear(valA, valB, amount);
-        }
-
-        public IReadOnlyDictionary<float, float[]> ToStepCurve()
-        {
-            Guard.IsFalse(_Linear, nameof(MaxDegree), CurveSampler.StepCurveError);
-            return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
-        }
-
-        public IReadOnlyDictionary<float, float[]> ToLinearCurve()
+        public IReadOnlyDictionary<float, T> ToLinearCurve()
         {
         {
-            Guard.IsTrue(_Linear, nameof(MaxDegree), CurveSampler.LinearCurveError);
-            return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
+            var traits = _Traits;
+            return _Sequence.ToDictionary(pair => pair.Key, pair => traits.Clone(pair.Value));
         }
         }
 
 
-        public IReadOnlyDictionary<float, (float[] TangentIn, float[] Value, float[] TangentOut)> ToSplineCurve()
+        public IReadOnlyDictionary<float, (T TangentIn, T Value, T TangentOut)> ToSplineCurve()
         {
         {
             throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
             throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
         }
 
 
-        public ICurveSampler<float[]> ToFastSampler()
+        public ICurveSampler<T> ToFastSampler()
         {
         {
-            var linear = _Linear;
-            return FastCurveSampler<float[]>.CreateFrom(_Sequence, chunk => new ArrayLinearSampler(chunk, linear)) ?? this;
+            var traits = _Traits;
+            return FastCurveSampler<T>.CreateFrom(_Sequence, chunk => new LinearSampler<T>(chunk, traits)) ?? this;
         }
         }
 
 
         #endregion
         #endregion

+ 66 - 0
src/SharpGLTF.Core/Animations/CurveSamplers.Step.cs

@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace SharpGLTF.Animations
+{
+    readonly struct StepSampler<T> :
+        ICurveSampler<T>,
+        IConvertibleCurve<T>
+    {
+        #region lifecycle
+
+        public StepSampler(IEnumerable<(float, T)> sequence, ISamplerTraits<T> traits)
+        {
+            _Sequence = sequence;
+            _Traits = traits;
+        }
+
+        #endregion
+
+        #region data
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly ISamplerTraits<T> _Traits;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+        private readonly IEnumerable<(float Key, T Value)> _Sequence;
+
+        public int MaxDegree => 0;
+
+        #endregion
+
+        #region API
+
+        public T GetPoint(float offset)
+        {
+            var (valA, _, _) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
+            return _Traits.Clone(valA);
+        }
+
+        public IReadOnlyDictionary<float, T> ToStepCurve()
+        {
+            var traits = _Traits;
+            return _Sequence.ToDictionary(pair => pair.Key, pair => traits.Clone(pair.Value));
+        }
+
+        public IReadOnlyDictionary<float, T> ToLinearCurve()
+        {
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
+        }
+
+        public IReadOnlyDictionary<float, (T TangentIn, T Value, T TangentOut)> ToSplineCurve()
+        {
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
+        }
+
+        public ICurveSampler<T> ToFastSampler()
+        {
+            var traits = _Traits;
+            return FastCurveSampler<T>.CreateFrom(_Sequence, chunk => new StepSampler<T>(chunk, traits)) ?? this;
+        }
+
+        #endregion
+    }
+}

+ 82 - 0
src/SharpGLTF.Core/Animations/CurveSamplers.Traits.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+using SEGMENT = System.ArraySegment<float>;
+using SPARSE = SharpGLTF.Transforms.SparseWeight8;
+
+namespace SharpGLTF.Animations
+{
+    interface ISamplerTraits<T>
+    {
+        T Clone(T value);
+        T InterpolateLinear(T left, T right, float amount);
+        T InterpolateCubic(T start, T outgoingTangent, T end, T incomingTangent, Single amount);
+    }
+
+    static class SamplerTraits
+    {
+        sealed class _Vector3 : ISamplerTraits<Vector3>
+        {
+            public Vector3 Clone(Vector3 value) { return value; }
+            public Vector3 InterpolateLinear(Vector3 left, Vector3 right, float amount) { return System.Numerics.Vector3.Lerp(left, right, amount); }
+            public Vector3 InterpolateCubic(Vector3 start, Vector3 outgoingTangent, Vector3 end, Vector3 incomingTangent, Single amount)
+            {
+                return CurveSampler.InterpolateCubic(start, outgoingTangent, end, incomingTangent, amount);
+            }
+        }
+        sealed class _Quaternion : ISamplerTraits<Quaternion>
+        {
+            public Quaternion Clone(Quaternion value) { return value; }
+            public Quaternion InterpolateLinear(Quaternion left, Quaternion right, float amount) { return System.Numerics.Quaternion.Slerp(left, right, amount); }
+            public Quaternion InterpolateCubic(Quaternion start, Quaternion outgoingTangent, Quaternion end, Quaternion incomingTangent, Single amount)
+            {
+                return CurveSampler.InterpolateCubic(start, outgoingTangent, end, incomingTangent, amount);
+            }
+        }
+        sealed class _Array : ISamplerTraits<Single[]>
+        {
+            public Single[] Clone(Single[] value) { return (Single[])value.Clone(); }
+            public float[] InterpolateLinear(float[] left, float[] right, float amount)
+            {
+                return CurveSampler.InterpolateLinear(left, right, amount);
+            }
+            public float[] InterpolateCubic(float[] start, float[] outgoingTangent, float[] end, float[] incomingTangent, float amount)
+            {
+                return CurveSampler.InterpolateCubic(start, outgoingTangent, end, incomingTangent, amount);
+            }
+        }
+        sealed class _Segment : ISamplerTraits<SEGMENT>
+        {
+            public SEGMENT Clone(SEGMENT value) { return new SEGMENT(value.ToArray()); }
+            public SEGMENT InterpolateLinear(SEGMENT left, SEGMENT right, float amount)
+            {
+                return new SEGMENT(CurveSampler.InterpolateLinear(left, right, amount));
+            }
+            public SEGMENT InterpolateCubic(SEGMENT start, SEGMENT outgoingTangent, SEGMENT end, SEGMENT incomingTangent, Single amount)
+            {
+                return new SEGMENT(CurveSampler.InterpolateCubic(start, outgoingTangent, end, incomingTangent, amount));
+            }
+        }
+        sealed class _Sparse : ISamplerTraits<SPARSE>
+        {
+            public SPARSE Clone(SPARSE value) { return value; }
+            public SPARSE InterpolateLinear(SPARSE left, SPARSE right, float amount)
+            {
+                return SPARSE.InterpolateLinear(left, right, amount);
+            }
+            public SPARSE InterpolateCubic(SPARSE start, SPARSE outgoingTangent, SPARSE end, SPARSE incomingTangent, Single amount)
+            {
+                return SPARSE.InterpolateCubic(start, outgoingTangent, end, incomingTangent, amount);
+            }
+        }
+
+        public static readonly ISamplerTraits<Vector3> Vector3 = new _Vector3();
+        public static readonly ISamplerTraits<Quaternion> Quaternion = new _Quaternion();
+        public static readonly ISamplerTraits<Single[]> Array = new _Array();
+        public static readonly ISamplerTraits<SPARSE> Sparse = new _Sparse();
+        public static readonly ISamplerTraits<SEGMENT> Segment = new _Segment();
+    }
+}

+ 1 - 0
src/SharpGLTF.Core/Animations/FastCurveSampler.cs

@@ -40,6 +40,7 @@ namespace SharpGLTF.Animations
             _Samplers = samplers.ToArray();
             _Samplers = samplers.ToArray();
         }
         }
 
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         private readonly ICurveSampler<T>[] _Samplers;
         private readonly ICurveSampler<T>[] _Samplers;
 
 
         public T GetPoint(float offset)
         public T GetPoint(float offset)

+ 1 - 1
src/SharpGLTF.Core/Runtime/NodeTemplate.cs

@@ -47,7 +47,7 @@ namespace SharpGLTF.Runtime
                 _Scale.SetCurve(index, curves.Scale?.CreateCurveSampler(isolateMemory));
                 _Scale.SetCurve(index, curves.Scale?.CreateCurveSampler(isolateMemory));
                 _Rotation.SetCurve(index, curves.Rotation?.CreateCurveSampler(isolateMemory));
                 _Rotation.SetCurve(index, curves.Rotation?.CreateCurveSampler(isolateMemory));
                 _Translation.SetCurve(index, curves.Translation?.CreateCurveSampler(isolateMemory));
                 _Translation.SetCurve(index, curves.Translation?.CreateCurveSampler(isolateMemory));
-                _Morphing.SetCurve(index, curves.MorphingSparse?.CreateCurveSampler(isolateMemory));
+                _Morphing.SetCurve(index, curves.GetMorphingSampler<Transforms.SparseWeight8>()?.CreateCurveSampler(isolateMemory));
             }
             }
 
 
             _UseAnimatedTransforms = _Scale.IsAnimated | _Rotation.IsAnimated | _Translation.IsAnimated;
             _UseAnimatedTransforms = _Scale.IsAnimated | _Rotation.IsAnimated | _Translation.IsAnimated;

+ 21 - 0
src/SharpGLTF.Core/Schema2/_Extensions.cs

@@ -19,6 +19,11 @@ namespace SharpGLTF.Schema2
 
 
             if (list.Count > 0)
             if (list.Count > 0)
             {
             {
+                for (int i = 0; i < list.Count; ++i)
+                {
+                    list[i] = 0;
+                }
+
                 foreach (var (index, weight) in weights.GetIndexedWeights())
                 foreach (var (index, weight) in weights.GetIndexedWeights())
                 {
                 {
                     list[index] = weight;
                     list[index] = weight;
@@ -26,6 +31,22 @@ namespace SharpGLTF.Schema2
             }
             }
         }
         }
 
 
+        public static void SetMorphWeights(this IList<Double> list, IReadOnlyList<float> weights)
+        {
+            if (weights == null) { list.Clear(); return; }
+
+            while (list.Count > weights.Count) list.RemoveAt(list.Count - 1);
+            while (list.Count < weights.Count) list.Add(0);
+
+            if (list.Count > 0)
+            {
+                for (int i = 0; i < list.Count; ++i)
+                {
+                    list[i] = weights[i];
+                }
+            }
+        }
+
         #endregion
         #endregion
 
 
         #region nullables
         #region nullables

+ 153 - 35
src/SharpGLTF.Core/Schema2/gltf.AnimationSampler.cs

@@ -9,8 +9,16 @@ using SharpGLTF.Collections;
 using SharpGLTF.Transforms;
 using SharpGLTF.Transforms;
 using SharpGLTF.Validation;
 using SharpGLTF.Validation;
 
 
+using ROLIST = System.Collections.Generic.IReadOnlyList<float>;
+using SPARSE8 = SharpGLTF.Transforms.SparseWeight8;
+using SEGMENT = System.ArraySegment<float>;
+
 namespace SharpGLTF.Schema2
 namespace SharpGLTF.Schema2
 {
 {
+    /// <summary>
+    /// Represents an interface to a curve made of time-value points.
+    /// </summary>
+    /// <typeparam name="T">The curve point value type.</typeparam>
     public interface IAnimationSampler<T>
     public interface IAnimationSampler<T>
     {
     {
         /// <summary>
         /// <summary>
@@ -47,11 +55,21 @@ namespace SharpGLTF.Schema2
         ICurveSampler<T> CreateCurveSampler(bool isolateMemory = false);
         ICurveSampler<T> CreateCurveSampler(bool isolateMemory = false);
     }
     }
 
 
+    /// <remarks>
+    /// This class can be casted to these interfaces:
+    /// <list type="table">
+    /// <item><see cref="IAnimationSampler{T}"/> with T: <see cref="Vector3"/> for Scale and Translation keys.</item>
+    /// <item><see cref="IAnimationSampler{T}"/> with T: <see cref="Quaternion"/> for Rotation keys.</item>
+    /// <item><see cref="IAnimationSampler{T}"/> with T: <see cref="SPARSE8"/> for Morph targets (limited to 8 weights).</item>
+    /// <item><see cref="IAnimationSampler{T}"/> with T: <see cref="Single"/>[] for Morph targets (unlimited weights).</item>
+    /// </list>
+    /// </remarks>
     sealed partial class AnimationSampler :
     sealed partial class AnimationSampler :
         IChildOf<Animation>,
         IChildOf<Animation>,
         IAnimationSampler<Vector3>,
         IAnimationSampler<Vector3>,
         IAnimationSampler<Quaternion>,
         IAnimationSampler<Quaternion>,
-        IAnimationSampler<SparseWeight8>,
+        IAnimationSampler<SPARSE8>,
+        IAnimationSampler<SEGMENT>,
         IAnimationSampler<Single[]>
         IAnimationSampler<Single[]>
     {
     {
         #region lifecycle
         #region lifecycle
@@ -83,6 +101,7 @@ namespace SharpGLTF.Schema2
             LogicalIndex = index;
             LogicalIndex = index;
         }
         }
 
 
+        /// <inheritdoc/>
         public AnimationInterpolationMode InterpolationMode
         public AnimationInterpolationMode InterpolationMode
         {
         {
             get => _interpolation.AsValue(_interpolationDefault);
             get => _interpolation.AsValue(_interpolationDefault);
@@ -166,28 +185,45 @@ namespace SharpGLTF.Schema2
             return accessor;
             return accessor;
         }
         }
 
 
-        private Accessor _CreateOutputAccessor(IReadOnlyList<SparseWeight8> output, int expandedCount)
+        private Accessor _CreateOutputAccessor(IReadOnlyList<SPARSE8> output, int itemsStride)
+        {
+            return _CreateOutputAccessor(output.Count, itemsStride, (y, x) => output[y][x]);
+        }
+
+        private Accessor _CreateOutputAccessor<T>(IReadOnlyList<T> output, int itemsStride)
+            where T : ROLIST
+        {
+            System.Diagnostics.Debug.Assert(output.All(item => item.Count == itemsStride));
+
+            float eval(int y, int x)
+            {
+                var row = output[y];
+                return x < row.Count ? row[x] : 0;
+            }
+
+            return _CreateOutputAccessor(output.Count, itemsStride, eval);
+        }
+
+        private Accessor _CreateOutputAccessor(int itemCount, int itemsStride, Func<int, int, float> output)
         {
         {
             Guard.NotNull(output, nameof(output));
             Guard.NotNull(output, nameof(output));
-            Guard.MustBeGreaterThan(output.Count, 0, nameof(output.Count));
-            Guard.MustBeGreaterThan(expandedCount, 0, nameof(expandedCount));
+            Guard.MustBeGreaterThan(itemCount, 0, nameof(itemCount));
+            Guard.MustBeGreaterThan(itemsStride, 0, nameof(itemsStride));
 
 
             var root = LogicalParent.LogicalParent;
             var root = LogicalParent.LogicalParent;
 
 
-            var buffer = root.CreateBufferView(output.Count * 4 * expandedCount);
+            var buffer = root.CreateBufferView(itemCount * 4 * itemsStride);
             var accessor = root.CreateAccessor("Animation.Output");
             var accessor = root.CreateAccessor("Animation.Output");
 
 
-            accessor.SetData(buffer, 0, output.Count * expandedCount, DimensionType.SCALAR, EncodingType.FLOAT, false);
+            accessor.SetData(buffer, 0, itemCount * itemsStride, DimensionType.SCALAR, EncodingType.FLOAT, false);
 
 
             var dst = accessor.AsScalarArray();
             var dst = accessor.AsScalarArray();
 
 
-            for (int i = 0; i < output.Count; ++i)
+            for (int y = 0; y < itemCount; ++y)
             {
             {
-                var src = output[i];
-
-                for (int j = 0; j < expandedCount; ++j)
+                for (int x = 0; x < itemsStride; ++x)
                 {
                 {
-                    dst[(i * expandedCount) + j] = src[j];
+                    dst[(y * itemsStride) + x] = output(y, x);
                 }
                 }
             }
             }
 
 
@@ -256,16 +292,27 @@ namespace SharpGLTF.Schema2
             _output = this._CreateOutputAccessor(values).LogicalIndex;
             _output = this._CreateOutputAccessor(values).LogicalIndex;
         }
         }
 
 
-        internal void SetKeys(IReadOnlyDictionary<Single, SparseWeight8> keyframes, int expandedCount)
+        internal void SetKeys<TWeights>(IReadOnlyDictionary<Single, TWeights> keyframes, int itemsStride)
+            where TWeights : ROLIST
         {
         {
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+            GuardAll.MustBeEqualTo(keyframes.Values.Select(item => item.Count), itemsStride, nameof(keyframes));
 
 
             var (keys, values) = _Split(keyframes);
             var (keys, values) = _Split(keyframes);
             _input = this._CreateInputAccessor(keys).LogicalIndex;
             _input = this._CreateInputAccessor(keys).LogicalIndex;
-            _output = this._CreateOutputAccessor(values, expandedCount).LogicalIndex;
+            _output = this._CreateOutputAccessor(values, itemsStride).LogicalIndex;
+        }
+
+        internal void SetKeys(IReadOnlyDictionary<Single, SPARSE8> keyframes, int itemsStride)
+        {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
+            var (keys, values) = _Split(keyframes);
+            _input = this._CreateInputAccessor(keys).LogicalIndex;
+            _output = this._CreateOutputAccessor(values, itemsStride).LogicalIndex;
         }
         }
 
 
-        internal void SetKeys(IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
+        internal void SetCubicKeys(IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         {
         {
             Guard.NotNull(keyframes, nameof(keyframes));
             Guard.NotNull(keyframes, nameof(keyframes));
             Guard.MustBeGreaterThan(keyframes.Count, 0, nameof(keyframes.Count));
             Guard.MustBeGreaterThan(keyframes.Count, 0, nameof(keyframes.Count));
@@ -283,7 +330,7 @@ namespace SharpGLTF.Schema2
             _output = this._CreateOutputAccessor(values).LogicalIndex;
             _output = this._CreateOutputAccessor(values).LogicalIndex;
         }
         }
 
 
-        internal void SetKeys(IReadOnlyDictionary<Single, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> keyframes)
+        internal void SetCubicKeys(IReadOnlyDictionary<Single, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> keyframes)
         {
         {
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
 
@@ -300,7 +347,24 @@ namespace SharpGLTF.Schema2
             _output = this._CreateOutputAccessor(values).LogicalIndex;
             _output = this._CreateOutputAccessor(values).LogicalIndex;
         }
         }
 
 
-        internal void SetKeys(IReadOnlyDictionary<Single, (SparseWeight8 TangentIn, SparseWeight8 Value, SparseWeight8 TangentOut)> keyframes, int expandedCount)
+        internal void SetCubicKeys<TWeights>(IReadOnlyDictionary<Single, (TWeights TangentIn, TWeights Value, TWeights TangentOut)> keyframes, int expandedCount)
+            where TWeights : ROLIST
+        {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+            Guard.MustBeGreaterThan(expandedCount, 0, nameof(expandedCount));
+
+            // splits the dictionary into separated input/output collections, also, the output will be flattened to plain Vector3 values.
+            var (keys, values) = _Split(keyframes);
+            System.Diagnostics.Debug.Assert(keys.Length * 3 == values.Length, "keys and values must have 1 to 3 ratio");
+
+            // fix for first incoming tangent and last outgoing tangent
+            // this might not be true for a looped animation, where first and last might be the same
+
+            _input = this._CreateInputAccessor(keys).LogicalIndex;
+            _output = this._CreateOutputAccessor(values, expandedCount).LogicalIndex;
+        }
+
+        internal void SetCubicKeys(IReadOnlyDictionary<Single, (SPARSE8 TangentIn, SPARSE8 Value, SPARSE8 TangentOut)> keyframes, int expandedCount)
         {
         {
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
             Guard.MustBeGreaterThan(expandedCount, 0, nameof(expandedCount));
             Guard.MustBeGreaterThan(expandedCount, 0, nameof(expandedCount));
@@ -318,6 +382,7 @@ namespace SharpGLTF.Schema2
             _output = this._CreateOutputAccessor(values, expandedCount).LogicalIndex;
             _output = this._CreateOutputAccessor(values, expandedCount).LogicalIndex;
         }
         }
 
 
+        /// <inheritdoc/>
         IEnumerable<(Single, Vector3)> IAnimationSampler<Vector3>.GetLinearKeys()
         IEnumerable<(Single, Vector3)> IAnimationSampler<Vector3>.GetLinearKeys()
         {
         {
             Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
             Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
@@ -328,6 +393,7 @@ namespace SharpGLTF.Schema2
             return keys.Zip(frames, (key, val) => (key, val));
             return keys.Zip(frames, (key, val) => (key, val));
         }
         }
 
 
+        /// <inheritdoc/>
         IEnumerable<(Single, Quaternion)> IAnimationSampler<Quaternion>.GetLinearKeys()
         IEnumerable<(Single, Quaternion)> IAnimationSampler<Quaternion>.GetLinearKeys()
         {
         {
             Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
             Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
@@ -338,7 +404,8 @@ 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()
+        /// <inheritdoc/>
+        IEnumerable<(Single, SPARSE8)> IAnimationSampler<SPARSE8>.GetLinearKeys()
         {
         {
             Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
             Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
 
 
@@ -350,6 +417,20 @@ namespace SharpGLTF.Schema2
             return keys.Zip(frames, (key, val) => (key, SparseWeight8.Create(val)));
             return keys.Zip(frames, (key, val) => (key, SparseWeight8.Create(val)));
         }
         }
 
 
+        /// <inheritdoc/>
+        IEnumerable<(Single, SEGMENT)> IAnimationSampler<SEGMENT>.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, new ArraySegment<float>(val)));
+        }
+
+        /// <inheritdoc/>
         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));
@@ -362,6 +443,7 @@ namespace SharpGLTF.Schema2
             return keys.Zip(frames, (key, val) => (key, val));
             return keys.Zip(frames, (key, val) => (key, val));
         }
         }
 
 
+        /// <inheritdoc/>
         IEnumerable<(Single, (Vector3, Vector3, Vector3))> IAnimationSampler<Vector3>.GetCubicKeys()
         IEnumerable<(Single, (Vector3, Vector3, Vector3))> IAnimationSampler<Vector3>.GetCubicKeys()
         {
         {
             Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
             Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
@@ -372,6 +454,7 @@ namespace SharpGLTF.Schema2
             return keys.Zip(frames, (key, val) => (key, val));
             return keys.Zip(frames, (key, val) => (key, val));
         }
         }
 
 
+        /// <inheritdoc/>
         IEnumerable<(Single, (Quaternion, Quaternion, Quaternion))> IAnimationSampler<Quaternion>.GetCubicKeys()
         IEnumerable<(Single, (Quaternion, Quaternion, Quaternion))> IAnimationSampler<Quaternion>.GetCubicKeys()
         {
         {
             Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
             Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
@@ -382,6 +465,7 @@ namespace SharpGLTF.Schema2
             return keys.Zip(frames, (key, val) => (key, val));
             return keys.Zip(frames, (key, val) => (key, val));
         }
         }
 
 
+        /// <inheritdoc/>
         IEnumerable<(Single, (Single[], Single[], Single[]))> IAnimationSampler<Single[]>.GetCubicKeys()
         IEnumerable<(Single, (Single[], Single[], Single[]))> IAnimationSampler<Single[]>.GetCubicKeys()
         {
         {
             Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
             Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
@@ -394,7 +478,7 @@ 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()
+        IEnumerable<(Single, (SEGMENT, SEGMENT, SEGMENT))> IAnimationSampler<SEGMENT>.GetCubicKeys()
         {
         {
             Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
             Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
 
 
@@ -403,27 +487,23 @@ namespace SharpGLTF.Schema2
             var keys = this.Input.AsScalarArray();
             var keys = this.Input.AsScalarArray();
             var frames = _GroupByTangentValueTangent(this.Output.AsMultiArray(dimensions));
             var frames = _GroupByTangentValueTangent(this.Output.AsMultiArray(dimensions));
 
 
-            return keys.Zip(frames, (key, val) => (key, SparseWeight8.AsTuple(val.TangentIn, val.Value, val.TangentOut)) );
+            return keys.Zip(frames, (key, val) => (key, (new SEGMENT(val.TangentIn), new SEGMENT(val.Value), new SEGMENT(val.TangentOut))));
         }
         }
 
 
-        private static IEnumerable<(T TangentIn, T Value, T TangentOut)> _GroupByTangentValueTangent<T>(IEnumerable<T> collection)
+        /// <inheritdoc/>
+        IEnumerable<(Single, (SPARSE8, SPARSE8, SPARSE8))> IAnimationSampler<SPARSE8>.GetCubicKeys()
         {
         {
-            using (var ptr = collection.GetEnumerator())
-            {
-                while (true)
-                {
-                    if (!ptr.MoveNext()) break;
-                    var a = ptr.Current;
-                    if (!ptr.MoveNext()) break;
-                    var b = ptr.Current;
-                    if (!ptr.MoveNext()) break;
-                    var c = ptr.Current;
+            Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
 
 
-                    yield return (a, b, c);
-                }
-            }
+            var dimensions = this.Output.Count / (this.Input.Count * 3);
+
+            var keys = this.Input.AsScalarArray();
+            var frames = _GroupByTangentValueTangent(this.Output.AsMultiArray(dimensions));
+
+            return keys.Zip(frames, (key, val) => (key, SPARSE8.AsTuple(val.TangentIn, val.Value, val.TangentOut)) );
         }
         }
 
 
+        /// <inheritdoc/>
         ICurveSampler<Vector3> IAnimationSampler<Vector3>.CreateCurveSampler(bool isolateMemory)
         ICurveSampler<Vector3> IAnimationSampler<Vector3>.CreateCurveSampler(bool isolateMemory)
         {
         {
             var xsampler = this as IAnimationSampler<Vector3>;
             var xsampler = this as IAnimationSampler<Vector3>;
@@ -438,6 +518,7 @@ namespace SharpGLTF.Schema2
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
 
 
+        /// <inheritdoc/>
         ICurveSampler<Quaternion> IAnimationSampler<Quaternion>.CreateCurveSampler(bool isolateMemory)
         ICurveSampler<Quaternion> IAnimationSampler<Quaternion>.CreateCurveSampler(bool isolateMemory)
         {
         {
             var xsampler = this as IAnimationSampler<Quaternion>;
             var xsampler = this as IAnimationSampler<Quaternion>;
@@ -452,9 +533,10 @@ namespace SharpGLTF.Schema2
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
 
 
-        ICurveSampler<SparseWeight8> IAnimationSampler<SparseWeight8>.CreateCurveSampler(bool isolateMemory)
+        /// <inheritdoc/>
+        ICurveSampler<SPARSE8> IAnimationSampler<SPARSE8>.CreateCurveSampler(bool isolateMemory)
         {
         {
-            var xsampler = this as IAnimationSampler<SparseWeight8>;
+            var xsampler = this as IAnimationSampler<SPARSE8>;
 
 
             switch (this.InterpolationMode)
             switch (this.InterpolationMode)
             {
             {
@@ -466,6 +548,7 @@ namespace SharpGLTF.Schema2
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
 
 
+        /// <inheritdoc/>
         ICurveSampler<Single[]> IAnimationSampler<Single[]>.CreateCurveSampler(bool isolateMemory)
         ICurveSampler<Single[]> IAnimationSampler<Single[]>.CreateCurveSampler(bool isolateMemory)
         {
         {
             var xsampler = this as IAnimationSampler<Single[]>;
             var xsampler = this as IAnimationSampler<Single[]>;
@@ -480,10 +563,44 @@ namespace SharpGLTF.Schema2
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
 
 
+        /// <inheritdoc/>
+        ICurveSampler<SEGMENT> IAnimationSampler<SEGMENT>.CreateCurveSampler(bool isolateMemory)
+        {
+            var xsampler = this as IAnimationSampler<SEGMENT>;
+
+            switch (this.InterpolationMode)
+            {
+                case AnimationInterpolationMode.STEP: return xsampler.GetLinearKeys().CreateSampler(false, isolateMemory);
+                case AnimationInterpolationMode.LINEAR: return xsampler.GetLinearKeys().CreateSampler(true, isolateMemory);
+                case AnimationInterpolationMode.CUBICSPLINE: return xsampler.GetCubicKeys().CreateSampler(isolateMemory);
+            }
+
+            throw new NotImplementedException();
+        }
+
+        private static IEnumerable<(T TangentIn, T Value, T TangentOut)> _GroupByTangentValueTangent<T>(IEnumerable<T> collection)
+        {
+            using (var ptr = collection.GetEnumerator())
+            {
+                while (true)
+                {
+                    if (!ptr.MoveNext()) break;
+                    var a = ptr.Current;
+                    if (!ptr.MoveNext()) break;
+                    var b = ptr.Current;
+                    if (!ptr.MoveNext()) break;
+                    var c = ptr.Current;
+
+                    yield return (a, b, c);
+                }
+            }
+        }
+
         #endregion
         #endregion
 
 
         #region validation
         #region validation
 
 
+        /// <inheritdoc/>
         protected override void OnValidateReferences(ValidationContext validate)
         protected override void OnValidateReferences(ValidationContext validate)
         {
         {
             base.OnValidateReferences(validate);
             base.OnValidateReferences(validate);
@@ -493,6 +610,7 @@ namespace SharpGLTF.Schema2
                 .IsNullOrIndex("Output", _output, this.LogicalParent.LogicalParent.LogicalAccessors);
                 .IsNullOrIndex("Output", _output, this.LogicalParent.LogicalParent.LogicalAccessors);
         }
         }
 
 
+        /// <inheritdoc/>
         protected override void OnValidateContent(ValidationContext validate)
         protected override void OnValidateContent(ValidationContext validate)
         {
         {
             base.OnValidateContent(validate);
             base.OnValidateContent(validate);

+ 36 - 4
src/SharpGLTF.Core/Schema2/gltf.Animations.cs

@@ -9,6 +9,8 @@ using SharpGLTF.Transforms;
 using SharpGLTF.Animations;
 using SharpGLTF.Animations;
 using SharpGLTF.Validation;
 using SharpGLTF.Validation;
 
 
+using WEIGHTS = System.Collections.Generic.IReadOnlyList<float>;
+
 namespace SharpGLTF.Schema2
 namespace SharpGLTF.Schema2
 {
 {
     [System.Diagnostics.DebuggerDisplay("Animation[{LogicalIndex}] {Name}")]
     [System.Diagnostics.DebuggerDisplay("Animation[{LogicalIndex}] {Name}")]
@@ -112,7 +114,7 @@ namespace SharpGLTF.Schema2
 
 
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
 
-            sampler.SetKeys(keyframes);
+            sampler.SetCubicKeys(keyframes);
 
 
             this._UseChannel(node, PropertyPath.scale)
             this._UseChannel(node, PropertyPath.scale)
                 .SetSampler(sampler);
                 .SetSampler(sampler);
@@ -140,7 +142,7 @@ namespace SharpGLTF.Schema2
 
 
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
 
-            sampler.SetKeys(keyframes);
+            sampler.SetCubicKeys(keyframes);
 
 
             this._UseChannel(node, PropertyPath.rotation)
             this._UseChannel(node, PropertyPath.rotation)
                 .SetSampler(sampler);
                 .SetSampler(sampler);
@@ -168,12 +170,42 @@ namespace SharpGLTF.Schema2
 
 
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
 
-            sampler.SetKeys(keyframes);
+            sampler.SetCubicKeys(keyframes);
 
 
             this._UseChannel(node, PropertyPath.translation)
             this._UseChannel(node, PropertyPath.translation)
                 .SetSampler(sampler);
                 .SetSampler(sampler);
         }
         }
 
 
+        public void CreateMorphChannel<TWeights>(Node node, IReadOnlyDictionary<Single, TWeights> keyframes, int morphCount, bool linear = true)
+            where TWeights : WEIGHTS
+        {
+            Guard.NotNull(node, nameof(node));
+            Guard.MustShareLogicalParent(this, node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
+            var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
+
+            sampler.SetKeys(keyframes, morphCount);
+
+            this._UseChannel(node, PropertyPath.weights)
+                .SetSampler(sampler);
+        }
+
+        public void CreateMorphChannel<TWeights>(Node node, IReadOnlyDictionary<Single, (TWeights TangentIn, TWeights Value, TWeights TangentOut)> keyframes, int morphCount)
+            where TWeights : WEIGHTS
+        {
+            Guard.NotNull(node, nameof(node));
+            Guard.MustShareLogicalParent(this, node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
+            var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
+
+            sampler.SetCubicKeys(keyframes, morphCount);
+
+            this._UseChannel(node, PropertyPath.weights)
+                .SetSampler(sampler);
+        }
+
         public void CreateMorphChannel(Node node, IReadOnlyDictionary<Single, SparseWeight8> keyframes, int morphCount, bool linear = true)
         public void CreateMorphChannel(Node node, IReadOnlyDictionary<Single, SparseWeight8> keyframes, int morphCount, bool linear = true)
         {
         {
             Guard.NotNull(node, nameof(node));
             Guard.NotNull(node, nameof(node));
@@ -196,7 +228,7 @@ namespace SharpGLTF.Schema2
 
 
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
 
-            sampler.SetKeys(keyframes, morphCount);
+            sampler.SetCubicKeys(keyframes, morphCount);
 
 
             this._UseChannel(node, PropertyPath.weights)
             this._UseChannel(node, PropertyPath.weights)
                 .SetSampler(sampler);
                 .SetSampler(sampler);

+ 5 - 0
src/SharpGLTF.Core/Schema2/gltf.Mesh.cs

@@ -56,6 +56,11 @@ namespace SharpGLTF.Schema2
             return _weights.Select(item => (float)item).ToList();
             return _weights.Select(item => (float)item).ToList();
         }
         }
 
 
+        public void SetMorphWeights(IReadOnlyList<float> weights)
+        {
+            _weights.SetMorphWeights(weights);
+        }
+
         public void SetMorphWeights(Transforms.SparseWeight8 weights)
         public void SetMorphWeights(Transforms.SparseWeight8 weights)
         {
         {
             int count = _primitives.Max(item => item.MorphTargetsCount);
             int count = _primitives.Max(item => item.MorphTargetsCount);

+ 42 - 22
src/SharpGLTF.Core/Schema2/gltf.Node.cs

@@ -759,23 +759,19 @@ namespace SharpGLTF.Schema2
             TargetNode = node;
             TargetNode = node;
             Animation = animation;
             Animation = animation;
 
 
-            Scale = null;
-            Rotation = null;
-            Translation = null;
-            Morphing = null;
-            MorphingSparse = null;
+            _ScaleSampler = null;
+            _RotationSampler = null;
+            _TranslationSampler = null;
+            _MorphSampler = null;
 
 
             foreach (var c in animation.FindChannels(node))
             foreach (var c in animation.FindChannels(node))
             {
             {
                 switch (c.TargetNodePath)
                 switch (c.TargetNodePath)
                 {
                 {
-                    case PropertyPath.scale: Scale = c.GetScaleSampler(); break;
-                    case PropertyPath.rotation: Rotation = c.GetRotationSampler(); break;
-                    case PropertyPath.translation: Translation = c.GetTranslationSampler(); break;
-                    case PropertyPath.weights:
-                        Morphing = c.GetMorphSampler();
-                        MorphingSparse = c.GetSparseMorphSampler();
-                        break;
+                    case PropertyPath.scale: _ScaleSampler = c._GetSampler(); break;
+                    case PropertyPath.rotation: _RotationSampler = c._GetSampler(); break;
+                    case PropertyPath.translation: _TranslationSampler = c._GetSampler(); break;
+                    case PropertyPath.weights: _MorphSampler = c._GetSampler(); break;
                 }
                 }
             }
             }
 
 
@@ -791,6 +787,11 @@ namespace SharpGLTF.Schema2
         public readonly Node TargetNode;
         public readonly Node TargetNode;
         public readonly Animation Animation;
         public readonly Animation Animation;
 
 
+        private readonly AnimationSampler _ScaleSampler;
+        private readonly AnimationSampler _RotationSampler;
+        private readonly AnimationSampler _TranslationSampler;
+        private readonly AnimationSampler _MorphSampler;
+
         #endregion
         #endregion
 
 
         #region  properties
         #region  properties
@@ -798,42 +799,61 @@ namespace SharpGLTF.Schema2
         /// <summary>
         /// <summary>
         /// True if any of <see cref="Scale"/>, <see cref="Rotation"/> or <see cref="Translation"/> is defined.
         /// True if any of <see cref="Scale"/>, <see cref="Rotation"/> or <see cref="Translation"/> is defined.
         /// </summary>
         /// </summary>
-        public bool HasTransformCurves => Scale != null || Rotation != null || Translation != null;
+        public bool HasTransformCurves => _ScaleSampler != null || _RotationSampler != null || _TranslationSampler != null;
 
 
         /// <summary>
         /// <summary>
         /// True if there's a morphing curve.
         /// True if there's a morphing curve.
         /// </summary>
         /// </summary>
-        public bool HasMorphingCurves => Morphing != null || MorphingSparse != null;
+        public bool HasMorphingCurves => _MorphSampler != null;
 
 
         /// <summary>
         /// <summary>
         /// Gets the Scale sampler, or null if there's no curve defined.
         /// Gets the Scale sampler, or null if there's no curve defined.
         /// </summary>
         /// </summary>
-        public readonly IAnimationSampler<Vector3> Scale;
+        public IAnimationSampler<Vector3> Scale => _ScaleSampler;
 
 
         /// <summary>
         /// <summary>
         /// Gets the Rotation sampler, or null if there's no curve defined.
         /// Gets the Rotation sampler, or null if there's no curve defined.
         /// </summary>
         /// </summary>
-        public readonly IAnimationSampler<Quaternion> Rotation;
+        public IAnimationSampler<Quaternion> Rotation => _RotationSampler;
 
 
         /// <summary>
         /// <summary>
         /// Gets the Translation sampler, or null if there's no curve defined.
         /// Gets the Translation sampler, or null if there's no curve defined.
         /// </summary>
         /// </summary>
-        public readonly IAnimationSampler<Vector3> Translation;
+        public IAnimationSampler<Vector3> Translation => _TranslationSampler;
 
 
         /// <summary>
         /// <summary>
         /// Gets the raw Morphing sampler, or null if there's no curve defined.
         /// Gets the raw Morphing sampler, or null if there's no curve defined.
         /// </summary>
         /// </summary>
-        public readonly IAnimationSampler<Single[]> Morphing;
+        [Obsolete("Use GetMorphingSampler<T>()", true)]
+        public IAnimationSampler<Single[]> Morphing => GetMorphingSampler<Single[]>();
 
 
         /// <summary>
         /// <summary>
         /// Gets the SparseWeight8 Morphing sampler, or null if there's no curve defined.
         /// Gets the SparseWeight8 Morphing sampler, or null if there's no curve defined.
         /// </summary>
         /// </summary>
-        public readonly IAnimationSampler<Transforms.SparseWeight8> MorphingSparse;
+        [Obsolete("Use GetMorphingSampler<T>()", true)]
+        public IAnimationSampler<Transforms.SparseWeight8> MorphingSparse => GetMorphingSampler<Transforms.SparseWeight8>();
 
 
         #endregion
         #endregion
 
 
         #region API
         #region API
 
 
+        /// <summary>
+        /// Gets the morphing sampler, or null if there's no curve defined.
+        /// </summary>
+        /// <typeparam name="TWeights">
+        /// It must be one of these:<br/>
+        /// <list type="table">
+        /// <item><see cref="float"/>[]</item>
+        /// <item><see cref="Transforms.SparseWeight8"/></item>
+        /// <item><see cref="ArraySegment{T}"/> of <see cref="float"/></item>
+        /// </list>
+        /// </typeparam>
+        /// <returns>A valid sampler, or null.</returns>
+        public IAnimationSampler<TWeights> GetMorphingSampler<TWeights>()
+        {
+            return _MorphSampler as IAnimationSampler<TWeights>;
+        }
+
         public TRANSFORM GetLocalTransform(Single time)
         public TRANSFORM GetLocalTransform(Single time)
         {
         {
             var xform = TargetNode.LocalTransform.GetDecomposed();
             var xform = TargetNode.LocalTransform.GetDecomposed();
@@ -845,9 +865,9 @@ namespace SharpGLTF.Schema2
             return new TRANSFORM(s, r, t);
             return new TRANSFORM(s, r, t);
         }
         }
 
 
-        public IReadOnlyList<float> GetMorphingWeights(Single time)
+        public IReadOnlyList<float> GetMorphingWeights<TWeight>(Single time)
         {
         {
-            return Morphing
+            return GetMorphingSampler<float[]>()
                 ?.CreateCurveSampler()
                 ?.CreateCurveSampler()
                 ?.GetPoint(time)
                 ?.GetPoint(time)
                 ?? TargetNode.MorphWeights;
                 ?? TargetNode.MorphWeights;
@@ -855,7 +875,7 @@ namespace SharpGLTF.Schema2
 
 
         public Transforms.SparseWeight8 GetSparseMorphingWeights(Single time)
         public Transforms.SparseWeight8 GetSparseMorphingWeights(Single time)
         {
         {
-            return MorphingSparse
+            return GetMorphingSampler<Transforms.SparseWeight8>()
                 ?.CreateCurveSampler()
                 ?.CreateCurveSampler()
                 ?.GetPoint(time)
                 ?.GetPoint(time)
                 ?? Transforms.SparseWeight8.Create(TargetNode.MorphWeights);
                 ?? Transforms.SparseWeight8.Create(TargetNode.MorphWeights);

+ 19 - 0
src/SharpGLTF.Toolkit/Animations/AnimatableProperty.cs

@@ -137,5 +137,24 @@ namespace SharpGLTF.Animations
         }
         }
 
 
         #endregion
         #endregion
+
+        #region extended API
+
+        public void SetValue(params float[] elements) { this.Value = _Convert(elements); }
+
+        private static T _Convert(float[] elements)
+        {
+            if (typeof(T) == typeof(Vector3)) return (T)(Object)new Vector3(elements[0], elements[1], elements[2]);
+            if (typeof(T) == typeof(Vector4)) return (T)(Object)new Vector4(elements[0], elements[1], elements[2], elements[3]);
+            if (typeof(T) == typeof(Quaternion)) return (T)(Object)new Quaternion(elements[0], elements[1], elements[2], elements[3]);
+
+            if (typeof(T) == typeof(Single[])) return (T)(Object)new ArraySegment<Single>(elements).ToArray();
+            if (typeof(T) == typeof(ArraySegment<Single>)) return (T)(Object)new ArraySegment<Single>(elements);
+            if (typeof(T) == typeof(Transforms.SparseWeight8)) return (T)(Object)Transforms.SparseWeight8.Create(elements);
+
+            throw new NotSupportedException();
+        }
+
+        #endregion
     }
     }
 }
 }

+ 173 - 28
src/SharpGLTF.Toolkit/Animations/CurveBuilder.cs

@@ -10,10 +10,9 @@ namespace SharpGLTF.Animations
     /// Represents an editable curve of <typeparamref name="T"/> elements.
     /// Represents an editable curve of <typeparamref name="T"/> elements.
     /// </summary>
     /// </summary>
     /// <typeparam name="T">An element of the curve.</typeparam>
     /// <typeparam name="T">An element of the curve.</typeparam>
-    public abstract class CurveBuilder<T>
-        : ICurveSampler<T>,
+    public abstract class CurveBuilder<T> :
+        ICurveSampler<T>,
         IConvertibleCurve<T>
         IConvertibleCurve<T>
-        where T : struct
     {
     {
         #region lifecycle
         #region lifecycle
 
 
@@ -42,6 +41,7 @@ namespace SharpGLTF.Animations
 
 
         #region properties
         #region properties
 
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         internal IReadOnlyDictionary<float, _CurveNode<T>> _DebugKeys => _Keys;
         internal IReadOnlyDictionary<float, _CurveNode<T>> _DebugKeys => _Keys;
 
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
@@ -53,12 +53,16 @@ namespace SharpGLTF.Animations
 
 
         #region abstract API
         #region abstract API
 
 
+        protected abstract bool AreEqual(T left, T right);
+
+        protected abstract T CloneValue(T value);
+
         /// <summary>
         /// <summary>
         /// Creates a <typeparamref name="T"/> instance from an <see cref="Single"/>[] array.
         /// Creates a <typeparamref name="T"/> instance from an <see cref="Single"/>[] array.
         /// </summary>
         /// </summary>
         /// <param name="values">An array of floats.</param>
         /// <param name="values">An array of floats.</param>
         /// <returns>A <typeparamref name="T"/> instance.</returns>
         /// <returns>A <typeparamref name="T"/> instance.</returns>
-        protected abstract T CreateValue(params float[] values);
+        protected abstract T CreateValue(IReadOnlyList<float> values);
 
 
         /// <summary>
         /// <summary>
         /// Samples the curve at a given <paramref name="offset"/>
         /// Samples the curve at a given <paramref name="offset"/>
@@ -77,8 +81,15 @@ namespace SharpGLTF.Animations
 
 
         public void RemoveKey(float offset) { _Keys.Remove(offset); }
         public void RemoveKey(float offset) { _Keys.Remove(offset); }
 
 
+        public void SetPoint(float offset, bool isLinear, params float[] elements)
+        {
+            SetPoint(offset, CreateValue(elements), isLinear);
+        }
+
         public void SetPoint(float offset, T value, bool isLinear = true)
         public void SetPoint(float offset, T value, bool isLinear = true)
         {
         {
+            value = CloneValue(value);
+
             if (_Keys.TryGetValue(offset, out _CurveNode<T> existing))
             if (_Keys.TryGetValue(offset, out _CurveNode<T> existing))
             {
             {
                 existing.Point = value;
                 existing.Point = value;
@@ -100,21 +111,35 @@ namespace SharpGLTF.Animations
         {
         {
             Guard.IsTrue(_Keys.ContainsKey(offset), nameof(offset));
             Guard.IsTrue(_Keys.ContainsKey(offset), nameof(offset));
 
 
+            tangent = CloneValue(tangent);
+
             offset -= float.Epsilon;
             offset -= float.Epsilon;
 
 
             var (keyA, keyB, _) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
             var (keyA, keyB, _) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
 
 
-            var a = _Keys[keyA];
-            var b = _Keys[keyB];
+            if (keyA == keyB)
+            {
+                var a = _Keys[keyA];
 
 
-            if (a.Degree == 1) a.OutgoingTangent = GetTangent(a.Point, b.Point);
+                a.Degree = 3;
+                a.IncomingTangent = tangent;
 
 
-            a.Degree = 3;
+                _Keys[keyA] = a;
+            }
+            else
+            {
+                var a = _Keys[keyA];
+                var b = _Keys[keyB];
 
 
-            b.IncomingTangent = tangent;
+                if (a.Degree == 1) a.OutgoingTangent = GetTangent(a.Point, b.Point);
 
 
-            _Keys[keyA] = a;
-            _Keys[keyB] = b;
+                a.Degree = 3;
+
+                b.IncomingTangent = tangent;
+
+                _Keys[keyA] = a;
+                _Keys[keyB] = b;
+            }
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -126,22 +151,36 @@ namespace SharpGLTF.Animations
         {
         {
             Guard.IsTrue(_Keys.ContainsKey(offset), nameof(offset));
             Guard.IsTrue(_Keys.ContainsKey(offset), nameof(offset));
 
 
-            var (keyA, keyB, _) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
+            tangent = CloneValue(tangent);
 
 
-            var a = _Keys[keyA];
-            var b = _Keys[keyB];
+            var (keyA, keyB, _) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
 
 
-            if (keyA != keyB)
+            if (keyA == keyB)
             {
             {
-                if (a.Degree == 1) b.IncomingTangent = GetTangent(a.Point, b.Point);
-                _Keys[keyB] = b;
+                var a = _Keys[keyA];
+
+                a.Degree = 3;
+                a.OutgoingTangent = tangent;
+
+                _Keys[keyA] = a;
             }
             }
+            else
+            {
+                var a = _Keys[keyA];
+                var b = _Keys[keyB];
 
 
-            a.Degree = 3;
+                if (keyA != keyB)
+                {
+                    if (a.Degree == 1) b.IncomingTangent = GetTangent(a.Point, b.Point);
+                    _Keys[keyB] = b;
+                }
 
 
-            a.OutgoingTangent = tangent;
+                a.Degree = 3;
 
 
-            _Keys[keyA] = a;
+                a.OutgoingTangent = tangent;
+
+                _Keys[keyA] = a;
+            }
         }
         }
 
 
         private protected (_CurveNode<T> A, _CurveNode<T> B, float Amount) FindSample(float offset)
         private protected (_CurveNode<T> A, _CurveNode<T> B, float Amount) FindSample(float offset)
@@ -159,8 +198,18 @@ namespace SharpGLTF.Animations
             {
             {
                 if (convertible.MaxDegree == 0)
                 if (convertible.MaxDegree == 0)
                 {
                 {
-                    var linear = convertible.ToStepCurve();
-                    foreach (var p in linear) this.SetPoint(p.Key, p.Value, false);
+                    var step = convertible.ToStepCurve();
+                    foreach (var p in step) this.SetPoint(p.Key, p.Value, false);
+
+                    #if DEBUG
+                    foreach (var p in step)
+                    {
+                        var dstKey = _Keys[p.Key];
+                        System.Diagnostics.Debug.Assert(dstKey.Degree <= 1);
+                        System.Diagnostics.Debug.Assert(AreEqual(dstKey.Point, p.Value));
+                    }
+                    #endif
+
                     return;
                     return;
                 }
                 }
 
 
@@ -168,6 +217,16 @@ namespace SharpGLTF.Animations
                 {
                 {
                     var linear = convertible.ToLinearCurve();
                     var linear = convertible.ToLinearCurve();
                     foreach (var p in linear) this.SetPoint(p.Key, p.Value);
                     foreach (var p in linear) this.SetPoint(p.Key, p.Value);
+
+                    #if DEBUG
+                    foreach (var p in linear)
+                    {
+                        var dstKey = _Keys[p.Key];
+                        System.Diagnostics.Debug.Assert(dstKey.Degree <= 1);
+                        System.Diagnostics.Debug.Assert(AreEqual(dstKey.Point, p.Value));
+                    }
+                    #endif
+
                     return;
                     return;
                 }
                 }
 
 
@@ -181,6 +240,17 @@ namespace SharpGLTF.Animations
                         this.SetOutgoingTangent(ppp.Key, ppp.Value.TangentOut);
                         this.SetOutgoingTangent(ppp.Key, ppp.Value.TangentOut);
                     }
                     }
 
 
+                    #if DEBUG
+                    foreach (var ppp in spline)
+                    {
+                        var dstKey = _Keys[ppp.Key];
+                        System.Diagnostics.Debug.Assert(dstKey.Degree == 3);
+                        System.Diagnostics.Debug.Assert(AreEqual(dstKey.Point, ppp.Value.Value));
+                        System.Diagnostics.Debug.Assert(AreEqual(dstKey.IncomingTangent, ppp.Value.TangentIn));
+                        System.Diagnostics.Debug.Assert(AreEqual(dstKey.OutgoingTangent, ppp.Value.TangentOut));
+                    }
+                    #endif
+
                     return;
                     return;
                 }
                 }
             }
             }
@@ -202,6 +272,15 @@ namespace SharpGLTF.Animations
                             this.SetPoint(key, value, isLinear);
                             this.SetPoint(key, value, isLinear);
                         }
                         }
 
 
+                        #if DEBUG
+                        foreach (var (key, value) in curve.GetLinearKeys())
+                        {
+                            var dstKey = _Keys[key];
+                            System.Diagnostics.Debug.Assert(dstKey.Degree <= 1);
+                            System.Diagnostics.Debug.Assert(AreEqual(dstKey.Point, value));
+                        }
+                        #endif
+
                         break;
                         break;
                     }
                     }
 
 
@@ -214,6 +293,17 @@ namespace SharpGLTF.Animations
                             this.SetOutgoingTangent(key, value.TangentOut);
                             this.SetOutgoingTangent(key, value.TangentOut);
                         }
                         }
 
 
+                        #if DEBUG
+                        foreach (var (key, value) in curve.GetCubicKeys())
+                        {
+                            var dstKey = _Keys[key];
+                            System.Diagnostics.Debug.Assert(dstKey.Degree == 3);
+                            System.Diagnostics.Debug.Assert(AreEqual(dstKey.Point, value.Value));
+                            System.Diagnostics.Debug.Assert(AreEqual(dstKey.IncomingTangent, value.TangentIn));
+                            System.Diagnostics.Debug.Assert(AreEqual(dstKey.OutgoingTangent, value.TangentOut));
+                        }
+                        #endif
+
                         break;
                         break;
                     }
                     }
 
 
@@ -265,12 +355,21 @@ namespace SharpGLTF.Animations
         IReadOnlyDictionary<float, T> IConvertibleCurve<T>.ToStepCurve()
         IReadOnlyDictionary<float, T> IConvertibleCurve<T>.ToStepCurve()
         {
         {
             if (MaxDegree != 0) throw new NotSupportedException();
             if (MaxDegree != 0) throw new NotSupportedException();
+            if (_Keys.Count == 0) return new Dictionary<float, T>();
 
 
-            return _Keys.ToDictionary(item => item.Key, item => item.Value.Point);
+            return _Keys.ToDictionary(item => item.Key, item => CloneValue(item.Value.Point));
         }
         }
 
 
         IReadOnlyDictionary<float, T> IConvertibleCurve<T>.ToLinearCurve()
         IReadOnlyDictionary<float, T> IConvertibleCurve<T>.ToLinearCurve()
         {
         {
+            if (MaxDegree != 1) throw new NotSupportedException();
+            if (_Keys.Count == 0) return new Dictionary<float, T>();
+
+            if (_Keys.All(item => item.Value.Degree == 1))
+            {
+                return _Keys.ToDictionary(item => item.Key, item => CloneValue(item.Value.Point));
+            }
+
             var d = new Dictionary<float, T>();
             var d = new Dictionary<float, T>();
 
 
             if (_Keys.Count == 0) return d;
             if (_Keys.Count == 0) return d;
@@ -324,6 +423,17 @@ namespace SharpGLTF.Animations
 
 
         IReadOnlyDictionary<float, (T TangentIn, T Value, T TangentOut)> IConvertibleCurve<T>.ToSplineCurve()
         IReadOnlyDictionary<float, (T TangentIn, T Value, T TangentOut)> IConvertibleCurve<T>.ToSplineCurve()
         {
         {
+            if (_Keys.Count == 0) return new Dictionary<float, (T TangentIn, T Value, T TangentOut)>();
+
+            if (_Keys.All(item => item.Value.Degree == 3))
+            {
+                return _Keys.ToDictionary
+                    (
+                    item => item.Key,
+                    item => (item.Value.IncomingTangent, item.Value.Point, item.Value.OutgoingTangent)
+                    );
+            }
+
             var d = new Dictionary<float, (T, T, T)>();
             var d = new Dictionary<float, (T, T, T)>();
 
 
             var orderedKeys = _Keys.Keys.ToList();
             var orderedKeys = _Keys.Keys.ToList();
@@ -374,29 +484,64 @@ namespace SharpGLTF.Animations
         #endregion
         #endregion
     }
     }
 
 
-    [System.Diagnostics.DebuggerDisplay("{IncomingTangent} -> {Point}[{Degree}] -> {OutgoingTangent}")]
+    [System.Diagnostics.DebuggerDisplay("{ToDebuggerDisplayString(),nq}")]
     struct _CurveNode<T>
     struct _CurveNode<T>
-        where T : struct
     {
     {
+        #region diagnostics
+
+        private string ToDebuggerDisplayString()
+        {
+            switch (Degree)
+            {
+                case 0: return $"{_ToString(Point)}";
+                case 1: return $"{_ToString(Point)}";
+                case 3: return $"{_ToString(IncomingTangent)} -> ({_ToString(Point)}) -> {_ToString(OutgoingTangent)}";
+            }
+
+            return "Unsupported";
+        }
+
+        private static string _ToString(T value)
+        {
+            if (value is ArraySegment<float> segm)
+            {
+                if (segm.Count < 20) return string.Join(" ", segm.ToArray());
+                return Transforms.SparseWeight8.Create(segm.ToArray()).ToString();
+            }
+
+            if (value is Transforms.SparseWeight8 sparse) return sparse.ToString();
+
+            return value.ToString();
+        }
+
+        #endregion
+
+        #region constructors
         public _CurveNode(T value, bool isLinear)
         public _CurveNode(T value, bool isLinear)
         {
         {
+            Degree = isLinear ? 1 : 0;
             IncomingTangent = default;
             IncomingTangent = default;
             Point = value;
             Point = value;
             OutgoingTangent = default;
             OutgoingTangent = default;
-            Degree = isLinear ? 1 : 0;
         }
         }
 
 
         public _CurveNode(T incoming, T value, T outgoing)
         public _CurveNode(T incoming, T value, T outgoing)
         {
         {
+            Degree = 3;
             IncomingTangent = incoming;
             IncomingTangent = incoming;
             Point = value;
             Point = value;
             OutgoingTangent = outgoing;
             OutgoingTangent = outgoing;
-            Degree = 3;
         }
         }
 
 
+        #endregion
+
+        #region data
+
+        public int Degree;
         public T IncomingTangent;
         public T IncomingTangent;
         public T Point;
         public T Point;
         public T OutgoingTangent;
         public T OutgoingTangent;
-        public int Degree;
+
+        #endregion
     }
     }
 }
 }

+ 81 - 10
src/SharpGLTF.Toolkit/Animations/CurveFactory.cs

@@ -1,8 +1,10 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Linq;
 using System.Numerics;
 using System.Numerics;
 using System.Text;
 using System.Text;
 
 
+using SEGMENT = System.ArraySegment<float>;
 using SPARSE = SharpGLTF.Transforms.SparseWeight8;
 using SPARSE = SharpGLTF.Transforms.SparseWeight8;
 
 
 namespace SharpGLTF.Animations
 namespace SharpGLTF.Animations
@@ -15,8 +17,9 @@ namespace SharpGLTF.Animations
             if (typeof(T) == typeof(Vector3)) return new Vector3CurveBuilder() as CurveBuilder<T>;
             if (typeof(T) == typeof(Vector3)) return new Vector3CurveBuilder() as CurveBuilder<T>;
             if (typeof(T) == typeof(Quaternion)) return new QuaternionCurveBuilder() as CurveBuilder<T>;
             if (typeof(T) == typeof(Quaternion)) return new QuaternionCurveBuilder() as CurveBuilder<T>;
             if (typeof(T) == typeof(SPARSE)) return new SparseCurveBuilder() as CurveBuilder<T>;
             if (typeof(T) == typeof(SPARSE)) return new SparseCurveBuilder() as CurveBuilder<T>;
+            if (typeof(T) == typeof(SEGMENT)) return new SegmentCurveBuilder() as CurveBuilder<T>;
 
 
-            throw new InvalidOperationException($"{nameof(T)} not supported.");
+            throw new InvalidOperationException($"{typeof(T).Name} not supported.");
         }
         }
 
 
         public static CurveBuilder<T> CreateCurveBuilder<T>(ICurveSampler<T> curve)
         public static CurveBuilder<T> CreateCurveBuilder<T>(ICurveSampler<T> curve)
@@ -25,8 +28,9 @@ namespace SharpGLTF.Animations
             if (curve is Vector3CurveBuilder v3cb) return v3cb.Clone() as CurveBuilder<T>;
             if (curve is Vector3CurveBuilder v3cb) return v3cb.Clone() as CurveBuilder<T>;
             if (curve is QuaternionCurveBuilder q4cb) return q4cb.Clone() as CurveBuilder<T>;
             if (curve is QuaternionCurveBuilder q4cb) return q4cb.Clone() as CurveBuilder<T>;
             if (curve is SparseCurveBuilder sscb) return sscb.Clone() as CurveBuilder<T>;
             if (curve is SparseCurveBuilder sscb) return sscb.Clone() as CurveBuilder<T>;
+            if (curve is SegmentCurveBuilder xscb) return xscb.Clone() as CurveBuilder<T>;
 
 
-            throw new InvalidOperationException($"{nameof(T)} not supported.");
+            throw new InvalidOperationException($"{typeof(T).Name} not supported.");
         }
         }
     }
     }
 
 
@@ -45,10 +49,12 @@ namespace SharpGLTF.Animations
 
 
         #region API
         #region API
 
 
-        protected override Vector3 CreateValue(params float[] values)
+        protected override bool AreEqual(Vector3 left, Vector3 right) { return left == right; }
+        protected override Vector3 CloneValue(Vector3 value) { return value; }
+        protected override Vector3 CreateValue(IReadOnlyList<float> values)
         {
         {
             Guard.NotNull(values, nameof(values));
             Guard.NotNull(values, nameof(values));
-            Guard.IsTrue(values.Length == 3, nameof(values));
+            Guard.IsTrue(values.Count == 3, nameof(values));
             return new Vector3(values[0], values[1], values[2]);
             return new Vector3(values[0], values[1], values[2]);
         }
         }
 
 
@@ -101,10 +107,12 @@ namespace SharpGLTF.Animations
 
 
         #region API
         #region API
 
 
-        protected override Quaternion CreateValue(params float[] values)
+        protected override bool AreEqual(Quaternion left, Quaternion right) { return left == right; }
+        protected override Quaternion CloneValue(Quaternion value) { return value; }
+        protected override Quaternion CreateValue(IReadOnlyList<float> values)
         {
         {
             Guard.NotNull(values, nameof(values));
             Guard.NotNull(values, nameof(values));
-            Guard.IsTrue(values.Length == 4, nameof(values));
+            Guard.IsTrue(values.Count == 4, nameof(values));
             return new Quaternion(values[0], values[1], values[2], values[3]);
             return new Quaternion(values[0], values[1], values[2], values[3]);
         }
         }
 
 
@@ -159,10 +167,9 @@ namespace SharpGLTF.Animations
 
 
         #region API
         #region API
 
 
-        protected override SPARSE CreateValue(params Single[] values)
-        {
-            return SPARSE.Create(values);
-        }
+        protected override bool AreEqual(SPARSE left, SPARSE right) { return left.Equals(right); }
+        protected override SPARSE CloneValue(SPARSE value) { return value; }
+        protected override SPARSE CreateValue(IReadOnlyList<float> values) { return SPARSE.Create(values); }
 
 
         protected override SPARSE GetTangent(SPARSE fromValue, SPARSE toValue)
         protected override SPARSE GetTangent(SPARSE fromValue, SPARSE toValue)
         {
         {
@@ -196,4 +203,68 @@ namespace SharpGLTF.Animations
 
 
         #endregion
         #endregion
     }
     }
+
+    [System.Diagnostics.DebuggerTypeProxy(typeof(Diagnostics._CurveBuilderDebugProxySparse))]
+    sealed class SegmentCurveBuilder : CurveBuilder<SEGMENT>, ICurveSampler<SEGMENT>
+    {
+        #region lifecycle
+
+        public SegmentCurveBuilder() { }
+
+        private SegmentCurveBuilder(SegmentCurveBuilder other)
+            : base(other)
+        {
+        }
+
+        public override CurveBuilder<SEGMENT> Clone() { return new SegmentCurveBuilder(this); }
+
+        #endregion
+
+        #region API
+
+        protected override bool AreEqual(SEGMENT left, SEGMENT right) { return left.AsSpan().SequenceEqual(right); }
+        protected override SEGMENT CloneValue(SEGMENT value)
+        {
+            Guard.IsTrue(value.Count > 0, nameof(value));
+            return new SEGMENT(value.ToArray());
+        }
+
+        protected override SEGMENT CreateValue(IReadOnlyList<float> values)
+        {
+            Guard.NotNull(values, nameof(values));
+            Guard.IsTrue(values.Count > 0, nameof(values));
+            return new SEGMENT(values.ToArray());
+        }
+
+        protected override SEGMENT GetTangent(SEGMENT fromValue, SEGMENT toValue)
+        {
+            return new SEGMENT(CurveSampler.Subtract(toValue, fromValue));
+        }
+
+        public override SEGMENT GetPoint(Single offset)
+        {
+            var sample = FindSample(offset);
+
+            switch (sample.A.Degree)
+            {
+                case 0:
+                    return sample.A.Point;
+
+                case 1:
+                    return new SEGMENT(CurveSampler.InterpolateLinear(sample.A.Point, sample.B.Point, sample.Amount));
+
+                case 3:
+                    return new SEGMENT(CurveSampler.InterpolateCubic
+                            (
+                            sample.A.Point, sample.A.OutgoingTangent,
+                            sample.B.Point, sample.B.IncomingTangent,
+                            sample.Amount
+                            ));
+                default:
+                    throw new NotSupportedException();
+            }
+        }
+
+        #endregion
+    }
 }
 }

+ 1 - 1
src/SharpGLTF.Toolkit/Geometry/Packed/PackedMeshBuilder.cs

@@ -97,7 +97,7 @@ namespace SharpGLTF.Geometry
                 p.CopyToMesh(dstMesh, materialEvaluator);
                 p.CopyToMesh(dstMesh, materialEvaluator);
             }
             }
 
 
-            dstMesh.SetMorphWeights(default);
+            dstMesh.SetMorphWeights(null);
 
 
             return dstMesh;
             return dstMesh;
         }
         }

+ 76 - 1
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs

@@ -156,6 +156,20 @@ namespace SharpGLTF.Scenes
             foreach (var t in animation.Tracks) dstNode.WithMorphingAnimation(t.Key, t.Value);
             foreach (var t in animation.Tracks) dstNode.WithMorphingAnimation(t.Key, t.Value);
         }
         }
 
 
+        public static void SetMorphAnimation(Node dstNode, Animations.AnimatableProperty<ArraySegment<float>> animation)
+        {
+            Guard.NotNull(dstNode, nameof(dstNode));
+            Guard.NotNull(dstNode.Mesh, nameof(dstNode.Mesh), "call after IOperator.ApplyTo");
+
+            if (animation == null) return;
+
+            var dstMesh = dstNode.Mesh;
+
+            dstMesh.SetMorphWeights(animation.Value);
+
+            foreach (var t in animation.Tracks) dstNode.WithMorphingAnimation(t.Key, t.Value);
+        }
+
         public void AddScene(Scene dstScene, SceneBuilder srcScene)
         public void AddScene(Scene dstScene, SceneBuilder srcScene)
         {
         {
             _Nodes.Clear();
             _Nodes.Clear();
@@ -474,6 +488,17 @@ namespace SharpGLTF.Scenes
 
 
         private static void _CopyMorphingAnimation(InstanceBuilder dstInst, Node srcNode)
         private static void _CopyMorphingAnimation(InstanceBuilder dstInst, Node srcNode)
         {
         {
+            bool hasDefaultMorphing = false;
+
+            var defMorphWeights = srcNode.GetMorphWeights();
+            if (defMorphWeights != null && defMorphWeights.Count > 0)
+            {
+                dstInst.Content.UseMorphing().SetValue(defMorphWeights.ToArray());
+                hasDefaultMorphing = true;
+            }
+
+            if (!hasDefaultMorphing) return;
+
             foreach (var anim in srcNode.LogicalParent.LogicalAnimations)
             foreach (var anim in srcNode.LogicalParent.LogicalAnimations)
             {
             {
                 var name = anim.Name;
                 var name = anim.Name;
@@ -481,7 +506,14 @@ namespace SharpGLTF.Scenes
 
 
                 var curves = srcNode.GetCurveSamplers(anim);
                 var curves = srcNode.GetCurveSamplers(anim);
 
 
-                if (curves.MorphingSparse != null) dstInst.Content.UseMorphing(name).SetCurve(curves.MorphingSparse);
+                var srcMorphing = curves.GetMorphingSampler<ArraySegment<float>>();
+                if (srcMorphing != null)
+                {
+                    var dstMorphing = dstInst.Content.UseMorphing(name);
+                    dstMorphing.SetCurve(srcMorphing);
+
+                    _VerifyCurveConversion(srcMorphing, dstMorphing, (a, b) => a.AsSpan().SequenceEqual(b));
+                }
             }
             }
         }
         }
 
 
@@ -489,6 +521,49 @@ namespace SharpGLTF.Scenes
 
 
         #region utilities
         #region utilities
 
 
+        internal static void _VerifyCurveConversion<T>(IAnimationSampler<T> a, Animations.IConvertibleCurve<T> b, Func<T, T, bool> equalityComparer)
+        {
+            if (a.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE)
+            {
+                if (b.MaxDegree != 3) throw new ArgumentException(nameof(b.MaxDegree));
+
+                var bb = b.ToSplineCurve();
+
+                foreach (var (ak, av) in a.GetCubicKeys())
+                {
+                    if (!bb.TryGetValue(ak, out var bv)) throw new ArgumentException(nameof(b));
+
+                    if (!equalityComparer(av.TangentIn, bv.TangentIn)) throw new ArgumentException(nameof(b));
+                    if (!equalityComparer(av.Value, bv.Value)) throw new ArgumentException(nameof(b));
+                    if (!equalityComparer(av.TangentOut, bv.TangentOut)) throw new ArgumentException(nameof(b));
+                }
+            }
+            else if (a.InterpolationMode == AnimationInterpolationMode.LINEAR)
+            {
+                if (b.MaxDegree != 1) throw new ArgumentException(nameof(b.MaxDegree));
+
+                var bb = b.ToLinearCurve();
+
+                foreach (var (ak, av) in a.GetLinearKeys())
+                {
+                    if (!bb.TryGetValue(ak, out var bv)) throw new ArgumentException(nameof(b));
+                    if (!equalityComparer(av, bv)) throw new ArgumentException(nameof(b));
+                }
+            }
+            if (a.InterpolationMode == AnimationInterpolationMode.STEP)
+            {
+                if (b.MaxDegree != 0) throw new ArgumentException(nameof(b.MaxDegree));
+
+                var bb = b.ToStepCurve();
+
+                foreach (var (ak, av) in a.GetLinearKeys())
+                {
+                    if (!bb.TryGetValue(ak, out var bv)) throw new ArgumentException(nameof(b));
+                    if (!equalityComparer(av, bv)) throw new ArgumentException(nameof(b));
+                }
+            }
+        }
+
         internal void _VerifyConversion(Scene gltfScene)
         internal void _VerifyConversion(Scene gltfScene)
         {
         {
             // renderable instances
             // renderable instances

+ 10 - 6
src/SharpGLTF.Toolkit/Scenes/Transformers.cs

@@ -61,7 +61,7 @@ namespace SharpGLTF.Scenes
         private Object _Content;
         private Object _Content;
 
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private Animations.AnimatableProperty<Transforms.SparseWeight8> _Morphings;
+        private Animations.AnimatableProperty<ArraySegment<float>> _Morphings;
 
 
         #endregion
         #endregion
 
 
@@ -89,7 +89,7 @@ namespace SharpGLTF.Scenes
         /// </summary>
         /// </summary>
         internal Object Content => _Content;
         internal Object Content => _Content;
 
 
-        public Animations.AnimatableProperty<Transforms.SparseWeight8> Morphings => _Morphings;
+        public Animations.AnimatableProperty<ArraySegment<float>> Morphings => _Morphings;
 
 
         /// <summary>
         /// <summary>
         /// Gets a value indicating whether <see cref="Content"/> implements <see cref="IRenderableContent"/>
         /// Gets a value indicating whether <see cref="Content"/> implements <see cref="IRenderableContent"/>
@@ -112,20 +112,24 @@ namespace SharpGLTF.Scenes
         /// <returns>A <see cref="NodeBuilder"/> instance, or NULL.</returns>
         /// <returns>A <see cref="NodeBuilder"/> instance, or NULL.</returns>
         public abstract NodeBuilder GetArmatureRoot();
         public abstract NodeBuilder GetArmatureRoot();
 
 
-        public Animations.AnimatableProperty<Transforms.SparseWeight8> UseMorphing()
+        public Animations.AnimatableProperty<ArraySegment<float>> UseMorphing()
         {
         {
             if (_Morphings == null)
             if (_Morphings == null)
             {
             {
-                _Morphings = new Animations.AnimatableProperty<Transforms.SparseWeight8>();
+                _Morphings = new Animations.AnimatableProperty<ArraySegment<float>>();
                 _Morphings.Value = default;
                 _Morphings.Value = default;
             }
             }
 
 
             return _Morphings;
             return _Morphings;
         }
         }
 
 
-        public Animations.CurveBuilder<Transforms.SparseWeight8> UseMorphing(string animationTrack)
+        public Animations.CurveBuilder<ArraySegment<float>> UseMorphing(string animationTrack)
         {
         {
-            return UseMorphing().UseTrackBuilder(animationTrack);
+            var m = UseMorphing();
+
+            if (m.Value.Count == 0) throw new InvalidOperationException("A default sequence of weights must be set before setting animated weights. Use UseMorphing().SetValue(...)");
+
+            return m.UseTrackBuilder(animationTrack);
         }
         }
 
 
         public abstract Matrix4x4 GetPoseWorldMatrix();
         public abstract Matrix4x4 GetPoseWorldMatrix();

+ 30 - 0
src/SharpGLTF.Toolkit/Schema2/AnimationExtensions.cs

@@ -78,6 +78,36 @@ namespace SharpGLTF.Schema2
             return node;
             return node;
         }
         }
 
 
+        public static Node WithMorphingAnimation<T>(this Node node, string animationName, Animations.ICurveSampler<T> sampler)
+            where T : IReadOnlyList<float>
+        {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNull(node.MorphWeights, nameof(node.MorphWeights), "Set node.MorphWeights before setting morphing animation");
+            Guard.MustBeGreaterThanOrEqualTo(node.MorphWeights.Count, 0, nameof(node.MorphWeights));
+
+            if (sampler is Animations.IConvertibleCurve<float[]> arrayCurve)
+            {
+                var animation = node.LogicalParent.UseAnimation(animationName);
+
+                var degree = arrayCurve.MaxDegree;
+                if (degree == 0) animation.CreateMorphChannel(node, arrayCurve.ToStepCurve(), node.MorphWeights.Count, false);
+                if (degree == 1) animation.CreateMorphChannel(node, arrayCurve.ToLinearCurve(), node.MorphWeights.Count, true);
+                if (degree == 3) animation.CreateMorphChannel(node, arrayCurve.ToSplineCurve(), node.MorphWeights.Count);
+            }
+
+            if (sampler is Animations.IConvertibleCurve<ArraySegment<float>> segmentCurve)
+            {
+                var animation = node.LogicalParent.UseAnimation(animationName);
+
+                var degree = segmentCurve.MaxDegree;
+                if (degree == 0) animation.CreateMorphChannel(node, segmentCurve.ToStepCurve(), node.MorphWeights.Count, false);
+                if (degree == 1) animation.CreateMorphChannel(node, segmentCurve.ToLinearCurve(), node.MorphWeights.Count, true);
+                if (degree == 3) animation.CreateMorphChannel(node, segmentCurve.ToSplineCurve(), node.MorphWeights.Count);
+            }
+
+            return node;
+        }
+
         public static Node WithRotationAnimation(this Node node, string animationName, Animations.ICurveSampler<Quaternion> sampler)
         public static Node WithRotationAnimation(this Node node, string animationName, Animations.ICurveSampler<Quaternion> sampler)
         {
         {
             Guard.NotNull(node, nameof(node));
             Guard.NotNull(node, nameof(node));

+ 5 - 5
tests/SharpGLTF.Core.Tests/Animations/AnimationSamplingTests.cs

@@ -87,18 +87,18 @@ namespace SharpGLTF
                 .Select(idx => (0.1f * (float)idx, new Vector3(idx, idx, idx)))
                 .Select(idx => (0.1f * (float)idx, new Vector3(idx, idx, idx)))
                 .ToArray();
                 .ToArray();
 
 
-            var defaultSampler = new Animations.Vector3LinearSampler(curve, true);
-            var fastSampler = defaultSampler.ToFastSampler();
+            var slowSampler = Animations.CurveSampler.CreateSampler(curve, true, false);
+            var fastSampler = Animations.CurveSampler.CreateSampler(curve, true, true);
 
 
-            foreach(var k in curve)
+            foreach (var k in curve)
             {
             {
-                Assert.AreEqual(k.Item2, defaultSampler.GetPoint(k.Item1));
+                Assert.AreEqual(k.Item2, slowSampler.GetPoint(k.Item1));
                 Assert.AreEqual(k.Item2, fastSampler.GetPoint(k.Item1));
                 Assert.AreEqual(k.Item2, fastSampler.GetPoint(k.Item1));
             }
             }
 
 
             for(float t=0; t < 100; t+=0.232f)
             for(float t=0; t < 100; t+=0.232f)
             {
             {
-                var dv = defaultSampler.GetPoint(t);
+                var dv = slowSampler.GetPoint(t);
                 var fv = fastSampler.GetPoint(t);
                 var fv = fastSampler.GetPoint(t);
 
 
                 Assert.AreEqual(dv, fv);
                 Assert.AreEqual(dv, fv);

+ 2 - 2
tests/SharpGLTF.Core.Tests/Schema2/LoadAndSave/LoadSampleTests.cs

@@ -357,14 +357,14 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
 
                 if (t < anim.Duration)
                 if (t < anim.Duration)
                 {
                 {
-                    var mw = curves.Morphing
+                    var mw = curves.GetMorphingSampler<float[]>()
                         .CreateCurveSampler()
                         .CreateCurveSampler()
                         .GetPoint(t);            
                         .GetPoint(t);            
                     
                     
                     TestContext.WriteLine($"    Morph Weights: {mw[0]} {mw[1]}");
                     TestContext.WriteLine($"    Morph Weights: {mw[0]} {mw[1]}");
                 }
                 }
 
 
-                var msw = curves.MorphingSparse
+                var msw = curves.GetMorphingSampler<Transforms.SparseWeight8>()
                     .CreateCurveSampler()
                     .CreateCurveSampler()
                     .GetPoint(t);
                     .GetPoint(t);
 
 

+ 113 - 4
tests/SharpGLTF.Toolkit.Tests/Scenes/SceneBuilderTests.cs

@@ -475,12 +475,12 @@ namespace SharpGLTF.Scenes
             morphBuilder.SetVertexDelta(morphBuilder.Positions.ElementAt(2), (Vector3.UnitY, Vector3.Zero));
             morphBuilder.SetVertexDelta(morphBuilder.Positions.ElementAt(2), (Vector3.UnitY, Vector3.Zero));
             morphBuilder.SetVertexDelta(morphBuilder.Positions.ElementAt(3), (Vector3.UnitY, Vector3.Zero));
             morphBuilder.SetVertexDelta(morphBuilder.Positions.ElementAt(3), (Vector3.UnitY, Vector3.Zero));
 
 
-            inst2.Content.UseMorphing().Value = Transforms.SparseWeight8.Create(1);
+            inst2.Content.UseMorphing().SetValue(1);
             
             
             var curve = inst2.Content.UseMorphing().UseTrackBuilder("Default");
             var curve = inst2.Content.UseMorphing().UseTrackBuilder("Default");
-            curve.SetPoint(0, Transforms.SparseWeight8.Create(0));
-            curve.SetPoint(1, Transforms.SparseWeight8.Create(1));
-            curve.SetPoint(2, Transforms.SparseWeight8.Create(0));
+            curve.SetPoint(0, true, 0);
+            curve.SetPoint(1, true, 1);
+            curve.SetPoint(2, true, 0);
 
 
             var gltf = scene.ToGltf2();
             var gltf = scene.ToGltf2();
 
 
@@ -790,5 +790,114 @@ namespace SharpGLTF.Scenes
             scene.AttachToCurrentTest("output.gltf");
             scene.AttachToCurrentTest("output.gltf");
         }
         }
 
 
+
+
+        [Test]
+        public void CreateMorphScene()
+        {
+            // 3D View 7.1908.9012.0 has an issue displaying off-center meshes with animated morph targets.
+
+            TestContext.CurrentContext.AttachShowDirLink();
+            TestContext.CurrentContext.AttachGltfValidatorLinks();
+
+            var meshMorphs = CreateMeshWith16MorphTargets();
+
+            var scene = new SceneBuilder();
+            var node = new NodeBuilder();
+
+            // var inst = scene.AddRigidMesh(mesh1, Matrix4x4.Identity);
+            // inst.Content.UseMorphing().SetValue(1);
+
+            var inst = scene.AddRigidMesh(meshMorphs, node);
+            inst.Content.UseMorphing().SetValue(1.5f, 1.25f, 1, 1, 1, 1, 0.5f, 0.25f, 0.5f, 1, 1, 0, 0.25f, 0.5f, 0.75f, 1.5f);
+
+            scene.AttachToCurrentTest("morph.glb");
+            scene.AttachToCurrentTest("morph.gltf");
+            scene.ToGltf2().DefaultScene.ToSceneBuilder().AttachToCurrentTest("morph-roundtrip.glb");
+
+            var morphAnim = inst.Content.UseMorphing("Default");
+
+            var wwww = new float[16];
+
+            for(int i=0; i < 16; ++i)
+            {
+                Array.Clear(wwww, 0, wwww.Length);
+                wwww[i] = 1;
+                morphAnim.SetPoint(i, true, wwww);
+            }
+
+            var rnd = new Random(154);
+
+            for (int i = 16; i < 24; ++i)
+            {
+                Array.Clear(wwww, 0, wwww.Length);
+                for(int j=0; j < wwww.Length; ++j) wwww[j] = (float)rnd.NextDouble();
+                morphAnim.SetPoint(i, true, wwww);
+            }
+
+            scene.AttachToCurrentTest("morph-anim.glb");
+            scene.AttachToCurrentTest("morph-anim.gltf");
+            scene.ToGltf2().DefaultScene.ToSceneBuilder().AttachToCurrentTest("morph-anim-roundtrip.glb");
+        }        
+
+        static MeshBuilder<VertexPositionNormal, VertexEmpty, VertexEmpty> CreateMeshWith16MorphTargets()
+        {
+            // create two materials
+
+            var pink = new MaterialBuilder("material1")
+                .WithChannelParam(KnownChannel.BaseColor, new Vector4(1, 0, 1, 1));
+
+            var blue = new MaterialBuilder("material2")
+                .WithChannelParam(KnownChannel.BaseColor, new Vector4(0, 0, 1, 1));
+
+            var mesh1 = VPOSNRM.CreateCompatibleMesh("shape1");
+            var prim1 = mesh1.UsePrimitive(pink);
+            var prim2 = mesh1.UsePrimitive(blue);
+
+            // create a mesh made of a strip of triangle pairs (quads), with 256 segments
+
+            for (int i = 0; i < 256; ++i)
+            {
+                var a = new VertexPositionNormal(i + 0, 0, +10, 0, 1, 0);
+                var b = new VertexPositionNormal(i + 1, 0, +10, 0, 1, 0);
+                var c = new VertexPositionNormal(i + 1, 0, -10, 0, 1, 0);
+                var d = new VertexPositionNormal(i + 0, 0, -10, 0, 1, 0);
+
+                prim1.AddQuadrangle(a, b, c, d);
+                prim2.AddQuadrangle(d, c, b, a);
+            }
+
+            // create a 16 morph targets
+
+            for (int i = 0; i < 16; ++i)
+            {
+                var morphTarget = mesh1.UseMorphTarget(i);
+
+                var idx = i * 16 + 8;
+
+                const float waveWidth = 5;
+
+                foreach (var baseVertex in morphTarget.Vertices)
+                {
+                    var morphedVertex = baseVertex;
+
+                    var distance = Math.Abs(baseVertex.Position.X - idx);
+                    if (distance > waveWidth) continue;
+
+                    distance *= (float)Math.PI / (waveWidth * 2);
+                    distance = 30 * (float)Math.Cos(distance);
+
+                    morphedVertex.Position += new Vector3(0, distance, 0);
+
+                    // this method sets a new modified vertex associated to the base vertex.
+                    // notice that this method works with absolute values, deltas are calculated internally.
+                    // alternatively, you can also set deltas with SetVertexDelta method.
+                    morphTarget.SetVertex(baseVertex, morphedVertex);
+                }                
+            }
+
+            return mesh1;
+        }
+
     }
     }
 }
 }