Browse Source

Downgraded System.Text.Json to previous version 4.7.2 which is preferred by NetCore3.1
Refactored some underlaying curve sampling classes (Breaking change) - but not many people, if any, will be using these directly.
Added more Guards and docs.
fixed #80.

Vicente Penades 5 years ago
parent
commit
c45a2d0a61

+ 34 - 12
src/SharpGLTF.Core/Animations/SamplerFactory.cs → src/SharpGLTF.Core/Animations/CurveSampler.cs

@@ -8,8 +8,27 @@ namespace SharpGLTF.Animations
     /// <summary>
     /// Utility class to create samplers from curve collections.
     /// </summary>
-    public static class SamplerFactory
+    public static class CurveSampler
     {
+        #region constants
+
+        internal const string StepCurveError = "This is a step curve (MaxDegree = 0), use ToStepCurve(); instead.";
+        internal const string LinearCurveError = "This is a linear curve (MaxDegree = 1), use ToLinearCurve(); instead.";
+        internal const string SplineCurveError = "This is a spline curve (MaxDegree = 3), use ToSplineCurve(); instead.";
+
+        internal static string CurveError(int maxDegree)
+        {
+            switch (maxDegree)
+            {
+                case 0: return StepCurveError;
+                case 1: return LinearCurveError;
+                case 3: return SplineCurveError;
+                default: return "Invalid curve degree";
+            }
+        }
+
+        #endregion
+
         #region sampler utils
 
         public static Vector3 CreateTangent(Vector3 fromValue, Vector3 toValue, Single scale = 1)
@@ -125,7 +144,7 @@ namespace SharpGLTF.Animations
         /// <param name="sequence">A sequence of float+<typeparamref name="T"/> pairs sorted in ascending order.</param>
         /// <param name="offset">the offset to look for in the sequence.</param>
         /// <returns>Two consecutive <typeparamref name="T"/> values and a float amount to LERP amount.</returns>
-        public static (T A, T B, Single Amount) FindPairContainingOffset<T>(this IEnumerable<(float Key, T Value)> sequence, float offset)
+        public static (T A, T B, Single Amount) FindRangeContainingOffset<T>(this IEnumerable<(float Key, T Value)> sequence, float offset)
         {
             Guard.NotNull(sequence, nameof(sequence));
 
@@ -135,12 +154,12 @@ namespace SharpGLTF.Animations
             (float Key, T Value)? right = null;
             (float Key, T Value)? prev = null;
 
-            var first = sequence.First();
-            if (offset < first.Key) offset = first.Key;
+            var (firstKey, _) = sequence.First();
+            if (offset < firstKey) offset = firstKey;
 
             foreach (var item in sequence)
             {
-                System.Diagnostics.Debug.Assert(condition: !prev.HasValue || prev.Value.Key < item.Key, "Values in the sequence must be sorted ascending.");
+                System.Diagnostics.Debug.Assert(!prev.HasValue || prev.Value.Key < item.Key, "Values in the sequence must be sorted ascending.");
 
                 if (item.Key == offset)
                 {
@@ -179,7 +198,7 @@ namespace SharpGLTF.Animations
         /// <param name="sequence">A sequence of offsets sorted in ascending order.</param>
         /// <param name="offset">the offset to look for in the sequence.</param>
         /// <returns>Two consecutive offsets and a LERP amount.</returns>
-        public static (Single A, Single B, Single Amount) FindPairContainingOffset(IEnumerable<float> sequence, float offset)
+        public static (Single A, Single B, Single Amount) FindRangeContainingOffset(IEnumerable<float> sequence, float offset)
         {
             Guard.NotNull(sequence, nameof(sequence));
 
@@ -226,6 +245,15 @@ namespace SharpGLTF.Animations
             return (left.Value, right.Value, amount);
         }
 
+        /// <summary>
+        /// Splits the input sequence into chunks of 1 second for faster access
+        /// </summary>
+        /// <remarks>
+        ///  The first and last keys outside the range of each chunk are duplicated, so each chunk can be evaluated for the whole second.
+        /// </remarks>
+        /// <typeparam name="T">The curve key type.</typeparam>
+        /// <param name="sequence">A timed sequence of curve keys.</param>
+        /// <returns>A sequence of 1 second chunks.</returns>
         internal static IEnumerable<(float, T)[]> SplitByTime<T>(this IEnumerable<(Single Time, T Value)> sequence)
         {
             if (!sequence.Any()) yield break;
@@ -323,12 +351,6 @@ namespace SharpGLTF.Animations
 
         #region sampler creation
 
-        internal static IEnumerable<T> Isolate<T>(this IEnumerable<T> collection, bool isolateMemory)
-        {
-            if (isolateMemory) collection = collection.ToArray();
-            return collection;
-        }
-
         public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, Vector3)> collection, bool isLinear = true, bool optimize = false)
         {
             if (collection == null) return null;

+ 29 - 49
src/SharpGLTF.Core/Animations/CubicSamplers.cs → src/SharpGLTF.Core/Animations/CurveSamplers.Cubic.cs

@@ -32,9 +32,9 @@ namespace SharpGLTF.Animations
 
         public Vector3 GetPoint(float offset)
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
-            return SamplerFactory.InterpolateCubic
+            return CurveSampler.InterpolateCubic
                 (
                 valA.Item2, valA.Item3,   // start, startTangentOut
                 valB.Item2, valB.Item1,   // end, endTangentIn
@@ -42,14 +42,14 @@ namespace SharpGLTF.Animations
                 );
         }
 
-        public IReadOnlyDictionary<float, Vector3> ToStepCurve()
+        IReadOnlyDictionary<float, Vector3> IConvertibleCurve<Vector3>.ToStepCurve()
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
 
-        public IReadOnlyDictionary<float, Vector3> ToLinearCurve()
+        IReadOnlyDictionary<float, Vector3> IConvertibleCurve<Vector3>.ToLinearCurve()
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
 
         public IReadOnlyDictionary<float, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> ToSplineCurve()
@@ -59,12 +59,7 @@ namespace SharpGLTF.Animations
 
         public ICurveSampler<Vector3> ToFastSampler()
         {
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new Vector3CubicSampler(item))
-                .Cast<ICurveSampler<Vector3>>();
-
-            return new FastSampler<Vector3>(split);
+            return FastCurveSampler<Vector3>.CreateFrom(_Sequence, chunk => new Vector3CubicSampler(chunk)) ?? this;
         }
 
         #endregion
@@ -96,9 +91,9 @@ namespace SharpGLTF.Animations
 
         public Quaternion GetPoint(float offset)
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
-            return SamplerFactory.InterpolateCubic
+            return CurveSampler.InterpolateCubic
                 (
                 valA.Item2, valA.Item3,   // start, startTangentOut
                 valB.Item2, valB.Item1,   // end, endTangentIn
@@ -106,14 +101,14 @@ namespace SharpGLTF.Animations
                 );
         }
 
-        public IReadOnlyDictionary<float, Quaternion> ToStepCurve()
+        IReadOnlyDictionary<float, Quaternion> IConvertibleCurve<Quaternion>.ToStepCurve()
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
 
-        public IReadOnlyDictionary<float, Quaternion> ToLinearCurve()
+        IReadOnlyDictionary<float, Quaternion> IConvertibleCurve<Quaternion>.ToLinearCurve()
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
 
         public IReadOnlyDictionary<float, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> ToSplineCurve()
@@ -123,12 +118,7 @@ namespace SharpGLTF.Animations
 
         public ICurveSampler<Quaternion> ToFastSampler()
         {
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new QuaternionCubicSampler(item))
-                .Cast<ICurveSampler<Quaternion>>();
-
-            return new FastSampler<Quaternion>(split);
+            return FastCurveSampler<Quaternion>.CreateFrom(_Sequence, chunk => new QuaternionCubicSampler(chunk)) ?? this;
         }
 
         #endregion
