Browse Source

Refactored Animation access to make it a bit faster, easier to use, and more accessible. Fixes #81.

Vicente Penades 5 years ago
parent
commit
062ef31305

+ 21 - 6
src/SharpGLTF.Core/Runtime/AnimatableProperty.cs

@@ -47,7 +47,7 @@ namespace SharpGLTF.Runtime
         /// <summary>
         /// Evaluates the value of this <see cref="AnimatableProperty{T}"/> at a given <paramref name="offset"/> for a given <paramref name="trackLogicalIndex"/>.
         /// </summary>
-        /// <param name="trackLogicalIndex">The index of the animation track</param>
+        /// <param name="trackLogicalIndex">The index of the animation track.</param>
         /// <param name="offset">The time offset within the curve</param>
         /// <returns>The evaluated value taken from the animation <paramref name="trackLogicalIndex"/>, or <see cref="Value"/> if a track was not found.</returns>
         public T GetValueAt(int trackLogicalIndex, float offset)
@@ -59,15 +59,30 @@ namespace SharpGLTF.Runtime
             return _Curves[trackLogicalIndex]?.GetPoint(offset) ?? this.Value;
         }
 
-        public void SetCurve(int logicalIndex, ICurveSampler<T> curveSampler)
+        /// <summary>
+        /// Sets the animation curves for this property.
+        /// </summary>
+        /// <param name="trackLogicalIndex">The index of the animation track.</param>
+        /// <param name="curveSampler">A curve sampler, or null if the curve is to be removed.</param>
+        public void SetCurve(int trackLogicalIndex, ICurveSampler<T> curveSampler)
         {
-            Guard.NotNull(curveSampler, nameof(curveSampler));
-            Guard.MustBeGreaterThanOrEqualTo(logicalIndex, 0, nameof(logicalIndex));
+            Guard.MustBeGreaterThanOrEqualTo(trackLogicalIndex, 0, nameof(trackLogicalIndex));
+
+            if (curveSampler == null)
+            {
+                if (_Curves != null && trackLogicalIndex < _Curves.Count)
+                {
+                    _Curves[trackLogicalIndex] = null;
+                    if (_Curves.All(item => item == null)) _Curves = null;
+                }
+
+                return;
+            }
 
             if (_Curves == null) _Curves = new List<ICurveSampler<T>>();
-            while (_Curves.Count <= logicalIndex) _Curves.Add(null);
+            while (_Curves.Count <= trackLogicalIndex) _Curves.Add(null);
 
-            _Curves[logicalIndex] = curveSampler;
+            _Curves[trackLogicalIndex] = curveSampler;
         }
 
         #endregion

+ 6 - 10
src/SharpGLTF.Core/Runtime/NodeTemplate.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Numerics;
 using System.Text;
 
@@ -36,17 +37,12 @@ namespace SharpGLTF.Runtime
             {
                 var index = anim.LogicalIndex;
 
-                var scaAnim = anim.FindScaleSampler(srcNode)?.CreateCurveSampler(isolateMemory);
-                if (scaAnim != null) _Scale.SetCurve(index, scaAnim);
+                var curves = srcNode.GetCurveSamplers(anim);
 
-                var rotAnim = anim.FindRotationSampler(srcNode)?.CreateCurveSampler(isolateMemory);
-                if (rotAnim != null) _Rotation.SetCurve(index, rotAnim);
-
-                var traAnim = anim.FindTranslationSampler(srcNode)?.CreateCurveSampler(isolateMemory);
-                if (traAnim != null) _Translation.SetCurve(index, traAnim);
-
-                var mrpAnim = anim.FindSparseMorphSampler(srcNode)?.CreateCurveSampler(isolateMemory);
-                if (mrpAnim != null) _Morphing.SetCurve(index, mrpAnim);
+                _Scale.SetCurve(index, curves.Scale?.CreateCurveSampler(isolateMemory));
+                _Rotation.SetCurve(index, curves.Rotation?.CreateCurveSampler(isolateMemory));
+                _Translation.SetCurve(index, curves.Translation?.CreateCurveSampler(isolateMemory));
+                _Morphing.SetCurve(index, curves.MorphingSparse?.CreateCurveSampler(isolateMemory));
             }
 
             _UseAnimatedTransforms = _Scale.IsAnimated | _Rotation.IsAnimated | _Translation.IsAnimated;

+ 159 - 0
src/SharpGLTF.Core/Schema2/gltf.AnimationChannel.cs

