Browse Source

Added barebones KHR_animation_pointer support [WIP]

Vicente Penades Armengot 10 months ago
parent
commit
11d19d2013

+ 99 - 0
src/SharpGLTF.Core/Animations/CurveSampler.cs

@@ -360,6 +360,20 @@ namespace SharpGLTF.Animations
             return result;
             return result;
         }
         }
 
 
+        public static Single InterpolateCubic(Single start, Single outgoingTangent, Single end, Single incomingTangent, Single amount)
+        {
+            var hermite = CreateHermitePointWeights(amount);
+
+            return (start * hermite.StartPosition) + (end * hermite.EndPosition) + (outgoingTangent * hermite.StartTangent) + (incomingTangent * hermite.EndTangent);
+        }
+
+        public static Vector2 InterpolateCubic(Vector2 start, Vector2 outgoingTangent, Vector2 end, Vector2 incomingTangent, Single amount)
+        {
+            var hermite = CreateHermitePointWeights(amount);
+
+            return (start * hermite.StartPosition) + (end * hermite.EndPosition) + (outgoingTangent * hermite.StartTangent) + (incomingTangent * hermite.EndTangent);
+        }
+
         public static Vector3 InterpolateCubic(Vector3 start, Vector3 outgoingTangent, Vector3 end, Vector3 incomingTangent, Single amount)
         public static Vector3 InterpolateCubic(Vector3 start, Vector3 outgoingTangent, Vector3 end, Vector3 incomingTangent, Single amount)
         {
         {
             var hermite = CreateHermitePointWeights(amount);
             var hermite = CreateHermitePointWeights(amount);
@@ -367,6 +381,13 @@ namespace SharpGLTF.Animations
             return (start * hermite.StartPosition) + (end * hermite.EndPosition) + (outgoingTangent * hermite.StartTangent) + (incomingTangent * hermite.EndTangent);
             return (start * hermite.StartPosition) + (end * hermite.EndPosition) + (outgoingTangent * hermite.StartTangent) + (incomingTangent * hermite.EndTangent);
         }
         }
 
 
+        public static Vector4 InterpolateCubic(Vector4 start, Vector4 outgoingTangent, Vector4 end, Vector4 incomingTangent, Single amount)
+        {
+            var hermite = CreateHermitePointWeights(amount);
+
+            return (start * hermite.StartPosition) + (end * hermite.EndPosition) + (outgoingTangent * hermite.StartTangent) + (incomingTangent * hermite.EndTangent);
+        }
+
         public static Quaternion InterpolateCubic(Quaternion start, Quaternion outgoingTangent, Quaternion end, Quaternion incomingTangent, Single amount)
         public static Quaternion InterpolateCubic(Quaternion start, Quaternion outgoingTangent, Quaternion end, Quaternion incomingTangent, Single amount)
         {
         {
             var hermite = CreateHermitePointWeights(amount);
             var hermite = CreateHermitePointWeights(amount);
@@ -400,6 +421,40 @@ namespace SharpGLTF.Animations
         private static bool _HasZero<T>(this IEnumerable<T> collection) { return collection == null || !collection.Any(); }
         private static bool _HasZero<T>(this IEnumerable<T> collection) { return collection == null || !collection.Any(); }
         private static bool _HasOne<T>(this IEnumerable<T> collection) { return !collection.Skip(1).Any(); }
         private static bool _HasOne<T>(this IEnumerable<T> collection) { return !collection.Skip(1).Any(); }
 
 
+        public static ICurveSampler<Single> CreateSampler(this IEnumerable<(Single, Single)> collection, bool isLinear = true, bool optimize = false)
+        {
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Single>.Create(collection);
+
+            if (isLinear)
+            {
+                var sampler = new LinearSampler<Single>(collection, SamplerTraits.Scalar);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
+            else
+            {
+                var sampler = new StepSampler<Single>(collection, SamplerTraits.Scalar);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
+        }
+
+        public static ICurveSampler<Vector2> CreateSampler(this IEnumerable<(Single, Vector2)> collection, bool isLinear = true, bool optimize = false)
+        {
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Vector2>.Create(collection);
+
+            if (isLinear)
+            {
+                var sampler = new LinearSampler<Vector2>(collection, SamplerTraits.Vector2);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
+            else
+            {
+                var sampler = new StepSampler<Vector2>(collection, SamplerTraits.Vector2);
+                return optimize ? sampler.ToFastSampler() : sampler;    
+            }
+        }
+
         public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, Vector3)> collection, bool isLinear = true, bool optimize = false)
         public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, Vector3)> collection, bool isLinear = true, bool optimize = false)
         {
         {
             if (collection._HasZero()) return null;
             if (collection._HasZero()) return null;
@@ -417,6 +472,23 @@ namespace SharpGLTF.Animations
             }
             }
         }
         }
 
 
+        public static ICurveSampler<Vector4> CreateSampler(this IEnumerable<(Single, Vector4)> collection, bool isLinear = true, bool optimize = false)
+        {
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Vector4>.Create(collection);
+
+            if (isLinear)
+            {
+                var sampler = new LinearSampler<Vector4>(collection, SamplerTraits.Vector4);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
+            else
+            {
+                var sampler = new StepSampler<Vector4>(collection, SamplerTraits.Vector4);
+                return optimize ? sampler.ToFastSampler() : sampler;
+            }
+        }
+
         public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(Single, Quaternion)> collection, bool isLinear = true, bool optimize = false)
         public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(Single, Quaternion)> collection, bool isLinear = true, bool optimize = false)
         {
         {
             if (collection._HasZero()) return null;
             if (collection._HasZero()) return null;
@@ -485,6 +557,24 @@ namespace SharpGLTF.Animations
             }
             }
         }
         }
 
 
