Ver código fonte

SceneBuilder WIP: more refactoring, figuring out a good API to edit animation curves.

Vicente Penades 6 anos atrás
pai
commit
36e378b237

+ 15 - 0
src/Shared/_Extensions.cs

@@ -164,6 +164,21 @@ namespace SharpGLTF
 
         #region linq
 
+        internal static int GetContentHashCode<T>(this IEnumerable<T> collection, int count = int.MaxValue)
+        {
+            if (collection == null) return 0;
+
+            int h = 0;
+
+            foreach (var element in collection.Take(count))
+            {
+                h ^= element == null ? 0 : element.GetHashCode();
+                h *= 17;
+            }
+
+            return h;
+        }
+
         internal static ArraySegment<T> Slice<T>(this T[] array, int offset)
         {
             return new ArraySegment<T>(array, offset, array.Length - offset);

+ 13 - 0
src/SharpGLTF.Core/Animations/SamplerFactory.cs

@@ -41,6 +41,19 @@ namespace SharpGLTF.Animations
     {
         #region sampler utils
 
+        public static Quaternion CreateTangent(this Quaternion fromValue, Quaternion toValue, float scale = 1)
+        {
+            var tangent = Quaternion.Concatenate(toValue, Quaternion.Inverse(fromValue));
+
+            if (scale == 1) return tangent;
+
+            // decompose into Axis - Angle pair
+            var axis = Vector3.Normalize(new Vector3(tangent.X, tangent.Y, tangent.Z));
+            var angle = Math.Acos(tangent.W) * 2;
+
+            return Quaternion.CreateFromAxisAngle(axis, scale * (float)angle);
+        }
+
         /// <summary>
         /// Calculates the Hermite point weights for a given <paramref name="amount"/>
         /// </summary>

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

@@ -111,7 +111,7 @@ namespace SharpGLTF.Schema2
         /// </summary>
         public Transforms.AffineTransform LocalTransform
         {
-            get => new Transforms.AffineTransform(_matrix, _scale, _rotation, _translation);
+            get => new Transforms.AffineTransform(_matrix, _translation, _rotation, _scale);
             set
             {
                 Guard.IsFalse(this._skin.HasValue, _NOTRANSFORMMESSAGE);

+ 9 - 9
src/SharpGLTF.Core/Transforms/AffineTransform.cs

@@ -21,22 +21,22 @@ namespace SharpGLTF.Transforms
             return new AffineTransform(matrix, null, null, null);
         }
 
-        public static AffineTransform Create(Vector3? s, Quaternion? r, Vector3? t)
+        public static AffineTransform Create(Vector3? translation, Quaternion? rotation, Vector3? scale)
         {
-            return new AffineTransform(null, s, r, t);
+            return new AffineTransform(null, translation, rotation, scale);
         }
 
-        internal AffineTransform(Matrix4x4? m, Vector3? s, Quaternion? r, Vector3? t)
+        internal AffineTransform(Matrix4x4? matrix, Vector3? translation, Quaternion? rotation, Vector3? scale)
         {
-            if (m.HasValue)
+            if (matrix.HasValue)
             {
-                Matrix4x4.Decompose(m.Value, out Scale, out Rotation, out Translation);
+                Matrix4x4.Decompose(matrix.Value, out Scale, out Rotation, out Translation);
             }
             else
             {
-                Rotation = r ?? Quaternion.Identity;
-                Scale = s ?? Vector3.One;
-                Translation = t ?? Vector3.Zero;
+                Rotation = rotation ?? Quaternion.Identity;
+                Scale = scale ?? Vector3.One;
+                Translation = translation ?? Vector3.Zero;
             }
         }
 
@@ -125,7 +125,7 @@ namespace SharpGLTF.Transforms
         {
             if (transform.HasValue) return transform.Value;
 
-            return new AffineTransform(null, scale, rotation, translation).Matrix;
+            return new AffineTransform(null, translation, rotation, scale).Matrix;
         }
 
         public static Matrix4x4 LocalToWorld(Matrix4x4 parentWorld, Matrix4x4 childLocal)

+ 0 - 47
src/SharpGLTF.Toolkit/Animations/Animatable.cs

@@ -1,47 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-using System.Numerics;
-using System.Text;
-
-namespace SharpGLTF.Animations
-{
-    /// <summary>
-    /// Represents a value that can be animated using <see cref="Animations.ICurveSampler{T}"/>.
-    /// </summary>
-    /// <typeparam name="T">The type of the value.</typeparam>
-    public class Animatable<T>
-    {
-        #region data
-
-        private Dictionary<string, ICurveSampler<T>> _Tracks;
-
-        public T Default { get; set; }
-
-        #endregion
-
-        #region properties
-
-        public IReadOnlyDictionary<string, ICurveSampler<T>> Tracks => _Tracks == null ? Collections.EmptyDictionary<string, ICurveSampler<T>>.Instance : _Tracks;
-
-        #endregion
-
-        #region API
-
-        public T GetValueAt(string track, float value)
-        {
-            if (_Tracks == null) return this.Default;
-
-            return _Tracks.TryGetValue(track, out ICurveSampler<T> sampler) ? sampler.GetPoint(value) : this.Default;
-        }
-
-        public void SetTrack(string track, ICurveSampler<T> curve)
-        {
-            Guard.NotNullOrEmpty(track, nameof(track));
-            if (curve == null) { _Tracks.Remove(track); return; }
-        }
-
-        #endregion
-    }
-}

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

@@ -0,0 +1,86 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.Animations
+{
+    /// <summary>
+    /// Represents a property value that can be animated using <see cref="Animations.ICurveSampler{T}"/>.
+    /// </summary>
+    /// <typeparam name="T">The type of the value.</typeparam>
+    public class AnimatableProperty<T>
+    {
+        #region data
+
+        private Dictionary<string, ICurveSampler<T>> _Tracks;
+
+        /// <summary>
+        /// Gets or sets the default value of this instance.
+        /// When animations are disabled, or there's no animation track available, this will be the returned value.
+        /// </summary>
+        public T Value { get; set; }
+
+        #endregion
+
+        #region properties
+
+        public IReadOnlyDictionary<string, ICurveSampler<T>> Tracks => _Tracks == null ? Collections.EmptyDictionary<string, ICurveSampler<T>>.Instance : _Tracks;
+
+        #endregion
+
+        #region API
+
+        /// <summary>
+        /// Evaluates the value of this <see cref="AnimatableProperty{T}"/> at a given <paramref name="offset"/> for a given <paramref name="track"/>.
+        /// </summary>
+        /// <param name="track">An animation track name, or null.</param>
+        /// <param name="offset">A time offset within the given animation track.</param>
+        /// <returns>The evaluated value taken from the animation <paramref name="track"/>, or <see cref="Value"/> if a track was not found.</returns>
+        public T GetValueAt(string track, float offset)
+        {
+            if (_Tracks == null) return this.Value;
+
+            return _Tracks.TryGetValue(track, out ICurveSampler<T> sampler) ? sampler.GetPoint(offset) : this.Value;
+        }
+
+        /// <summary>
+        /// Assigns an animation curve to a given track.
+        /// </summary>
+        /// <param name="track">The name of the track.</param>
+        /// <param name="curve">A <see cref="ICurveSampler{T}"/> instance, or null to remove a track./param>
+        public void SetTrack(string track, ICurveSampler<T> curve)
+        {
+            Guard.NotNullOrEmpty(track, nameof(track));
+
+            if (curve != null)
+            {
+                var convertible = curve as IConvertibleCurve<T>;
+                Guard.NotNull(convertible, nameof(curve), $"Provided {nameof(ICurveSampler<T>)} {nameof(curve)} must implement {nameof(IConvertibleCurve<T>)} interface.");
+            }
+
+            // remove track
+            if (curve == null)
+            {
+                if (_Tracks == null) return;
+                _Tracks.Remove(track);
+                if (_Tracks.Count == 0) _Tracks = null;
+                return;
+            }
+
+            // insert track
+            if (_Tracks == null) _Tracks = new Dictionary<string, ICurveSampler<T>>();
+
+            _Tracks[track] = curve;
+        }
+
+        public CurveBuilder<T> UseTrackBuilder(string track)
+        {
+            throw new NotImplementedException();
+        }
+
+        #endregion
+    }
+}

+ 44 - 1
src/SharpGLTF.Toolkit/Animations/Curves.cs

@@ -6,6 +6,50 @@ using System.Linq;
 
 namespace SharpGLTF.Animations
 {
+    //------------------------------------------------------
+    // this code is in hibernation mode - DO NOT USE
+    //------------------------------------------------------
+
+    // the idea is that depending on the calls we do to this interface, it upgrades the data under the hood.
+    public abstract class CurveBuilder<T>
+    {
+        #region data
+
+        internal SortedDictionary<float, _CurveNode<T>> _Keys = new SortedDictionary<float, _CurveNode<T>>();
+
+        #endregion
+
+        public IReadOnlyCollection<float> Keys => _Keys.Keys;
+
+        public void RemoveKey(float offset) { _Keys.Remove(offset); }
+
+        public void SetKey(float offset, T value, bool isLinear = true)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void SetKey(float offset, T value, T incomingTangent, int outgoingTangent)
+        {
+            throw new NotImplementedException();
+        }
+
+        public CurveBuilder<T> WithKey(float offset, T value, bool isLinear = true)
+        {
+            SetKey(offset, value, isLinear);
+            return this;
+        }
+
+        public CurveBuilder<T> WithKey(float offset, T value, T incomingTangent, int outgoingTangent)
+        {
+            SetKey(offset, value, incomingTangent, outgoingTangent);
+            return this;
+        }
+    }
+
+    public sealed class Vector3CurveBuilder : CurveBuilder<Vector3>
+    {
+
+    }
 
     [System.Diagnostics.DebuggerDisplay("[{_Offset}] = {Sample}")]
     struct CurvePoint<T>
@@ -112,7 +156,6 @@ namespace SharpGLTF.Animations
     }
 
     struct _CurveNode<T>
-        where T : struct
     {
         public T IncomingTangent;
         public T Point;

+ 58 - 17
src/SharpGLTF.Toolkit/Materials/ChannelBuilder.cs

@@ -31,6 +31,46 @@ namespace SharpGLTF.Materials
 
         private readonly String _Key;
 
+        /// <summary>
+        /// Gets or sets the <see cref="ChannelBuilder"/> paramenter.
+        /// Its meaning depends on <see cref="Key"/>.
+        /// </summary>
+        public Vector4 Parameter { get; set; }
+
+        public TextureBuilder Texture { get; private set; }
+
+        public static bool AreEqual(ChannelBuilder a, ChannelBuilder b)
+        {
+            #pragma warning disable IDE0041 // Use 'is null' check
+            if (Object.ReferenceEquals(a, b)) return true;
+            if (Object.ReferenceEquals(a, null)) return false;
+            if (Object.ReferenceEquals(b, null)) return false;
+            #pragma warning restore IDE0041 // Use 'is null' check
+
+            if (!Object.ReferenceEquals(a._Parent, b._Parent)) return false;
+
+            if (a._Key != b._Key) return false;
+
+            if (a.Parameter != b.Parameter) return false;
+
+            if (!TextureBuilder.AreEqual(a.Texture, b.Texture)) return false;
+
+            return true;
+        }
+
+        public static int GetContentHashCode(ChannelBuilder x)
+        {
+            if (x == null) return 0;
+
+            var h = x._Key.GetHashCode();
+
+            h ^= x.Parameter.GetHashCode();
+
+            h ^= TextureBuilder.GetContentHashCode(x.Texture);
+
+            return h;
+        }
+
         #endregion
 
         #region properties
@@ -40,13 +80,7 @@ namespace SharpGLTF.Materials
         /// </summary>
         public String Key => _Key;
 
-        /// <summary>
-        /// Gets or sets the <see cref="ChannelBuilder"/> paramenter.
-        /// Its meaning depends on <see cref="Key"/>.
-        /// </summary>
-        public Vector4 Parameter { get; set; }
-
-        public TextureBuilder Texture { get; private set; }
+        public static IEqualityComparer<ChannelBuilder> ContentComparer => _ContentComparer.Default;
 
         #endregion
 
@@ -81,18 +115,25 @@ namespace SharpGLTF.Materials
         public void RemoveTexture() { Texture = null; }
 
         #endregion
-    }
 
-    public enum KnownChannels
-    {
-        Normal,
-        Occlusion,
-        Emissive,
+        #region Support types
 
-        BaseColor,
-        MetallicRoughness,
+        sealed class _ContentComparer : IEqualityComparer<ChannelBuilder>
+        {
+            public static readonly _ContentComparer Default = new _ContentComparer();
+
+            public bool Equals(ChannelBuilder x, ChannelBuilder y)
+            {
+                return ChannelBuilder.AreEqual(x, y);
+            }
+
+            public int GetHashCode(ChannelBuilder obj)
+            {
+                return ChannelBuilder.GetContentHashCode(obj);
+            }
+        }
 
-        Diffuse,
-        SpecularGlossiness,
+        #endregion
     }
+
 }

+ 80 - 10
src/SharpGLTF.Toolkit/Materials/MaterialBuilder.cs

@@ -4,8 +4,6 @@ using System.Linq;
 using System.Numerics;
 using System.Text;
 
-using ALPHA = SharpGLTF.Schema2.AlphaMode;
-
 namespace SharpGLTF.Materials
 {
     [System.Diagnostics.DebuggerDisplay("{Name} {ShaderStyle}")]
@@ -35,22 +33,73 @@ namespace SharpGLTF.Materials
 
         private MaterialBuilder _CompatibilityFallbackMaterial;
 
-        #endregion
-
-        #region properties
-
+        /// <summary>
+        /// Gets or sets the name of this <see cref="MaterialBuilder"/> instance.
+        /// </summary>
         public string Name { get; set; }
 
-        public IReadOnlyCollection<ChannelBuilder> Channels => _Channels;
-
-        public ALPHA AlphaMode { get; set; } = ALPHA.OPAQUE;
+        public AlphaMode AlphaMode { get; set; } = AlphaMode.OPAQUE;
 
         public Single AlphaCutoff { get; set; } = 0.5f;
 
+        /// <summary>
+        /// Gets or sets a value indicating whether triangles must be rendered from both sides.
+        /// </summary>
         public Boolean DoubleSided { get; set; } = false;
 
         public String ShaderStyle { get; set; } = SHADERPBRMETALLICROUGHNESS;
 
+        public static bool AreEqual(MaterialBuilder x, MaterialBuilder y)
+        {
+            #pragma warning disable IDE0041 // Use 'is null' check
+            if (Object.ReferenceEquals(x, y)) return true;
+            if (Object.ReferenceEquals(x, null)) return false;
+            if (Object.ReferenceEquals(y, null)) return false;
+            #pragma warning restore IDE0041 // Use 'is null' check
+
+            // Although .Name is not strictly a material property,
+            // it identifies a specific material during Runtime that
+            // might be relevant and needs to be preserved.
+            // If an author needs materials to be merged, it's better
+            // to keep the Name as null, or to use a common name like "Default".
+
+            if (x.Name != y.Name) return false;
+            if (x.AlphaMode != y.AlphaMode) return false;
+            if (x.AlphaCutoff != y.AlphaCutoff) return false;
+            if (x.DoubleSided != y.DoubleSided) return false;
+            if (x.ShaderStyle != y.ShaderStyle) return false;
+
+            if (!AreEqual(x._CompatibilityFallbackMaterial, y._CompatibilityFallbackMaterial)) return false;
+
+            return Enumerable.SequenceEqual(x._Channels, y._Channels, ChannelBuilder.ContentComparer);
+        }
+
+        public static int GetContentHashCode(MaterialBuilder x)
+        {
+            if (x == null) return 0;
+
+            var h = x.Name == null ? 0 : x.Name.GetHashCode();
+
+            h ^= x.AlphaMode.GetHashCode();
+            h ^= x.AlphaCutoff.GetHashCode();
+            h ^= x.DoubleSided.GetHashCode();
+            h ^= x.ShaderStyle.GetHashCode();
+
+            h ^= x._Channels
+                .Select(item => ChannelBuilder.GetContentHashCode(item))
+                .GetContentHashCode();
+
+            h ^= GetContentHashCode(x._CompatibilityFallbackMaterial);
+
+            return h;
+        }
+
+        #endregion
+
+        #region properties
+
+        public IReadOnlyCollection<ChannelBuilder> Channels => _Channels;
+
         public MaterialBuilder CompatibilityFallback
         {
             get => _CompatibilityFallbackMaterial;
@@ -61,6 +110,8 @@ namespace SharpGLTF.Materials
             }
         }
 
+        public static IEqualityComparer<MaterialBuilder> ContentComparer => _ContentComparer.Default;
+
         #endregion
 
         #region API
@@ -131,7 +182,7 @@ namespace SharpGLTF.Materials
             return ch;
         }
 
-        public MaterialBuilder WithAlpha(ALPHA alphaMode = ALPHA.OPAQUE, Single alphaCutoff = 0.5f)
+        public MaterialBuilder WithAlpha(AlphaMode alphaMode = AlphaMode.OPAQUE, Single alphaCutoff = 0.5f)
         {
             this.AlphaMode = alphaMode;
             this.AlphaCutoff = alphaCutoff;
@@ -239,5 +290,24 @@ namespace SharpGLTF.Materials
         public MaterialBuilder WithEmissive(Vector3 emissiveFactor) { return WithChannelParam("Emissive", new Vector4(emissiveFactor, 0)); }
 
         #endregion
+
+        #region support types
+
+        sealed class _ContentComparer : IEqualityComparer<MaterialBuilder>
+        {
+            public static readonly _ContentComparer Default = new _ContentComparer();
+
+            public bool Equals(MaterialBuilder x, MaterialBuilder y)
+            {
+                return MaterialBuilder.AreEqual(x, y);
+            }
+
+            public int GetHashCode(MaterialBuilder obj)
+            {
+                return MaterialBuilder.GetContentHashCode(obj);
+            }
+        }
+
+        #endregion
     }
 }