@@ -160,7 +150,7 @@ namespace SharpGLTF.Animations
 
         public Transforms.SparseWeight8 GetPoint(float offset)
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
             return Transforms.SparseWeight8.InterpolateCubic
                 (
@@ -170,14 +160,14 @@ namespace SharpGLTF.Animations
                 );
         }
 
-        public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToStepCurve()
+        IReadOnlyDictionary<float, Transforms.SparseWeight8> IConvertibleCurve<Transforms.SparseWeight8>.ToStepCurve()
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
 
-        public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToLinearCurve()
+        IReadOnlyDictionary<float, Transforms.SparseWeight8> IConvertibleCurve<Transforms.SparseWeight8>.ToLinearCurve()
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
 
         public IReadOnlyDictionary<float, (Transforms.SparseWeight8 TangentIn, Transforms.SparseWeight8 Value, Transforms.SparseWeight8 TangentOut)> ToSplineCurve()
@@ -187,21 +177,16 @@ namespace SharpGLTF.Animations
 
         public ICurveSampler<Transforms.SparseWeight8> ToFastSampler()
         {
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new SparseCubicSampler(item))
-                .Cast<ICurveSampler<Transforms.SparseWeight8>>();
-
-            return new FastSampler<Transforms.SparseWeight8>(split);
+            return FastCurveSampler<Transforms.SparseWeight8>.CreateFrom(_Sequence, chunk => new SparseCubicSampler(chunk)) ?? this;
         }
 
         #endregion
     }
 
     /// <summary>
-    /// Defines a <see cref="float"/>[] curve sampler that can be sampled with CUBIC interpolation.
+    /// Defines a <see cref="Single"/>[] curve sampler that can be sampled with CUBIC interpolation.
     /// </summary>
-    readonly struct ArrayCubicSampler : ICurveSampler<float[]>, IConvertibleCurve<float[]>
+    readonly struct ArrayCubicSampler : ICurveSampler<Single[]>, IConvertibleCurve<Single[]>
     {
         #region lifecycle
 
@@ -224,9 +209,9 @@ namespace SharpGLTF.Animations
 
         public float[] GetPoint(float offset)
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
-            return SamplerFactory.InterpolateCubic
+            return CurveSampler.InterpolateCubic
                 (
                 valA.Item2, valA.Item3,   // start, startTangentOut
                 valB.Item2, valB.Item1,   // end, endTangentIn
@@ -234,14 +219,14 @@ namespace SharpGLTF.Animations
                 );
         }
 
-        public IReadOnlyDictionary<float, float[]> ToStepCurve()
+        IReadOnlyDictionary<float, float[]> IConvertibleCurve<Single[]>.ToStepCurve()
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
 
-        public IReadOnlyDictionary<float, float[]> ToLinearCurve()
+        IReadOnlyDictionary<float, float[]> IConvertibleCurve<Single[]>.ToLinearCurve()
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.SplineCurveError);
         }
 
         public IReadOnlyDictionary<float, (float[] TangentIn, float[] Value, float[] TangentOut)> ToSplineCurve()
@@ -251,12 +236,7 @@ namespace SharpGLTF.Animations
 
         public ICurveSampler<float[]> ToFastSampler()
         {
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new ArrayCubicSampler(item))
-                .Cast<ICurveSampler<float[]>>();
-
-            return new FastSampler<float[]>(split);
+            return FastCurveSampler<float[]>.CreateFrom(_Sequence, chunk => new ArrayCubicSampler(chunk)) ?? this;
         }
 
         #endregion

+ 26 - 46
src/SharpGLTF.Core/Animations/LinearSamplers.cs → src/SharpGLTF.Core/Animations/CurveSamplers.Linear.cs

@@ -43,19 +43,19 @@ namespace SharpGLTF.Animations
 
         public T GetPoint(float offset) { return _Value; }
 
-        public IReadOnlyDictionary<float, T> ToLinearCurve()
+        public IReadOnlyDictionary<float, T> ToStepCurve()
         {
             return new Dictionary<float, T> { [0] = _Value };
         }
 
-        public IReadOnlyDictionary<float, (T TangentIn, T Value, T TangentOut)> ToSplineCurve()
+        public IReadOnlyDictionary<float, T> ToLinearCurve()
         {
-            return new Dictionary<float, (T TangentIn, T Value, T TangentOut)> { [0] = (default, _Value, default) };
+            return new Dictionary<float, T> { [0] = _Value };
         }
 
-        public IReadOnlyDictionary<float, T> ToStepCurve()
+        public IReadOnlyDictionary<float, (T TangentIn, T Value, T TangentOut)> ToSplineCurve()
         {
-            return new Dictionary<float, T> { [0] = _Value };
+            return new Dictionary<float, (T TangentIn, T Value, T TangentOut)> { [0] = (default, _Value, default) };
         }
 
         #endregion