+        public static ICurveSampler<Single> CreateSampler(this IEnumerable<(Single, (Single, Single, Single))> collection, bool optimize = false)
+        {
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Single>.Create(collection);
+
+            var sampler = new CubicSampler<Single>(collection, SamplerTraits.Scalar);
+            return optimize ? sampler.ToFastSampler() : sampler;
+        }
+
+        public static ICurveSampler<Vector2> CreateSampler(this IEnumerable<(Single, (Vector2, Vector2, Vector2))> collection, bool optimize = false)
+        {
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Vector2>.Create(collection);
+
+            var sampler = new CubicSampler<Vector2>(collection, SamplerTraits.Vector2);
+            return optimize ? sampler.ToFastSampler() : sampler;
+        }
+
         public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, (Vector3, Vector3, Vector3))> collection, bool optimize = false)
         public static ICurveSampler<Vector3> CreateSampler(this IEnumerable<(Single, (Vector3, Vector3, Vector3))> collection, bool optimize = false)
         {
         {
             if (collection._HasZero()) return null;
             if (collection._HasZero()) return null;
@@ -494,6 +584,15 @@ namespace SharpGLTF.Animations
             return optimize ? sampler.ToFastSampler() : sampler;
             return optimize ? sampler.ToFastSampler() : sampler;
         }
         }
 
 
+        public static ICurveSampler<Vector4> CreateSampler(this IEnumerable<(Single, (Vector4, Vector4, Vector4))> collection, bool optimize = false)
+        {
+            if (collection._HasZero()) return null;
+            if (collection._HasOne()) return FixedSampler<Vector4>.Create(collection);
+
+            var sampler = new CubicSampler<Vector4>(collection, SamplerTraits.Vector4);
+            return optimize ? sampler.ToFastSampler() : sampler;
+        }
+
         public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(Single, (Quaternion, Quaternion, Quaternion))> collection, bool optimize = false)
         public static ICurveSampler<Quaternion> CreateSampler(this IEnumerable<(Single, (Quaternion, Quaternion, Quaternion))> collection, bool optimize = false)
         {
         {
             if (collection._HasZero()) return null;
             if (collection._HasZero()) return null;

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

@@ -18,6 +18,26 @@ namespace SharpGLTF.Animations
 
 
     static class SamplerTraits
     static class SamplerTraits
     {
     {
+        sealed class _Scalar : ISamplerTraits<Single>
+        {
+            public Single Clone(Single value) { return value; }
+            public Single InterpolateLinear(Single left, Single right, float amount) { return left * (1f-amount) + right * amount; }
+            public Single InterpolateCubic(Single start, Single outgoingTangent, Single end, Single incomingTangent, Single amount)
+            {
+                return CurveSampler.InterpolateCubic(start, outgoingTangent, end, incomingTangent, amount);
+            }
+        }
+
+        sealed class _Vector2 : ISamplerTraits<Vector2>
+        {
+            public Vector2 Clone(Vector2 value) { return value; }
+            public Vector2 InterpolateLinear(Vector2 left, Vector2 right, float amount) { return System.Numerics.Vector2.Lerp(left, right, amount); }
+            public Vector2 InterpolateCubic(Vector2 start, Vector2 outgoingTangent, Vector2 end, Vector2 incomingTangent, Single amount)
+            {
+                return CurveSampler.InterpolateCubic(start, outgoingTangent, end, incomingTangent, amount);
+            }
+        }
+
         sealed class _Vector3 : ISamplerTraits<Vector3>
         sealed class _Vector3 : ISamplerTraits<Vector3>
         {
         {
             public Vector3 Clone(Vector3 value) { return value; }
             public Vector3 Clone(Vector3 value) { return value; }
@@ -28,6 +48,16 @@ namespace SharpGLTF.Animations
             }
             }
         }
         }
 
 
+        sealed class _Vector4 : ISamplerTraits<Vector4>
+        {
+            public Vector4 Clone(Vector4 value) { return value; }
+            public Vector4 InterpolateLinear(Vector4 left, Vector4 right, float amount) { return System.Numerics.Vector4.Lerp(left, right, amount); }
+            public Vector4 InterpolateCubic(Vector4 start, Vector4 outgoingTangent, Vector4 end, Vector4 incomingTangent, Single amount)
+            {
+                return CurveSampler.InterpolateCubic(start, outgoingTangent, end, incomingTangent, amount);
+            }
+        }
+
         sealed class _Quaternion : ISamplerTraits<Quaternion>
         sealed class _Quaternion : ISamplerTraits<Quaternion>
         {
         {
             public Quaternion Clone(Quaternion value) { return value; }
             public Quaternion Clone(Quaternion value) { return value; }
@@ -80,7 +110,10 @@ namespace SharpGLTF.Animations
             }
             }
         }
         }
 
 
+        public static readonly ISamplerTraits<Single> Scalar = new _Scalar();
+        public static readonly ISamplerTraits<Vector2> Vector2 = new _Vector2();
         public static readonly ISamplerTraits<Vector3> Vector3 = new _Vector3();
         public static readonly ISamplerTraits<Vector3> Vector3 = new _Vector3();
+        public static readonly ISamplerTraits<Vector4> Vector4 = new _Vector4();
         public static readonly ISamplerTraits<Quaternion> Quaternion = new _Quaternion();
         public static readonly ISamplerTraits<Quaternion> Quaternion = new _Quaternion();
         public static readonly ISamplerTraits<Single[]> Array = new _Array();
         public static readonly ISamplerTraits<Single[]> Array = new _Array();
         public static readonly ISamplerTraits<SPARSE> Sparse = new _Sparse();
         public static readonly ISamplerTraits<SPARSE> Sparse = new _Sparse();

+ 60 - 0
src/SharpGLTF.Core/Schema2/Generated/ext.AnimPointer.g.cs