@@ -0,0 +1,159 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Linq;
+using System.Numerics;
+
+using SharpGLTF.Collections;
+using SharpGLTF.Transforms;
+using SharpGLTF.Validation;
+
+namespace SharpGLTF.Schema2
+{
+    sealed partial class AnimationChannelTarget
+    {
+        #region lifecycle
+
+        internal AnimationChannelTarget() { }
+
+        internal AnimationChannelTarget(Node targetNode, PropertyPath targetPath)
+        {
+            _node = targetNode.LogicalIndex;
+            _path = targetPath;
+        }
+
+        #endregion
+
+        #region data
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        internal int? _NodeId => this._node;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        internal PropertyPath _NodePath => this._path;
+
+        #endregion
+
+        #region Validation
+
+        protected override void OnValidateReferences(ValidationContext validate)
+        {
+            base.OnValidateReferences(validate);
+
+            validate.IsNullOrIndex("Node", _node, validate.Root.LogicalNodes);
+        }
+
+        #endregion
+    }
+
+    [System.Diagnostics.DebuggerDisplay("AnimChannel LogicalNode[{TargetNode.LogicalIndex}].{TargetNodePath}")]
+    public sealed partial class AnimationChannel : IChildOf<Animation>
+    {
+        #region lifecycle
+        internal AnimationChannel() { }
+
+        internal AnimationChannel(Node targetNode, PropertyPath targetPath)
+        {
+            _target = new AnimationChannelTarget(targetNode, targetPath);
+            _sampler = -1;
+        }
+
+        internal void SetSampler(AnimationSampler sampler)
+        {
+            Guard.NotNull(sampler, nameof(sampler));
+            Guard.IsTrue(this.LogicalParent == sampler.LogicalParent, nameof(sampler));
+
+            _sampler = sampler.LogicalIndex;
+        }
+
+        void IChildOf<Animation>._SetLogicalParent(Animation parent, int index)
+        {
+            LogicalParent = parent;
+            LogicalIndex = index;
+        }
+
+        #endregion
+
+        #region properties
+
+        /// <summary>
+        /// Gets the zero-based index of this <see cref="Animation"/> at <see cref="ModelRoot.LogicalAnimations"/>
+        /// </summary>
+        public int LogicalIndex { get; private set; } = -1;
+
+        /// <summary>
+        /// Gets the <see cref="Animation"/> instance that owns this object.
+        /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        public Animation LogicalParent { get; private set; }
+
+        /// <summary>
+        /// Gets the <see cref="Node"/> which property is to be bound with this animation.
+        /// </summary>
+        public Node TargetNode
+        {
+            get
+            {
+                var idx = this._target?._NodeId ?? -1;
+                if (idx < 0) return null;
+                return this.LogicalParent.LogicalParent.LogicalNodes[idx];
+            }
+        }
+
+        /// <summary>
+        /// Gets which property of the <see cref="Node"/> pointed by <see cref="TargetNode"/> is to be bound with this animation.
+        /// </summary>
+        public PropertyPath TargetNodePath => this._target?._NodePath ?? PropertyPath.translation;
+
+        #endregion
+
+        #region API
+
+        internal AnimationSampler _GetSampler() { return this.LogicalParent._Samplers[this._sampler]; }
+
+        public IAnimationSampler<Vector3> GetScaleSampler()
+        {
+            if (TargetNodePath != PropertyPath.scale) return null;
+            return _GetSampler();
+        }
+
+        public IAnimationSampler<Quaternion> GetRotationSampler()
+        {
+            if (TargetNodePath != PropertyPath.rotation) return null;
+            return _GetSampler();
+        }
+
+        public IAnimationSampler<Vector3> GetTranslationSampler()
+        {
+            if (TargetNodePath != PropertyPath.translation) return null;
+            return _GetSampler();
+        }
+
+        public IAnimationSampler<SparseWeight8> GetSparseMorphSampler()
+        {
+            if (TargetNodePath != PropertyPath.weights) return null;
+            return _GetSampler();
+        }
+
+        public IAnimationSampler<float[]> GetMorphSampler()
+        {
+            if (TargetNodePath != PropertyPath.weights) return null;
+            return _GetSampler();
+        }
+
+        #endregion
+
+        #region Validation
+
+        protected override void OnValidateReferences(ValidationContext validate)
+        {
+            base.OnValidateReferences(validate);
+
+            validate.IsNullOrIndex("Sampler", _sampler, this.LogicalParent._Samplers);
+        }
+
+        #endregion
+    }
+
+    
+}

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

@@ -30,7 +30,7 @@ namespace SharpGLTF.Schema2
         /// Gets the cubic animation entries fot <see cref="AnimationInterpolationMode.CUBICSPLINE"/> mode.
         /// </summary>
         /// <returns>A sequence of Time-(TangentIn,Value,TangentOut) keys.</returns>