@@ -89,7 +89,7 @@ namespace SharpGLTF.Animations
 
         public Vector3 GetPoint(float offset)
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
             if (!_Linear) return valA;
 
@@ -98,30 +98,25 @@ namespace SharpGLTF.Animations
 
         public IReadOnlyDictionary<float, Vector3> ToStepCurve()
         {
-            Guard.IsFalse(_Linear, nameof(_Linear));
+            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(_Linear));
+            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 NotImplementedException();
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
 
         public ICurveSampler<Vector3> ToFastSampler()
         {
             var linear = _Linear;
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new Vector3LinearSampler(item, linear))
-                .Cast<ICurveSampler<Vector3>>();
-
-            return new FastSampler<Vector3>(split);
+            return FastCurveSampler<Vector3>.CreateFrom(_Sequence, chunk => new Vector3LinearSampler(chunk, linear)) ?? this;
         }
 
         #endregion
@@ -155,7 +150,7 @@ namespace SharpGLTF.Animations
 
         public Quaternion GetPoint(float offset)
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
             if (!_Linear) return valA;
 
@@ -164,30 +159,25 @@ namespace SharpGLTF.Animations
 
         public IReadOnlyDictionary<float, Quaternion> ToStepCurve()
         {
-            Guard.IsFalse(_Linear, nameof(_Linear));
+            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(_Linear));
+            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 NotImplementedException();
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
 
         public ICurveSampler<Quaternion> ToFastSampler()
         {
             var linear = _Linear;
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new QuaternionLinearSampler(item, linear))
-                .Cast<ICurveSampler<Quaternion>>();
-
-            return new FastSampler<Quaternion>(split);
+            return FastCurveSampler<Quaternion>.CreateFrom(_Sequence, chunk => new QuaternionLinearSampler(chunk, linear)) ?? this;
         }
 
         #endregion
@@ -221,7 +211,7 @@ namespace SharpGLTF.Animations
 
         public Transforms.SparseWeight8 GetPoint(float offset)
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
             if (!_Linear) return valA;
 
@@ -232,29 +222,24 @@ namespace SharpGLTF.Animations
 
         public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToStepCurve()
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
 
         public IReadOnlyDictionary<float, Transforms.SparseWeight8> ToLinearCurve()
         {
-            Guard.IsTrue(_Linear, nameof(_Linear));
+            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()
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
 
         public ICurveSampler<Transforms.SparseWeight8> ToFastSampler()
         {
             var linear = _Linear;
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new SparseLinearSampler(item, linear))
-                .Cast<ICurveSampler<Transforms.SparseWeight8>>();
-
-            return new FastSampler<Transforms.SparseWeight8>(split);
+            return FastCurveSampler<Transforms.SparseWeight8>.CreateFrom(_Sequence, chunk => new SparseLinearSampler(chunk, linear)) ?? this;
         }
 
         #endregion
@@ -288,39 +273,34 @@ namespace SharpGLTF.Animations
 
         public float[] GetPoint(float offset)
         {
-            var (valA, valB, amount) = SamplerFactory.FindPairContainingOffset(_Sequence, offset);
+            var (valA, valB, amount) = CurveSampler.FindRangeContainingOffset(_Sequence, offset);
 
             if (!_Linear) return valA;
 
-            return SamplerFactory.InterpolateLinear(valA, valB, amount);
+            return CurveSampler.InterpolateLinear(valA, valB, amount);
         }
 
         public IReadOnlyDictionary<float, float[]> ToStepCurve()
         {
-            Guard.IsFalse(_Linear, nameof(_Linear));
+            Guard.IsFalse(_Linear, nameof(MaxDegree), CurveSampler.StepCurveError);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
         }
 
         public IReadOnlyDictionary<float, float[]> ToLinearCurve()
         {
-            Guard.IsTrue(_Linear, nameof(_Linear));
+            Guard.IsTrue(_Linear, nameof(MaxDegree), CurveSampler.LinearCurveError);
             return _Sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
         }
 
         public IReadOnlyDictionary<float, (float[] TangentIn, float[] Value, float[] TangentOut)> ToSplineCurve()
         {
-            throw new NotImplementedException();
+            throw new NotSupportedException(CurveSampler.CurveError(MaxDegree));
         }
 
         public ICurveSampler<float[]> ToFastSampler()
         {
             var linear = _Linear;
-            var split = _Sequence
-                .SplitByTime()
-                .Select(item => new ArrayLinearSampler(item, linear))
-                .Cast<ICurveSampler<float[]>>();
-
-            return new FastSampler<float[]>(split);
+            return FastCurveSampler<float[]>.CreateFrom(_Sequence, chunk => new ArrayLinearSampler(chunk, linear)) ?? this;
         }
 
         #endregion

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

@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace SharpGLTF.Animations
+{
+    /// <summary>
+    /// Wraps a collection of samplers split over time to speed up key retrieval.
+    /// </summary>
+    /// <typeparam name="T">The value sampled at any offset</typeparam>
+    readonly struct FastCurveSampler<T> : ICurveSampler<T>
+    {
+        /// <summary>
+        /// Creates a new, read only <see cref="ICurveSampler{T}"/> that has been optimized for fast sampling.
+        /// </summary>
+        /// <remarks>
+        /// Sampling a raw curve with a large number of keys can be underperformant. This code splits the keys into 1 second
+        /// chunks that can be accessed at much faster speed.
+        /// </remarks>
+        /// <typeparam name="TKey">The value of a key (may include tangents)</typeparam>
+        /// <param name="sequence">A sequence of Time-Key entries, ordered by Time.</param>
+        /// <param name="chunkFactory">A curve chunk factory function.</param>
+        /// <returns>The new, optimized curve sampler.</returns>
+        public static ICurveSampler<T> CreateFrom<TKey>(IEnumerable<(float, TKey)> sequence, Func<(float, TKey)[], ICurveSampler<T>> chunkFactory)
+        {
+            // not enough keys, or not worth optimizing it.
+            if (!sequence.Skip(3).Any()) return null;
+
+            var split = sequence
+                .SplitByTime()
+                .Select(item => chunkFactory.Invoke(item))
+                .Cast<ICurveSampler<T>>();
+
+            return new FastCurveSampler<T>(split);
+        }
+
+        private FastCurveSampler(IEnumerable<ICurveSampler<T>> samplers)
+        {
+            _Samplers = samplers.ToArray();
+        }
+
+        private readonly ICurveSampler<T>[] _Samplers;
+
+        public T GetPoint(float offset)
+        {
+            if (offset < 0) offset = 0;
+
+            var index = (int)offset;
+
+            if (index >= _Samplers.Length) index = _Samplers.Length - 1;
+
+            return _Samplers[index].GetPoint(offset);
+        }
+    }
+}