@@ -0,0 +1,60 @@
+// <auto-generated/>
+
+//------------------------------------------------------------------------------------------------
+//      This file has been programatically generated; DON´T EDIT!
+//------------------------------------------------------------------------------------------------
+
+#pragma warning disable SA1001
+#pragma warning disable SA1027
+#pragma warning disable SA1028
+#pragma warning disable SA1121
+#pragma warning disable SA1205
+#pragma warning disable SA1309
+#pragma warning disable SA1402
+#pragma warning disable SA1505
+#pragma warning disable SA1507
+#pragma warning disable SA1508
+#pragma warning disable SA1652
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Numerics;
+using System.Text.Json;
+
+namespace SharpGLTF.Schema2
+{
+	using Collections;
+
+	/// <summary>
+	/// Extension object providing the JSON Pointer to the animated property.
+	/// </summary>
+	#if NET6_0_OR_GREATER
+	[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+	#endif
+	[global::System.CodeDom.Compiler.GeneratedCodeAttribute("SharpGLTF.CodeGen", "1.0.0.0")]
+	partial class AnimationPointer : ExtraProperties
+	{
+	
+		private String _pointer;
+		
+	
+		protected override void SerializeProperties(Utf8JsonWriter writer)
+		{
+			base.SerializeProperties(writer);
+			SerializeProperty(writer, "pointer", _pointer);
+		}
+	
+		protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader)
+		{
+			switch (jsonPropertyName)
+			{
+				case "pointer": _pointer = DeserializePropertyValue<String>(ref reader); break;
+				default: base.DeserializeProperty(jsonPropertyName,ref reader); break;
+			}
+		}
+	
+	}
+
+}

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

@@ -585,7 +585,7 @@ namespace SharpGLTF.Schema2
         {
         {
             SourceBufferView.ValidateBufferUsagePlainData(validate, false); // as per glTF specification, animation accessors must not have ByteStride
             SourceBufferView.ValidateBufferUsagePlainData(validate, false); // as per glTF specification, animation accessors must not have ByteStride
 
 
-            validate.IsAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR, DimensionType.VEC3, DimensionType.VEC4);
+            validate.IsAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR, DimensionType.VEC2, DimensionType.VEC3, DimensionType.VEC4);
         }
         }
 
 
         #endregion
         #endregion

+ 49 - 39
src/SharpGLTF.Core/Schema2/gltf.AnimationChannel.cs

@@ -7,54 +7,31 @@ using System.Numerics;
 using SharpGLTF.Collections;
 using SharpGLTF.Collections;
 using SharpGLTF.Transforms;
 using SharpGLTF.Transforms;
 using SharpGLTF.Validation;
 using SharpGLTF.Validation;