-        IEnumerable<(Single Key, (T TangentIn, T Value, T TangentOut))> GetCubicKeys();
+        IEnumerable<(Single Key, (T TangentIn, T Value, T TangentOut) Value)> GetCubicKeys();
 
         /// <summary>
         /// Creates an interpolation sampler that can be used to query the value of the curve at any time.

+ 70 - 149
src/SharpGLTF.Core/Schema2/gltf.Animations.cs

@@ -28,7 +28,7 @@ namespace SharpGLTF.Schema2
 
         internal IReadOnlyList<AnimationSampler> _Samplers => _samplers;
 
-        internal IReadOnlyList<AnimationChannel> _Channels => _channels;
+        public IReadOnlyList<AnimationChannel> Channels => _channels;
 
         public float Duration => _samplers.Select(item => item.Duration).Max();
 
@@ -41,6 +41,28 @@ namespace SharpGLTF.Schema2
             return base.GetLogicalChildren().Concat(_samplers).Concat(_channels);
         }
 
+        public IEnumerable<AnimationChannel> FindChannels(Node node)
+        {
+            Guard.NotNull(node, nameof(node));
+            Guard.MustShareLogicalParent(this, node, nameof(node));
+
+            return Channels.Where(item => item.TargetNode == node);
+        }
+
+        private AnimationChannel _FindChannel(Node node, PropertyPath path)
+        {
+            return FindChannels(node).FirstOrDefault(item => item.TargetNodePath == path);
+        }
+
+        public AnimationChannel FindScaleChannel(Node node) => _FindChannel(node, PropertyPath.scale);
+        public AnimationChannel FindRotationChannel(Node node) => _FindChannel(node, PropertyPath.rotation);
+        public AnimationChannel FindTranslationChannel(Node node) => _FindChannel(node, PropertyPath.translation);
+        public AnimationChannel FindMorphChannel(Node node) => _FindChannel(node, PropertyPath.weights);
+
+        #endregion
+
+        #region API - Create
+
         private AnimationSampler _CreateSampler(AnimationInterpolationMode interpolation)
         {
             var sampler = new AnimationSampler(interpolation);
@@ -55,6 +77,7 @@ namespace SharpGLTF.Schema2
         /// </remarks>
         private AnimationChannel _UseChannel(Node node, PropertyPath path)
         {
+            Guard.NotNull(node, nameof(node));
             Guard.MustShareLogicalParent(this, node, nameof(node));
 
             var channel = _channels.FirstOrDefault(item => item.TargetNode == node && item.TargetNodePath == path);
@@ -70,6 +93,7 @@ namespace SharpGLTF.Schema2
         public void CreateScaleChannel(Node node, IReadOnlyDictionary<Single, Vector3> keyframes, bool linear = true)
         {
             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);
@@ -83,6 +107,7 @@ namespace SharpGLTF.Schema2
         public void CreateScaleChannel(Node node, IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         {
             Guard.NotNull(node, nameof(node));
+            Guard.MustShareLogicalParent(this, node, nameof(node));
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
@@ -96,6 +121,7 @@ namespace SharpGLTF.Schema2
         public void CreateRotationChannel(Node node, IReadOnlyDictionary<Single, Quaternion> keyframes, bool linear = true)
         {
             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);
@@ -109,6 +135,7 @@ namespace SharpGLTF.Schema2
         public void CreateRotationChannel(Node node, IReadOnlyDictionary<Single, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> keyframes)
         {
             Guard.NotNull(node, nameof(node));
+            Guard.MustShareLogicalParent(this, node, nameof(node));
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
@@ -122,6 +149,7 @@ namespace SharpGLTF.Schema2
         public void CreateTranslationChannel(Node node, IReadOnlyDictionary<Single, Vector3> keyframes, bool linear = true)
         {
             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);
@@ -135,6 +163,7 @@ namespace SharpGLTF.Schema2
         public void CreateTranslationChannel(Node node, IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         {
             Guard.NotNull(node, nameof(node));
+            Guard.MustShareLogicalParent(this, node, nameof(node));
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
@@ -148,6 +177,7 @@ namespace SharpGLTF.Schema2
         public void CreateMorphChannel(Node node, IReadOnlyDictionary<Single, SparseWeight8> keyframes, int morphCount, bool linear = true)
         {
             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);
@@ -161,6 +191,7 @@ 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.MustShareLogicalParent(this, node, nameof(node));
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
             var sampler = this._CreateSampler(AnimationInterpolationMode.CUBICSPLINE);
@@ -171,69 +202,6 @@ namespace SharpGLTF.Schema2
                 .SetSampler(sampler);
         }
 
-        private AnimationChannel FindChannel(Node node, PropertyPath path)
-        {
-            return _channels.FirstOrDefault(item => item.TargetNode == node && item.TargetNodePath == path);
-        }
-
-        public IAnimationSampler<Vector3> FindScaleSampler(Node node) { return FindChannel(node, PropertyPath.scale)?.Sampler; }
-
-        public IAnimationSampler<Quaternion> FindRotationSampler(Node node) { return FindChannel(node, PropertyPath.rotation)?.Sampler; }
-
-        public IAnimationSampler<Vector3> FindTranslationSampler(Node node) { return FindChannel(node, PropertyPath.translation)?.Sampler; }
-
-        public IAnimationSampler<Single[]> FindMorphSampler(Node node) { return FindChannel(node, PropertyPath.weights)?.Sampler; }
-
-        public IAnimationSampler<SparseWeight8> FindSparseMorphSampler(Node node) { return FindChannel(node, PropertyPath.weights)?.Sampler; }
-
-        public AffineTransform GetLocalTransform(Node node, Single time)
-        {
-            Guard.NotNull(node, nameof(node));
-            Guard.MustShareLogicalParent(this, node, nameof(node));
-
-            var xform = node.LocalTransform;
-
-            var sfunc = FindScaleSampler(node)?.CreateCurveSampler();
-            var rfunc = FindRotationSampler(node)?.CreateCurveSampler();
-            var tfunc = FindTranslationSampler(node)?.CreateCurveSampler();
-
-            if (sfunc != null) xform.Scale = sfunc.GetPoint(time);
-            if (rfunc != null) xform.Rotation = rfunc.GetPoint(time);
-            if (tfunc != null) xform.Translation = tfunc.GetPoint(time);
-
-            return xform;
-        }
-
-        public IReadOnlyList<float> GetMorphWeights(Node node, Single time)
-        {
-            Guard.NotNull(node, nameof(node));
-
-            var morphWeights = node.MorphWeights;
-            if (morphWeights == null || morphWeights.Count == 0) return morphWeights;
-
-            Guard.MustShareLogicalParent(this, node, nameof(node));
-
-            var mfunc = FindMorphSampler(node)?.CreateCurveSampler();
-            if (mfunc == null) return morphWeights;
-
-            return mfunc.GetPoint(time);
-        }
-
-        public SparseWeight8 GetSparseMorphWeights(Node node, Single time)
-        {
-            Guard.NotNull(node, nameof(node));
-
-            var morphWeights = node.MorphWeights;
-            if (morphWeights == null || morphWeights.Count == 0) return default;
-
-            Guard.MustShareLogicalParent(this, node, nameof(node));
-
-            var mfunc = FindSparseMorphSampler(node)?.CreateCurveSampler();
-            if (mfunc == null) return default;
-
-            return mfunc.GetPoint(time);
-        }
-
         #endregion
 
         #region Validation
@@ -253,112 +221,65 @@ namespace SharpGLTF.Schema2
         }
 
         #endregion
-    }
-
-    sealed partial class AnimationChannelTarget
-    {
-        #region lifecycle
-
-        internal AnimationChannelTarget() { }
-
-        internal AnimationChannelTarget(Node targetNode, PropertyPath targetPath)
-        {
-            _node = targetNode.LogicalIndex;
-            _path = targetPath;
-        }
 
-        #endregion
+        #region obsolete
 
-        #region data
+        [Obsolete("Use FindScaleChannel(node)?.GetScaleSampler()")]
+        public IAnimationSampler<Vector3> FindScaleSampler(Node node) => FindScaleChannel(node)?.GetScaleSampler();
 
-        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        internal int? _NodeId => this._node;
+        [Obsolete("Use FindRotationChannel(node)?.GetRotationSampler()")]
+        public IAnimationSampler<Quaternion> FindRotationSampler(Node node) => FindRotationChannel(node)?.GetRotationSampler();
 
-        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        internal PropertyPath _NodePath => this._path;
+        [Obsolete("Use FindTranslationChannel(node)?.GetTranslationSampler()")]
+        public IAnimationSampler<Vector3> FindTranslationSampler(Node node) => FindTranslationChannel(node)?.GetTranslationSampler();
 
-        #endregion
+        [Obsolete("Use FindMorphChannel(node)?.GetMorphSampler()")]
+        public IAnimationSampler<Single[]> FindMorphSampler(Node node) => FindMorphChannel(node)?.GetMorphSampler();
 
-        #region Validation
+        [Obsolete("Use FindMorphChannel(node)?.GetSparseMorphSampler()")]
+        public IAnimationSampler<SparseWeight8> FindSparseMorphSampler(Node node) => FindMorphChannel(node)?.GetSparseMorphSampler();
 
-        protected override void OnValidateReferences(ValidationContext validate)
+        [Obsolete("Use node.GetCurveSamplers(anim).GetLocalTransform(time)")]
+        public AffineTransform GetLocalTransform(Node node, Single time)
         {
-            base.OnValidateReferences(validate);
-
-            validate.IsNullOrIndex("Node", _node, validate.Root.LogicalNodes);
-        }
-
-        #endregion
-    }
-
-    [System.Diagnostics.DebuggerDisplay("AnimChannel LogicalNode[{TargetNode.LogicalIndex}].{TargetNodePath}")]
-    sealed partial class AnimationChannel : IChildOf<Animation>
-    {
-        #region lifecycle
-
-        internal AnimationChannel() { }
+            Guard.NotNull(node, nameof(node));
+            Guard.MustShareLogicalParent(this, node, nameof(node));
 
-        internal AnimationChannel(Node targetNode, PropertyPath targetPath)
-        {
-            _target = new AnimationChannelTarget(targetNode, targetPath);
-            _sampler = -1;
+            return node.GetCurveSamplers(this).GetLocalTransform(time);
         }
 
-        internal void SetSampler(AnimationSampler sampler)
+        [Obsolete("Use node.GetCurveSamplers(anim).GetMorphingWeights(time)")]
+        public IReadOnlyList<float> GetMorphWeights(Node node, Single time)
         {
-            Guard.NotNull(sampler, nameof(sampler));
-            Guard.IsTrue(this.LogicalParent == sampler.LogicalParent, nameof(sampler));
-
-            _sampler = sampler.LogicalIndex;
-        }
-
-        #endregion
-
-        #region properties
+            Guard.NotNull(node, nameof(node));
 
-        /// <summary>
-        /// Gets the zero-based index of this <see cref="Animation"/> at <see cref="ModelRoot.LogicalAnimations"/>
-        /// </summary>
-        public int LogicalIndex { get; private set; } = -1;
+            var morphWeights = node.MorphWeights;
+            if (morphWeights == null || morphWeights.Count == 0) return morphWeights;
 
-        /// <summary>
-        /// Gets the <see cref="Animation"/> instance that owns this object.
-        /// </summary>
-        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Animation LogicalParent { get; private set; }
+            Guard.MustShareLogicalParent(this, node, nameof(node));
 
-        void IChildOf<Animation>._SetLogicalParent(Animation parent, int index)
-        {
-            LogicalParent = parent;
-            LogicalIndex = index;
+            return FindMorphChannel(node)
+                ?.GetMorphSampler()
+                ?.CreateCurveSampler()
+                ?.GetPoint(time)
+                ?? default;
         }
 
-        /// <summary>
-        /// Gets the <see cref="AnimationSampler"/> instance used by this <see cref="AnimationChannel"/>.
-        /// </summary>
-        public AnimationSampler Sampler => this.LogicalParent._Samplers[this._sampler];
-
-        public Node TargetNode
+        [Obsolete("Use node.GetCurveSamplers(anim).GetSparseMorphingWeights(time)")]
+        public SparseWeight8 GetSparseMorphWeights(Node node, Single time)
         {
-            get
-            {
-                var idx = this._target?._NodeId ?? -1;
-                if (idx < 0) return null;
-                return this.LogicalParent.LogicalParent.LogicalNodes[idx];
-            }
-        }
-
-        public PropertyPath TargetNodePath => this._target?._NodePath ?? PropertyPath.translation;
-
-        #endregion
+            Guard.NotNull(node, nameof(node));
 
-        #region Validation
+            var morphWeights = node.MorphWeights;
+            if (morphWeights == null || morphWeights.Count == 0) return default;
 
-        protected override void OnValidateReferences(ValidationContext validate)
-        {
-            base.OnValidateReferences(validate);
+            Guard.MustShareLogicalParent(this, node, nameof(node));
 
-            validate.IsNullOrIndex("Sampler", _sampler, this.LogicalParent._Samplers);
+            return FindMorphChannel(node)
+                ?.GetSparseMorphSampler()
+                ?.CreateCurveSampler()
+                ?.GetPoint(time)
+                ?? default;
         }
 
         #endregion

+ 9 - 1
src/SharpGLTF.Core/Schema2/gltf.ExtraProperties.cs

@@ -11,13 +11,21 @@ using JsonToken = System.Text.Json.JsonTokenType;
 
 namespace SharpGLTF.Schema2
 {
+    public interface IExtraProperties
+    {
+        IReadOnlyCollection<JsonSerializable> Extensions { get; }
+
+        IO.JsonContent Extras { get; set; }
+    }
+
     /// <summary>
     /// Represents the base class for all glTF 2 Schema objects.
     /// </summary>
     /// <remarks>
     /// Defines the <see cref="Extras"/> property for every glTF object.
     /// </remarks>
-    public abstract class ExtraProperties : JsonSerializable
+    public abstract class ExtraProperties : JsonSerializable,
+        IExtraProperties
     {
         #region data
 

+ 175 - 6
src/SharpGLTF.Core/Schema2/gltf.Node.cs

@@ -5,6 +5,10 @@ using System.Numerics;
 
 namespace SharpGLTF.Schema2
 {
+    /// <summary>
+    /// Represents an abstract interface for a visual hierarchy.
+    /// Implemented by <see cref="Node"/> and <see cref="Scene"/>.
+    /// </summary>
     public interface IVisualNodeContainer
     {
         IEnumerable<Node> VisualChildren { get; }
@@ -287,12 +291,22 @@ namespace SharpGLTF.Schema2
 
                 if (root.LogicalAnimations.Count == 0) return false;
 
-                // check if it's affected by animations.
-                if (root.LogicalAnimations.Any(anim => anim.FindScaleSampler(this) != null)) return true;
-                if (root.LogicalAnimations.Any(anim => anim.FindRotationSampler(this) != null)) return true;
-                if (root.LogicalAnimations.Any(anim => anim.FindTranslationSampler(this) != null)) return true;
+                // check if it's affected by any animation channel.
 
-                return false;
+                bool _isTransformPath(PropertyPath path)
+                {
+                    if (path == PropertyPath.scale) return true;
+                    if (path == PropertyPath.rotation) return true;
+                    if (path == PropertyPath.translation) return true;
+                    // since morph weights are not part of the node transform, they're not handled here.
+                    return false;
+                }
+
+                return root
+                    .LogicalAnimations
+                    .SelectMany(item => item.FindChannels(this))
+                    .Where(item => _isTransformPath(item.TargetNodePath))
+                    .Any();
             }
         }
 
@@ -315,7 +329,7 @@ namespace SharpGLTF.Schema2
         {
             if (animation == null) return this.LocalTransform;
 
-            return animation.GetLocalTransform(this, time);
+            return this.GetCurveSamplers(animation).GetLocalTransform(time);
         }
 
         public Matrix4x4 GetWorldMatrix(Animation animation, float time)
@@ -331,6 +345,7 @@ namespace SharpGLTF.Schema2
         {
             if (!_mesh.HasValue) return Array.Empty<Single>();
 
+            // if the node doesn't have default morph weights, fall back to mesh weights.
             if (_weights == null || _weights.Count == 0) return Mesh.MorphWeights;
 
             return _weights.Select(item => (float)item).ToList();
@@ -470,6 +485,18 @@ namespace SharpGLTF.Schema2
 
         #endregion
 
+        #region API
+
+        public NodeCurveSamplers GetCurveSamplers(Animation animation)
+        {
+            Guard.NotNull(animation, nameof(animation));
+            Guard.MustShareLogicalParent(this, animation, nameof(animation));
+
+            return new NodeCurveSamplers(this, animation);
+        }
+
+        #endregion
+
         #region validation
 
         protected override void OnValidateReferences(Validation.ValidationContext validate)
@@ -675,4 +702,146 @@ namespace SharpGLTF.Schema2
             }
         }
     }
+
+    /// <summary>
+    /// Represents an proxy to acccess the animation curves of a <see cref="Node"/>.
+    /// Use <see cref="Node.GetCurveSamplers(Animation)"/> for access.
+    /// </summary>
+    [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
+    public readonly struct NodeCurveSamplers
+    {
+        #region debug
+
+        private string _GetDebuggerDisplay()
+        {
+            if (TargetNode == null || Animation == null) return "Null";
+
+            var txt = $"Node[{TargetNode.LogicalIndex}ᴵᵈˣ]";
+            if (!string.IsNullOrWhiteSpace(this.TargetNode.Name)) txt += $" {this.TargetNode.Name}";
+
+            txt += " <<< ";
+
+            txt += $"Animation[{Animation.LogicalIndex}ᴵᵈˣ]";
+            if (!string.IsNullOrWhiteSpace(this.Animation.Name)) txt += $" {this.Animation.Name}";
+
+            return txt;
+        }
+
+        #endregion
+
+        #region constructor
+
+        internal NodeCurveSamplers(Node node, Animation animation)
+        {
+            TargetNode = node;
+            Animation = animation;
+
+            Scale = null;
+            Rotation = null;
+            Translation = null;
+            Morphing = null;
+            MorphingSparse = null;
+
+            foreach (var c in animation.FindChannels(node))
+            {
+                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;
+                }
+            }
+
+            // if we have morphing animation, we might require to check this...
+            // var morphWeights = node.MorphWeights;
+            // if (morphWeights == null || morphWeights.Count == 0) { Morphing = null; MorphingSparse = null; }
+        }
+
+        #endregion
+
+        #region data
+
+        public readonly Node TargetNode;
+        public readonly Animation Animation;
+
+        #endregion
+
+        #region  properties
+
+        /// <summary>
+        /// True if any of <see cref="Scale"/>, <see cref="Rotation"/> or <see cref="Translation"/> is defined.
+        /// </summary>
+        public bool HasTransformCurves => Scale != null || Rotation != null || Translation != null;
+
+        /// <summary>
+        /// True if there's a morphing curve.
+        /// </summary>
+        public bool HasMorphingCurves => Morphing != null || MorphingSparse != null;
+
+        /// <summary>
+        /// Gets the Scale sampler, or null if there's no curve defined.
+        /// </summary>
+        public readonly IAnimationSampler<Vector3> Scale;
+
+        /// <summary>
+        /// Gets the Rotation sampler, or null if there's no curve defined.
+        /// </summary>
+        public readonly IAnimationSampler<Quaternion> Rotation;
+
+        /// <summary>
+        /// Gets the Translation sampler, or null if there's no curve defined.
+        /// </summary>
+        public readonly IAnimationSampler<Vector3> Translation;
+
+        /// <summary>
+        /// Gets the raw Morphing sampler, or null if there's no curve defined.
+        /// </summary>
+        public readonly IAnimationSampler<Single[]> Morphing;
+
+        /// <summary>
+        /// Gets the SparseWeight8 Morphing sampler, or null if there's no curve defined.
+        /// </summary>
+        public readonly IAnimationSampler<Transforms.SparseWeight8> MorphingSparse;
+
+        #endregion
+
+        #region API
+
+        public Transforms.AffineTransform GetLocalTransform(Single time)
+        {
+            var xform = TargetNode.LocalTransform;
+
+            var sfunc = Scale?.CreateCurveSampler();
+            var rfunc = Rotation?.CreateCurveSampler();
+            var tfunc = Translation?.CreateCurveSampler();
+
+            if (sfunc != null) xform.Scale = sfunc.GetPoint(time);
+            if (rfunc != null) xform.Rotation = rfunc.GetPoint(time);
+            if (tfunc != null) xform.Translation = tfunc.GetPoint(time);
+
+            return xform;
+        }
+
+        public IReadOnlyList<float> GetMorphingWeights(Single time)
+        {
+            return Morphing
+                ?.CreateCurveSampler()
+                ?.GetPoint(time)
+                ?? TargetNode.MorphWeights;
+        }
+
+        public Transforms.SparseWeight8 GetSparseMorphingWeights(Single time)
+        {
+            return MorphingSparse
+                ?.CreateCurveSampler()
+                ?.GetPoint(time)
+                ?? Transforms.SparseWeight8.Create(TargetNode.MorphWeights);
+        }
+
+        #endregion
+    }
 }

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

@@ -176,9 +176,9 @@ namespace SharpGLTF.Animations
                     var spline = convertible.ToSplineCurve();
                     foreach (var ppp in spline)
                     {
-                        this.SetPoint(ppp.Key, ppp.Value.Item2);
-                        this.SetIncomingTangent(ppp.Key, ppp.Value.Item1);
-                        this.SetOutgoingTangent(ppp.Key, ppp.Value.Item3);
+                        this.SetPoint(ppp.Key, ppp.Value.Value);
+                        this.SetIncomingTangent(ppp.Key, ppp.Value.TangentIn);
+                        this.SetOutgoingTangent(ppp.Key, ppp.Value.TangentOut);
                     }
 
                     return;
@@ -188,6 +188,39 @@ namespace SharpGLTF.Animations
             throw new NotImplementedException();
         }
 
+        public void SetCurve(Schema2.IAnimationSampler<T> curve)
+        {
+            switch (curve.InterpolationMode)
+            {
+                case Schema2.AnimationInterpolationMode.STEP:
+                case Schema2.AnimationInterpolationMode.LINEAR:
+                    {
+                        var isLinear = curve.InterpolationMode == Schema2.AnimationInterpolationMode.LINEAR;
+
+                        foreach (var (key, value) in curve.GetLinearKeys())
+                        {
+                            this.SetPoint(key, value, isLinear);
+                        }
+
+                        break;
+                    }
+
+                case Schema2.AnimationInterpolationMode.CUBICSPLINE:
+                    {
+                        foreach (var (key, value) in curve.GetCubicKeys())
+                        {
+                            this.SetPoint(key, value.Value);
+                            this.SetIncomingTangent(key, value.TangentIn);
+                            this.SetOutgoingTangent(key, value.TangentOut);
+                        }
+
+                        break;
+                    }
+
+                default: throw new NotImplementedException();
+            }
+        }
+
         #endregion
 
         #region With* API

+ 7 - 9
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs

@@ -376,14 +376,11 @@ namespace SharpGLTF.Scenes
                 var name = anim.Name;
                 if (string.IsNullOrWhiteSpace(name)) name = anim.LogicalIndex.ToString(System.Globalization.CultureInfo.InvariantCulture);
 
-                var scaAnim = anim.FindScaleSampler(srcNode)?.CreateCurveSampler();
-                if (scaAnim != null) dstNode.UseScale(name).SetCurve(scaAnim);
+                var curves = srcNode.GetCurveSamplers(anim);
 
-                var rotAnim = anim.FindRotationSampler(srcNode)?.CreateCurveSampler();
-                if (rotAnim != null) dstNode.UseRotation(name).SetCurve(rotAnim);
-
-                var traAnim = anim.FindTranslationSampler(srcNode)?.CreateCurveSampler();
-                if (traAnim != null) dstNode.UseTranslation(name).SetCurve(traAnim);
+                if (curves.Scale != null) dstNode.UseScale(name).SetCurve(curves.Scale);
+                if (curves.Rotation != null) dstNode.UseRotation(name).SetCurve(curves.Rotation);
+                if (curves.Translation != null) dstNode.UseTranslation(name).SetCurve(curves.Translation);
             }
         }
 
@@ -394,8 +391,9 @@ namespace SharpGLTF.Scenes
                 var name = anim.Name;
                 if (string.IsNullOrWhiteSpace(name)) name = anim.LogicalIndex.ToString(System.Globalization.CultureInfo.InvariantCulture);
 
-                var mrpAnim = anim.FindSparseMorphSampler(srcNode)?.CreateCurveSampler();
-                if (mrpAnim != null) dstInst.Content.UseMorphing(name).SetCurve(mrpAnim);
+                var curves = srcNode.GetCurveSamplers(anim);
+
+                if (curves.MorphingSparse != null) dstInst.Content.UseMorphing(name).SetCurve(curves.MorphingSparse);
             }
         }
 

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