+ 0 - 32
src/SharpGLTF.Core/Animations/FastSampler.cs

@@ -1,32 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace SharpGLTF.Animations
-{
-    /// <summary>
-    /// Wraps a collection of samplers split over time to speed up key retrieval.
-    /// </summary>
-    /// <typeparam name="T">The value sampled at any offset</typeparam>
-    readonly struct FastSampler<T> : ICurveSampler<T>
-    {
-        public FastSampler(IEnumerable<ICurveSampler<T>> samplers)
-        {
-            _Samplers = samplers.ToArray();
-        }
-
-        private readonly ICurveSampler<T>[] _Samplers;
-
-        public T GetPoint(float offset)
-        {
-            if (offset < 0) offset = 0;
-
-            var index = (int)offset;
-
-            if (index >= _Samplers.Length) index = _Samplers.Length - 1;
-
-            return _Samplers[index].GetPoint(offset);
-        }
-    }
-}

+ 20 - 1
src/SharpGLTF.Core/Animations/Interfaces.cs

@@ -5,11 +5,16 @@ using System.Text;
 namespace SharpGLTF.Animations
 {
     /// <summary>
-    /// Defines a curve that can be sampled at specific points.
+    /// Defines a curve that can be sampled at any point.
     /// </summary>
     /// <typeparam name="T">The type of a point in the curve.</typeparam>
     public interface ICurveSampler<T>
     {
+        /// <summary>
+        /// Samples the curve at the given offset.
+        /// </summary>
+        /// <param name="offset">The curve offset to sample.</param>
+        /// <returns>The value of the curve at <paramref name="offset"/>.</returns>
         T GetPoint(Single offset);
     }
 
@@ -27,8 +32,22 @@ namespace SharpGLTF.Animations
         /// </summary>
         int MaxDegree { get; }
 
+        /// <summary>
+        /// Gets a STEP interpolated curve. Use only when <see cref="MaxDegree"/> is 0.
+        /// </summary>
+        /// <returns>A Time-Value dictionary</returns>
         IReadOnlyDictionary<Single, T> ToStepCurve();
+
+        /// <summary>
+        /// Gets a LINEAR interpolated curve. Use only when <see cref="MaxDegree"/> is 1.
+        /// </summary>
+        /// <returns>A Time-Value dictionary</returns>
         IReadOnlyDictionary<Single, T> ToLinearCurve();
+
+        /// <summary>
+        /// Gets a CUBIC interpolated curve. Use only when <see cref="MaxDegree"/> is 3.
+        /// </summary>
+        /// <returns>A Time-Value dictionary</returns>
         IReadOnlyDictionary<Single, (T TangentIn, T Value, T TangentOut)> ToSplineCurve();
     }
 }

+ 8 - 8
src/SharpGLTF.Core/Memory/MemoryImage.cs

@@ -37,14 +37,14 @@ namespace SharpGLTF.Memory
         internal static Byte[] DefaultPngImage => Convert.FromBase64String(DEFAULT_PNG_IMAGE);
 
         internal static readonly string[] _EmbeddedHeaders =
-                { EMBEDDED_OCTET_STREAM
-                , EMBEDDED_GLTF_BUFFER
-                , EMBEDDED_JPEG_BUFFER
-                , EMBEDDED_PNG_BUFFER
-                , EMBEDDED_DDS_BUFFER
-                , EMBEDDED_WEBP_BUFFER
-                , EMBEDDED_KTX2_BUFFER
-                };
+            { EMBEDDED_OCTET_STREAM
+            , EMBEDDED_GLTF_BUFFER
+            , EMBEDDED_JPEG_BUFFER
+            , EMBEDDED_PNG_BUFFER
+            , EMBEDDED_DDS_BUFFER
+            , EMBEDDED_WEBP_BUFFER
+            , EMBEDDED_KTX2_BUFFER
+            };
 
         public static MemoryImage Empty => default;
 

+ 48 - 1
src/SharpGLTF.Core/Schema2/gltf.AnimationSampler.cs

@@ -93,7 +93,14 @@ namespace SharpGLTF.Schema2
 
         public Accessor Output => this.LogicalParent.LogicalParent.LogicalAccessors[this._output];
 
-        public float Duration { get { var keys = Input.AsScalarArray(); return keys.Count == 0 ? 0 : keys[keys.Count - 1]; } }
+        public float Duration
+        {
+            get
+            {
+                var keys = Input.AsScalarArray();
+                return keys.Count == 0 ? 0 : keys[keys.Count - 1];
+            }
+        }
 
         #endregion
 