+ 29 - 0
src/SharpGLTF.Toolkit/Materials/MaterialEnums.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF.Materials
+{
+    /// <summary>
+    /// The alpha rendering mode of the material.
+    /// </summary>
+    public enum AlphaMode
+    {
+        OPAQUE,
+        MASK,
+        BLEND,
+    }
+
+    public enum KnownChannels
+    {
+        Normal,
+        Occlusion,
+        Emissive,
+
+        BaseColor,
+        MetallicRoughness,
+
+        Diffuse,
+        SpecularGlossiness,
+    }
+}

+ 110 - 11
src/SharpGLTF.Toolkit/Materials/TextureBuilder.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Numerics;
 using System.Text;
 
@@ -32,14 +33,70 @@ namespace SharpGLTF.Materials
         private BYTES _PrimaryImageContent;
         private BYTES _FallbackImageContent;
 
+        public int CoordinateSet { get; set; } = 0;
+
+        public TEXMIPMAP MinFilter { get; set; } = TEXMIPMAP.DEFAULT;
+
+        public TEXLERP MagFilter { get; set; } = TEXLERP.DEFAULT;
+
+        public TEXWRAP WrapS { get; set; } = TEXWRAP.REPEAT;
+
+        public TEXWRAP WrapT { get; set; } = TEXWRAP.REPEAT;
+
         private TextureTransformBuilder _Transform;
 