+using System.Xml.Linq;
+using System.IO;
+using System.Reflection;
 
 
 namespace SharpGLTF.Schema2
 namespace SharpGLTF.Schema2
 {
 {
-    sealed partial class AnimationChannelTarget
+    [System.Diagnostics.DebuggerDisplay("AnimChannel {TargetPointerPath}")]
+    public sealed partial class AnimationChannel : IChildOfList<Animation>
     {
     {
         #region lifecycle
         #region lifecycle
+        internal AnimationChannel() { }
 
 
-        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)
+        /// <summary>
+        /// Sets the target property of this animation channel
+        /// </summary>
+        /// <param name="pointerPath">The path, as defined by AnimationChannel, as in '/nodes/0/rotation'</param>
+        internal AnimationChannel(string pointerPath)
         {
         {
-            base.OnValidateReferences(validate);
-
-            validate.IsNullOrIndex("Node", _node, validate.Root.LogicalNodes);
+            _SetChannelTarget(new AnimationChannelTarget(pointerPath));            
+            _sampler = -1;
         }
         }
 
 
-        #endregion
-    }
-
-    [System.Diagnostics.DebuggerDisplay("AnimChannel LogicalNode[{TargetNode.LogicalIndex}].{TargetNodePath}")]
-    public sealed partial class AnimationChannel : IChildOfList<Animation>
-    {
-        #region lifecycle
-        internal AnimationChannel() { }
-
         internal AnimationChannel(Node targetNode, PropertyPath targetPath)
         internal AnimationChannel(Node targetNode, PropertyPath targetPath)
         {
         {
-            _target = new AnimationChannelTarget(targetNode, targetPath);
+            _SetChannelTarget(new AnimationChannelTarget(targetNode, targetPath));            
             _sampler = -1;
             _sampler = -1;
         }
         }
 
 
@@ -72,6 +49,15 @@ namespace SharpGLTF.Schema2
             LogicalIndex = index;
             LogicalIndex = index;
         }
         }
 
 
+        protected override IEnumerable<ExtraProperties> GetLogicalChildren()
+        {
+            var children = base.GetLogicalChildren();
+
+            if (_target != null) children = children.Append(_target);
+
+            return children;
+        }
+
         #endregion
         #endregion
 
 
         #region properties
         #region properties
@@ -87,14 +73,30 @@ namespace SharpGLTF.Schema2
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         public Animation LogicalParent { get; private set; }
         public Animation LogicalParent { get; private set; }
 
 
+        /// <summary>
+        /// Gets the path to the property being animated by this channel.
+        /// </summary>
+        /// <remarks>
+        /// <para>
+        /// The format is defined by <see href="https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_animation_pointer">KHR_animation_pointer</see>
+        /// </para>
+        /// <para>
+        /// examples:<br/>
+        /// "/nodes/0/rotation"<br/>
+        /// "/materials/0/pbrMetallicRoughness/baseColorFactor"<br/>
+        /// </para>
+        /// </remarks>
+        public string TargetPointerPath => this._target?.GetPointerPath() ?? null;
+
         /// <summary>
         /// <summary>
         /// Gets the <see cref="Node"/> which property is to be bound with this animation.
         /// Gets the <see cref="Node"/> which property is to be bound with this animation.
         /// </summary>
         /// </summary>
+        [Obsolete("Use TargetPointerPath whenever possible")]
         public Node TargetNode
         public Node TargetNode
         {
         {
             get
             get
             {
             {
-                var idx = this._target?._NodeId ?? -1;
+                var idx = this._target?.GetNodeIndex() ?? -1;
                 if (idx < 0) return null;
                 if (idx < 0) return null;
                 return this.LogicalParent.LogicalParent.LogicalNodes[idx];
                 return this.LogicalParent.LogicalParent.LogicalNodes[idx];
             }
             }
@@ -103,12 +105,20 @@ namespace SharpGLTF.Schema2
         /// <summary>
         /// <summary>
         /// Gets which property of the <see cref="Node"/> pointed by <see cref="TargetNode"/> is to be bound with this animation.
         /// Gets which property of the <see cref="Node"/> pointed by <see cref="TargetNode"/> is to be bound with this animation.
         /// </summary>
         /// </summary>
-        public PropertyPath TargetNodePath => this._target?._NodePath ?? PropertyPath.translation;
+        /// <remarks>
+        /// If the target is anything other than a <see cref="Node"/> transform property, the returned value will be <see cref="PropertyPath.pointer"/>
+        /// </remarks>
+        public PropertyPath TargetNodePath => this._target?.GetNodePath() ?? PropertyPath.translation;
 
 
         #endregion
         #endregion
 
 
         #region API
         #region API
 
 
+        private void _SetChannelTarget(AnimationChannelTarget target)
+        {
+            new Collections.ChildSetter<AnimationChannel>(this).SetProperty(ref _target, target);
+        }
+
         internal AnimationSampler _GetSampler() { return this.LogicalParent._Samplers[this._sampler]; }
         internal AnimationSampler _GetSampler() { return this.LogicalParent._Samplers[this._sampler]; }
 
 
         public IAnimationSampler<Vector3> GetScaleSampler()
         public IAnimationSampler<Vector3> GetScaleSampler()

+ 188 - 0
src/SharpGLTF.Core/Schema2/gltf.AnimationChannelTarget.cs

@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Linq;
+using System.Numerics;
+
+using SharpGLTF.Collections;
+using SharpGLTF.Transforms;
+using SharpGLTF.Validation;
+using System.Xml.Linq;
+using System.IO;
+using System.Reflection;
+
+namespace SharpGLTF.Schema2
+{
+    /// <summary>
+    /// Child of <see cref="AnimationChannel"/>
+    /// </summary>
+    sealed partial class AnimationChannelTarget : IChildOf<AnimationChannel>
+    {
+        #region lifecycle
+
+        internal AnimationChannelTarget() { }
+
+        internal AnimationChannelTarget(Node targetNode, PropertyPath targetPath)
+        {
+            _node = targetNode.LogicalIndex;
+            _path = targetPath;
+        }
+
+        internal AnimationChannelTarget(string pointer)
+        {
+            if (AnimationPointer.TryParseNodeTransform(pointer, out var nidx, out var nprop))
+            {
+                _node = nidx;
+                _path = nprop;
+                this.RemoveExtensions<AnimationPointer>();
+            }
+            else
+            {
+                _node = null;
+                _path = PropertyPath.pointer;
+                var aptr = this.UseExtension<AnimationPointer>();
+                aptr.Pointer = pointer;
+            }
+        }
+
+        AnimationChannel IChildOf<AnimationChannel>.LogicalParent => _Parent;
+
+        void IChildOf<AnimationChannel>.SetLogicalParent(AnimationChannel parent)
+        {
+            _Parent = parent;
+        }
+
+        #endregion
+
+        #region data
+
+        private AnimationChannel _Parent;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        internal PropertyPath _NodePath => this._path;
+
+        #endregion
+
+        #region API
+
+        public int GetNodeIndex()
+        {
+            if (this._node != null) return this._node.Value;
+
+            if (_NodePath == PropertyPath.pointer)
+            {
+                var aptr = this.GetExtension<AnimationPointer>();
+                if (aptr != null && AnimationPointer.TryParseNodeTransform(aptr.Pointer, out var nidx, out _)) return nidx;
+            }
+
+            return -1;
+        }
+
+        public PropertyPath GetNodePath()
+        {
+            if (_NodePath == PropertyPath.pointer)
+            {
+                var aptr = this.GetExtension<AnimationPointer>();
+                if (aptr != null && AnimationPointer.TryParseNodeTransform(aptr.Pointer, out _, out var nprop)) return nprop;
+            }
+
+            return _NodePath;
+        }
+
+        public string GetPointerPath()
+        {
+            var aptr = this.GetExtension<AnimationPointer>();
+            if (aptr != null) return aptr.Pointer;
+
+            if (this._node == null || this._node.Value < 0) return null;
+
+            return $"/nodes/{this._node.Value}/{_NodePath}";
+        }
+
+        #endregion
+
+        #region Validation
+
+        protected override void OnValidateReferences(ValidationContext validate)
+        {
+            base.OnValidateReferences(validate);
+
+            validate.IsNullOrIndex("Node", _node, validate.Root.LogicalNodes);
+
+            var aptr = this.GetExtension<AnimationPointer>();
+            if (aptr != null)
+            {
+
+            }
+        }
+
+        
+
+        #endregion
+    }
+
+    /// <summary>
+    /// Extends <see cref="AnimationChannelTarget"/> with extra targets
+    /// </summary>
+    /// <remarks>
+    /// <see href="https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_animation_pointer">KHR_animation_pointer specification</see>
+    /// </remarks>
+    sealed partial class AnimationPointer
+    {
+        #region lifecycle
+        public AnimationPointer(AnimationChannelTarget parent)
+        {
+            _LogicalParent = parent;
+        }
+
+        private AnimationChannelTarget _LogicalParent;
+
+        #endregion
+
+        #region properties
+
+        public string Pointer
+        {
+            get => this._pointer;
+            set => this._pointer = value;
+        }
+
+        #endregion
+
+        #region API
+
+        /// <summary>
+        /// Parses the pointer path to see if it can be converted to a standard nodeIndex-PropertyPath path.
+        /// </summary>
+        /// <param name="pointer">The path to try parse.</param>
+        /// <param name="nodeIndex">the logical index of the node.</param>
+        /// <param name="property">the transformation property.</param>
+        /// <returns>true if the parsing succeeded.</returns>
+        public static bool TryParseNodeTransform(string pointer, out int nodeIndex, out PropertyPath property)
+        {
+            nodeIndex = -1;
+            property = PropertyPath.pointer;
+
+            if (pointer == null || !pointer.StartsWith("/nodes/")) return false;
+
+
+            pointer = pointer.Substring(7);
+            var next = pointer.IndexOf('/');
+
+            if (!int.TryParse(pointer.Substring(0, next), System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var idx)) return false;
+
+            var tail = pointer.Substring(next + 1);
+            switch (tail)
+            {
+                case "scale": nodeIndex = idx; property = PropertyPath.scale; return true;
+                case "rotation": nodeIndex = idx; property = PropertyPath.rotation; return true;
+                case "translation": nodeIndex = idx; property = PropertyPath.translation; return true;
+                case "weights": nodeIndex = idx; property = PropertyPath.weights; return true;
+            }
+
+            return false;
+        }
+
+        #endregion
+    }
+}

+ 261 - 0
src/SharpGLTF.Core/Schema2/gltf.AnimationSampler.cs

@@ -66,7 +66,10 @@ namespace SharpGLTF.Schema2
     /// </remarks>
     /// </remarks>
     sealed partial class AnimationSampler :
     sealed partial class AnimationSampler :
         IChildOfList<Animation>,
         IChildOfList<Animation>,
+        IAnimationSampler<Single>,
+        IAnimationSampler<Vector2>,
         IAnimationSampler<Vector3>,
         IAnimationSampler<Vector3>,
+        IAnimationSampler<Vector4>,
         IAnimationSampler<Quaternion>,
         IAnimationSampler<Quaternion>,
         IAnimationSampler<SPARSE8>,
         IAnimationSampler<SPARSE8>,
         IAnimationSampler<SEGMENT>,
         IAnimationSampler<SEGMENT>,
@@ -144,6 +147,50 @@ namespace SharpGLTF.Schema2
             return accessor;
             return accessor;
         }
         }
 
 
+        private Accessor _CreateOutputAccessor(IReadOnlyList<Single> 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 * 1);
+
+            System.Diagnostics.Debug.Assert(buffer.ByteStride == 0);
+
+            var accessor = root.CreateAccessor("Animation.Output");
+
+            accessor.SetData(buffer, 0, output.Count, DimensionType.SCALAR, EncodingType.FLOAT, false);
+
+            Memory.EncodedArrayUtils._CopyTo(output, accessor.AsScalarArray());
+
+            accessor.UpdateBounds();
+
+            return accessor;
+        }
+
+        private Accessor _CreateOutputAccessor(IReadOnlyList<Vector2> 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 * 2);
+
+            System.Diagnostics.Debug.Assert(buffer.ByteStride == 0);
+
+            var accessor = root.CreateAccessor("Animation.Output");
+
+            accessor.SetData(buffer, 0, output.Count, DimensionType.VEC2, EncodingType.FLOAT, false);
+
+            Memory.EncodedArrayUtils._CopyTo(output, accessor.AsVector2Array());
+
+            accessor.UpdateBounds();
+
+            return accessor;
+        }
+
         private Accessor _CreateOutputAccessor(IReadOnlyList<Vector3> output)
         private Accessor _CreateOutputAccessor(IReadOnlyList<Vector3> output)
         {
         {
             Guard.NotNull(output, nameof(output));
             Guard.NotNull(output, nameof(output));
@@ -166,6 +213,28 @@ namespace SharpGLTF.Schema2
             return accessor;
             return accessor;
         }
         }
 
 