@@ -101,6 +108,9 @@ namespace SharpGLTF.Schema2
 
         private Accessor _CreateInputAccessor(IReadOnlyList<Single> input)
         {
+            Guard.NotNull(input, nameof(input));
+            Guard.MustBeGreaterThan(input.Count, 0, nameof(input.Count));
+
             var root = LogicalParent.LogicalParent;
 
             var buffer = root.CreateBufferView(input.Count * 4);
@@ -117,6 +127,9 @@ namespace SharpGLTF.Schema2
 
         private Accessor _CreateOutputAccessor(IReadOnlyList<Vector3> output)
         {
+            Guard.NotNull(output, nameof(output));
+            Guard.MustBeGreaterThan(output.Count, 0, nameof(output.Count));
+
             var root = LogicalParent.LogicalParent;
 
             var buffer = root.CreateBufferView(output.Count * 4 * 3);
@@ -136,6 +149,9 @@ namespace SharpGLTF.Schema2
 
         private Accessor _CreateOutputAccessor(IReadOnlyList<Quaternion> output)
         {
+            Guard.NotNull(output, nameof(output));
+            Guard.MustBeGreaterThan(output.Count, 0, nameof(output.Count));
+
             var root = LogicalParent.LogicalParent;
 
             var buffer = root.CreateBufferView(output.Count * 4 * 4);
@@ -152,6 +168,10 @@ namespace SharpGLTF.Schema2
 
         private Accessor _CreateOutputAccessor(IReadOnlyList<SparseWeight8> output, int expandedCount)
         {
+            Guard.NotNull(output, nameof(output));
+            Guard.MustBeGreaterThan(output.Count, 0, nameof(output.Count));
+            Guard.MustBeGreaterThan(expandedCount, 0, nameof(expandedCount));
+
             var root = LogicalParent.LogicalParent;
 
             var buffer = root.CreateBufferView(output.Count * 4 * expandedCount);
@@ -178,6 +198,8 @@ namespace SharpGLTF.Schema2
 
         private static (Single[] Keys, TValue[] Values) _Split<TValue>(IReadOnlyDictionary<Single, TValue> keyframes)
         {
+            Guard.NotNull(keyframes, nameof(keyframes));
+
             var sorted = keyframes
                 .OrderBy(item => item.Key)
                 .ToList();
@@ -196,6 +218,8 @@ namespace SharpGLTF.Schema2
 
         private static (Single[] Keys, TValue[] Values) _Split<TValue>(IReadOnlyDictionary<Single, (TValue TangentIn, TValue Value, TValue TangentOut)> keyframes)
         {
+            Guard.NotNull(keyframes, nameof(keyframes));
+
             var sorted = keyframes
                 .OrderBy(item => item.Key)
                 .ToList();
@@ -216,6 +240,8 @@ namespace SharpGLTF.Schema2
 
         internal void SetKeys(IReadOnlyDictionary<Single, Vector3> keyframes)
         {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var (keys, values) = _Split(keyframes);
             _input = this._CreateInputAccessor(keys).LogicalIndex;
             _output = this._CreateOutputAccessor(values).LogicalIndex;
@@ -223,6 +249,8 @@ namespace SharpGLTF.Schema2
 
         internal void SetKeys(IReadOnlyDictionary<Single, Quaternion> keyframes)
         {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var (keys, values) = _Split(keyframes);
             _input = this._CreateInputAccessor(keys).LogicalIndex;
             _output = this._CreateOutputAccessor(values).LogicalIndex;
@@ -230,6 +258,8 @@ namespace SharpGLTF.Schema2
 
         internal void SetKeys(IReadOnlyDictionary<Single, SparseWeight8> keyframes, int expandedCount)
         {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var (keys, values) = _Split(keyframes);
             _input = this._CreateInputAccessor(keys).LogicalIndex;
             _output = this._CreateOutputAccessor(values, expandedCount).LogicalIndex;
@@ -237,8 +267,14 @@ namespace SharpGLTF.Schema2
 
         internal void SetKeys(IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         {
+            Guard.NotNull(keyframes, nameof(keyframes));
+            Guard.MustBeGreaterThan(keyframes.Count, 0, nameof(keyframes.Count));
+
+            // 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
             values[0] = Vector3.Zero;
             values[values.Length - 1] = Vector3.Zero;
@@ -249,8 +285,13 @@ namespace SharpGLTF.Schema2
 
         internal void SetKeys(IReadOnlyDictionary<Single, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> keyframes)
         {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
+            // 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
             values[0] = default;
             values[values.Length - 1] = default;
@@ -261,8 +302,14 @@ namespace SharpGLTF.Schema2
 
         internal void SetKeys(IReadOnlyDictionary<Single, (SparseWeight8 TangentIn, SparseWeight8 Value, SparseWeight8 TangentOut)> keyframes, int expandedCount)
         {
+            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
             values[0] = default;
             values[values.Length - 1] = default;

+ 24 - 0
src/SharpGLTF.Core/Schema2/gltf.Animations.cs

@@ -69,6 +69,9 @@ namespace SharpGLTF.Schema2
 
         public void CreateScaleChannel(Node node, IReadOnlyDictionary<Single, Vector3> keyframes, bool linear = true)
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
 
             sampler.SetKeys(keyframes);
@@ -79,6 +82,9 @@ namespace SharpGLTF.Schema2
 
         public void CreateScaleChannel(Node node, IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
             sampler.SetKeys(keyframes);
@@ -89,6 +95,9 @@ namespace SharpGLTF.Schema2
 
         public void CreateRotationChannel(Node node, IReadOnlyDictionary<Single, Quaternion> keyframes, bool linear = true)
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
 
             sampler.SetKeys(keyframes);
@@ -99,6 +108,9 @@ namespace SharpGLTF.Schema2
 
         public void CreateRotationChannel(Node node, IReadOnlyDictionary<Single, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> keyframes)
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
             sampler.SetKeys(keyframes);
@@ -109,6 +121,9 @@ namespace SharpGLTF.Schema2
 
         public void CreateTranslationChannel(Node node, IReadOnlyDictionary<Single, Vector3> keyframes, bool linear = true)
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
 
             sampler.SetKeys(keyframes);
@@ -119,6 +134,9 @@ namespace SharpGLTF.Schema2
 
         public void CreateTranslationChannel(Node node, IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
             sampler.SetKeys(keyframes);
@@ -129,6 +147,9 @@ namespace SharpGLTF.Schema2
 
         public void CreateMorphChannel(Node node, IReadOnlyDictionary<Single, SparseWeight8> keyframes, int morphCount, bool linear = true)
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
 
             sampler.SetKeys(keyframes, morphCount);
@@ -139,6 +160,9 @@ namespace SharpGLTF.Schema2
 
         public void CreateMorphChannel(Node node, IReadOnlyDictionary<Single, (SparseWeight8 TangentIn, SparseWeight8 Value, SparseWeight8 TangentOut)> keyframes, int morphCount)
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
 
             sampler.SetKeys(keyframes, morphCount);

+ 2 - 1
src/SharpGLTF.Core/SharpGLTF.Core.csproj

@@ -24,7 +24,8 @@
   </ItemGroup>  
   
   <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
-    <PackageReference Include="System.Text.Json" Version="5.0.0" />
+    <!-- TODO: Only target 5.0.0 when we add Net5 -->
+    <PackageReference Include="System.Text.Json" Version="4.7.2" />
   </ItemGroup>
 
   <ItemGroup>

+ 1 - 1
src/SharpGLTF.Core/Transforms/SparseWeight8.cs

@@ -428,7 +428,7 @@ namespace SharpGLTF.Transforms
         /// <returns>A new <see cref="SparseWeight8"/></returns>
         public static SparseWeight8 InterpolateCubic(in SparseWeight8 x, in SparseWeight8 xt, in SparseWeight8 y, in SparseWeight8 yt, float amount)
         {
-            var basis = Animations.SamplerFactory.CreateHermitePointWeights(amount);
+            var basis = Animations.CurveSampler.CreateHermitePointWeights(amount);
 
             return _OperateCubic(x, xt, y, yt, (xx, xxt, yy, yyt) => (xx * basis.StartPosition) + (yy * basis.EndPosition) + (xxt * basis.StartTangent) + (yyt * basis.EndTangent));
         }

+ 10 - 9
src/SharpGLTF.Toolkit/Animations/AnimatableProperty.cs

@@ -92,17 +92,15 @@ namespace SharpGLTF.Animations
         /// Assigns an animation curve to a given track.
         /// </summary>
         /// <param name="track">The name of the track.</param>
-        /// <param name="curve">A <see cref="ICurveSampler{T}"/> instance, or null to remove a track.</param>
+        /// <param name="curve">
+        /// A <see cref="ICurveSampler{T}"/> instance which also
+        /// implements <see cref="IConvertibleCurve{T}"/>,
+        /// or null to remove a track.
+        /// </param>
         public void SetTrack(string track, ICurveSampler<T> curve)
         {
             Guard.NotNullOrEmpty(track, nameof(track));
 
-            if (curve != null)
-            {
-                var convertible = curve as IConvertibleCurve<T>;
-                Guard.NotNull(convertible, nameof(curve), $"Provided {nameof(ICurveSampler<T>)} {nameof(curve)} must implement {nameof(IConvertibleCurve<T>)} interface.");
-            }
-
             // remove track
             if (curve == null)
             {
@@ -112,6 +110,9 @@ namespace SharpGLTF.Animations
                 return;
             }
 
+            curve.GetPoint(0); // make a single evaluation to ensure it's an evaluable curve.
+            Guard.IsTrue(curve is IConvertibleCurve<T>, nameof(curve), $"Provided {nameof(ICurveSampler<T>)} {nameof(curve)} must implement {nameof(IConvertibleCurve<T>)} interface.");
+
             // insert track
             if (_Tracks == null) _Tracks = new Dictionary<string, ICurveSampler<T>>();
 
@@ -124,13 +125,13 @@ namespace SharpGLTF.Animations
 
             if (_Tracks == null || !_Tracks.TryGetValue(track, out ICurveSampler<T> sampler))
             {
-                sampler = CurveFactory.CreateCurveBuilder<T>() as ICurveSampler<T>;
+                sampler = CurveFactory.CreateCurveBuilder<T>();
                 SetTrack(track, sampler);
             }
 
             if (sampler is CurveBuilder<T> builder) return builder;
 
-            throw new NotImplementedException();
+            throw new NotImplementedException($"Underlaying curve must be of type CurveBuilder<{nameof(T)}>");
 
             // TODO: CurveFactory.CreateCurveBuilder(sampler);
         }

+ 12 - 3
src/SharpGLTF.Toolkit/Animations/CurveBuilder.cs

@@ -102,7 +102,7 @@ namespace SharpGLTF.Animations
 
             offset -= float.Epsilon;
 
-            var (keyA, keyB, _) = SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset);
+            var (keyA, keyB, _) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
 
             var a = _Keys[keyA];
             var b = _Keys[keyB];
@@ -126,7 +126,7 @@ namespace SharpGLTF.Animations
         {
             Guard.IsTrue(_Keys.ContainsKey(offset), nameof(offset));
 
-            var (keyA, keyB, _) = SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset);
+            var (keyA, keyB, _) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
 
             var a = _Keys[keyA];
             var b = _Keys[keyB];
@@ -148,7 +148,7 @@ namespace SharpGLTF.Animations
         {
             if (_Keys.Count == 0) return (default(_CurveNode<T>), default(_CurveNode<T>), 0);
 
-            var (keyA, keyB, amount) = SamplerFactory.FindPairContainingOffset(_Keys.Keys, offset);
+            var (keyA, keyB, amount) = CurveSampler.FindRangeContainingOffset(_Keys.Keys, offset);
 
             return (_Keys[keyA], _Keys[keyB], amount);
         }
@@ -240,6 +240,15 @@ namespace SharpGLTF.Animations
         {
             var d = new Dictionary<float, T>();
 
+            if (_Keys.Count == 0) return d;
+
+            if (Keys.Count == 1)
+            {
+                var k = _Keys.First();
+                d[k.Key] = k.Value.Point;
+                return d;
+            }
+
             var orderedKeys = _Keys.Keys.ToList();
 
             for (int i = 0; i < orderedKeys.Count - 1; ++i)

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

@@ -54,7 +54,7 @@ namespace SharpGLTF.Animations
 
         protected override Vector3 GetTangent(Vector3 fromValue, Vector3 toValue)
         {
-            return SamplerFactory.CreateTangent(fromValue, toValue);
+            return CurveSampler.CreateTangent(fromValue, toValue);
         }
 
         public override Vector3 GetPoint(Single offset)
@@ -70,7 +70,7 @@ namespace SharpGLTF.Animations
                     return Vector3.Lerp(sample.A.Point, sample.B.Point, sample.Amount);
 
                 case 3:
-                    return SamplerFactory.InterpolateCubic
+                    return CurveSampler.InterpolateCubic
                             (
                             sample.A.Point, sample.A.OutgoingTangent,
                             sample.B.Point, sample.B.IncomingTangent,
@@ -110,7 +110,7 @@ namespace SharpGLTF.Animations
 
         protected override Quaternion GetTangent(Quaternion fromValue, Quaternion toValue)
         {
-            return SamplerFactory.CreateTangent(fromValue, toValue);
+            return CurveSampler.CreateTangent(fromValue, toValue);
         }
 
         public override Quaternion GetPoint(float offset)
@@ -126,7 +126,7 @@ namespace SharpGLTF.Animations
                     return Quaternion.Slerp(sample.A.Point, sample.B.Point, sample.Amount);
 
                 case 3:
-                    return SamplerFactory.InterpolateCubic
+                    return CurveSampler.InterpolateCubic
                             (
                             sample.A.Point, sample.A.OutgoingTangent,
                             sample.B.Point, sample.B.IncomingTangent,

+ 1 - 1
src/SharpGLTF.Toolkit/Debug/DebugViews.cs

@@ -101,7 +101,7 @@ namespace SharpGLTF.Debug
 
         protected override Quaternion GetTangent(Quaternion a, Quaternion b)
         {
-            return Animations.SamplerFactory.CreateTangent(a, b);
+            return Animations.CurveSampler.CreateTangent(a, b);
         }
     }
 

+ 14 - 0
src/SharpGLTF.Toolkit/Scenes/Content.cs

@@ -158,4 +158,18 @@ namespace SharpGLTF.Scenes
 
         #endregion
     }
+
+    [System.Diagnostics.DebuggerDisplay("Custom")]
+    partial class EmptyContent : ICloneable
+    {
+        #region lifecycle
+
+        public EmptyContent() { }
+
+        public Object Clone() { return new EmptyContent(this); }
+
+        private EmptyContent(EmptyContent other) {  }
+
+        #endregion
+    }
 }

+ 6 - 0
src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs

@@ -286,6 +286,12 @@ namespace SharpGLTF.Scenes
         /// </summary>
         /// <param name="collection">A collection of <see cref="NodeBuilder"/> elements.</param>
         /// <param name="namePrefix">The name prefix.</param>
+        /// <remarks>
+        /// This was originally intended to help in solving the problem that many engines don't
+        /// support two nodes to have the same name. But ultimately, it's these engine's responsability
+        /// to deal with glTF specifications.
+        /// </remarks>
+        [Obsolete("It does not belong here.")]
         public static void Rename(IEnumerable<NodeBuilder> collection, string namePrefix)
         {
             if (collection == null) return;

+ 12 - 0
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.cs

@@ -227,6 +227,18 @@ namespace SharpGLTF.Scenes
             return instance;
         }
 
+        public InstanceBuilder AddNode(NodeBuilder node)
+        {
+            Guard.NotNull(node, nameof(node));
+
+            var content = new EmptyContent();
+            var instance = new InstanceBuilder(this);
+            _Instances.Add(instance);
+            instance.Content = new RigidTransformer(content, node);
+            return instance;
+        }
+
+        [Obsolete("It does not belong here.")]
         public void RenameAllNodes(string namePrefix)
         {
             var allNodes = Instances

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

@@ -30,6 +30,7 @@ namespace SharpGLTF.Schema2
                 if (degree == 1) animation.CreateScaleChannel(node, curve.ToLinearCurve(), true);
                 if (degree == 3) animation.CreateScaleChannel(node, curve.ToSplineCurve());
             }
+            else throw new ArgumentException("Must implement IConvertibleCurve<Vector3>", nameof(sampler));
 
             return node;
         }
@@ -47,6 +48,7 @@ namespace SharpGLTF.Schema2
                 if (degree == 1) animation.CreateTranslationChannel(node, curve.ToLinearCurve(), true);
                 if (degree == 3) animation.CreateTranslationChannel(node, curve.ToSplineCurve());
             }
+            else throw new ArgumentException("Must implement IConvertibleCurve<Vector3>", nameof(sampler));
 
             return node;
         }
@@ -83,12 +85,16 @@ namespace SharpGLTF.Schema2
                 if (degree == 1) animation.CreateRotationChannel(node, curve.ToLinearCurve(), true);
                 if (degree == 3) animation.CreateRotationChannel(node, curve.ToSplineCurve());
             }
+            else throw new ArgumentException("Must implement IConvertibleCurve<Quaternion>", nameof(sampler));
 
             return node;
         }
 
         public static Node WithScaleAnimation(this Node node, string animationName, params (Single Key, Vector3 Value)[] keyframes)
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var keys = keyframes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
 
             return node.WithScaleAnimation(animationName, keys);
@@ -96,6 +102,9 @@ namespace SharpGLTF.Schema2
 
         public static Node WithRotationAnimation(this Node node, string animationName, params (Single Key, Quaternion Value)[] keyframes)
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var keys = keyframes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
 
             return node.WithRotationAnimation(animationName, keys);
@@ -103,6 +112,9 @@ namespace SharpGLTF.Schema2
 
         public static Node WithTranslationAnimation(this Node node, string animationName, params (Single Key, Vector3 Value)[] keyframes)
         {
+            Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
             var keys = keyframes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
 
             return node.WithTranslationAnimation(animationName, keys);
@@ -111,6 +123,7 @@ namespace SharpGLTF.Schema2
         public static Node WithScaleAnimation(this Node node, string animationName, IReadOnlyDictionary<Single, Vector3> keyframes)
         {
             Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
             var root = node.LogicalParent;
 
@@ -124,6 +137,7 @@ namespace SharpGLTF.Schema2
         public static Node WithRotationAnimation(this Node node, string animationName, IReadOnlyDictionary<Single, Quaternion> keyframes)
         {
             Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
             var root = node.LogicalParent;
 
@@ -137,6 +151,7 @@ namespace SharpGLTF.Schema2
         public static Node WithTranslationAnimation(this Node node, string animationName, IReadOnlyDictionary<Single, Vector3> keyframes)
         {
             Guard.NotNull(node, nameof(node));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
             var root = node.LogicalParent;
 

+ 12 - 12
tests/SharpGLTF.Tests/Animations/AnimationSamplingTests.cs

@@ -54,22 +54,22 @@ namespace SharpGLTF
             }
 
 
-            var r0 = Animations.SamplerFactory.SplitByTime(anim0).ToArray();
+            var r0 = Animations.CurveSampler.SplitByTime(anim0).ToArray();
             Assert.AreEqual(1, r0.Length);
             Assert.AreEqual(1, r0[0].Length);
 
-            var r1 = Animations.SamplerFactory.SplitByTime(anim1).ToArray();
+            var r1 = Animations.CurveSampler.SplitByTime(anim1).ToArray();
             Assert.AreEqual(1, r1.Length);
             Assert.AreEqual(2, r1[0].Length);
 
-            var r2 = Animations.SamplerFactory.SplitByTime(anim2).ToArray();
+            var r2 = Animations.CurveSampler.SplitByTime(anim2).ToArray();
             Assert.AreEqual(4, r2.Length);
             Assert.AreEqual(3, r2[0].Length); 
             Assert.AreEqual(2, r2[1].Length); checkSegment(1, r2[1]);
             Assert.AreEqual(2, r2[2].Length); checkSegment(2, r2[2]);
             Assert.AreEqual(3, r2[3].Length); checkSegment(3, r2[3]);
 
-            var r3 = Animations.SamplerFactory.SplitByTime(anim3).ToArray();
+            var r3 = Animations.CurveSampler.SplitByTime(anim3).ToArray();
             Assert.AreEqual(6, r3.Length);
             Assert.AreEqual(1, r3[0].Length); 
             Assert.AreEqual(1, r3[1].Length); 
@@ -118,7 +118,7 @@ namespace SharpGLTF
 
             for (float amount = 0; amount <= 1; amount += 0.01f)
             {
-                var (startPosition, endPosition, startTangent, endTangent) = Animations.SamplerFactory.CreateHermitePointWeights(amount);
+                var (startPosition, endPosition, startTangent, endTangent) = Animations.CurveSampler.CreateHermitePointWeights(amount);
 
                 var p = Vector2.Zero;
 
@@ -134,8 +134,8 @@ namespace SharpGLTF
 
             float k = 0.3f;
 
-            var hb = Animations.SamplerFactory.CreateHermitePointWeights(k);
-            var ht = Animations.SamplerFactory.CreateHermiteTangentWeights(k);
+            var hb = Animations.CurveSampler.CreateHermitePointWeights(k);
+            var ht = Animations.CurveSampler.CreateHermiteTangentWeights(k);
 
             var pp = p1 * hb.StartPosition + p4 * hb.EndPosition + (p2 - p1) * 4 * hb.StartTangent + (p4 - p3) * 4 * hb.EndTangent;
             var pt = p1 * ht.StartPosition + p4 * ht.EndPosition + (p2 - p1) * 4 * ht.StartTangent + (p4 - p3) * 4 * ht.EndTangent;
@@ -160,7 +160,7 @@ namespace SharpGLTF
 
             for (float amount = 0; amount <= 1; amount += 0.1f)
             {
-                var (startPosition, endPosition, startTangent, endTangent) = Animations.SamplerFactory.CreateHermitePointWeights(amount);
+                var (startPosition, endPosition, startTangent, endTangent) = Animations.CurveSampler.CreateHermitePointWeights(amount);
 
                 var p = Vector2.Zero;
 
@@ -195,7 +195,7 @@ namespace SharpGLTF
             var qt = Quaternion.Concatenate(q2, Quaternion.Conjugate(q1));            
             var q2bis = Quaternion.Concatenate(qt, q1); // roundtrip; Q2 == Q2BIS
 
-            NumericsAssert.AreEqual(qt, Animations.SamplerFactory.CreateTangent(q1, q2), 0.000001f);
+            NumericsAssert.AreEqual(qt, Animations.CurveSampler.CreateTangent(q1, q2), 0.000001f);
 
             var angles = new List<Vector2>();
 
@@ -205,7 +205,7 @@ namespace SharpGLTF
                 var sq = Quaternion.Normalize(Quaternion.Slerp(q1, q2, amount));
 
                 // hermite interpolation with a unit tangent
-                var hermite = Animations.SamplerFactory.CreateHermitePointWeights(amount);
+                var hermite = Animations.CurveSampler.CreateHermitePointWeights(amount);
                 var hq = default(Quaternion);
                 hq += q1 * hermite.StartPosition;
                 hq += q2 * hermite.EndPosition;
@@ -248,7 +248,7 @@ namespace SharpGLTF
         [Test]
         public void TestVector3CubicSplineSampling()
         {
-            var sampler = Animations.SamplerFactory.CreateSampler(_TransAnim);
+            var sampler = Animations.CurveSampler.CreateSampler(_TransAnim);
 
             var points = new List<Vector3>();
 
@@ -267,7 +267,7 @@ namespace SharpGLTF
         [Test]
         public void TestQuaternionCubicSplineSampling()
         {
-            var sampler = Animations.SamplerFactory.CreateSampler(_RotAnim);
+            var sampler = Animations.CurveSampler.CreateSampler(_RotAnim);
 
             var a = sampler.GetPoint(0);
             var b = sampler.GetPoint(1);

+ 1 - 1
tests/SharpGLTF.Tests/SharpGLTF.Core.Tests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>netcoreapp3;net471</TargetFrameworks>
+    <TargetFrameworks>netcoreapp3.1;net471</TargetFrameworks>
     <IsPackable>false</IsPackable>
     <RootNamespace>SharpGLTF</RootNamespace>
     <LangVersion>latest</LangVersion>

+ 30 - 0
tests/SharpGLTF.Toolkit.Tests/Animation/CurveBuilderTests.cs

@@ -163,4 +163,34 @@ namespace SharpGLTF.Animations
                 .AttachToCurrentTest("plot.png");
         }
     }
+
+    [Category("Toolkit.Animations")]
+    public class TrackBuilderTests
+    {
+        [Test]
+        public void CreateOneKey()
+        {
+            var node = new Scenes.NodeBuilder("someNode");
+
+            var tb = node.UseTranslation().UseTrackBuilder("track1");
+
+            tb.SetPoint(0, new Vector3(1,2,3));
+
+            var scene = new Scenes.SceneBuilder();
+            scene.AddNode(node);            
+
+            var glTF = scene.ToGltf2();
+
+            var runtime = Runtime.SceneTemplate.Create(glTF.DefaultScene, true);
+            var instance = runtime.CreateInstance();
+
+            var instanceNode = instance.Armature.LogicalNodes.First(n => n.Name == "someNode");
+
+            instanceNode.SetAnimationFrame(0, 7);
+            var nodeMatrix = instanceNode.LocalMatrix;
+
+            Assert.AreEqual(new Vector3(1, 2, 3), nodeMatrix.Translation);
+        }
+
+    }
 }

+ 1 - 1
tests/SharpGLTF.Toolkit.Tests/SharpGLTF.Toolkit.Tests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>netcoreapp3;net471</TargetFrameworks>
+    <TargetFrameworks>netcoreapp3.1;net471</TargetFrameworks>
     <IsPackable>false</IsPackable>
     <RootNamespace>SharpGLTF</RootNamespace>
     <LangVersion>latest</LangVersion>