+        public static bool AreEqual(TextureBuilder a, TextureBuilder b)
+        {
+            #pragma warning disable IDE0041 // Use 'is null' check
+            if (Object.ReferenceEquals(a, b)) return true;
+            if (Object.ReferenceEquals(a, null)) return false;
+            if (Object.ReferenceEquals(b, null)) return false;
+            #pragma warning restore IDE0041 // Use 'is null' check
+
+            if (!Object.ReferenceEquals(a._Parent, b._Parent)) return false;
+
+            if (a.CoordinateSet != b.CoordinateSet) return false;
+
+            if (a.MinFilter != b.MinFilter) return false;
+            if (a.MagFilter != b.MagFilter) return false;
+            if (a.WrapS != b.WrapS) return false;
+            if (a.WrapT != b.WrapT) return false;
+
+            if (!_AreArraysContentEqual(a._PrimaryImageContent, b._PrimaryImageContent)) return false;
+            if (!_AreArraysContentEqual(a._FallbackImageContent, b._FallbackImageContent)) return false;
+
+            if (TextureTransformBuilder.AreEqual(a._Transform, b._Transform)) return false;
+
+            return true;
+        }
+
+        private static bool _AreArraysContentEqual(BYTES a, BYTES b)
+        {
+            if (a.Equals(b)) return true;
+
+            return Enumerable.SequenceEqual(a, b);
+        }
+
+        public static int GetContentHashCode(TextureBuilder x)
+        {
+            if (x == null) return 0;
+
+            var h = x.CoordinateSet.GetHashCode();
+            h ^= x.MinFilter.GetHashCode();
+            h ^= x.MagFilter.GetHashCode();
+            h ^= x.WrapS.GetHashCode();
+            h ^= x.WrapT.GetHashCode();
+
+            h ^= x._PrimaryImageContent.GetContentHashCode(16);
+            h ^= x._FallbackImageContent.GetContentHashCode(16);
+
+            return h;
+        }
+
         #endregion
 
         #region properties
 