+        private Accessor _CreateOutputAccessor(IReadOnlyList<Vector4> 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);
+
+            System.Diagnostics.Debug.Assert(buffer.ByteStride == 0);
+
+            var accessor = root.CreateAccessor("Animation.Output");
+
+            accessor.SetData(buffer, 0, output.Count, DimensionType.VEC4, EncodingType.FLOAT, false);
+
+            Memory.EncodedArrayUtils._CopyTo(output, accessor.AsVector4Array());
+
+            accessor.UpdateBounds();
+
+            return accessor;
+        }
+
         private Accessor _CreateOutputAccessor(IReadOnlyList<Quaternion> output)
         private Accessor _CreateOutputAccessor(IReadOnlyList<Quaternion> output)
         {
         {
             Guard.NotNull(output, nameof(output));
             Guard.NotNull(output, nameof(output));
@@ -274,6 +343,24 @@ namespace SharpGLTF.Schema2
             return (keys, vals);
             return (keys, vals);
         }
         }
 
 
+        internal void SetKeys(IReadOnlyDictionary<Single, Single> keyframes)
+        {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
+            var (keys, values) = _Split(keyframes);
+            _input = this._CreateInputAccessor(keys).LogicalIndex;
+            _output = this._CreateOutputAccessor(values).LogicalIndex;
+        }
+
+        internal void SetKeys(IReadOnlyDictionary<Single, Vector2> keyframes)
+        {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
+            var (keys, values) = _Split(keyframes);
+            _input = this._CreateInputAccessor(keys).LogicalIndex;
+            _output = this._CreateOutputAccessor(values).LogicalIndex;
+        }
+
         internal void SetKeys(IReadOnlyDictionary<Single, Vector3> keyframes)
         internal void SetKeys(IReadOnlyDictionary<Single, Vector3> keyframes)
         {
         {
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
@@ -283,6 +370,15 @@ namespace SharpGLTF.Schema2
             _output = this._CreateOutputAccessor(values).LogicalIndex;
             _output = this._CreateOutputAccessor(values).LogicalIndex;
         }
         }
 
 
+        internal void SetKeys(IReadOnlyDictionary<Single, Vector4> keyframes)
+        {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
+            var (keys, values) = _Split(keyframes);
+            _input = this._CreateInputAccessor(keys).LogicalIndex;
+            _output = this._CreateOutputAccessor(values).LogicalIndex;
+        }
+
         internal void SetKeys(IReadOnlyDictionary<Single, Quaternion> keyframes)
         internal void SetKeys(IReadOnlyDictionary<Single, Quaternion> keyframes)
         {
         {
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
@@ -312,6 +408,42 @@ namespace SharpGLTF.Schema2
             _output = this._CreateOutputAccessor(values, itemsStride).LogicalIndex;
             _output = this._CreateOutputAccessor(values, itemsStride).LogicalIndex;
         }
         }
 
 
