Prechádzať zdrojové kódy

Breaking change: Refactored SparseWeight8 and VertexSkinning interfaces and structures to make it more strict, and avoid some bad practices and pitfalls.

Vicente Penades 4 rokov pred
rodič
commit
c542d65265

+ 7 - 2
src/SharpGLTF.Core/Runtime/MeshDecoder.Schema2.cs

@@ -332,8 +332,13 @@ namespace SharpGLTF.Runtime
         public Transforms.SparseWeight8 GetSkinWeights(int vertexIndex)
         {
             if (_Weights0 == null) return default;
-            if (_Weights1 == null) return new Transforms.SparseWeight8(_Joints0[vertexIndex], _Weights0[vertexIndex]);
-            return new Transforms.SparseWeight8(_Joints0[vertexIndex], _Joints1[vertexIndex], _Weights0[vertexIndex], _Weights1[vertexIndex]);
+
+            var idx0123 = _Joints0[vertexIndex];
+            var idx4567 = _Joints1 == null ? XYZW.Zero : _Joints1[vertexIndex];
+            var wgt0123 = _Weights0[vertexIndex];
+            var wgt4567 = _Weights1 == null ? XYZW.Zero : _Weights1[vertexIndex];
+
+            return Transforms.SparseWeight8.CreateUnchecked(idx0123, idx4567, wgt0123, wgt4567);
         }
 
         #endregion

+ 238 - 43
src/SharpGLTF.Core/Transforms/IndexWeight.cs