-        public int CoordinateSet { get; set; } = 0;
-
         /// <summary>
         /// Gets or sets the default image bytes to use by this <see cref="TextureBuilder"/>,
         /// Supported formats are: PNG, JPG, DDS and WEBP
@@ -60,16 +117,10 @@ namespace SharpGLTF.Materials
             set => WithFallbackImage(value);
         }
 
-        public TEXMIPMAP MinFilter { get; set; } = TEXMIPMAP.DEFAULT;
-
-        public TEXLERP MagFilter { get; set; } = TEXLERP.DEFAULT;
-
-        public TEXWRAP WrapS { get; set; } = TEXWRAP.REPEAT;
-
-        public TEXWRAP WrapT { get; set; } = TEXWRAP.REPEAT;
-
         public TextureTransformBuilder Transform => _Transform;
 
+        public static IEqualityComparer<TextureBuilder> ContentComparer => _ContentComparer.Default;
+
         #endregion
 
         #region API
@@ -113,7 +164,7 @@ namespace SharpGLTF.Materials
         {
             if (image.Count > 0)
             {
-                Guard.IsTrue(image._IsJpgImage() || image._IsPngImage(), nameof(image), "Must be JPG, PNG");
+                Guard.IsTrue(image._IsJpgImage() || image._IsPngImage(), nameof(image), "Must be JPG or PNG");
             }
             else
             {
@@ -150,10 +201,31 @@ namespace SharpGLTF.Materials
         }
 
         #endregion
+
+        #region support types
+
+        sealed class _ContentComparer : IEqualityComparer<TextureBuilder>
+        {
+            public static readonly _ContentComparer Default = new _ContentComparer();
+
+            public bool Equals(TextureBuilder x, TextureBuilder y)
+            {
+                return TextureBuilder.AreEqual(x, y);
+            }
+
+            public int GetHashCode(TextureBuilder obj)
+            {
+                return TextureBuilder.GetContentHashCode(obj);
+            }
+        }
+
+        #endregion
     }
 
     public class TextureTransformBuilder
     {
+        #region lifecycle
+
         internal TextureTransformBuilder(Vector2 offset, Vector2 scale, float rotation = 0, int? coordSetOverride = null)
         {
             this.Offset = offset;
@@ -162,6 +234,10 @@ namespace SharpGLTF.Materials
             this.CoordinateSetOverride = coordSetOverride;
         }
 
+        #endregion
+
+        #region data
+
         public Vector2 Offset { get; set; }
 
         public Vector2 Scale { get; set; } = Vector2.One;
@@ -174,6 +250,27 @@ namespace SharpGLTF.Materials
         /// </summary>
         public int? CoordinateSetOverride { get; set; }
 
+        public static bool AreEqual(TextureTransformBuilder a, TextureTransformBuilder b)
+        {
+            #pragma warning disable IDE0041 // Use 'is null' check
+            if (Object.ReferenceEquals(a, b)) return true;
+            if (Object.ReferenceEquals(a, null)) return false;
+            if (Object.ReferenceEquals(b, null)) return false;
+            #pragma warning restore IDE0041 // Use 'is null' check
+
+            if (a.Offset != b.Offset) return false;
+            if (a.Scale != b.Scale) return false;
+            if (a.Rotation != b.Rotation) return false;
+
+            if (a.CoordinateSetOverride != b.CoordinateSetOverride) return false;
+
+            return true;
+        }
+
+        #endregion
+
+        #region properties
+
         internal bool IsDefault
         {
             get
@@ -185,5 +282,7 @@ namespace SharpGLTF.Materials
                 return false;
             }
         }
