Browse Source

WIP redesigning curves API

Vicente Penades 6 years ago
parent
commit
8df9b732b0

+ 15 - 5
src/SharpGLTF.Toolkit/Animations/Animatable.cs

@@ -1,16 +1,22 @@
 using System;
+using System.Collections;
 using System.Collections.Generic;
+using System.Linq;
 using System.Numerics;
 using System.Text;
 
 namespace SharpGLTF.Animations
 {
+    /// <summary>
+    /// Represents a value that can be animated.
+    /// </summary>
+    /// <typeparam name="T">The type of the value.</typeparam>
     public class Animatable<T>
         where T : struct
     {
         #region data
 
-        private Dictionary<string, Curve<T>> _Tracks = new Dictionary<string, Curve<T>>();
+        private Dictionary<string, ICurveSampler<T>> _Tracks;
 
         public T Default { get; set; }
 
@@ -18,7 +24,7 @@ namespace SharpGLTF.Animations
 
         #region properties
 
-        public IReadOnlyDictionary<string, Curve<T>> Tracks => _Tracks;
+        public IReadOnlyDictionary<string, ICurveSampler<T>> Tracks => _Tracks == null ? Collections.EmptyDictionary<string, ICurveSampler<T>>.Instance : _Tracks;
 
         #endregion
 
@@ -26,12 +32,16 @@ namespace SharpGLTF.Animations
 
         public T GetValueAt(string track, float value)
         {
-            return _Tracks.TryGetValue(track, out Curve<T> sampler) ? sampler.GetPoint(value) : this.Default;
+            if (_Tracks == null) return this.Default;
+
+            return _Tracks.TryGetValue(track, out ICurveSampler<T> sampler) ? sampler.GetPoint(value) : this.Default;
         }
 
-        public Curve<T> UseCurve(string track)
+        public ICurveSampler<T> UseCurve(string track)
         {
-            if (_Tracks.TryGetValue(track, out Curve<T> curve)) return curve;
+            if (_Tracks == null) _Tracks = new Dictionary<string, ICurveSampler<T>>();
+
+            if (_Tracks.TryGetValue(track, out ICurveSampler<T> curve)) return curve;
 
             _Tracks[track] = curve = CurveFactory.CreateSplineCurve<T>();
 

+ 65 - 28
src/SharpGLTF.Toolkit/Animations/Curves.cs

@@ -6,8 +6,36 @@ using System.Linq;
 
 namespace SharpGLTF.Animations
 {
-    // TODO: define just ONE kind of curve: spline with flags, where a flag might indicate if the current segment is linear or spline.
-    // when converting to gltf, check if all segments are linear, and use the appropiate encoding.
+    /// <summary>
+    /// Defines a curve that can be sampled at specific points.
+    /// </summary>
+    /// <typeparam name="T">The type of a point in the curve.</typeparam>
+    public interface ICurveSampler<T>
+    {
+        T GetPoint(float offset);
+    }
+
+    /// <summary>
+    /// Defines methods that convert the current curve to a Step, Linear or Spline curve.
+    /// </summary>
+    /// <typeparam name="T">The type of a point of the curve</typeparam>
+    public interface IConvertibleCurve<T>
+        where T : struct
+    {
+        /// <summary>
+        /// Gets a value indicating the maximum degree of the curve, values are:
+        /// 0: all elements use STEP interpolation.
+        /// 1: some elements use LINEAR interpolation.
+        /// 2: some elements use CUBIC interpolation
+        /// </summary>
+        int Degree { get; }
+
+        IReadOnlyDictionary<float, T> ToStepCurve();
+        IReadOnlyDictionary<float, T> ToLinearCurve();
+        IReadOnlyDictionary<float, (T, T, T)> ToSplineCurve();
+    }
+
+    
 
     [System.Diagnostics.DebuggerDisplay("[{_Offset}] = {Sample}")]
     public struct CurvePoint<T>
@@ -166,7 +194,7 @@ namespace SharpGLTF.Animations
         }
     }
 
-    struct _SplineNode<T>
+    struct _CurveNode<T>
         where T : struct
     {
         public T IncomingTangent;
@@ -178,15 +206,15 @@ namespace SharpGLTF.Animations
     /// <summary>
     /// Represents a collection of consecutive nodes that can be sampled into a continuous curve.
     /// </summary>
-    /// <typeparam name="Tout">The type of value evaluated at any point in the curve.</typeparam>
-    public abstract class Curve<Tout>
-        where Tout : struct
+    /// <typeparam name="T">The type of value evaluated at any point in the curve.</typeparam>
+    public abstract class Curve<T> : IConvertibleCurve<T>, ICurveSampler<T>
+        where T : struct
     {
         #region lifecycle
 
         public Curve() { }
 
-        protected Curve(Curve<Tout> other)
+        protected Curve(Curve<T> other)
         {
             foreach (var kvp in other._Keys)
             {
@@ -198,7 +226,7 @@ namespace SharpGLTF.Animations
 
         #region data
 
-        internal SortedDictionary<float, _SplineNode<Tout>> _Keys = new SortedDictionary<float, _SplineNode<Tout>>();
+        internal SortedDictionary<float, _CurveNode<T>> _Keys = new SortedDictionary<float, _CurveNode<T>>();
 
         #endregion
 
@@ -206,11 +234,10 @@ namespace SharpGLTF.Animations
 
         public IReadOnlyCollection<float> Keys => _Keys.Keys;
 
-        public bool IsStepInterpolation => _Keys.Values.All(item => item.OutgoingMode == 0);
-
-        public bool IsLinearInterpolation =>  _Keys.Values.Any(item => item.OutgoingMode == 1) && !IsSplineInterpolation;
-
-        public bool IsSplineInterpolation => _Keys.Values.Any(item => item.OutgoingMode == 2);
+        /// <summary>
+        /// Gets a value indicating if the keys of this curve are at least Step, Linear, or Spline.
+        /// </summary>
+        public int Degree => _Keys.Values.Select(item => item.OutgoingMode).Max();
 
         #endregion
 
@@ -218,13 +245,13 @@ namespace SharpGLTF.Animations
 
         public void RemoveKey(float key) { _Keys.Remove(key); }
 
-        internal _SplineNode<Tout>? GetKey(float key) { return _Keys.TryGetValue(key, out _SplineNode<Tout> value) ? value : (_SplineNode<Tout>?)null; }
+        internal _CurveNode<T>? GetKey(float key) { return _Keys.TryGetValue(key, out _CurveNode<T> value) ? value : (_CurveNode<T>?)null; }
 
-        internal void SetKey(float key, _SplineNode<Tout> value) { _Keys[key] = value; }
+        internal void SetKey(float key, _CurveNode<T> value) { _Keys[key] = value; }
 
-        internal (_SplineNode<Tout>, _SplineNode<Tout>, float) FindSample(float offset)
+        internal (_CurveNode<T>, _CurveNode<T>, float) FindSample(float offset)
         {
-            if (_Keys.Count == 0) return (default(_SplineNode<Tout>), default(_SplineNode<Tout>), 0);
+            if (_Keys.Count == 0) return (default(_CurveNode<T>), default(_CurveNode<T>), 0);
 
             var offsets = _FindPairContainingOffset(_Keys.Keys, offset);
 
@@ -285,15 +312,15 @@ namespace SharpGLTF.Animations
             return (left.Value, right.Value, amount);
         }
 
-        public abstract Tout GetPoint(float offset);
+        public abstract T GetPoint(float offset);
 
-        public abstract void SetPoint(float offset, Tout value);
+        public abstract void SetPoint(float offset, T value);
 
-        public abstract Tout GetTangent(float offset);
+        public abstract T GetTangent(float offset);
 
-        public abstract void SetTangentIn(float key, Tout value, float scale);
+        public abstract void SetTangentIn(float key, T value, float scale);
 
-        public abstract void SetTangentOut(float key, Tout value, float scale);
+        public abstract void SetTangentOut(float key, T value, float scale);
 
         public bool SplitAt(float offset)
         {
@@ -320,17 +347,18 @@ namespace SharpGLTF.Animations
             return true;
         }
 
-        public IReadOnlyDictionary<float, Tout> ToStepCurve()
+        public IReadOnlyDictionary<float, T> ToStepCurve()
         {
-            Guard.IsTrue(IsStepInterpolation, nameof(IsStepInterpolation));
+            Guard.IsTrue(Degree == 0, nameof(Degree));
+
+            // todo: if Degree is not zero we might export sampled data at 60FPS
+
             return _Keys.ToDictionary(item => item.Key, item => item.Value.Point);
         }
 
-        public IReadOnlyDictionary<float, Tout> ToLinearCurve()
+        public IReadOnlyDictionary<float, T> ToLinearCurve()
         {
-            Guard.IsTrue(IsStepInterpolation, nameof(IsStepInterpolation));
-
-            var d = new Dictionary<float, Tout>();
+            var d = new Dictionary<float, T>();
 
             if (_Keys.Count == 0) return d;
 
@@ -366,6 +394,15 @@ namespace SharpGLTF.Animations
             return d;
         }
 
+        public IReadOnlyDictionary<float, (T, T, T)> ToSplineCurve()
+        {
+            throw new NotImplementedException();
+
+            var d = new Dictionary<float, (T, T, T)>();
+
+            return d;
+        }
+
         #endregion
     }
 

+ 48 - 0
src/SharpGLTF.Toolkit/Collections/EmptyDictionary.cs

@@ -0,0 +1,48 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace SharpGLTF.Collections
+{
+    /// <summary>
+    /// Represents an empty, read-only dictionary to use as a safe replacement of NULL.
+    /// </summary>
+    /// <typeparam name="TKey">The type of keys in the read-only dictionary.</typeparam>
+    /// <typeparam name="TValue">The type of values in the read-only dictionary.</typeparam>
+    sealed class EmptyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
+    {
+        #region lifecycle
+
+        static EmptyDictionary() { }
+
+        private EmptyDictionary() { }
+
+        private static readonly EmptyDictionary<TKey, TValue> _Instance = new EmptyDictionary<TKey, TValue>();
+
+        public static IReadOnlyDictionary<TKey, TValue> Instance => _Instance;
+
+        #endregion
+
+        #region API
+
+        public TValue this[TKey key] => throw new KeyNotFoundException();
+
+        public IEnumerable<TKey> Keys => Enumerable.Empty<TKey>();
+
+        public IEnumerable<TValue> Values => Enumerable.Empty<TValue>();
+
+        public int Count => 0;
+
+        public bool ContainsKey(TKey key) { return false; }
+
+        public bool TryGetValue(TKey key, out TValue value) { value = default; return false; }
+
+        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() { yield break; }
+
+        IEnumerator IEnumerable.GetEnumerator() { yield break; }
+
+        #endregion
+    }
+}

+ 39 - 18
src/SharpGLTF.Toolkit/Schema2/AnimationExtensions.cs

@@ -15,49 +15,70 @@ namespace SharpGLTF.Schema2
             return animation ?? root.CreateAnimation(name);
         }
 
-        public static Node WithScaleAnimation(this Node node, string animationName, Animations.Curve<Vector3> curve)
+        public static Node WithScaleAnimation(this Node node, string animationName, Animations.ICurveSampler<Vector3> sampler)
         {
-            var animation = node
-                .LogicalParent
-                .UseAnimation(animationName);
+            if (sampler is Animations.IConvertibleCurve<Vector3> curve)
+            {
+                var animation = node.LogicalParent.UseAnimation(animationName);
+
+                var degree = curve.Degree;
+                if (degree == 0) animation.CreateScaleChannel(node, curve.ToStepCurve(), false);
+                if (degree == 1) animation.CreateScaleChannel(node, curve.ToLinearCurve(), true);
+                if (degree == 2) animation.CreateScaleChannel(node, curve.ToSplineCurve());
+            }
 
-            animation.CreateScaleChannel(node, curve.ToLinearCurve());
             return node;
         }
 
-        public static Node WithTranslationAnimation(this Node node, string animationName, Animations.Curve<Vector3> curve)
+        public static Node WithTranslationAnimation(this Node node, string animationName, Animations.ICurveSampler<Vector3> sampler)
         {
-            var animation = node
-                .LogicalParent
-                .UseAnimation(animationName);
+            if (sampler is Animations.IConvertibleCurve<Vector3> curve)
+            {
+                var animation = node.LogicalParent.UseAnimation(animationName);
+
+                var degree = curve.Degree;
+                if (degree == 0) animation.CreateTranslationChannel(node, curve.ToStepCurve(), false);
+                if (degree == 1) animation.CreateTranslationChannel(node, curve.ToLinearCurve(), true);
+                if (degree == 2) animation.CreateTranslationChannel(node, curve.ToSplineCurve());
+            }
 
-            animation.CreateTranslationChannel(node, curve.ToLinearCurve());
             return node;
         }
 
-        public static Node WithRotationAnimation(this Node node, string animationName, Animations.Curve<Quaternion> curve)
+        public static Node WithRotationAnimation(this Node node, string animationName, Animations.ICurveSampler<Quaternion> sampler)
         {
-            var animation = node
-                .LogicalParent
-                .UseAnimation(animationName);
+            if (sampler is Animations.IConvertibleCurve<Quaternion> curve)
+            {
+                var animation = node.LogicalParent.UseAnimation(animationName);
+
+                var degree = curve.Degree;
+                if (degree == 0) animation.CreateRotationChannel(node, curve.ToStepCurve(), false);
+                if (degree == 1) animation.CreateRotationChannel(node, curve.ToLinearCurve(), true);
+                if (degree == 2) animation.CreateRotationChannel(node, curve.ToSplineCurve());
+            }
 
-            animation.CreateRotationChannel(node, curve.ToLinearCurve());
             return node;
         }
 
         public static Node WithScaleAnimation(this Node node, string animationName, params (Single, Vector3)[] keyframes)
         {
-            return node.WithScaleAnimation(animationName, keyframes.ToDictionary(kvp => kvp.Item1, kvp => kvp.Item2));
+            var keys = keyframes.ToDictionary(kvp => kvp.Item1, kvp => kvp.Item2);
+
+            return node.WithScaleAnimation(animationName, keys);
         }
 
         public static Node WithRotationAnimation(this Node node, string animationName, params (Single, Quaternion)[] keyframes)
         {
-            return node.WithRotationAnimation(animationName, keyframes.ToDictionary(kvp => kvp.Item1, kvp => kvp.Item2));
+            var keys = keyframes.ToDictionary(kvp => kvp.Item1, kvp => kvp.Item2);
+
+            return node.WithRotationAnimation(animationName, keys);
         }
 
         public static Node WithTranslationAnimation(this Node node, string animationName, params (Single, Vector3)[] keyframes)
         {
-            return node.WithTranslationAnimation(animationName, keyframes.ToDictionary(kvp => kvp.Item1, kvp => kvp.Item2));
+            var keys = keyframes.ToDictionary(kvp => kvp.Item1, kvp => kvp.Item2);
+
+            return node.WithTranslationAnimation(animationName, keys);
         }
 
         public static Node WithScaleAnimation(this Node node, string animationName, IReadOnlyDictionary<Single, Vector3> keyframes)

+ 1 - 1
tests/SharpGLTF.Tests/Scenes/SceneBuilderTests.cs

@@ -45,7 +45,7 @@ namespace SharpGLTF.Scenes
 
             var pivot = new NodeBuilder();
 
-            var tcurve = pivot.UseTranslation().UseCurve("default");
+            var tcurve = pivot.UseTranslation().UseCurve("default") as Animations.Curve<Vector3>;
 
             var c = new Animations.CurvePoint<Vector3>(tcurve, 0);