+        internal void SetCubicKeys(IReadOnlyDictionary<Single, (Single TangentIn, Single Value, Single 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] = 0f;
+            values[values.Length - 1] = 0f;
+
+            _input = this._CreateInputAccessor(keys).LogicalIndex;
+            _output = this._CreateOutputAccessor(values).LogicalIndex;
+        }
+
+        internal void SetCubicKeys(IReadOnlyDictionary<Single, (Vector2 TangentIn, Vector2 Value, Vector2 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] = Vector2.Zero;
+            values[values.Length - 1] = Vector2.Zero;
+
+            _input = this._CreateInputAccessor(keys).LogicalIndex;
+            _output = this._CreateOutputAccessor(values).LogicalIndex;
+        }
+
         internal void SetCubicKeys(IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         internal void SetCubicKeys(IReadOnlyDictionary<Single, (Vector3 TangentIn, Vector3 Value, Vector3 TangentOut)> keyframes)
         {
         {
             Guard.NotNull(keyframes, nameof(keyframes));
             Guard.NotNull(keyframes, nameof(keyframes));
@@ -330,6 +462,24 @@ namespace SharpGLTF.Schema2
             _output = this._CreateOutputAccessor(values).LogicalIndex;
             _output = this._CreateOutputAccessor(values).LogicalIndex;
         }
         }
 
 
+        internal void SetCubicKeys(IReadOnlyDictionary<Single, (Vector4 TangentIn, Vector4 Value, Vector4 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] = Vector4.Zero;
+            values[values.Length - 1] = Vector4.Zero;
+
+            _input = this._CreateInputAccessor(keys).LogicalIndex;
+            _output = this._CreateOutputAccessor(values).LogicalIndex;
+        }
+
         internal void SetCubicKeys(IReadOnlyDictionary<Single, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> keyframes)
         internal void SetCubicKeys(IReadOnlyDictionary<Single, (Quaternion TangentIn, Quaternion Value, Quaternion TangentOut)> keyframes)
         {
         {
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
@@ -382,6 +532,28 @@ namespace SharpGLTF.Schema2
             _output = this._CreateOutputAccessor(values, expandedCount).LogicalIndex;
             _output = this._CreateOutputAccessor(values, expandedCount).LogicalIndex;
         }
         }
 
 
+        /// <inheritdoc/>
+        IEnumerable<(Single, Single)> IAnimationSampler<Single>.GetLinearKeys()
+        {
+            Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
+
+            var keys = this.Input.AsScalarArray();
+            var frames = this.Output.AsScalarArray();
+
+            return keys.Zip(frames, (key, val) => (key, val));
+        }
+
+        /// <inheritdoc/>
+        IEnumerable<(Single, Vector2)> IAnimationSampler<Vector2>.GetLinearKeys()
+        {
+            Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
+
+            var keys = this.Input.AsScalarArray();
+            var frames = this.Output.AsVector2Array();
+
+            return keys.Zip(frames, (key, val) => (key, val));
+        }
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         IEnumerable<(Single, Vector3)> IAnimationSampler<Vector3>.GetLinearKeys()
         IEnumerable<(Single, Vector3)> IAnimationSampler<Vector3>.GetLinearKeys()
         {
         {
@@ -393,6 +565,17 @@ namespace SharpGLTF.Schema2
             return keys.Zip(frames, (key, val) => (key, val));
             return keys.Zip(frames, (key, val) => (key, val));
         }
         }
 
 
+        /// <inheritdoc/>
+        IEnumerable<(Single, Vector4)> IAnimationSampler<Vector4>.GetLinearKeys()
+        {
+            Guard.IsFalse(this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
+
+            var keys = this.Input.AsScalarArray();
+            var frames = this.Output.AsVector4Array();
+
+            return keys.Zip(frames, (key, val) => (key, val));
+        }
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         IEnumerable<(Single, Quaternion)> IAnimationSampler<Quaternion>.GetLinearKeys()
         IEnumerable<(Single, Quaternion)> IAnimationSampler<Quaternion>.GetLinearKeys()
         {
         {
@@ -443,6 +626,28 @@ namespace SharpGLTF.Schema2
             return keys.Zip(frames, (key, val) => (key, val));
             return keys.Zip(frames, (key, val) => (key, val));
         }
         }
 
 
+        /// <inheritdoc/>
+        IEnumerable<(Single, (Single, Single, Single))> IAnimationSampler<Single>.GetCubicKeys()
+        {
+            Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
+
+            var keys = this.Input.AsScalarArray();
+            var frames = _GroupByTangentValueTangent(this.Output.AsScalarArray());
+
+            return keys.Zip(frames, (key, val) => (key, val));
+        }
+
+        /// <inheritdoc/>
+        IEnumerable<(Single, (Vector2, Vector2, Vector2))> IAnimationSampler<Vector2>.GetCubicKeys()
+        {
+            Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
+
+            var keys = this.Input.AsScalarArray();
+            var frames = _GroupByTangentValueTangent(this.Output.AsVector2Array());
+
+            return keys.Zip(frames, (key, val) => (key, val));
+        }
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         IEnumerable<(Single, (Vector3, Vector3, Vector3))> IAnimationSampler<Vector3>.GetCubicKeys()
         IEnumerable<(Single, (Vector3, Vector3, Vector3))> IAnimationSampler<Vector3>.GetCubicKeys()
         {
         {
@@ -454,6 +659,17 @@ namespace SharpGLTF.Schema2
             return keys.Zip(frames, (key, val) => (key, val));
             return keys.Zip(frames, (key, val) => (key, val));
         }
         }
 
 
+        /// <inheritdoc/>
+        IEnumerable<(Single, (Vector4, Vector4, Vector4))> IAnimationSampler<Vector4>.GetCubicKeys()
+        {
+            Guard.IsFalse(this.InterpolationMode != AnimationInterpolationMode.CUBICSPLINE, nameof(InterpolationMode));
+
+            var keys = this.Input.AsScalarArray();
+            var frames = _GroupByTangentValueTangent(this.Output.AsVector4Array());
+
+            return keys.Zip(frames, (key, val) => (key, val));
+        }
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         IEnumerable<(Single, (Quaternion, Quaternion, Quaternion))> IAnimationSampler<Quaternion>.GetCubicKeys()
         IEnumerable<(Single, (Quaternion, Quaternion, Quaternion))> IAnimationSampler<Quaternion>.GetCubicKeys()
         {
         {
@@ -503,6 +719,36 @@ namespace SharpGLTF.Schema2
             return keys.Zip(frames, (key, val) => (key, SPARSE8.AsTuple(val.TangentIn, val.Value, val.TangentOut)) );
             return keys.Zip(frames, (key, val) => (key, SPARSE8.AsTuple(val.TangentIn, val.Value, val.TangentOut)) );
         }
         }
 
 
+        /// <inheritdoc/>
+        ICurveSampler<Single> IAnimationSampler<Single>.CreateCurveSampler(bool isolateMemory)
+        {
+            var xsampler = this as IAnimationSampler<Single>;
+
+            switch (this.InterpolationMode)
+            {
+                case AnimationInterpolationMode.STEP: return xsampler.GetLinearKeys().CreateSampler(false, isolateMemory);
+                case AnimationInterpolationMode.LINEAR: return xsampler.GetLinearKeys().CreateSampler(true, isolateMemory);
+                case AnimationInterpolationMode.CUBICSPLINE: return xsampler.GetCubicKeys().CreateSampler(isolateMemory);
+            }
+
+            throw new NotImplementedException();
+        }
+
+        /// <inheritdoc/>
+        ICurveSampler<Vector2> IAnimationSampler<Vector2>.CreateCurveSampler(bool isolateMemory)
+        {
+            var xsampler = this as IAnimationSampler<Vector2>;
+
+            switch (this.InterpolationMode)
+            {
+                case AnimationInterpolationMode.STEP: return xsampler.GetLinearKeys().CreateSampler(false, isolateMemory);
+                case AnimationInterpolationMode.LINEAR: return xsampler.GetLinearKeys().CreateSampler(true, isolateMemory);
+                case AnimationInterpolationMode.CUBICSPLINE: return xsampler.GetCubicKeys().CreateSampler(isolateMemory);
+            }
+
+            throw new NotImplementedException();
+        }
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         ICurveSampler<Vector3> IAnimationSampler<Vector3>.CreateCurveSampler(bool isolateMemory)
         ICurveSampler<Vector3> IAnimationSampler<Vector3>.CreateCurveSampler(bool isolateMemory)
         {
         {
@@ -518,6 +764,21 @@ namespace SharpGLTF.Schema2
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
 
 
+        /// <inheritdoc/>
+        ICurveSampler<Vector4> IAnimationSampler<Vector4>.CreateCurveSampler(bool isolateMemory)
+        {
+            var xsampler = this as IAnimationSampler<Vector4>;
+
+            switch (this.InterpolationMode)
+            {
+                case AnimationInterpolationMode.STEP: return xsampler.GetLinearKeys().CreateSampler(false, isolateMemory);
+                case AnimationInterpolationMode.LINEAR: return xsampler.GetLinearKeys().CreateSampler(true, isolateMemory);
+                case AnimationInterpolationMode.CUBICSPLINE: return xsampler.GetCubicKeys().CreateSampler(isolateMemory);
+            }
+
+            throw new NotImplementedException();
+        }
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         ICurveSampler<Quaternion> IAnimationSampler<Quaternion>.CreateCurveSampler(bool isolateMemory)
         ICurveSampler<Quaternion> IAnimationSampler<Quaternion>.CreateCurveSampler(bool isolateMemory)
         {
         {

+ 60 - 6
src/SharpGLTF.Core/Schema2/gltf.Animations.cs

@@ -10,6 +10,7 @@ using SharpGLTF.Animations;
 using SharpGLTF.Validation;
 using SharpGLTF.Validation;
 
 
 using WEIGHTS = System.Collections.Generic.IReadOnlyList<float>;
 using WEIGHTS = System.Collections.Generic.IReadOnlyList<float>;
+using System.Xml.Linq;
 
 
 namespace SharpGLTF.Schema2
 namespace SharpGLTF.Schema2
 {
 {
@@ -43,24 +44,29 @@ namespace SharpGLTF.Schema2
             return base.GetLogicalChildren().Concat(_samplers).Concat(_channels);
             return base.GetLogicalChildren().Concat(_samplers).Concat(_channels);
         }
         }
 
 
+        public IEnumerable<AnimationChannel> FindChannels(string rootPath)
+        {
+            return Channels.Where(item => item.TargetPointerPath.StartsWith(rootPath));
+        }
+
         public IEnumerable<AnimationChannel> FindChannels(Node node)
         public IEnumerable<AnimationChannel> FindChannels(Node node)
         {
         {
             Guard.NotNull(node, nameof(node));
             Guard.NotNull(node, nameof(node));
             Guard.MustShareLogicalParent(this, node, nameof(node));
             Guard.MustShareLogicalParent(this, node, nameof(node));
 
 
             return Channels.Where(item => item.TargetNode == 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 FindScaleChannel(Node node) => _FindChannel(node, PropertyPath.scale);
         public AnimationChannel FindRotationChannel(Node node) => _FindChannel(node, PropertyPath.rotation);
         public AnimationChannel FindRotationChannel(Node node) => _FindChannel(node, PropertyPath.rotation);
         public AnimationChannel FindTranslationChannel(Node node) => _FindChannel(node, PropertyPath.translation);
         public AnimationChannel FindTranslationChannel(Node node) => _FindChannel(node, PropertyPath.translation);
         public AnimationChannel FindMorphChannel(Node node) => _FindChannel(node, PropertyPath.weights);
         public AnimationChannel FindMorphChannel(Node node) => _FindChannel(node, PropertyPath.weights);
 
 
+        private AnimationChannel _FindChannel(Node node, PropertyPath path)
+        {
+            return FindChannels(node).FirstOrDefault(item => item.TargetNodePath == path);
+        }
+
         #endregion
         #endregion
 
 
         #region API - Create
         #region API - Create
@@ -92,6 +98,54 @@ namespace SharpGLTF.Schema2
             return channel;
             return channel;
         }
         }
 
 
+        /// <remarks>
+        /// There can only be one <see cref="AnimationChannel"/> for every node and path
+        /// </remarks>
+        private AnimationChannel _UseChannel(string pointerPath)
+        {
+            var channel = new AnimationChannel(pointerPath);
+
+            _channels.Add(channel);
+
+            return channel;
+        }
+
+        public void CreateMaterialPropertyChannel<T>(Material material, string propertyName, IReadOnlyDictionary<Single, T> keyframes, bool linear = true)
+        {
+            Guard.NotNull(material, nameof(material));
+            Guard.MustShareLogicalParent(this, material, nameof(material));
+            Guard.NotNullOrEmpty(propertyName, nameof(propertyName));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
+            DangerousCreatePointerChannel<T>($"/materials/{material.LogicalIndex}/{propertyName}", keyframes, linear);
+        }
+
+        /// <summary>
+        /// Creates an animation channel, dynamically targeting an object in the DOM using a KHR_animation_pointer path.
+        /// </summary>
+        /// <typeparam name="T">The type of the property targeted by the path.</typeparam>
+        /// <param name="pointerPath">The path to the porperty, ex: '/nodes/0/rotation'.</param>
+        /// <param name="keyframes">The keyframes to set</param>
+        /// <param name="linear">Whether the keyframes are linearly interporlated or not.</param>        
+        public void DangerousCreatePointerChannel<T>(string pointerPath, IReadOnlyDictionary<Single, T> keyframes, bool linear = true)
+        {
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+
+            var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
+
+            switch(keyframes)
+            {
+                case IReadOnlyDictionary<Single, Single> typed:  sampler.SetKeys(typed); break;
+                case IReadOnlyDictionary<Single, Vector2> typed: sampler.SetKeys(typed); break;
+                case IReadOnlyDictionary<Single, Vector3> typed: sampler.SetKeys(typed); break;
+                case IReadOnlyDictionary<Single, Vector4> typed: sampler.SetKeys(typed); break;
+                case IReadOnlyDictionary<Single, Quaternion> typed: sampler.SetKeys(typed); break;
+                default: throw new NotSupportedException(typeof(T).Name);
+            }            
+
+            this._UseChannel(pointerPath).SetSampler(sampler);
+        }
+
         public void CreateScaleChannel(Node node, IReadOnlyDictionary<Single, Vector3> keyframes, bool linear = true)
         public void CreateScaleChannel(Node node, IReadOnlyDictionary<Single, Vector3> keyframes, bool linear = true)
         {
         {
             Guard.NotNull(node, nameof(node));
             Guard.NotNull(node, nameof(node));

+ 3 - 1
src/SharpGLTF.Core/Schema2/gltf.ExtensionsFactory.cs

@@ -55,7 +55,9 @@ namespace SharpGLTF.Schema2
             RegisterExtension<Texture, TextureKTX2>("KHR_texture_basisu", p => new TextureKTX2(p));
             RegisterExtension<Texture, TextureKTX2>("KHR_texture_basisu", p => new TextureKTX2(p));
 
 
             RegisterExtension<ModelRoot, XmpPackets>("KHR_xmp_json_ld", p => new XmpPackets(p));
             RegisterExtension<ModelRoot, XmpPackets>("KHR_xmp_json_ld", p => new XmpPackets(p));
-            RegisterExtension<ExtraProperties, XmpPacketReference>("KHR_xmp_json_ld", p => new XmpPacketReference(p));                        
+            RegisterExtension<ExtraProperties, XmpPacketReference>("KHR_xmp_json_ld", p => new XmpPacketReference(p));
+
+            RegisterExtension<AnimationChannelTarget, AnimationPointer>("KHR_animation_pointer", p => new AnimationPointer(p));
         }
         }
 
 
         #endregion
         #endregion

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

@@ -39,8 +39,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
 
             try
             try
             {
             {
-                model = ModelRoot.Load(f, settings);
-                Assert.That(model, Is.Not.Null);
+                model = ModelRoot.Load(f, settings);                
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -48,6 +47,10 @@ namespace SharpGLTF.Schema2.LoadAndSave
                 Assert.Fail(ex.Message);
                 Assert.Fail(ex.Message);
             }
             }
 
 
+            Assert.That(model, Is.Not.Null);
+
+            if (model == null) return null;
+
             var perf_load = perf.ElapsedMilliseconds;
             var perf_load = perf.ElapsedMilliseconds;
 
 
             // do a model clone and compare it
             // do a model clone and compare it
@@ -106,18 +109,23 @@ namespace SharpGLTF.Schema2.LoadAndSave
         {            
         {            
             TestContext.CurrentContext.AttachGltfValidatorLinks();
             TestContext.CurrentContext.AttachGltfValidatorLinks();
 
 
-            Assert.Multiple( () =>
-            {
+            #if !DEBUG
+            Assert.Multiple( () => {
+            #endif
+
                 foreach (var f in TestFiles.GetSampleModelsPaths())
                 foreach (var f in TestFiles.GetSampleModelsPaths())
                 {
                 {
-                    if (f.Contains("SuzanneMorphSparse")) continue; // temporarily skipping due to empty BufferView issue
-                    if (f.Contains("AnimatedColorsCube")) continue; // KHR_Animation_Pointer not supported yet
+                    if (f.Contains("SuzanneMorphSparse")) continue; // temporarily skipping due to empty BufferView issue                    
+                    if (f.Contains("SunglassesKhronos")) continue; // KHR_materials_specular is declared but not used
 
 
-                    if (!f.Contains(section)) continue;
+                if (!f.Contains(section)) continue;
 
 
                     _LoadModel(f);
                     _LoadModel(f);
                 }
                 }
+
+            #if !DEBUG
             } );
             } );
+            #endif
         }
         }
 
 
         [Test]
         [Test]
@@ -171,6 +179,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
             roundtripInstanced.AttachToCurrentTest($"{ff}.roundtrip.instancing.glb");            
             roundtripInstanced.AttachToCurrentTest($"{ff}.roundtrip.instancing.glb");            
         }
         }
 
 
+        [TestCase("AnimationPointerUVs.gltf")]
         [TestCase("IridescenceMetallicSpheres.gltf")]
         [TestCase("IridescenceMetallicSpheres.gltf")]
         [TestCase("SpecGlossVsMetalRough.gltf")]
         [TestCase("SpecGlossVsMetalRough.gltf")]
         [TestCase(@"TextureTransformTest.gltf")]
         [TestCase(@"TextureTransformTest.gltf")]