+
+        #endregion
     }
 }

+ 1 - 1
src/SharpGLTF.Toolkit/Scenes/Content.cs

@@ -154,7 +154,7 @@ namespace SharpGLTF.Scenes
 
         private IRenderableContent _Target;
 
-        private readonly List<Animations.Animatable<float>> _MorphWeights = new List<Animations.Animatable<float>>();
+        private readonly List<Animations.AnimatableProperty<float>> _MorphWeights = new List<Animations.AnimatableProperty<float>>();
 
         #endregion
 

+ 36 - 21
src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs

@@ -47,18 +47,18 @@ namespace SharpGLTF.Scenes
 
         public bool HasAnimations => Scale?.Tracks.Count > 0 || Rotation?.Tracks.Count > 0 || Translation?.Tracks.Count > 0;
 
-        public Animations.Animatable<Vector3> Scale { get; private set; }
+        public Animations.AnimatableProperty<Vector3> Scale { get; private set; }
 
-        public Animations.Animatable<Quaternion> Rotation { get; private set; }
+        public Animations.AnimatableProperty<Quaternion> Rotation { get; private set; }
 
-        public Animations.Animatable<Vector3> Translation { get; private set; }
+        public Animations.AnimatableProperty<Vector3> Translation { get; private set; }
 
         /// <summary>
         /// Gets or sets the local transform <see cref="Matrix4x4"/> of this <see cref="NodeBuilder"/>.
         /// </summary>
         public Matrix4x4 LocalMatrix
         {
-            get => Transforms.AffineTransform.Evaluate(_Matrix, Scale?.Default, Rotation?.Default, Translation?.Default);
+            get => Transforms.AffineTransform.Evaluate(_Matrix, Scale?.Value, Rotation?.Value, Translation?.Value);
             set
             {
                 if (value == Matrix4x4.Identity)
@@ -85,7 +85,7 @@ namespace SharpGLTF.Scenes
                 ?
                 Transforms.AffineTransform.Create(_Matrix.Value)
                 :
-                Transforms.AffineTransform.Create(Scale?.Default, Rotation?.Default, Translation?.Default);
+                Transforms.AffineTransform.Create(Translation?.Value, Rotation?.Value, Scale?.Value);
             set
             {
                 Guard.IsTrue(value.IsValid, nameof(value));
@@ -94,20 +94,20 @@ namespace SharpGLTF.Scenes
 
                 if (value.Scale != Vector3.One)
                 {
-                    if (Scale == null) Scale = new Animations.Animatable<Vector3>();
-                    Scale.Default = value.Scale;
+                    if (Scale == null) Scale = new Animations.AnimatableProperty<Vector3>();
+                    Scale.Value = value.Scale;
                 }
 
                 if (value.Rotation != Quaternion.Identity)
                 {
-                    if (Rotation == null) Rotation = new Animations.Animatable<Quaternion>();
-                    Rotation.Default = value.Rotation;
+                    if (Rotation == null) Rotation = new Animations.AnimatableProperty<Quaternion>();
+                    Rotation.Value = value.Rotation;
                 }
 
                 if (value.Translation != Vector3.Zero)
                 {
-                    if (Translation == null) Translation = new Animations.Animatable<Vector3>();
-                    Translation.Default = value.Scale;
+                    if (Translation == null) Translation = new Animations.AnimatableProperty<Vector3>();
+                    Translation.Value = value.Scale;
                 }
             }
         }
@@ -141,39 +141,54 @@ namespace SharpGLTF.Scenes
             return c;
         }
 
-        public Animations.Animatable<Vector3> UseScale()
+        public Animations.AnimatableProperty<Vector3> UseScale()
         {
             if (Scale == null)
             {
-                Scale = new Animations.Animatable<Vector3>();
-                Scale.Default = Vector3.One;
+                Scale = new Animations.AnimatableProperty<Vector3>();
+                Scale.Value = Vector3.One;
             }
 
             return Scale;
         }
 
-        public Animations.Animatable<Quaternion> UseRotation()
+        public Animations.CurveBuilder<Vector3> UseScale(string animationTrack)
+        {
+            return UseScale().UseTrackBuilder(animationTrack);
+        }
+
+        public Animations.AnimatableProperty<Quaternion> UseRotation()
         {
             if (Rotation == null)
             {
-                Rotation = new Animations.Animatable<Quaternion>();
-                Rotation.Default = Quaternion.Identity;
+                Rotation = new Animations.AnimatableProperty<Quaternion>();
+                Rotation.Value = Quaternion.Identity;
             }
 
             return Rotation;
         }
 
-        public Animations.Animatable<Vector3> UseTranslation()
+        public Animations.CurveBuilder<Quaternion> UseRotation(string animationTrack)
+        {
+            return UseRotation().UseTrackBuilder(animationTrack);
+        }
+
+        public Animations.AnimatableProperty<Vector3> UseTranslation()
         {
             if (Translation == null)
             {
-                Translation = new Animations.Animatable<Vector3>();
-                Translation.Default = Vector3.One;
+                Translation = new Animations.AnimatableProperty<Vector3>();
+                Translation.Value = Vector3.One;
             }
 
             return Translation;
         }
 