@@ -6,18 +6,31 @@ using System.Text;
 namespace SharpGLTF.Transforms
 {
     [System.Diagnostics.DebuggerDisplay("{Index} = {Weight}")]
-    readonly struct IndexWeight
+    [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
+    readonly struct IndexWeight : IEquatable<IndexWeight>
     {
-        #region constructor
+        #region implicit
 
         public static implicit operator IndexWeight((int Index, float Weight) pair) { return new IndexWeight(pair.Index, pair.Weight); }
 
+        public static implicit operator IndexWeight(KeyValuePair<int, float> pair) { return new IndexWeight(pair.Key, pair.Value); }
+
+        #endregion
+
+        #region constructor
+
         public IndexWeight((int Index, float Weight) pair)
         {
             Index = pair.Index;
             Weight = pair.Weight;
         }
 
+        public IndexWeight(KeyValuePair<int, float> pair)
+        {
+            Index = pair.Key;
+            Weight = pair.Value;
+        }
+
         public IndexWeight(int i, float w)
         {
             Index = i;
@@ -31,68 +44,253 @@ namespace SharpGLTF.Transforms
         public readonly int Index;
         public readonly float Weight;
 
+        public bool Equals(IndexWeight other)
+        {
+            return this.Index == other.Index && this.Weight == other.Weight;
+        }
+
+        public bool IsGreaterThan(in IndexWeight other)
+        {
+            var tw = Math.Abs(this.Weight);
+            var ow = Math.Abs(other.Weight);
+
+            if (tw > ow) return true;
+            if (tw == ow && this.Index < other.Index) return true;
+
+            return false;
+        }
+
         #endregion
 
-        #region API
+        #region operators
 
         public static IndexWeight operator +(IndexWeight a, IndexWeight b)
         {
-            System.Diagnostics.Debug.Assert(a.Index == b.Index);
+            if (a.Index != b.Index) throw new InvalidOperationException(nameof(b));
             return new IndexWeight(a.Index, a.Weight + b.Weight);
         }
 
-        public static int IndexOf(Span<IndexWeight> span, int index)
+        public static IndexWeight operator +(IndexWeight a, float w)
         {
-            for (int i = 0; i < span.Length; ++i)
+            return new IndexWeight(a.Index, a.Weight + w);
+        }
+
+        #endregion
+
+        #region API
+
+        /// <summary>
+        /// Checks if the collection of <see cref="IndexWeight"/> pairs is well formed.
+        /// </summary>
+        /// <remarks>
+        /// A collection is considered malformed when:<br/>
+        /// <list type="bullet">
+        /// <item>Weightless Items have indices different than zero.</item>
+        /// <item>The same index appears more than once.</item>
+        /// </list>
+        /// Indices are not required to be sorted in any order.
+        /// </remarks>
+        /// <param name="iw">The collection of pairs.</param>
+        /// <param name="err">the error message when is malformed.</param>
+        /// <returns>True if collection is wellformed. False otherwise.</returns>
+        public static bool IsWellFormed(ReadOnlySpan<IndexWeight> iw, out string err)
+        {
+            for (int i = 0; i < iw.Length; ++i)
             {
-                if (span[i].Index == index) return i;
+                var item = iw[i];
+                if (item.Weight == 0)
+                {
+                    if (item.Index != 0) { err = "weightless items must have index 0."; return false; }
+                    continue;
+                }
+
+                for (int j = 0; j < i; ++j)
+                {
+                    var prev = iw[j];
+                    if (prev.Weight == 0) continue;
+                    if (item.Index == prev.Index) { err = "indices must be unique."; return false; }
+                }
             }
 
-            return -1;
+            err = null;
+            return true;
         }
 
-        private static IndexWeight GetIndexedWeight(in SparseWeight8 src, int offset)
+        /// <summary>
+        /// Adds the given <see cref="IndexWeight"/> pair to the given collection,<br/>
+        /// trying to keep the collection sorted.
+        /// </summary>
+        /// <param name="buffer">The destination buffer, which might be larger than the collection.</param>
+        /// <param name="length">The current collecion length.</param>
+        /// <param name="item">The <see cref="IndexWeight"/> pair to add.</param>
+        /// <returns>The new collection length.</returns>
+        public static int InsertSorted(Span<IndexWeight> buffer, int length, IndexWeight item)
         {
-            switch (offset)
+            System.Diagnostics.Debug.Assert(buffer.Length >= length);
+            System.Diagnostics.Debug.Assert(item.Weight._IsFinite());
+
+            if (item.Weight == 0) return length;
+
+            // check if the index already exist
+
+            for (int i = 0; i < length; ++i)
+            {
+                if (buffer[i].Index == item.Index)
+                {
+                    // add weight to existing item
+                    buffer[i] += item;
+
+                    // since we've altered the weight, we might
+                    // need to move this value up to keep the
+                    // collection sorted by weight.
+
+                    while (i > 1)
+                    {
+                        var r = Math.Abs(buffer[i - 1].Weight).CompareTo(buffer[i].Weight);
+                        if (r == 1) break;
+                        if (r == 0 && buffer[i - 1].Index < item.Index) break;
+
+                        // swap values
+
+                        var tmp = buffer[i - 1];
+                        buffer[i - 1] = buffer[i];
+                        buffer[i] = tmp;
+
+                        --i;
+                    }
+
+                    return length;
+                }
+            }
+
+            // find insertion index
+
+            var idx = length;
+            var wgt = Math.Abs(item.Weight);
+
+            for (int i = 0; i < length; ++i)
+            {
+                // first we compare by weights;
+                // if weights are equal, we compare by index,
+                // so larger weights and smaller indices take precendence.
+
+                var r = Math.Abs(buffer[i].Weight).CompareTo(wgt);
+                if (r == 1) continue;
+                if (r == 0 && buffer[i].Index < item.Index) continue;
+                idx = i;
+                break;
+            }
+
+            if (idx >= buffer.Length) return buffer.Length; // can't insert; already full;
+
+            length = Math.Min(length + 1, buffer.Length);
+
+            // shift tail of collection
+            for (int i = length - 1; i > idx; --i)
             {
-                case 0: return new IndexWeight(src.Index0, src.Weight0);
-                case 1: return new IndexWeight(src.Index1, src.Weight1);
-                case 2: return new IndexWeight(src.Index2, src.Weight2);
-                case 3: return new IndexWeight(src.Index3, src.Weight3);
-                case 4: return new IndexWeight(src.Index4, src.Weight4);
-                case 5: return new IndexWeight(src.Index5, src.Weight5);
-                case 6: return new IndexWeight(src.Index6, src.Weight6);
-                case 7: return new IndexWeight(src.Index7, src.Weight7);
-                default: throw new ArgumentOutOfRangeException(nameof(offset));
+                buffer[i] = buffer[i - 1];
             }
+
+            buffer[idx] = item;
+
+            return length;
         }
 
-        public static int CopyTo(in SparseWeight8 src, Span<IndexWeight> dst)
+        public static int InsertUnsorted(Span<IndexWeight> sparse, in System.Numerics.Vector4 idx0123, in System.Numerics.Vector4 wgt0123)
         {
-            System.Diagnostics.Debug.Assert(dst.Length >= 8);
+            int idx = 0;
 
-            var offset = 0;
+            if (wgt0123.X != 0)
+            {
+                sparse[0] = ((int)idx0123.X, wgt0123.X);
+                ++idx;
+            }
 
-            for (int i = 0; i < 8; ++i)
+            if (wgt0123.Y != 0)
             {
-                var pair = GetIndexedWeight(src, i);
-                if (pair.Weight == 0) continue;
+                var y = (int)idx0123.Y;
+                if (idx == 1 && sparse[0].Index == y) { sparse[0] += (y, wgt0123.Y); }
+                else { sparse[idx++] = (y, wgt0123.Y); }
+            }
 
-                var idx = IndexOf(dst.Slice(0, offset), pair.Index);
+            if (wgt0123.Z != 0)
+            {
+                var z = (int)idx0123.Z;
+                if (idx > 0 && sparse[0].Index == z) { sparse[0] += (z, wgt0123.Z); }
+                else if (idx > 1 && sparse[1].Index == z) { sparse[1] += (z, wgt0123.Z); }
+                else { sparse[idx++] = (z, wgt0123.Z); }
+            }
 
-                if (idx < 0)
-                {
-                    // the index doesn't exist, insert it.
-                    dst[offset++] = pair;
-                }
-                else
+            if (wgt0123.W != 0)
+            {
+                var w = (int)idx0123.W;
+                if (idx > 0 && sparse[0].Index == w) { sparse[0] += (w, wgt0123.W); }
+                else if (idx > 1 && sparse[1].Index == w) { sparse[1] += (w, wgt0123.W); }
+                else if (idx > 2 && sparse[2].Index == w) { sparse[2] += (w, wgt0123.W); }
+                else { sparse[idx++] = (w, wgt0123.W); }
+            }
+
+            return idx;
+        }
+
+        /// <summary>
+        /// Adds the given <see cref="IndexWeight"/> pair to the given collection.
+        /// </summary>
+        /// <param name="buffer">The destination buffer, which might be larger than the collection.</param>
+        /// <param name="length">The current collecion length.</param>
+        /// <param name="item">The <see cref="IndexWeight"/> pair to add.</param>
+        /// <returns>The new collection length.</returns>
+        public static int InsertUnsorted(Span<IndexWeight> buffer, int length, IndexWeight item)
+        {
+            System.Diagnostics.Debug.Assert(buffer.Length >= length);
+            System.Diagnostics.Debug.Assert(item.Weight._IsFinite());
+
+            if (item.Weight == 0) return length;
+
+            // check if the index already exist
+
+            for (int i = 0; i < length; ++i)
+            {
+                if (buffer[i].Index == item.Index)
                 {
-                    // the index already exists, so we aggregate the weights
-                    dst[idx] += pair;
+                    // add weight to existing item and exit
+
+                    // TODO: adding a positive and a negative weight can lead to a weightless item;
+                    // in which case it should be removed from the collection.
+
+                    var w = buffer[i].Weight + item.Weight;
+
+                    buffer[i] = w == 0 ? default : new IndexWeight(item.Index, w);
+
+                    return length;
                 }
             }
 
-            return offset;
+            // try to append at the end
+
+            if (length < buffer.Length)
+            {
+                buffer[length] = item;
+                return length + 1;
+            }
+
+            // collection is already full, try find insertion index
+            // by looking for the "smallest" item in the current
+            // collection.
+
+            var idx = -1;
+            var curr = item;
+
+            for (int i = 0; i < buffer.Length; ++i)
+            {
+                if (buffer[i].IsGreaterThan(curr)) continue;
+                idx = i;
+                curr = buffer[i];
+            }
+
+            if (idx >= 0) buffer[idx] = item;
+
+            return length;
         }
 
         public static int CopyTo(in SparseWeight8 src, Span<int> dstIndices, Span<float> dstWeights, int dstLength)
@@ -102,10 +300,9 @@ namespace SharpGLTF.Transforms
             System.Diagnostics.Debug.Assert(dstWeights.Length >= dstLength, $"{nameof(dstWeights)}.Length must be at least {nameof(dstLength)}");
             System.Diagnostics.Debug.Assert(dstWeights.Slice(0, dstLength).ToArray().All(item => item == 0), "All weights must be zero");
 
-            for (int i = 0; i < 8; ++i)
+            foreach (var pair in src._GetPairs())
             {
-                var pair = GetIndexedWeight(src, i);
-                if (pair.Weight == 0) continue;
+                System.Diagnostics.Debug.Assert(pair.Weight != 0);
 
                 var idx = dstIndices
                     .Slice(0, dstLength)
@@ -132,6 +329,8 @@ namespace SharpGLTF.Transforms
         {
             for (int i = 0; i < pairs.Length - 1; ++i)
             {
+                // repeat len times until the collection is sorted.
+
                 bool sorted = true;
 
                 for (int j = 1; j < pairs.Length; ++j)
@@ -141,11 +340,7 @@ namespace SharpGLTF.Transforms
                     var kk = pairs[k];
                     var jj = pairs[j];
 
-                    var kw = Math.Abs(kk.Weight);
-                    var jw = Math.Abs(jj.Weight);
-
-                    if (kw  > jw) continue;
-                    if (kw == jw && kk.Index < jj.Index) continue;
+                    if (kk.IsGreaterThan(jj)) continue;
 
                     pairs[k] = jj;
                     pairs[j] = kk;

+ 244 - 144
src/SharpGLTF.Core/Transforms/SparseWeight8.cs

@@ -11,13 +11,23 @@ namespace SharpGLTF.Transforms
     /// Represents a sparse collection of non zero weight values, with a maximum of 8 weights.
     /// </summary>
     /// <remarks>
-    /// <see cref="SparseWeight8"/> is being used in two different contexts:
-    /// - As an utility class to define per vertex joint weights in mesh skinning.
-    /// - As an animation key in morph targets; a mesh can have many morph targets, but realistically and due to GPU limitations, only up to 8 morph targets can be blended at the same time.
+    /// <see cref="SparseWeight8"/> is being used in two different contexts:<br/>
+    /// <list type="bullet">
+    /// <item>As an utility structure to define per vertex joint weights in mesh skinning.</item>
+    /// <item>As an animation key in morph targets; a mesh can have many morph targets,
+    /// but realistically and due to GPU limitations, only up to 8 morph targets can
+    /// be blended at the same time.
+    /// </item>
+    /// </list>
+    /// Constructors are designed so weightless values are not taken into account,<br/>
+    /// and duplicated indices are merged, so indices are expected to be unique.
+    /// <para>
+    /// Use static Create* methods to construct instances of <see cref="SparseWeight8"/>.
+    /// </para>
     /// </remarks>
     [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
     [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
-    public readonly struct SparseWeight8
+    public readonly struct SparseWeight8 : IEquatable<SparseWeight8>
     {
         #region debug
 
@@ -34,7 +44,7 @@ namespace SharpGLTF.Transforms
 
         #endregion
 
-        #region constructors
+        #region factory
 
         /// <summary>
         /// Creates a new <see cref="SparseWeight8"/> from a weights collection.
@@ -49,7 +59,7 @@ namespace SharpGLTF.Transforms
 
         /// <summary>
         /// Creates a new <see cref="SparseWeight8"/> from a weights collection.
-        /// If there's more than 8 non zero values, the 8 most representative values are taken
+        /// If there's more than 8 weighted values, the 8 heaviest values are taken.
         /// </summary>
         /// <param name="weights">A sequence of weight values.</param>
         /// <returns>A <see cref="SparseWeight8"/> instance.</returns>
@@ -57,93 +67,131 @@ namespace SharpGLTF.Transforms
         {
             if (weights == null) return default;
 
-            var indexedWeights = weights
-                .Select((val, idx) => (idx, val))
-                .Where(item => item.val != 0)
-                .OrderByDescending(item => Math.Abs(item.val) )
-                .Take(8)
-                .ToArray();
+            Span<IndexWeight> sparse = stackalloc IndexWeight[8];
+
+            int index = 0;
+            int count = 0;
 
-            return Create(indexedWeights);
+            foreach (var w in weights)
+            {
+                if (w != 0) count = IndexWeight.InsertUnsorted(sparse, count, (index, w));
+                index++;
+            }
+
+            return new SparseWeight8(sparse);
         }
 
         /// <summary>
         /// Creates a new <see cref="SparseWeight8"/> from an indexed weight collection.
-        /// If there's more than 8 non zero values, the 8 most representative values are taken
+        /// If there's more than 8 weighted values, the 8 heaviest values are taken.
         /// </summary>
-        /// <param name="indexedWeights">A sequence of indexed weight values.</param>
+        /// <param name="indexedWeights">A sequence of indexed weight pairs.</param>
         /// <returns>A <see cref="SparseWeight8"/> instance.</returns>
         public static SparseWeight8 Create(params (int Index, float Weight)[] indexedWeights)
         {
-            if (indexedWeights == null) return default;
-
-            Span<IndexWeight> sparse = stackalloc IndexWeight[indexedWeights.Length];
-
-            int o = 0;
-
-            for (int i = 0; i < indexedWeights.Length; ++i)
-            {
-                var p = indexedWeights[i];
-                if (p.Weight == 0) continue;
+            return Create((IEnumerable<(int Index, float Weight)>)indexedWeights);
+        }
 
-                Guard.MustBeGreaterThanOrEqualTo(p.Index, 0, nameof(indexedWeights));
+        /// <summary>
+        /// Creates a new <see cref="SparseWeight8"/> from an indexed weight collection.
+        /// If there's more than 8 weighted values, the 8 heaviest values are taken.
+        /// </summary>
+        /// <param name="indexedWeights">A sequence of indexed weight pairs.</param>
+        /// <returns>A <see cref="SparseWeight8"/> instance.</returns>
+        public static SparseWeight8 Create(IEnumerable<(int Index, float Weight)> indexedWeights)
+        {
+            if (indexedWeights == null) return default;
 
-                sparse[o++] = p;
-            }
+            Span<IndexWeight> sparse = stackalloc IndexWeight[8];
 
-            sparse = sparse.Slice(0, o);
+            int count = 0;
 
-            if (indexedWeights.Length > 8)
+            foreach (var iw in indexedWeights)
             {
-                IndexWeight.BubbleSortByWeight(sparse);
-                sparse = sparse.Slice(0, 8);
+                if (iw.Weight == 0) continue;
+                count = IndexWeight.InsertUnsorted(sparse, count, iw);
             }
 
             return new SparseWeight8(sparse);
         }
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="SparseWeight8"/> struct.
+        /// Creates a new <see cref="SparseWeight8"/> struct.
         /// </summary>
+        /// <remarks>
+        /// Repeating indices will have their weights merged.
+        /// </remarks>
         /// <param name="idx0123">The indices of weights 0 to 3.</param>
         /// <param name="wgt0123">The weights of indices 0 to 3.</param>
-        public SparseWeight8(in Vector4 idx0123, in Vector4 wgt0123)
+        /// <returns>A <see cref="SparseWeight8"/> instance.</returns>
+        public static SparseWeight8 Create(in Vector4 idx0123, in Vector4 wgt0123)
         {
-            Index0 = (int)idx0123.X;
-            Index1 = (int)idx0123.Y;
-            Index2 = (int)idx0123.Z;
-            Index3 = (int)idx0123.W;
+            Span<IndexWeight> sparse = stackalloc IndexWeight[8];
 
-            Index4 = 0;
-            Index5 = 0;
-            Index6 = 0;
-            Index7 = 0;
+            IndexWeight.InsertUnsorted(sparse, idx0123, wgt0123);
 
-            Weight0 = wgt0123.X;
-            Weight1 = wgt0123.Y;
-            Weight2 = wgt0123.Z;
-            Weight3 = wgt0123.W;
+            return new SparseWeight8(sparse);
+        }
+
+        /// <summary>
+        /// Creates a new <see cref="SparseWeight8"/> struct.
+        /// </summary>
+        /// <remarks>
+        /// Repeating indices will have their weights merged.
+        /// </remarks>
+        /// <param name="idx0123">The first 4 indices.</param>
+        /// <param name="idx4567">The next 4 indices.</param>
+        /// <param name="wgt0123">The first 4 weights.</param>
+        /// <param name="wgt4567">The next 4 weights.</param>
+        /// <returns>A <see cref="SparseWeight8"/> instance.</returns>
+        public static SparseWeight8 Create(in Vector4 idx0123, in Vector4 idx4567, in Vector4 wgt0123, in Vector4 wgt4567)
+        {
+            Span<IndexWeight> sparse = stackalloc IndexWeight[8];
+            int count = IndexWeight.InsertUnsorted(sparse, idx0123, wgt0123);
+
+            count = IndexWeight.InsertUnsorted(sparse, count, ((int)idx4567.X, wgt4567.X));
+            count = IndexWeight.InsertUnsorted(sparse, count, ((int)idx4567.Y, wgt4567.Y));
+            count = IndexWeight.InsertUnsorted(sparse, count, ((int)idx4567.Z, wgt4567.Z));
+            count = IndexWeight.InsertUnsorted(sparse, count, ((int)idx4567.W, wgt4567.W));
+
+            return new SparseWeight8(sparse);
+        }
 
-            Weight4 = 0;
-            Weight5 = 0;
-            Weight6 = 0;
-            Weight7 = 0;
+        /// <summary>
+        /// Creates a new <see cref="SparseWeight8"/> struct.
+        /// </summary>
+        /// <remarks>
+        /// Unlike <see cref="Create(in Vector4, in Vector4, in Vector4, in Vector4)"/>, this method<br/>
+        /// is a direct call to the constructor, so it's very fast. But it doesn't validate the input<br/>
+        /// values, so it's intended to be used in limited scenarios, where performance is paramount.
+        /// </remarks>
+        /// <param name="idx0123">The first 4 indices.</param>
+        /// <param name="idx4567">The next 4 indices.</param>
+        /// <param name="wgt0123">The first 4 weights.</param>
+        /// <param name="wgt4567">The next 4 weights.</param>
+        /// <returns>A <see cref="SparseWeight8"/> instance.</returns>
+        public static SparseWeight8 CreateUnchecked(in Vector4 idx0123, in Vector4 idx4567, in Vector4 wgt0123, in Vector4 wgt4567)
+        {
+            return new SparseWeight8(idx0123, idx4567, wgt0123, wgt4567);
         }
 
+        #endregion
+
+        #region constructors
+
         /// <summary>
         /// Initializes a new instance of the <see cref="SparseWeight8"/> struct.
         /// </summary>
-        /// <param name="idx0123">The indices of weights 0 to 3.</param>
-        /// <param name="idx4567">The indices of weights 4 to 7.</param>
-        /// <param name="wgt0123">The weights of indices 0 to 3.</param>
-        /// <param name="wgt4567">The weights of indices 4 to 7.</param>
-        public SparseWeight8(in Vector4 idx0123, in Vector4 idx4567, in Vector4 wgt0123, in Vector4 wgt4567)
+        /// <param name="idx0123">The first 4 indices.</param>
+        /// <param name="idx4567">The next 4 indices.</param>
+        /// <param name="wgt0123">The first 4 weights.</param>
+        /// <param name="wgt4567">The next 4 weights.</param>
+        private SparseWeight8(in Vector4 idx0123, in Vector4 idx4567, in Vector4 wgt0123, in Vector4 wgt4567)
         {
             Index0 = (int)idx0123.X;
             Index1 = (int)idx0123.Y;
             Index2 = (int)idx0123.Z;
             Index3 = (int)idx0123.W;
-
             Index4 = (int)idx4567.X;
             Index5 = (int)idx4567.Y;
             Index6 = (int)idx4567.Z;
@@ -153,52 +201,54 @@ namespace SharpGLTF.Transforms
             Weight1 = wgt0123.Y;
             Weight2 = wgt0123.Z;
             Weight3 = wgt0123.W;
-
             Weight4 = wgt4567.X;
             Weight5 = wgt4567.Y;
             Weight6 = wgt4567.Z;
             Weight7 = wgt4567.W;
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SparseWeight8"/> struct.
+        /// </summary>
+        /// <param name="iw">A collection of 8 <see cref="IndexWeight"/> pairs.</param>
         private SparseWeight8(ReadOnlySpan<IndexWeight> iw)
         {
-            System.Diagnostics.Debug.Assert(iw.Length <= 8, nameof(iw));
+            #if DEBUG
+            if (iw.Length != 8) throw new ArgumentException(nameof(iw));
+            if (!IndexWeight.IsWellFormed(iw, out var err)) throw new ArgumentException(err, nameof(iw));
+            #endif
 
-            this = default;
-
-            if (iw.Length < 1) return;
             this.Index0 = iw[0].Index;
             this.Weight0 = iw[0].Weight;
 
-            if (iw.Length < 2) return;
             this.Index1 = iw[1].Index;
             this.Weight1 = iw[1].Weight;
 
-            if (iw.Length < 3) return;
             this.Index2 = iw[2].Index;
             this.Weight2 = iw[2].Weight;
 
-            if (iw.Length < 4) return;
             this.Index3 = iw[3].Index;
             this.Weight3 = iw[3].Weight;
 
-            if (iw.Length < 5) return;
             this.Index4 = iw[4].Index;
             this.Weight4 = iw[4].Weight;
 
-            if (iw.Length < 6) return;
             this.Index5 = iw[5].Index;
             this.Weight5 = iw[5].Weight;
 
-            if (iw.Length < 7) return;
             this.Index6 = iw[6].Index;
             this.Weight6 = iw[6].Weight;
 
-            if (iw.Length < 8) return;
             this.Index7 = iw[7].Index;
             this.Weight7 = iw[7].Weight;
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SparseWeight8"/> struct<br/>
+        /// from another instance, and multiplying the weights by a scale.
+        /// </summary>
+        /// <param name="sparse">The source <see cref="SparseWeight8"/>.</param>
+        /// <param name="scale">The scale.</param>
         private SparseWeight8(in SparseWeight8 sparse, float scale)
         {
             Index0 = sparse.Index0;
@@ -219,11 +269,6 @@ namespace SharpGLTF.Transforms
             Weight7 = sparse.Weight7 * scale;
         }
 
-        internal static (SparseWeight8 TangentIn, SparseWeight8 Value, SparseWeight8 TangentOut) AsTuple(float[] tangentIn, float[] value, float[] tangentOut)
-        {
-            return (Create(tangentIn), Create(value), Create(tangentOut));
-        }
-
         #endregion
 
         #region data
@@ -252,43 +297,75 @@ namespace SharpGLTF.Transforms
         public readonly int Index7;
         public readonly float Weight7;
 
-        public static bool AreWeightsEqual(in SparseWeight8 x, in SparseWeight8 y)
+        public override int GetHashCode()
         {
-            const int STACKSIZE = 8 * 2;
-
-            Span<int>   indices = stackalloc int[STACKSIZE];
-            Span<float> xWeights = stackalloc float[STACKSIZE];
-            Span<float> yWeights = stackalloc float[STACKSIZE];
+            // we calculate the hash form the highest weight.
 
-            int offset = 0;
-            offset = IndexWeight.CopyTo(x, indices, xWeights, offset);
-            offset = IndexWeight.CopyTo(y, indices, yWeights, offset);
+            float h = 0, w;
 
-            xWeights = xWeights.Slice(0, offset);
-            yWeights = yWeights.Slice(0, offset);
+            w = Math.Abs(this.Weight0); if (w > h) { h = w; }
+            w = Math.Abs(this.Weight1); if (w > h) { h = w; }
+            w = Math.Abs(this.Weight2); if (w > h) { h = w; }
+            w = Math.Abs(this.Weight3); if (w > h) { h = w; }
+            w = Math.Abs(this.Weight4); if (w > h) { h = w; }
+            w = Math.Abs(this.Weight5); if (w > h) { h = w; }
+            w = Math.Abs(this.Weight6); if (w > h) { h = w; }
+            w = Math.Abs(this.Weight7); if (w > h) { h = w; }
 
-            return xWeights.SequenceEqual(yWeights);
+            return h.GetHashCode();
         }
 
-        public int GetWeightsHashCode()
+        internal static bool AreEqual(in SparseWeight8 x, in SparseWeight8 y)
         {
-            Span<IndexWeight> iw = stackalloc IndexWeight[8];
+            const int STACKSIZE = 8;
 
-            var c = IndexWeight.CopyTo(this, iw);
+            Span<IndexWeight> xWeights = stackalloc IndexWeight[STACKSIZE];
+            Span<IndexWeight> yWeights = stackalloc IndexWeight[STACKSIZE];
 
-            iw = iw.Slice(0, c);
+            x.CopyTo(xWeights);
+            y.CopyTo(yWeights);
 
-            IndexWeight.BubbleSortByIndex(iw);
+            #if DEBUG
+            if (!IndexWeight.IsWellFormed(xWeights, out var errx)) throw new ArgumentException(errx, nameof(x));
+            if (!IndexWeight.IsWellFormed(yWeights, out var erry)) throw new ArgumentException(erry, nameof(y));
+            #endif
 
-            int h = 0;
+            for (int i = 0; i < STACKSIZE; ++i)
+            {
+                var xItem = xWeights[i];
+                if (xItem.Weight == 0) continue;
+
+                bool match = false;
+
+                for (int j = 0; j < STACKSIZE; ++j)
+                {
+                    var yItem = yWeights[j];
+                    if (yItem.Weight == 0) continue;
+                    if (xItem.Index == yItem.Index)
+                    {
+                        if (xItem.Weight != yItem.Weight) return false;
+                        yWeights[j] = default;
+                        match = true;
+                        break;
+                    }
+                }
+
+                if (!match) return false;
+            }
 
-            for (int i = 0; i < iw.Length; ++i)
+            for (int i = 0; i < STACKSIZE; ++i)
             {
-                h += iw[i].GetHashCode();
-                h *= 17;
+                if (yWeights[i].Weight != 0) return false;
             }
 
-            return h;
+            return true;
+        }
+
+        public bool Equals(SparseWeight8 other) { return AreEqual(this, other); }
+
+        public override bool Equals(object obj)
+        {
+            return obj is SparseWeight8 other && AreEqual(this, other);
         }
 
         #endregion
@@ -324,9 +401,14 @@ namespace SharpGLTF.Transforms
         {
             Span<IndexWeight> iw = stackalloc IndexWeight[8];
 
-            var c = IndexWeight.CopyTo(sparse, iw);
-
-            iw = iw.Slice(0, c);
+            iw[0] = (sparse.Index0, sparse.Weight0);
+            iw[1] = (sparse.Index1, sparse.Weight1);
+            iw[2] = (sparse.Index2, sparse.Weight2);
+            iw[3] = (sparse.Index3, sparse.Weight3);
+            iw[4] = (sparse.Index4, sparse.Weight4);
+            iw[5] = (sparse.Index5, sparse.Weight5);
+            iw[6] = (sparse.Index6, sparse.Weight6);
+            iw[7] = (sparse.Index7, sparse.Weight7);
 
             IndexWeight.BubbleSortByWeight(iw);
 
@@ -343,11 +425,9 @@ namespace SharpGLTF.Transforms
         {
             Span<IndexWeight> iw = stackalloc IndexWeight[8];
 
-            var c = IndexWeight.CopyTo(sparse, iw);
-
-            iw = iw.Slice(0, c);
+            var c = sparse.InsertTo(iw);
 
-            IndexWeight.BubbleSortByIndex(iw);
+            IndexWeight.BubbleSortByIndex(iw.Slice(0, c));
 
             return new SparseWeight8(iw);
         }
@@ -467,20 +547,18 @@ namespace SharpGLTF.Transforms
             return r;
         }
 
-        public SparseWeight8 GetReducedWeights(int maxWeights)
+        public SparseWeight8 GetTrimmed(int maxWeights)
         {
             Span<IndexWeight> entries = stackalloc IndexWeight[8];
 
-            IndexWeight.CopyTo(this, entries);
-            IndexWeight.BubbleSortByWeight(entries);
-
-            for (int i = maxWeights; i < entries.Length; ++i) entries[i] = default;
+            this.InsertTo(entries.Slice(0, maxWeights));
 
-            var reduced = new SparseWeight8(entries);
-
-            var scale = reduced.WeightSum == 0f ? 0f : this.WeightSum / reduced.WeightSum;
+            return new SparseWeight8(entries);
+        }
 
-            return Multiply(reduced, scale);
+        public SparseWeight8 GetNormalized()
+        {
+            return Multiply(this, 1f / this.WeightSum);
         }
 
         public override string ToString()
@@ -525,24 +603,15 @@ namespace SharpGLTF.Transforms
 
             // perform operation element by element
 
-            int r = 0;
-            Span<IndexWeight> rrr = stackalloc IndexWeight[STACKSIZE];
+            int len = 0;
+            Span<IndexWeight> rrr = stackalloc IndexWeight[8];
 
             for (int i = 0; i < offset; ++i)
             {
                 var ww = operationFunc(xxx[i], yyy[i]);
-
                 if (ww == 0) continue;
 
-                rrr[r++] = new IndexWeight(indices[i], ww);
-            }
-
-            rrr = rrr.Slice(0, r);
-
-            if (rrr.Length > 8)
-            {
-                IndexWeight.BubbleSortByWeight(rrr);
-                rrr = rrr.Slice(0, 8);
+                len = IndexWeight.InsertUnsorted(rrr, len, (indices[i], ww));
             }
 
             return new SparseWeight8(rrr);
@@ -584,24 +653,15 @@ namespace SharpGLTF.Transforms
 
             // perform operation element by element
 
-            int r = 0;
-            Span<IndexWeight> rrr = stackalloc IndexWeight[STACKSIZE];
+            int len = 0;
+            Span<IndexWeight> rrr = stackalloc IndexWeight[8];
 
             for (int i = 0; i < offset; ++i)
             {
                 var ww = operationFunc(xxx[i], yyy[i], zzz[i], www[i]);
-
                 if (ww == 0) continue;
 
-                rrr[r++] = new IndexWeight(indices[i], ww);
-            }
-
-            rrr = rrr.Slice(0, r);
-
-            if (rrr.Length > 8)
-            {
-                IndexWeight.BubbleSortByWeight(rrr);
-                rrr = rrr.Slice(0, 8);
+                len = IndexWeight.InsertUnsorted(rrr, len, (indices[i], ww));
             }
 
             return new SparseWeight8(rrr);
@@ -657,17 +717,12 @@ namespace SharpGLTF.Transforms
             var sum = this.WeightSum;
             if (sum >= 1) return this;
 
-            Span<IndexWeight> weights = stackalloc IndexWeight[8 + 1];
+            Span<IndexWeight> weights = stackalloc IndexWeight[8];
 
-            var offset = IndexWeight.CopyTo(this, weights);
-            weights[offset++] = new IndexWeight(complementIndex, 1 - sum);
-            weights = weights.Slice(0, offset);
+            var offset = this.InsertTo(weights);
 
-            if (offset > 8)
-            {
-                IndexWeight.BubbleSortByWeight(weights);
-                weights = weights.Slice(0, 8);
-            }
+            // accumulate complement
+            offset = IndexWeight.InsertUnsorted(weights, offset, new IndexWeight(complementIndex, 1 - sum));
 
             return new SparseWeight8(weights);
         }
@@ -688,6 +743,51 @@ namespace SharpGLTF.Transforms
             return idx;
         }
 
+        internal IEnumerable<IndexWeight> _GetPairs()
+        {
+            if (Weight0 != 0) yield return new IndexWeight(Index0, Weight0);
+            if (Weight1 != 0) yield return new IndexWeight(Index1, Weight1);
+            if (Weight2 != 0) yield return new IndexWeight(Index2, Weight2);
+            if (Weight3 != 0) yield return new IndexWeight(Index3, Weight3);
+            if (Weight4 != 0) yield return new IndexWeight(Index4, Weight4);
+            if (Weight5 != 0) yield return new IndexWeight(Index5, Weight5);
+            if (Weight6 != 0) yield return new IndexWeight(Index6, Weight6);
+            if (Weight7 != 0) yield return new IndexWeight(Index7, Weight7);
+        }
+
+        internal int InsertTo(Span<IndexWeight> dst)
+        {
+            var offset = 0;
+
+            if (Weight0 != 0) offset = IndexWeight.InsertUnsorted(dst, offset, (Index0, Weight0));
+            if (Weight1 != 0) offset = IndexWeight.InsertUnsorted(dst, offset, (Index1, Weight1));
+            if (Weight2 != 0) offset = IndexWeight.InsertUnsorted(dst, offset, (Index2, Weight2));
+            if (Weight3 != 0) offset = IndexWeight.InsertUnsorted(dst, offset, (Index3, Weight3));
+            if (Weight4 != 0) offset = IndexWeight.InsertUnsorted(dst, offset, (Index4, Weight4));
+            if (Weight5 != 0) offset = IndexWeight.InsertUnsorted(dst, offset, (Index5, Weight5));
+            if (Weight6 != 0) offset = IndexWeight.InsertUnsorted(dst, offset, (Index6, Weight6));
+            if (Weight7 != 0) offset = IndexWeight.InsertUnsorted(dst, offset, (Index7, Weight7));
+
+            return offset;
+        }
+
+        internal void CopyTo(Span<IndexWeight> dst)
+        {
+            dst[0] = (Index0, Weight0);
+            dst[1] = (Index1, Weight1);
+            dst[2] = (Index2, Weight2);
+            dst[3] = (Index3, Weight3);
+            dst[4] = (Index4, Weight4);
+            dst[5] = (Index5, Weight5);
+            dst[6] = (Index6, Weight6);
+            dst[7] = (Index7, Weight7);
+        }
+
+        internal static (SparseWeight8 TangentIn, SparseWeight8 Value, SparseWeight8 TangentOut) AsTuple(float[] tangentIn, float[] value, float[] tangentOut)
+        {
+            return (Create(tangentIn), Create(value), Create(tangentOut));
+        }
+
         #endregion
     }
 }

+ 1 - 1
src/SharpGLTF.Toolkit/Geometry/MeshBuilderToolkit.cs

@@ -60,7 +60,7 @@ namespace SharpGLTF.Geometry
             var indices = meshes
                 .SelectMany(item => item.Primitives)
                 .SelectMany(item => item.Vertices)
-                .Select(item => item.GetSkinning().GetWeights().MaxIndex);
+                .Select(item => item.GetSkinning().GetBindings().MaxIndex);
 
             var maxIndex = indices.Any() ? indices.Max() : 0;
 

+ 1 - 1
src/SharpGLTF.Toolkit/Geometry/Packed/PackedEncoding.cs

@@ -17,7 +17,7 @@ namespace SharpGLTF.Geometry
         {
             if (JointsEncoding.HasValue) return;
 
-            var indices = vertices.Select(item => item.GetSkinning().GetWeights().MaxIndex);
+            var indices = vertices.Select(item => item.GetSkinning().GetBindings().MaxIndex);
             var maxIndex = indices.Any() ? indices.Max() : 0;
             JointsEncoding = maxIndex < 256 ? ENCODING.UNSIGNED_BYTE : ENCODING.UNSIGNED_SHORT;
         }

+ 6 - 6
src/SharpGLTF.Toolkit/Geometry/VertexBufferColumns.cs

@@ -150,8 +150,8 @@ namespace SharpGLTF.Geometry
             {
                 if (this.Joints0 != null)
                 {
-                    if (this.Joints1 != null) skinning = new Transforms.SparseWeight8(Joints0[i], Joints1[i], Weights0[i], Weights1[i]);
-                    else skinning = new Transforms.SparseWeight8(Joints0[i], Weights0[i]);
+                    if (this.Joints1 != null) skinning = Transforms.SparseWeight8.Create(Joints0[i], Joints1[i], Weights0[i], Weights1[i]);
+                    else skinning = Transforms.SparseWeight8.Create(Joints0[i], Weights0[i]);
                 }
 
                 if (this.Positions != null)
@@ -292,13 +292,13 @@ namespace SharpGLTF.Geometry
             {
                 if (Joints1 != null && Weights1 != null)
                 {
-                    var sparse = new Transforms.SparseWeight8(Joints0[index], Joints1[index], Weights0[index], Weights1[index]);
-                    s.SetWeights(sparse);
+                    var sparse = Transforms.SparseWeight8.Create(Joints0[index], Joints1[index], Weights0[index], Weights1[index]);
+                    s.SetBindings(sparse);
                 }
                 else
                 {
-                    var sparse = new Transforms.SparseWeight8(Joints0[index], Weights0[index]);
-                    s.SetWeights(sparse);
+                    var sparse = Transforms.SparseWeight8.Create(Joints0[index], Weights0[index]);
+                    s.SetBindings(sparse);
                 }
             }
 

+ 112 - 83
src/SharpGLTF.Toolkit/Geometry/VertexBuilder.cs

@@ -40,31 +40,40 @@ namespace SharpGLTF.Geometry
     /// </summary>
     /// <typeparam name="TvG">
     /// The vertex fragment type with Position, Normal and Tangent.<br/>
-    /// Valid types are:<br/>
-    /// - <see cref="VertexPosition"/><br/>
-    /// - <see cref="VertexPositionNormal"/><br/>
-    /// - <see cref="VertexPositionNormalTangent"/>
+    /// <br/>Valid types are:
+    /// <list type="table">
+    /// <item><see cref="VertexPosition"/></item>
+    /// <item><see cref="VertexPositionNormal"/></item>
+    /// <item><see cref="VertexPositionNormalTangent"/></item>
+    /// </list>
     /// </typeparam>
     /// <typeparam name="TvM">
-    /// The vertex fragment type with Colors and Texture Coordinates.<br/>
-    /// Valid types are:<br/>
-    /// - <see cref="VertexEmpty"/><br/>
-    /// - <see cref="VertexColor1"/><br/>
-    /// - <see cref="VertexTexture1"/><br/>
-    /// - <see cref="VertexColor1Texture1"/><br/>
-    /// - <see cref="VertexColor1Texture2"/><br/>
-    /// - <see cref="VertexColor2Texture1"/><br/>
-    /// - <see cref="VertexColor2Texture2"/>
+    /// The vertex fragment type with Colors, Texture Coordinates, and custom attributes.<br/>
+    /// <br/>Valid types are:
+    /// <list type="table">
+    /// <item><see cref="VertexEmpty"/></item>
+    /// <item><see cref="VertexColor1"/></item>
+    /// <item><see cref="VertexColor2"/></item>
+    /// <item><see cref="VertexTexture1"/></item>
+    /// <item><see cref="VertexTexture2"/></item>
+    /// <item><see cref="VertexColor1Texture1"/></item>
+    /// <item><see cref="VertexColor2Texture1"/></item>
+    /// <item><see cref="VertexColor2Texture1"/></item>
+    /// <item><see cref="VertexColor2Texture2"/></item>
+    /// <item>Custom vertex material fragment types.</item>
+    /// </list>
     /// </typeparam>
     /// <typeparam name="TvS">
-    /// The vertex fragment type with Skin Joint Weights.
-    /// Valid types are:<br/>
-    /// - <see cref="VertexEmpty"/><br/>
-    /// - <see cref="VertexJoints4"/><br/>
-    /// - <see cref="VertexJoints8"/>
+    /// The vertex fragment type with Skin Joint Weights.<br/>
+    /// <br/>Valid types are:
+    /// <list type="table">
+    /// <item><see cref="VertexEmpty"/></item>
+    /// <item><see cref="VertexJoints4"/></item>
+    /// <item><see cref="VertexJoints8"/></item>
+    /// </list>
     /// </typeparam>
     [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
-    public partial struct VertexBuilder<TvG, TvM, TvS> : IVertexBuilder
+    public partial struct VertexBuilder<TvG, TvM, TvS> : IVertexBuilder, IEquatable<VertexBuilder<TvG, TvM, TvS>>
         where TvG : struct, IVertexGeometry
         where TvM : struct, IVertexMaterial
         where TvS : struct, IVertexSkinning
@@ -123,8 +132,8 @@ namespace SharpGLTF.Geometry
 
             for (int i = 0; i < Skinning.MaxBindings; ++i)
             {
-                var jw = Skinning.GetJointBinding(i);
-                if (!jw.Weight._IsFinite() || jw.Weight < 0 || jw.Index < 0) sb.Append($" ❌𝐉𝐖{i} {jw.Index}:{jw.Weight}");
+                var (jidx, jwgt) = Skinning.GetBinding(i);
+                if (!jwgt._IsFinite() || jwgt < 0 || jidx < 0) sb.Append($" ❌𝐉𝐖{i} {jidx}:{jwgt}");
             }
 
             return sb.ToString();
@@ -141,7 +150,7 @@ namespace SharpGLTF.Geometry
             Skinning = s;
         }
 
-        public VertexBuilder(in TvG g, in TvM m, params (int, float)[] bindings)
+        public VertexBuilder(in TvG g, in TvM m, params (int JointIndex, float Weight)[] bindings)
         {
             Geometry = g;
             Material = m;
@@ -149,7 +158,7 @@ namespace SharpGLTF.Geometry
             var sparse = Transforms.SparseWeight8.Create(bindings);
 
             Skinning = default;
-            Skinning.SetWeights(sparse);
+            Skinning.SetBindings(sparse);
         }
 
         public VertexBuilder(in TvG g, in TvM m, in Transforms.SparseWeight8 bindings)
@@ -157,7 +166,7 @@ namespace SharpGLTF.Geometry
             Geometry = g;
             Material = m;
             Skinning = default;
-            Skinning.SetWeights(bindings);
+            Skinning.SetBindings(bindings);
         }
 
         public VertexBuilder(in TvG g, in TvM m)
@@ -181,7 +190,7 @@ namespace SharpGLTF.Geometry
             Skinning = default;
         }
 
-        public VertexBuilder(in TvG g, params (int Index, float Weight)[] bindings)
+        public VertexBuilder(in TvG g, params (int JointIndex, float Weight)[] bindings)
         {
             Geometry = g;
             Material = default;
@@ -189,7 +198,7 @@ namespace SharpGLTF.Geometry
             var sparse = Transforms.SparseWeight8.Create(bindings);
 
             Skinning = default;
-            Skinning.SetWeights(sparse);
+            Skinning.SetBindings(sparse);
         }
 
         public VertexBuilder(TvG g, Transforms.SparseWeight8 bindings)
@@ -197,7 +206,7 @@ namespace SharpGLTF.Geometry
             Geometry = g;
             Material = default;
             Skinning = default;
-            Skinning.SetWeights(bindings);
+            Skinning.SetBindings(bindings);
         }
 
         public static implicit operator VertexBuilder<TvG, TvM, TvS>(in (TvG Geo, TvM Mat, TvS Skin) tuple)
@@ -271,6 +280,14 @@ namespace SharpGLTF.Geometry
         public TvM Material;
         public TvS Skinning;
 
+        public override bool Equals(object obj) { return obj is VertexBuilder<TvG, TvM, TvS> other && AreEqual(this, other); }
+        public bool Equals(VertexBuilder<TvG, TvM, TvS> other) { return AreEqual(this, other); }
+        public static bool operator ==(in VertexBuilder<TvG, TvM, TvS> a, in VertexBuilder<TvG, TvM, TvS> b) { return AreEqual(a, b); }
+        public static bool operator !=(in VertexBuilder<TvG, TvM, TvS> a, in VertexBuilder<TvG, TvM, TvS> b) { return !AreEqual(a, b); }
+        public static bool AreEqual(in VertexBuilder<TvG, TvM, TvS> a, in VertexBuilder<TvG, TvM, TvS> b)
+        {
+            return a.Position.Equals(b.Position) && a.Material.Equals(b.Material) && a.Skinning.Equals(b.Skinning);
+        }
         public override int GetHashCode() { return Geometry.GetHashCode(); }
 
         #endregion
@@ -288,6 +305,62 @@ namespace SharpGLTF.Geometry
 
         #region API
 
+        public void Validate()
+        {
+            VertexPreprocessorLambdas.ValidateVertexGeometry(Geometry);
+            VertexPreprocessorLambdas.ValidateVertexMaterial(Material);
+            VertexPreprocessorLambdas.ValidateVertexSkinning(Skinning);
+        }
+
+        #pragma warning disable CA1000 // Do not declare static members on generic types
+
+        public static MeshBuilder<TMaterial, TvG, TvM, TvS> CreateCompatibleMesh<TMaterial>(string name = null)
+        {
+            return new MeshBuilder<TMaterial, TvG, TvM, TvS>(name);
+        }
+
+        public static MeshBuilder<TvG, TvM, TvS> CreateCompatibleMesh(string name = null)
+        {
+            return new MeshBuilder<TvG, TvM, TvS>(name);
+        }
+
+        #pragma warning restore CA1000 // Do not declare static members on generic types
+
+        IVertexGeometry IVertexBuilder.GetGeometry() { return this.Geometry; }
+
+        IVertexMaterial IVertexBuilder.GetMaterial() { return this.Material; }
+
+        IVertexSkinning IVertexBuilder.GetSkinning() { return this.Skinning; }
+
+        void IVertexBuilder.SetGeometry(IVertexGeometry geometry)
+        {
+            Guard.NotNull(geometry, nameof(geometry));
+            this.Geometry = geometry.ConvertToGeometry<TvG>();
+        }
+
+        void IVertexBuilder.SetMaterial(IVertexMaterial material)
+        {
+            Guard.NotNull(material, nameof(material));
+            this.Material = material.ConvertToMaterial<TvM>();
+        }
+
+        void IVertexBuilder.SetSkinning(IVertexSkinning skinning)
+        {
+            Guard.NotNull(skinning, nameof(skinning));
+            this.Skinning = skinning.ConvertToSkinning<TvS>();
+        }
+
+        #endregion
+
+        #region With* fluent API
+
+        public VertexBuilder<TvG, TvM, TvS> TransformedBy(in Matrix4x4 transform)
+        {
+            var clone = this;
+            clone.Geometry.ApplyTransform(transform);
+            return clone;
+        }
+
         public VertexBuilder<TvG, TvM, TvS> WithGeometry(in Vector3 position)
         {
             var v = this;
@@ -336,77 +409,33 @@ namespace SharpGLTF.Geometry
             return v;
         }
 
-        public VertexBuilder<TvG, TvM, TvS> WithSkinning(params (int Index, float Weight)[] bindings)
+        public VertexBuilder<TvG, TvM, TvS> WithSkinning(in Transforms.SparseWeight8 sparse)
         {
             var v = this;
-
-            int i = 0;
-
-            while (i < bindings.Length)
-            {
-                v.Skinning.SetJointBinding(i, bindings[i].Index, bindings[i].Weight);
-                ++i;
-            }
-
-            while (i < bindings.Length)
-            {
-                v.Skinning.SetJointBinding(i, 0, 0);
-                ++i;
-            }
-
+            v.Skinning.SetBindings(sparse);
             return v;
         }
 
-        public VertexBuilder<TvG, TvM, TvS> TransformedBy(in Matrix4x4 transform)
+        public VertexBuilder<TvG, TvM, TvS> WithSkinning(params (int Index, float Weight)[] bindings)
         {
-            var clone = this;
-            clone.Geometry.ApplyTransform(transform);
-            return clone;
-        }
+            var v = this;
 
-        public void Validate()
-        {
-            VertexPreprocessorLambdas.ValidateVertexGeometry(Geometry);
-            VertexPreprocessorLambdas.ValidateVertexMaterial(Material);
-            VertexPreprocessorLambdas.ValidateVertexSkinning(Skinning);
-        }
+            var sparse = Transforms.SparseWeight8.Create(bindings);
 
-        #pragma warning disable CA1000 // Do not declare static members on generic types
+            v.Skinning.SetBindings(sparse);
 
-        public static MeshBuilder<TMaterial, TvG, TvM, TvS> CreateCompatibleMesh<TMaterial>(string name = null)
-        {
-            return new MeshBuilder<TMaterial, TvG, TvM, TvS>(name);
+            return v;
         }
 
-        public static MeshBuilder<TvG, TvM, TvS> CreateCompatibleMesh(string name = null)
+        public VertexBuilder<TvG, TvM, TvS> WithSkinning(IEnumerable<(int Index, float Weight)> bindings)
         {
-            return new MeshBuilder<TvG, TvM, TvS>(name);
-        }
-
-        #pragma warning restore CA1000 // Do not declare static members on generic types
-
-        IVertexGeometry IVertexBuilder.GetGeometry() { return this.Geometry; }
-
-        IVertexMaterial IVertexBuilder.GetMaterial() { return this.Material; }
-
-        IVertexSkinning IVertexBuilder.GetSkinning() { return this.Skinning; }
+            var v = this;
 
-        void IVertexBuilder.SetGeometry(IVertexGeometry geometry)
-        {
-            Guard.NotNull(geometry, nameof(geometry));
-            this.Geometry = geometry.ConvertToGeometry<TvG>();
-        }
+            var sparse = Transforms.SparseWeight8.Create(bindings);
 
-        void IVertexBuilder.SetMaterial(IVertexMaterial material)
-        {
-            Guard.NotNull(material, nameof(material));
-            this.Material = material.ConvertToMaterial<TvM>();
-        }
+            v.Skinning.SetBindings(sparse);
 
-        void IVertexBuilder.SetSkinning(IVertexSkinning skinning)
-        {
-            Guard.NotNull(skinning, nameof(skinning));
-            this.Skinning = skinning.ConvertToSkinning<TvS>();
+            return v;
         }
 
         #endregion

+ 6 - 4
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexEmpty.cs

@@ -63,14 +63,16 @@ namespace SharpGLTF.Geometry.VertexTypes
         Vector2 IVertexMaterial.GetTexCoord(int index) { throw new ArgumentOutOfRangeException(nameof(index)); }
 
         /// <inheritdoc/>
-        public SparseWeight8 GetWeights() { return default; }
+        public SparseWeight8 GetBindings() { return default; }
 
         /// <inheritdoc/>
-        public void SetWeights(in SparseWeight8 weights) { throw new NotSupportedException(); }
+        public void SetBindings(in SparseWeight8 weights) { throw new NotSupportedException(); }
 
-        void IVertexSkinning.SetJointBinding(int index, int joint, float weight) { throw new ArgumentOutOfRangeException(nameof(index)); }
+        /// <inheritdoc/>
+        public void SetBindings(params (int Index, float Weight)[] bindings) { throw new NotSupportedException(); }
 
-        (int, float) IVertexSkinning.GetJointBinding(int index) { throw new ArgumentOutOfRangeException(nameof(index)); }
+        /// <inheritdoc/>
+        (int Index, float Weight) IVertexSkinning.GetBinding(int index) { throw new ArgumentOutOfRangeException(nameof(index)); }
 
         #endregion
     }

+ 13 - 9
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexGeometry.cs

@@ -7,13 +7,17 @@ using System.Text;
 namespace SharpGLTF.Geometry.VertexTypes
 {
     /// <summary>
-    /// Represents the interface that must be implemented by a geometry vertex fragment.<br/>
-    /// Implemented by:<br/>
-    /// - <see cref="VertexPosition"/><br/>
-    /// - <see cref="VertexPositionNormal"/><br/>
-    /// - <see cref="VertexPositionNormalTangent"/><br/>
-    /// - <see cref="VertexGeometryDelta"/><br/>
+    /// Represents the interface that must be implemented by a geometry vertex fragment.
     /// </summary>
+    /// <remarks>
+    /// Implemented by:
+    /// <list type="table">
+    /// <item><see cref="VertexPosition"/></item>
+    /// <item><see cref="VertexPositionNormal"/></item>
+    /// <item><see cref="VertexPositionNormalTangent"/></item>
+    /// <item><see cref="VertexGeometryDelta"/></item>
+    /// </list>
+    /// </remarks>
     public interface IVertexGeometry
     {
         /// <summary>
@@ -87,7 +91,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     {
         #region debug
 
-        private string _GetDebuggerDisplay() => $"𝐏:{Position}";
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
 
         #endregion
 
@@ -179,7 +183,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     {
         #region debug
 
-        private string _GetDebuggerDisplay() => $"𝐏:{Position} 𝚴:{Normal}";
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
 
         #endregion
 
@@ -281,7 +285,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     {
         #region debug
 
-        private string _GetDebuggerDisplay() => $"𝐏:{Position} 𝚴:{Normal} 𝚻:{Tangent}";
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
 
         #endregion
 

+ 24 - 18
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexMaterial.cs

@@ -8,17 +8,23 @@ using ENCODING = SharpGLTF.Schema2.EncodingType;
 namespace SharpGLTF.Geometry.VertexTypes
 {
     /// <summary>
-    /// Represents the interface that must be implemented by a material vertex fragment.<br/>
-    /// Implemented by:<br/>
-    /// - <see cref="VertexColor1"/><br/>
-    /// - <see cref="VertexColor2"/><br/>
-    /// - <see cref="VertexTexture1"/><br/>
-    /// - <see cref="VertexTexture2"/><br/>
-    /// - <see cref="VertexColor1Texture1"/><br/>
-    /// - <see cref="VertexColor1Texture2"/><br/>
-    /// - <see cref="VertexColor2Texture1"/><br/>
-    /// - <see cref="VertexColor2Texture2"/><br/>
+    /// Represents the interface that must be implemented by a material vertex fragment.
     /// </summary>
+    /// <remarks>
+    /// Implemented by:
+    /// <list type="table">
+    /// <item><see cref="VertexEmpty"/></item>
+    /// <item><see cref="VertexColor1"/></item>
+    /// <item><see cref="VertexColor2"/></item>
+    /// <item><see cref="VertexTexture1"/></item>
+    /// <item><see cref="VertexTexture2"/></item>
+    /// <item><see cref="VertexColor1Texture1"/></item>
+    /// <item><see cref="VertexColor1Texture2"/></item>
+    /// <item><see cref="VertexColor2Texture1"/></item>
+    /// <item><see cref="VertexColor2Texture2"/></item>
+    /// <item>And also by other custom vertex material fragment types.</item>
+    /// </list>
+    /// </remarks>
     public interface IVertexMaterial
     {
         /// <summary>
@@ -70,7 +76,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     {
         #region debug
 
-        private string _GetDebuggerDisplay() => $"𝐂:{Color}";
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
 
         #endregion
 
@@ -150,7 +156,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     {
         #region debug
 
-        private string _GetDebuggerDisplay() => $"𝐂₀:{Color0} 𝐂₁:{Color1}";
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
 
         #endregion
 
@@ -237,7 +243,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     {
         #region debug
 
-        private string _GetDebuggerDisplay() => $"𝐔𝐕:{TexCoord}";
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
 
         #endregion
 
@@ -316,7 +322,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     {
         #region debug
 
-        private string _GetDebuggerDisplay() => $"𝐔𝐕₀:{TexCoord0} 𝐔𝐕₁:{TexCoord1}";
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
 
         #endregion
 
@@ -405,7 +411,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     {
         #region debug
 
-        private string _GetDebuggerDisplay() => $"𝐂:{Color} 𝐔𝐕:{TexCoord}";
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
 
         #endregion
 
@@ -490,7 +496,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     {
         #region debug
 
-        private string _GetDebuggerDisplay() => $"𝐂:{Color} 𝐔𝐕₀:{TexCoord0} 𝐔𝐕₁:{TexCoord1}";
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
 
         #endregion
 
@@ -588,7 +594,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     {
         #region debug
 
-        private string _GetDebuggerDisplay() => $"𝐂₀:{Color0} 𝐂₁:{Color1} 𝐔𝐕:{TexCoord}";
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
 
         #endregion
 
@@ -693,7 +699,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     {
         #region debug
 
-        private string _GetDebuggerDisplay() => $"𝐂₀:{Color0} 𝐂₁:{Color1} 𝐔𝐕₀:{TexCoord0} 𝐔𝐕₁:{TexCoord1}";
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
 
         #endregion
 

+ 5 - 5
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexPreprocessorLambdas.cs

@@ -117,7 +117,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
             for (int i = 0; i < vertex.MaxBindings; ++i)
             {
-                var (index, weight) = vertex.GetJointBinding(i);
+                var (index, weight) = vertex.GetBinding(i);
 
                 Guard.MustBeGreaterThanOrEqualTo(index, 0, $"Joint{i}");
                 Guard.IsTrue(weight._IsFinite(), $"Weight{i}", "Values are not finite.");
@@ -240,14 +240,14 @@ namespace SharpGLTF.Geometry.VertexTypes
             // Apparently the consensus is that weights are required to be normalized.
             // More here: https://github.com/KhronosGroup/glTF/issues/1213
 
-            var sparse = Transforms.SparseWeight8.OrderedByWeight(vertex.GetWeights());
+            var sparse = Transforms.SparseWeight8.OrderedByWeight(vertex.GetBindings());
 
             var sum = sparse.WeightSum;
-            if (sum == 0) return default(TvS);
 
-            sparse = Transforms.SparseWeight8.Multiply(sparse, 1.0f / sum);
+            if (sum == 0) return default(TvS);
+            if (sum != 1) sparse = Transforms.SparseWeight8.Multiply(sparse, 1.0f / sum);
 
-            vertex.SetWeights(sparse);
+            vertex.SetBindings(sparse);
 
             return vertex;
         }

+ 142 - 68
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexSkinning.cs

@@ -9,11 +9,16 @@ using ENCODING = SharpGLTF.Schema2.EncodingType;
 namespace SharpGLTF.Geometry.VertexTypes
 {
     /// <summary>
-    /// Represents the interface that must be implemented by a skiining vertex fragment.
-    /// Implemented by:<br/>
-    /// - <see cref="VertexJoints4"/><br/>
-    /// - <see cref="VertexJoints8"/><br/>
+    /// Represents the interface that must be implemented by a skinning vertex fragment.
     /// </summary>
+    /// <remarks>
+    /// Implemented by:
+    /// <list type="table">
+    /// <item><see cref="VertexEmpty"/></item>
+    /// <item><see cref="VertexJoints4"/></item>
+    /// <item><see cref="VertexJoints8"/></item>
+    /// </list>
+    /// </remarks>
     public interface IVertexSkinning
     {
         /// <summary>
@@ -24,31 +29,29 @@ namespace SharpGLTF.Geometry.VertexTypes
         /// <summary>
         /// Gets a joint-weight pair.
         /// </summary>
-        /// <param name="index">An index from 0 to <see cref="MaxBindings"/>.</param>
+        /// <param name="index">An index from 0 to <see cref="MaxBindings"/> exclusive.</param>
         /// <returns>The joint-weight pair.</returns>
-        (int Index, float Weight) GetJointBinding(int index);
+        (int Index, float Weight) GetBinding(int index);
 
         /// <summary>
-        /// Sets a joint-weight pair.
+        /// Sets the packed joints-weights.
         /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
         /// </summary>
-        /// <param name="index">An index from 0 to <see cref="MaxBindings"/>.</param>
-        /// <param name="joint">The joint index.</param>
-        /// <param name="weight">The weight of the joint.</param>
-        void SetJointBinding(int index, int joint, float weight);
+        /// <param name="bindings">The packed joints-weights.</param>
+        void SetBindings(in SparseWeight8 bindings);
 
         /// <summary>
         /// Sets the packed joints-weights.
         /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
         /// </summary>
-        /// <param name="weights">The packed joints-weights.</param>
-        void SetWeights(in SparseWeight8 weights);
+        /// <param name="bindings">the list of joint indices and weights.</param>
+        void SetBindings(params (int Index, float Weight)[] bindings);
 
         /// <summary>
         /// Gets the packed joints-weights.
         /// </summary>
         /// <returns>The packed joints-weights.</returns>
-        SparseWeight8 GetWeights();
+        SparseWeight8 GetBindings();
 
         /// <summary>
         /// Gets the indices of the first 4 joints.
@@ -74,8 +77,15 @@ namespace SharpGLTF.Geometry.VertexTypes
     /// <summary>
     /// Defines a Vertex attribute with up to 65535 bone joints and 4 weights.
     /// </summary>
-    public struct VertexJoints4 : IVertexSkinning
+    [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
+    public struct VertexJoints4 : IVertexSkinning, IEquatable<VertexJoints4>
     {
+        #region debug
+
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
+
+        #endregion
+
         #region constructors
 
         public VertexJoints4(int jointIndex)
@@ -84,31 +94,65 @@ namespace SharpGLTF.Geometry.VertexTypes
             Weights = Vector4.UnitX;
         }
 
-        public VertexJoints4(params (int, float)[] bindings)
+        public VertexJoints4(params (int JointIndex, float Weight)[] bindings)
             : this( SparseWeight8.Create(bindings) ) { }
 
         public VertexJoints4(in SparseWeight8 weights)
         {
-            var w4 = SparseWeight8.OrderedByWeight(weights);
+            var ordered = SparseWeight8.OrderedByWeight(weights);
 
-            Joints = new Vector4(w4.Index0, w4.Index1, w4.Index2, w4.Index3);
-            Weights = new Vector4(w4.Weight0, w4.Weight1, w4.Weight2, w4.Weight3);
+            Joints = new Vector4(ordered.Index0, ordered.Index1, ordered.Index2, ordered.Index3);
+            Weights = new Vector4(ordered.Weight0, ordered.Weight1, ordered.Weight2, ordered.Weight3);
 
             // renormalize
             var w = Vector4.Dot(Weights, Vector4.One);
             if (w != 0 && w != 1) Weights /= w;
+
+            // we must be sure that pairs are sorted by weight
+            System.Diagnostics.Debug.Assert(Weights.X >= Weights.Y);
+            System.Diagnostics.Debug.Assert(Weights.Y >= Weights.Z);
+            System.Diagnostics.Debug.Assert(Weights.Z >= Weights.W);
         }
 
         #endregion
 
         #region data
 
+        /// <summary>
+        /// Stores the indices of the 4 joints.
+        /// </summary>
+        /// <remarks>
+        /// <para><b>⚠️ AVOID SETTING THIS VALUE DIRECTLY ⚠️</b></para>
+        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// </remarks>
         [VertexAttribute("JOINTS_0", ENCODING.UNSIGNED_SHORT, false)]
         public Vector4 Joints;
 
+        /// <summary>
+        /// Stores the weights of the 4 joints.
+        /// </summary>
+        /// <remarks>
+        /// <para><b>⚠️ AVOID SETTING THIS VALUE DIRECTLY ⚠️</b></para>
+        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// </remarks>
         [VertexAttribute("WEIGHTS_0")]
         public Vector4 Weights;
 
+        public override bool Equals(object obj) { return obj is VertexJoints4 other && AreEqual(this, other); }
+        public bool Equals(VertexJoints4 other) { return AreEqual(this, other); }
+        public static bool operator ==(in VertexJoints4 a, in VertexJoints4 b) { return AreEqual(a, b); }
+        public static bool operator !=(in VertexJoints4 a, in VertexJoints4 b) { return !AreEqual(a, b); }
+        public static bool AreEqual(in VertexJoints4 a, in VertexJoints4 b)
+        {
+            // technically we should compare index-weights pairs,
+            // but it's expensive, and these values are expected
+            // to be already sorted by weight, unless filled manually.
+
+            return a.Joints == b.Joints && a.Weights == b.Weights;
+        }
+
+        public override int GetHashCode() { return Joints.GetHashCode(); }
+
         public int MaxBindings => 4;
 
         #endregion
@@ -129,13 +173,16 @@ namespace SharpGLTF.Geometry.VertexTypes
         #region API
 
         /// <inheritdoc/>
-        public SparseWeight8 GetWeights() { return new SparseWeight8(this.Joints, this.Weights); }
+        public SparseWeight8 GetBindings() { return SparseWeight8.Create(this.Joints, this.Weights); }
+
+        /// <inheritdoc/>
+        public void SetBindings(in SparseWeight8 bindings) { this = new VertexJoints4(bindings); }
 
         /// <inheritdoc/>
-        public void SetWeights(in SparseWeight8 weights) { this = new VertexJoints4(weights); }
+        public void SetBindings(params (int Index, float Weight)[] bindings) { this = new VertexJoints4(bindings); }
 
         /// <inheritdoc/>
-        public (int, float) GetJointBinding(int index)
+        public (int Index, float Weight) GetBinding(int index)
         {
             switch (index)
             {
@@ -147,33 +194,21 @@ namespace SharpGLTF.Geometry.VertexTypes
             }
         }
 
-        /// <inheritdoc/>
-        public void SetJointBinding(int index, int joint, float weight)
-        {
-            switch (index)
-            {
-                case 0: { this.Joints.X = joint; this.Weights.X = weight; return; }
-                case 1: { this.Joints.Y = joint; this.Weights.Y = weight; return; }
-                case 2: { this.Joints.Z = joint; this.Weights.Z = weight; return; }
-                case 3: { this.Joints.W = joint; this.Weights.W = weight; return; }
-                default: throw new ArgumentOutOfRangeException(nameof(index));
-            }
-        }
-
-        public void InPlaceSort()
-        {
-            var sparse = new SparseWeight8(this.Joints, this.Weights);
-            this = new VertexJoints4(sparse);
-        }
-
         #endregion
     }
 
     /// <summary>
     /// Defines a Vertex attribute with up to 65535 bone joints and 8 weights.
     /// </summary>
-    public struct VertexJoints8 : IVertexSkinning
+    [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
+    public struct VertexJoints8 : IVertexSkinning, IEquatable<VertexJoints8>
     {
+        #region debug
+
+        private string _GetDebuggerDisplay() => VertexUtils._GetDebuggerDisplay(this);
+
+        #endregion
+
         #region constructors
 
         public VertexJoints8(int jointIndex)
@@ -184,39 +219,92 @@ namespace SharpGLTF.Geometry.VertexTypes
             Weights1 = Vector4.Zero;
         }
 
-        public VertexJoints8(params (int, float)[] bindings)
+        public VertexJoints8(params (int JointIndex, float Weight)[] bindings)
             : this(SparseWeight8.Create(bindings)) { }
 
         public VertexJoints8(in SparseWeight8 weights)
         {
-            var w8 = SparseWeight8.OrderedByWeight(weights);
+            var ordered = SparseWeight8.OrderedByWeight(weights);
 
-            Joints0 = new Vector4(w8.Index0, w8.Index1, w8.Index2, w8.Index3);
-            Joints1 = new Vector4(w8.Index4, w8.Index5, w8.Index6, w8.Index7);
-            Weights0 = new Vector4(w8.Weight0, w8.Weight1, w8.Weight2, w8.Weight3);
-            Weights1 = new Vector4(w8.Weight4, w8.Weight5, w8.Weight6, w8.Weight7);
+            Joints0 = new Vector4(ordered.Index0, ordered.Index1, ordered.Index2, ordered.Index3);
+            Joints1 = new Vector4(ordered.Index4, ordered.Index5, ordered.Index6, ordered.Index7);
+            Weights0 = new Vector4(ordered.Weight0, ordered.Weight1, ordered.Weight2, ordered.Weight3);
+            Weights1 = new Vector4(ordered.Weight4, ordered.Weight5, ordered.Weight6, ordered.Weight7);
 
             // renormalize
             var w = Vector4.Dot(Weights0, Vector4.One) + Vector4.Dot(Weights1, Vector4.One);
             if (w != 0 && w != 1) { Weights0 /= w; Weights1 /= w; }
+
+            // we must be sure that pairs are sorted by weight
+            System.Diagnostics.Debug.Assert(Weights0.X >= Weights0.Y);
+            System.Diagnostics.Debug.Assert(Weights0.Y >= Weights0.Z);
+            System.Diagnostics.Debug.Assert(Weights0.Z >= Weights0.W);
+            System.Diagnostics.Debug.Assert(Weights0.W >= Weights1.X);
+            System.Diagnostics.Debug.Assert(Weights1.X >= Weights1.Y);
+            System.Diagnostics.Debug.Assert(Weights1.Y >= Weights1.Z);
+            System.Diagnostics.Debug.Assert(Weights1.Z >= Weights1.W);
         }
 
         #endregion
 
         #region data
 
+        /// <summary>
+        /// Stores the indices of the first 4 joints.
+        /// </summary>
+        /// <remarks>
+        /// <para><b>⚠️ AVOID SETTING THIS VALUE DIRECTLY ⚠️</b></para>
+        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// </remarks>
         [VertexAttribute("JOINTS_0", ENCODING.UNSIGNED_SHORT, false)]
         public Vector4 Joints0;
 
+        /// <summary>
+        /// Stores the indices of the next 4 joints.
+        /// </summary>
+        /// <remarks>
+        /// <para><b>⚠️ AVOID SETTING THIS VALUE DIRECTLY ⚠️</b></para>
+        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// </remarks>
         [VertexAttribute("JOINTS_1", ENCODING.UNSIGNED_SHORT, false)]
         public Vector4 Joints1;
 
+        /// <summary>
+        /// Stores the weights of the first 4 joints.
+        /// </summary>
+        /// <remarks>
+        /// <para><b>⚠️ AVOID SETTING THESE VALUES DIRECTLY ⚠️</b></para>
+        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// </remarks>
         [VertexAttribute("WEIGHTS_0")]
         public Vector4 Weights0;
 
+        /// <summary>
+        /// Stores the weights of the next 4 joints.
+        /// </summary>
+        /// <remarks>
+        /// <para><b>⚠️ AVOID SETTING THESE VALUES DIRECTLY ⚠️</b></para>
+        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// </remarks>
         [VertexAttribute("WEIGHTS_1")]
         public Vector4 Weights1;
 
+        public override bool Equals(object obj) { return obj is VertexJoints8 other && AreEqual(this, other); }
+        public bool Equals(VertexJoints8 other) { return AreEqual(this, other); }
+        public static bool operator ==(in VertexJoints8 a, in VertexJoints8 b) { return AreEqual(a, b); }
+        public static bool operator !=(in VertexJoints8 a, in VertexJoints8 b) { return !AreEqual(a, b); }
+        public static bool AreEqual(in VertexJoints8 a, in VertexJoints8 b)
+        {
+            // technically we should compare index-weights pairs,
+            // but it's expensive, and these values are expected
+            // to be already sorted by weight, unless filled manually.
+
+            return a.Joints0 == b.Joints0 && a.Joints1 == b.Joints1
+                && a.Weights0 == b.Weights0 && a.Weights1 == b.Weights1;
+        }
+
+        public override int GetHashCode() { return Joints0.GetHashCode(); }
+
         public int MaxBindings => 8;
 
         #endregion
@@ -237,13 +325,16 @@ namespace SharpGLTF.Geometry.VertexTypes
         #region API
 
         /// <inheritdoc/>
-        public SparseWeight8 GetWeights() { return new SparseWeight8(this.Joints0, this.Joints1, this.Weights0, this.Weights1); }
+        public SparseWeight8 GetBindings() { return SparseWeight8.CreateUnchecked(this.Joints0, this.Joints1, this.Weights0, this.Weights1); }
 
         /// <inheritdoc/>
-        public void SetWeights(in SparseWeight8 weights) { this = new VertexJoints8(weights); }
+        public void SetBindings(in SparseWeight8 weights) { this = new VertexJoints8(weights); }
 
         /// <inheritdoc/>
-        public (int Index, float Weight) GetJointBinding(int index)
+        public void SetBindings(params (int Index, float Weight)[] bindings) { this = new VertexJoints8(bindings); }
+
+        /// <inheritdoc/>
+        public (int Index, float Weight) GetBinding(int index)
         {
             switch (index)
             {
@@ -259,23 +350,6 @@ namespace SharpGLTF.Geometry.VertexTypes
             }
         }
 
-        /// <inheritdoc/>
-        public void SetJointBinding(int index, int joint, float weight)
-        {
-            switch (index)
-            {
-                case 0: { this.Joints0.X = joint; this.Weights0.X = weight; return; }
-                case 1: { this.Joints0.Y = joint; this.Weights0.Y = weight; return; }
-                case 2: { this.Joints0.Z = joint; this.Weights0.Z = weight; return; }
-                case 3: { this.Joints0.W = joint; this.Weights0.W = weight; return; }
-                case 4: { this.Joints1.X = joint; this.Weights1.X = weight; return; }
-                case 5: { this.Joints1.Y = joint; this.Weights1.Y = weight; return; }
-                case 6: { this.Joints1.Z = joint; this.Weights1.Z = weight; return; }
-                case 7: { this.Joints1.W = joint; this.Weights1.W = weight; return; }
-                default: throw new ArgumentOutOfRangeException(nameof(index));
-            }
-        }
-
         #endregion
     }
 }

+ 2 - 9
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexUtils.Builder.cs

@@ -1,12 +1,7 @@
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using System.Numerics;
 
-using SharpGLTF.Memory;
-using DIMENSIONS = SharpGLTF.Schema2.DimensionType;
-using ENCODING = SharpGLTF.Schema2.EncodingType;
-
 namespace SharpGLTF.Geometry.VertexTypes
 {
     static partial class VertexUtils
@@ -168,12 +163,10 @@ namespace SharpGLTF.Geometry.VertexTypes
             where TvS : struct, IVertexSkinning
         {
             if (src is TvS srcTyped) return srcTyped;
-
-            var sparse = src.MaxBindings > 0 ? src.GetWeights() : default;
+            var srcWeights = src.MaxBindings > 0 ? src.GetBindings() : default;
 
             var dst = default(TvS);
-
-            if (dst.MaxBindings > 0) dst.SetWeights(sparse);
+            if (dst.MaxBindings > 0) dst.SetBindings(srcWeights);
 
             return dst;
         }

+ 58 - 0
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexUtils.Diagnostic.cs

@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.Geometry.VertexTypes
+{
+    static partial class VertexUtils
+    {
+        private static readonly char[] _SubscriptNumbers = new char[] { '₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉' };
+
+        public static string _GetDebuggerDisplay(IVertexGeometry geo)
+        {
+            var txt = $"𝐏:{geo.GetPosition()}";
+
+            if (geo.TryGetNormal(out Vector3 n)) txt += $" 𝚴:{n}";
+            if (geo.TryGetTangent(out Vector4 t)) txt += $" 𝚻:{t}";
+
+            return txt;
+        }
+
+        public static string _GetDebuggerDisplay(IVertexMaterial mat)
+        {
+            var txt = string.Empty;
+
+            for (int i = 0; i < mat.MaxColors; ++i)
+            {
+                if (txt.Length > 0) txt += " ";
+                txt += $"𝐂{_SubscriptNumbers[i]}:{mat.GetColor(i)}";
+            }
+
+            for (int i = 0; i < mat.MaxTextCoords; ++i)
+            {
+                if (txt.Length > 0) txt += " ";
+                txt += $"𝐔𝐕{_SubscriptNumbers[i]}:{mat.GetTexCoord(i)}";
+            }
+
+            return txt;
+        }
+
+        public static string _GetDebuggerDisplay(IVertexSkinning skin)
+        {
+            var txt = string.Empty;
+
+            for (int i = 0; i < skin.MaxBindings; ++i)
+            {
+                var (joint, weight) = skin.GetBinding(i);
+                if (weight == 0) continue;
+
+                if (txt.Length != 0) txt += " ";
+
+                txt += $"<𝐉:{joint} 𝐖:{weight}>";
+            }
+
+            return txt;
+        }
+    }
+}

+ 71 - 11
tests/SharpGLTF.Tests/Transforms/SparseWeight8Tests.cs

@@ -39,7 +39,7 @@ namespace SharpGLTF.Transforms
             Assert.AreEqual(array2.Sum(), indexedSparse.WeightSum, 0.000001f);
             CollectionAssert.AreEqual(array2, indexedSparse.Expand(array2.Length));
 
-            Assert.IsTrue(SparseWeight8.AreWeightsEqual(sparse, indexedSparse));
+            Assert.IsTrue(SparseWeight8.AreEqual(sparse, indexedSparse));
 
             // sort by weights
             var sByWeights = SparseWeight8.OrderedByWeight(sparse);
@@ -54,8 +54,8 @@ namespace SharpGLTF.Transforms
             CheckIndexOrdered(sByWeights);
 
             // equality
-            Assert.IsTrue(SparseWeight8.AreWeightsEqual(sByIndices, sByWeights));
-            Assert.AreEqual(sByIndices.GetWeightsHashCode(), sByWeights.GetWeightsHashCode());
+            Assert.IsTrue(SparseWeight8.AreEqual(sByIndices, sByWeights));
+            Assert.AreEqual(sByIndices.GetHashCode(), sByWeights.GetHashCode());
 
             // sum
             var sum = SparseWeight8.Add(sByIndices, sByWeights);
@@ -68,6 +68,60 @@ namespace SharpGLTF.Transforms
             }
         }
 
+
+        [Test]
+        public void TestSparseCreation()
+        {
+            var sparse = SparseWeight8.Create
+                (
+                (9, 9),
+                (8, 2),
+                (5, 1), // we set these weights separately
+                (5, 1), // to check that 5 will pass 8
+                (5, 1), // in the sorted set.
+                (7, 1)
+                );
+
+            Assert.AreEqual(3, sparse[5]);
+            Assert.AreEqual(1, sparse[7]);
+            Assert.AreEqual(2, sparse[8]);
+            Assert.AreEqual(9, sparse[9]);
+        }
+
+        [Test]
+        public void TestCreateSparseFromVectors()
+        {
+            CollectionAssert.AreEqual
+                (
+                SparseWeight8.Create(new System.Numerics.Vector4(0, 1, 2, 3), new System.Numerics.Vector4(1, 1, 1, 1)).Expand(4),
+                SparseWeight8.Create(1, 1, 1, 1).Expand(4)
+                );
+
+            CollectionAssert.AreEqual
+                (
+                SparseWeight8.Create(new System.Numerics.Vector4(0, 1, 2, 3), new System.Numerics.Vector4(1, 2, 3, 4)).Expand(4),
+                SparseWeight8.Create(1, 2, 3, 4).Expand(4)
+                );
+
+            CollectionAssert.AreEqual
+                (
+                SparseWeight8.Create(new System.Numerics.Vector4(0, 1, 2, 3), new System.Numerics.Vector4(4, 3, 2, 1)).Expand(4),
+                SparseWeight8.Create(4, 3, 2, 1).Expand(4)
+                );
+
+            CollectionAssert.AreEqual
+                (
+                SparseWeight8.Create(new System.Numerics.Vector4(0, 2, 2, 3), new System.Numerics.Vector4(4, 3, 2, 1)).Expand(4),
+                SparseWeight8.Create(4, 0, 5, 1).Expand(4)
+                );
+
+            CollectionAssert.AreEqual
+                (
+                SparseWeight8.Create(new System.Numerics.Vector4(1, 1, 1, 1), new System.Numerics.Vector4(1, 1, 1, 1)).Expand(4),
+                SparseWeight8.Create(0, 4, 0, 0).Expand(4)
+                );
+        }
+
         /// <summary>
         /// Creates a new array with only the 8 most relevant weights.
         /// </summary>
@@ -145,13 +199,13 @@ namespace SharpGLTF.Transforms
         [Test]
         public void TestSparseEquality()
         {
-            Assert.IsTrue(SparseWeight8.AreWeightsEqual(SparseWeight8.Create(0, 1), SparseWeight8.Create(0, 1)));
+            Assert.IsTrue(SparseWeight8.AreEqual(SparseWeight8.Create(0, 1), SparseWeight8.Create(0, 1)));
 
-            Assert.IsFalse(SparseWeight8.AreWeightsEqual(SparseWeight8.Create(0, 1), SparseWeight8.Create(0, 1, 0.25f)));
-            Assert.IsFalse(SparseWeight8.AreWeightsEqual(SparseWeight8.Create(0, 1), SparseWeight8.Create(1, 0)));
+            Assert.IsFalse(SparseWeight8.AreEqual(SparseWeight8.Create(0, 1), SparseWeight8.Create(0, 1, 0.25f)));
+            Assert.IsFalse(SparseWeight8.AreEqual(SparseWeight8.Create(0, 1), SparseWeight8.Create(1, 0)));
 
             // check if two "half weights" are equal to one "full weight"
-            Assert.IsTrue(SparseWeight8.AreWeightsEqual(SparseWeight8.Create((3, 5), (3, 5)), SparseWeight8.Create((3, 10))));
+            //Assert.IsTrue(SparseWeight8.AreWeightsEqual(SparseWeight8.Create((3, 5), (3, 5)), SparseWeight8.Create((3, 10))));
         }
 
         [Test]
@@ -216,15 +270,21 @@ namespace SharpGLTF.Transforms
         [Test]
         public void TestSparseWeightReduction()
         {
-            var a = SparseWeight8.Create(5, 3, 2, 4, 0, 4, 2);
+            var a = SparseWeight8.Create(5, 3, 2, 4, 0, 4, 2, 6, 3, 6, 1);
 
-            var b = a.GetReducedWeights(4);
+            var b = a.GetTrimmed(4);
+
+            Assert.AreEqual(4, b.GetNonZeroWeights().Count());
+            
+            Assert.AreEqual(a[0], b[0]);
+            Assert.AreEqual(a[3], b[3]);
+            Assert.AreEqual(a[7], b[7]);
+            Assert.AreEqual(a[9], b[9]);
 
             Assert.AreEqual(0, b.Weight4);
             Assert.AreEqual(0, b.Weight5);
             Assert.AreEqual(0, b.Weight6);
-            Assert.AreEqual(0, b.Weight7);
-            Assert.AreEqual(a.WeightSum, b.WeightSum, 0.00001f);
+            Assert.AreEqual(0, b.Weight7);            
         }
     }
 }

+ 76 - 2
tests/SharpGLTF.Toolkit.Tests/Geometry/VertexTypes/VertexSkinningTests.cs

@@ -1,11 +1,15 @@
 using System;
 using System.Collections.Generic;
+using System.Numerics;
 using System.Text;
 
 using NUnit.Framework;
 
 namespace SharpGLTF.Geometry.VertexTypes
-{    
+{
+    using VERTEXSKINNED4 = VertexBuilder<VertexPosition, VertexEmpty, VertexJoints4>;
+    using VERTEXSKINNED8 = VertexBuilder<VertexPosition, VertexEmpty, VertexJoints8>;
+
     [Category("Toolkit")]
     public class VertexSkinningTests
     {
@@ -17,6 +21,76 @@ namespace SharpGLTF.Geometry.VertexTypes
             var txt = v._GetDebuggerDisplay();
         }
 
+        [Test]
+        public void TestSkinnedVertexEquality()
+        {
+            var p = new Vector3(0, 3, 0);
+
+            var w0 = Transforms.SparseWeight8.CreateUnchecked(new Vector4(0, 1, 2, 3), new Vector4(4, 5, 6, 7), new Vector4(4, 3, 2, 1) * 0.6f, new Vector4(4, 3, 2, 1) * 0.4f);
+            var w1 = Transforms.SparseWeight8.CreateUnchecked(new Vector4(3, 2, 1, 0), new Vector4(7, 6, 5, 4), new Vector4(1, 2, 3, 4) * 0.6f, new Vector4(1, 2, 3, 4) * 0.4f);
+
+            // the index/weight pairs are ordered diferently...
+            Assert.AreNotEqual(w0.Index0, w1.Index0);
+            Assert.AreNotEqual(w0.Weight0, w1.Weight0);
+
+            // but they should be effectively the same.
+            Assert.AreEqual(w0, w1);
+
+            var v0 = new VERTEXSKINNED4()
+                .WithGeometry(p)
+                .WithSkinning(w0);
+
+            var v1 = new VERTEXSKINNED4()
+                .WithGeometry(p)
+                .WithSkinning(w1);
+
+            Assert.AreEqual(v0, v1);
+
+            var v2 = new VERTEXSKINNED8()
+                .WithGeometry(p)
+                .WithSkinning(w0);
+
+            var v3 = new VERTEXSKINNED8()
+                .WithGeometry(p)
+                .WithSkinning(w1);
+
+            Assert.AreEqual(v0, v1);
+        }
+
+        [Test]
+        public void TestVertexBuilderSkin8()
+        {
+            var v0 = new VERTEXSKINNED8()
+                .WithGeometry(new Vector3(0, 3, 0))
+                .WithSkinning
+                    (
+                    (0, 0.3f),
+                    (1, 0.2f),
+                    (2, 0.1f),
+                    (3, 0.1f),
+                    (4, 0.1f),
+                    (5, 0.1f),
+                    (6, 0.1f)
+                    );
+
+            v0.Validate();
+
+            var v1 = new VERTEXSKINNED8()
+                .WithGeometry(new Vector3(0, 3, 0))
+                .WithSkinning
+                    (
+                    (0, 3),
+                    (1, 2),
+                    (2, 1),
+                    (3, 3),
+                    (4, 4),
+                    (5, 5),
+                    (6, 6)
+                    );
+
+            v1.Validate();
+        }
+
         [Test]
         public void TestVertexSkinningDowngradeFrom8To4Joints()
         {
@@ -33,7 +107,7 @@ namespace SharpGLTF.Geometry.VertexTypes
             // we downgrade to 4 bindings; remaining bindings should be interpolated to keep weighting 1.
             var v4 = v8.ConvertToSkinning<VertexJoints4>();
 
-            var sparse = v4.GetWeights();
+            var sparse = v4.GetBindings();
 
             Assert.AreEqual(1, sparse.WeightSum, 0.00001f);