@@ -337,13 +337,21 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
                 TestContext.WriteLine($"Animation at {t}");
 
+                var curves = node.GetCurveSamplers(anim);
+
                 if (t < anim.Duration)
                 {
-                    var mw = anim.GetMorphWeights(node, t);
+                    var mw = curves.Morphing
+                        .CreateCurveSampler()
+                        .GetPoint(t);            
+                    
                     TestContext.WriteLine($"    Morph Weights: {mw[0]} {mw[1]}");
                 }
 
-                var msw = anim.GetSparseMorphWeights(node, t);
+                var msw = curves.MorphingSparse
+                    .CreateCurveSampler()
+                    .GetPoint(t);
+
                 TestContext.WriteLine($"    Morph Sparse : {msw.Weight0} {msw.Weight1}");
 
                 var triangles = model.DefaultScene

+ 8 - 4
tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSpecialModelsTest.cs

@@ -147,10 +147,14 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
             var boundingSphere = Runtime.MeshDecoder.EvaluateBoundingSphere(model.DefaultScene);
 
-            var channel = model.LogicalAnimations[1].FindRotationSampler(model.LogicalNodes[5]);
-
-            var node5_R_00 = channel.CreateCurveSampler(true).GetPoint(0);
-            var node5_R_01 = channel.CreateCurveSampler(true).GetPoint(1);
+            var sampler = model
+                .LogicalNodes[5]
+                .GetCurveSamplers(model.LogicalAnimations[1])
+                .Rotation
+                .CreateCurveSampler(true);
+
+            var node5_R_00 = sampler.GetPoint(0);
+            var node5_R_01 = sampler.GetPoint(1);
 
             Assert.AreEqual(node5_R_00, node5_R_01);