+        public Animations.CurveBuilder<Vector3> UseTranslation(string animationTrack)
+        {
+            return UseTranslation().UseTrackBuilder(animationTrack);
+        }
+
         public void SetScaleTrack(string track, Animations.ICurveSampler<Vector3> curve) { UseScale().SetTrack(track, curve); }
 
         public void SetTranslationTrack(string track, Animations.ICurveSampler<Vector3> curve) { UseTranslation().SetTrack(track, curve); }
@@ -188,7 +203,7 @@ namespace SharpGLTF.Scenes
             var rotation = Rotation?.GetValueAt(animationTrack, time);
             var translation = Translation?.GetValueAt(animationTrack, time);
 
-            return Transforms.AffineTransform.Create(scale, rotation, translation);
+            return Transforms.AffineTransform.Create(translation, rotation, scale);
         }
 
         public Matrix4x4 GetWorldMatrix(string animationTrack, float time)

+ 25 - 3
src/SharpGLTF.Toolkit/Schema2/MaterialExtensions.cs

@@ -163,13 +163,35 @@ namespace SharpGLTF.Schema2
 
         #region transfer API
 
+        public static Schema2.AlphaMode ToSchema2(this Materials.AlphaMode alpha)
+        {
+            switch (alpha)
+            {
+                case Materials.AlphaMode.BLEND: return Schema2.AlphaMode.BLEND;
+                case Materials.AlphaMode.MASK: return Schema2.AlphaMode.MASK;
+                case Materials.AlphaMode.OPAQUE: return Schema2.AlphaMode.OPAQUE;
+                default: throw new NotImplementedException(alpha.ToString());
+            }
+        }
+
+        public static Materials.AlphaMode ToToolkit(this Schema2.AlphaMode alpha)
+        {
+            switch (alpha)
+            {
+                case Schema2.AlphaMode.BLEND: return Materials.AlphaMode.BLEND;
+                case Schema2.AlphaMode.MASK: return Materials.AlphaMode.MASK;
+                case Schema2.AlphaMode.OPAQUE: return Materials.AlphaMode.OPAQUE;
+                default: throw new NotImplementedException(alpha.ToString());
+            }
+        }
+
         public static void CopyTo(this Material srcMaterial, Materials.MaterialBuilder dstMaterial)
         {
             Guard.NotNull(srcMaterial, nameof(srcMaterial));
             Guard.NotNull(dstMaterial, nameof(dstMaterial));
 
             dstMaterial.Name = srcMaterial.Name;
-            dstMaterial.AlphaMode = srcMaterial.Alpha;
+            dstMaterial.AlphaMode = srcMaterial.Alpha.ToToolkit();
             dstMaterial.AlphaCutoff = srcMaterial.AlphaCutoff;
             dstMaterial.DoubleSided = srcMaterial.DoubleSided;
 
@@ -188,7 +210,7 @@ namespace SharpGLTF.Schema2
                 dstMaterial = new Materials.MaterialBuilder(srcMaterial.Name).WithFallback(dstMaterial);
 
                 dstMaterial.Name = srcMaterial.Name;
-                dstMaterial.AlphaMode = srcMaterial.Alpha;
+                dstMaterial.AlphaMode = srcMaterial.Alpha.ToToolkit();
                 dstMaterial.AlphaCutoff = srcMaterial.AlphaCutoff;
                 dstMaterial.DoubleSided = srcMaterial.DoubleSided;
 
@@ -254,7 +276,7 @@ namespace SharpGLTF.Schema2
 
             srcMaterial.ValidateForSchema2();
 
-            dstMaterial.Alpha = srcMaterial.AlphaMode;
+            dstMaterial.Alpha = srcMaterial.AlphaMode.ToSchema2();
             dstMaterial.AlphaCutoff = srcMaterial.AlphaCutoff;
             dstMaterial.DoubleSided = srcMaterial.DoubleSided;
 

+ 69 - 19
tests/SharpGLTF.Tests/AnimationSamplingTests.cs

@@ -12,13 +12,14 @@ namespace SharpGLTF
     [Category("Core")]
     public class AnimationSamplingTests
     {
-        [Test]
-        public void TestHermiteInterpolation1()
+        [TestCase(0, 0, 0, 1, 1, 1, 1, 0)]
+        [TestCase(0, 0, 0.1f, 5, 0.7f, 3, 1, 0)]
+        public void TestHermiteInterpolation1(float p1x, float p1y, float p2x, float p2y, float p3x, float p3y, float p4x, float p4y)
         {
-            var p1 = new Vector2(0, 0);
-            var p2 = new Vector2(0, 1);
-            var p3 = new Vector2(1, 1);
-            var p4 = new Vector2(1, 0);
+            var p1 = new Vector2(p1x, p1y);
+            var p2 = new Vector2(p2x, p2y);
+            var p3 = new Vector2(p3x, p3y);
+            var p4 = new Vector2(p4x, p4y);
 
             var ppp = new List<Vector2>();
 
@@ -56,34 +57,83 @@ namespace SharpGLTF
         }
 
         [Test]
-        public void TestHermiteInterpolation2()
+        public void TestHermiteAsLinearInterpolation()
         {
-            var p1 = new Vector2(0, 0);
-            var p2 = new Vector2(0.1f, 5);
-            var p3 = new Vector2(0.7f, 3);
-            var p4 = new Vector2(1, 0);            
+            var p1 = new Vector2(1, 0);
+            var p2 = new Vector2(3, 1);
+            var t = p2 - p1;
 
             var ppp = new List<Vector2>();
 
-            for (float amount = 0; amount <= 1; amount += 0.01f)
+            for (float amount = 0; amount <= 1; amount += 0.1f)
             {
                 var hermite = Animations.SamplerFactory.CreateHermitePointWeights(amount);
 
                 var p = Vector2.Zero;
 
                 p += p1 * hermite.Item1;
-                p += p4 * hermite.Item2;
-                p += (p2-p1) * 4 * hermite.Item3;
-                p += (p4-p3) * 4 * hermite.Item4;
+                p += p2 * hermite.Item2;
+                p += t * hermite.Item3;
+                p += t * hermite.Item4;
 
                 ppp.Add(p);
             }
 
-            var series1 = ppp.ToPointSeries();
-            var series2 = new[] { p1, p2, p3, p4 }.ToLineSeries();
+            var series1 = ppp.ToPointSeries().WithLineType(Plotting.LineType.Star);
+
+            new[] { series1 }.AttachToCurrentTest("plot.png");
+        }
+
+        [Test]
+        public void TestHermiteAsSphericalInterpolation()
+        {
+            // given two quaternions, we must find a tangent quaternion so that the quaternion
+            // hermite interpolation gives roughly the same results as a plain spherical interpolation.
 
-            new[] { series1, series2 }.AttachToCurrentTest("plot.png");
-        }        
+            // reference implementation with matrices
+            var m1 = Matrix4x4.CreateFromAxisAngle(Vector3.UnitX, 1);            
+            var m2 = Matrix4x4.CreateFromAxisAngle(Vector3.UnitY, 2);            
+            var mt = Matrix4x4.Multiply(m2, Matrix4x4.Transpose(m1));            
+            var m2bis = Matrix4x4.Multiply(mt, m1); // roundtrip; M2 == M2BIS
+
+            // implementation with quaternions
+            var q1 = Quaternion.CreateFromAxisAngle(Vector3.UnitX, 1);
+            var q2 = Quaternion.CreateFromAxisAngle(Vector3.UnitY, 2);
+            var qt = Quaternion.Concatenate(q2, Quaternion.Conjugate(q1));            
+            var q2bis = Quaternion.Concatenate(qt, q1); // roundtrip; Q2 == Q2BIS
+
+            NumericsAssert.AreEqual(qt, Animations.SamplerFactory.CreateTangent(q1, q2), 0.000001f);
+
+            var angles = new List<Vector2>();
+
+            for (float amount = 0; amount <= 1; amount += 0.025f)
+            {
+                // slerp interpolation
+                var sq = Quaternion.Normalize(Quaternion.Slerp(q1, q2, amount));
+
+                // hermite interpolation with a unit tangent
+                var hermite = Animations.SamplerFactory.CreateHermitePointWeights(amount);
+                var hq = default(Quaternion);
+                hq += q1 * hermite.Item1;
+                hq += q2 * hermite.Item2;
+                hq += qt * hermite.Item3;
+                hq += qt * hermite.Item4;
+                hq = Quaternion.Normalize(hq);
+                
+                // check
+                NumericsAssert.AreEqual(sq, hq, 0.1f);
+                NumericsAssert.AngleLessOrEqual(sq, hq, 0.22f);
+
+                // diff
+                var a = VectorsUtils.GetAngle(sq, hq) * 180.0f / 3.141592f;
+                angles.Add(new Vector2(amount, a));                          
+            }
+
+            angles.ToPointSeries()
+                .WithLineType(Plotting.LineType.Continuous)
+                .AttachToCurrentTest("plot.png");
+        
+        }
 
         private static (float, (Vector3, Vector3, Vector3))[] _TransAnim = new []
         {

+ 4 - 4
tests/SharpGLTF.Tests/Memory/MemoryArrayTests.cs

@@ -46,11 +46,11 @@ namespace SharpGLTF.Memory
 
             var v4n = new Vector4Array(bytes, 0, Schema2.EncodingType.UNSIGNED_BYTE, true);
             v4n[1] = v1;
-            VectorUtils.AreEqual(v4n[1], v1, 0.1f);
+            NumericsAssert.AreEqual(v4n[1], v1, 0.1f);
 
             var v4u = new Vector4Array(bytes, 0, Schema2.EncodingType.UNSIGNED_BYTE, false);
             v4u[1] = v2;
-            VectorUtils.AreEqual(v4u[1], v2);
+            NumericsAssert.AreEqual(v4u[1], v2);
         }
 
         [Test]
@@ -65,11 +65,11 @@ namespace SharpGLTF.Memory
             var v4u = new Vector4Array(bytes, 4, 5, 8, Schema2.EncodingType.UNSIGNED_BYTE, false);
 
             v4n[1] = v1;
-            VectorUtils.AreEqual(v4n[1], v1, 0.1f);
+            NumericsAssert.AreEqual(v4n[1], v1, 0.1f);
 
             
             v4u[1] = v2;
-            VectorUtils.AreEqual(v4u[1], v2);
+            NumericsAssert.AreEqual(v4u[1], v2);
         }
 
         [Test]

+ 9 - 2
tests/SharpGLTF.Tests/Plotting.cs

@@ -23,13 +23,18 @@ namespace SharpGLTF
             Cross = 2,
             Star = 3,
             Circle = 4,
-            X = 5,
+            X = 5,            
             Square2 = 6,
             Triangle = 7,
             CircleWithCross = 8,
             CircleWithDot = 9,
 
-            Continuous = 65536
+            CHAR_X = 88,
+            CHAR_Y = 89,
+            CHAR_Z = 90,
+            CHAR_W = 87,
+
+            Continuous = 65536                
         }
 
         public struct Point2
@@ -100,6 +105,8 @@ namespace SharpGLTF
 
             #region API
 
+            public Point2Series WithLineType(LineType t) { LineType = t; return this; }
+
             public void DrawToFile(string filePath)
             {
                 DrawToFile(filePath, this);

+ 6 - 7
tests/SharpGLTF.Tests/Scenes/SceneBuilderTests.cs

@@ -42,18 +42,17 @@ namespace SharpGLTF.Scenes
             TestContext.CurrentContext.AttachShowDirLink();
             TestContext.CurrentContext.AttachGltfValidatorLinks();
 
-            var mesh = new Cube<Materials.MaterialBuilder>(new Materials.MaterialBuilder())
-                .ToMesh(Matrix4x4.Identity);
+            var cube = new Cube<Materials.MaterialBuilder>(Materials.MaterialBuilder.CreateDefault());
 
             var pivot = new NodeBuilder();
 
-            var curve = new[] { (0.0f, Vector3.Zero), (1.0f, Vector3.One) };            
+            pivot.UseTranslation("track1")
+                .WithKey(0, Vector3.Zero)
+                .WithKey(1, Vector3.One);
 
-            pivot.SetTranslationTrack("default", curve.CreateSampler());
-
-            var scene = new SceneBuilder();
+            var scene = new SceneBuilder();            
 
-            scene.AddMesh(mesh, pivot);
+            scene.AddMesh(cube.ToMesh(Matrix4x4.Identity), pivot);
 
             scene.AttachToCurrentTest("animated.glb");
             scene.AttachToCurrentTest("animated.gltf");

+ 2 - 2
tests/SharpGLTF.Tests/Schema2/Authoring/MeshBuilderCreationTests.cs

@@ -328,7 +328,7 @@ namespace SharpGLTF.Schema2.Authoring
 
             var materials = Enumerable
                 .Range(0, 10)
-                .Select(idx => new MaterialBuilder()
+                .Select(idx => MaterialBuilder.CreateDefault()
                 .WithChannelParam("BaseColor", new Vector4(rnd.NextVector3(),1)))
                 .ToList();
 
@@ -369,7 +369,7 @@ namespace SharpGLTF.Schema2.Authoring
             // create a mesh
             var cube = new MeshBuilder<VPOSNRM>("cube");
             cube.VertexPreprocessor.SetDebugPreprocessors();
-            cube.AddCube(new MaterialBuilder(), Matrix4x4.Identity);
+            cube.AddCube(MaterialBuilder.CreateDefault(), Matrix4x4.Identity);
             cube.Validate();
 
             // create a new gltf model

+ 94 - 6
tests/SharpGLTF.Tests/Utils.cs

@@ -236,7 +236,7 @@ namespace SharpGLTF
         }        
     }
 
-    static class VectorUtils
+    static class VectorsUtils
     {
         public static Single NextSingle(this Random rnd)
         {
@@ -258,12 +258,100 @@ namespace SharpGLTF
             return new Vector4(rnd.NextSingle(), rnd.NextSingle(), rnd.NextSingle(), rnd.NextSingle());
         }
 
-        public static void AreEqual(Vector4 a, Vector4 b, double delta = 0)
+        public static float GetAngle(Quaternion a, Quaternion b)
         {
-            Assert.AreEqual(a.X, b.X, delta);
-            Assert.AreEqual(a.Y, b.Y, delta);
-            Assert.AreEqual(a.Z, b.Z, delta);
-            Assert.AreEqual(a.W, b.W, delta);
+            var w = Quaternion.Concatenate(b, Quaternion.Inverse(a)).W;
+
+            if (w < -1) w = -1;
+            if (w > 1) w = 1;
+
+            return (float)Math.Acos(w) * 2;
+        }
+
+        public static float GetAngle(Vector3 a, Vector3 b)
+        {
+            a = Vector3.Normalize(a);
+            b = Vector3.Normalize(b);
+
+            var c = Vector3.Dot(a, b);
+            if (c > 1) c = 1;
+            if (c < -1) c = -1;
+
+            return (float)Math.Acos(c);
+        }
+    }
+
+    static class NumericsAssert
+    {
+        public static void AreEqual(Vector2 expected, Vector2 actual, double delta = 0)
+        {
+            Assert.AreEqual(expected.X, actual.X, delta, "X");
+            Assert.AreEqual(expected.Y, actual.Y, delta, "Y");
+        }
+
+        public static void AreEqual(Vector3 expected, Vector3 actual, double delta = 0)
+        {
+            Assert.AreEqual(expected.X, actual.X, delta, "X");
+            Assert.AreEqual(expected.Y, actual.Y, delta, "Y");
+            Assert.AreEqual(expected.Z, actual.Z, delta, "Z");
+        }
+
+        public static void AreEqual(Vector4 expected, Vector4 actual, double delta = 0)
+        {
+            Assert.AreEqual(expected.X, actual.X, delta, "X");
+            Assert.AreEqual(expected.Y, actual.Y, delta, "Y");
+            Assert.AreEqual(expected.Z, actual.Z, delta, "Z");
+            Assert.AreEqual(expected.W, actual.W, delta, "W");
+        }
+
+        public static void AreEqual(Quaternion expected, Quaternion actual, double delta = 0)
+        {
+            Assert.AreEqual(expected.X, actual.X, delta, "X");
+            Assert.AreEqual(expected.Y, actual.Y, delta, "Y");
+            Assert.AreEqual(expected.Z, actual.Z, delta, "Z");
+            Assert.AreEqual(expected.W, actual.W, delta, "W");
+        }
+
+        public static void IsNormal(Vector2 vector, double delta = 0)
+        {
+            var lenSquared = vector.X * vector.X + vector.Y * vector.Y;
+
+            Assert.AreEqual(1, lenSquared, delta * delta, "Length");
+        }
+
+        public static void IsNormal(Vector3 vector, double delta = 0)
+        {
+            var lenSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z;
+
+            Assert.AreEqual(1, lenSquared, delta * delta, "Length");
+        }
+
+        public static void IsNormal(Vector4 vector, double delta = 0)
+        {
+            var lenSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z + vector.W * vector.W;
+
+            Assert.AreEqual(1, lenSquared, delta * delta, "Length");
+        }
+
+        public static void IsNormal(Quaternion vector, double delta = 0)
+        {
+            var lenSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z + vector.W * vector.W;
+
+            Assert.AreEqual(1, lenSquared, delta * delta, "Length");
+        }        
+
+        public static void AngleLessOrEqual(Vector3 a, Vector3 b, float radians)
+        {
+            var angle = VectorsUtils.GetAngle(a, b);
+
+            Assert.LessOrEqual(angle, radians, "Angle");
+        }
+
+        public static void AngleLessOrEqual(Quaternion a, Quaternion b, float radians)
+        {
+            var angle = VectorsUtils.GetAngle(a, b);
+
+            Assert.LessOrEqual(angle, radians, "Angle");
         }
     }
 }