瀏覽代碼

SparseWeight8 struct nearly finished

Vicente Penades 6 年之前
父節點
當前提交
9f95eed2d4

+ 186 - 0
src/SharpGLTF.Core/Transforms/IndexWeight.cs

@@ -0,0 +1,186 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace SharpGLTF.Transforms
+{
+
+    [System.Diagnostics.DebuggerDisplay("{Index} = {Weight}")]
+    readonly struct IndexWeight
+    {
+        #region constructor
+
+        public IndexWeight((int, float) pair)
+        {
+            Index = pair.Item1;
+            Weight = pair.Item2;
+        }
+
+        public static implicit operator IndexWeight((int, float) pair) {return new IndexWeight(pair.Item1, pair.Item2); }
+
+        public IndexWeight(int i, float w)
+        {
+            Index = i;
+            Weight = w;
+        }
+
+        #endregion
+
+        #region data
+
+        public readonly int Index;
+        public readonly float Weight;
+
+        #endregion
+
+        #region API
+
+        public static IndexWeight operator +(IndexWeight a, IndexWeight b)
+        {
+            System.Diagnostics.Debug.Assert(a.Index == b.Index);
+            return new IndexWeight(a.Index, a.Weight + b.Weight);
+        }
+
+        public static int IndexOf(Span<IndexWeight> span, int index)
+        {
+            for (int i = 0; i < span.Length; ++i)
+            {
+                if (span[i].Index == index) return i;
+            }
+
+            return -1;
+        }
+
+        private static IndexWeight GetSparseWeight(in SparseWeight8 src, int sparseIndex)
+        {
+            switch (sparseIndex)
+            {
+                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(sparseIndex));
+            }
+        }
+
+        public static int CopyTo(in SparseWeight8 src, Span<IndexWeight> dst)
+        {
+            System.Diagnostics.Debug.Assert(dst.Length >= 8);
+
+            var offset = 0;
+
+            for (int i = 0; i < 8; ++i)
+            {
+                var pair = GetSparseWeight(src, i);
+                if (pair.Weight == 0) continue;
+
+                var idx = IndexOf(dst.Slice(0, offset), pair.Index);
+
+                if (idx < 0)
+                {
+                    // the index doesn't exist, insert it.
+                    dst[offset++] = pair;
+                }
+                else
+                {
+                    // the index already exists, so we aggregate the weights
+                    dst[idx] += pair;
+                }
+            }
+
+            return offset;
+        }
+
+        public static int CopyTo(in SparseWeight8 src, Span<int> dstIndices, Span<float> dstWeights, int dstLength)
+        {
+            System.Diagnostics.Debug.Assert(dstIndices.Length == dstWeights.Length);
+            System.Diagnostics.Debug.Assert(dstIndices.Length >= dstLength, $"{nameof(dstIndices)}.Length must be at least {nameof(dstLength)}");
+            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)
+            {
+                var pair = GetSparseWeight(src, i);
+                if (pair.Weight == 0) continue;
+
+                var idx = dstIndices
+                    .Slice(0, dstLength)
+                    .IndexOf(pair.Index);
+
+                if (idx < 0)
+                {
+                    // the index doesn't exist, insert it.
+                    dstIndices[dstLength] = pair.Index;
+                    dstWeights[dstLength] = pair.Weight;
+                    ++dstLength;
+                }
+                else
+                {
+                    // the index already exists, so we aggregate the weights
+                    dstWeights[idx] += pair.Weight;
+                }
+            }
+
+            return dstLength;
+        }
+
+        public static void BubbleSortByWeight(Span<IndexWeight> pairs)
+        {
+            for (int i = 0; i < pairs.Length - 1; ++i)
+            {
+                bool sorted = true;
+
+                for (int j = 1; j < pairs.Length; ++j)
+                {
+                    var k = j - 1;
+
+                    var kk = pairs[k];
+                    var jj = pairs[j];
+
+                    if (kk.Weight  > jj.Weight) continue;
+                    if (kk.Weight == jj.Weight && kk.Index < jj.Index) continue;
+
+                    pairs[k] = jj;
+                    pairs[j] = kk;
+
+                    sorted = false;
+                }
+
+                if (sorted) return;
+            }
+        }
+
+        public static void BubbleSortByIndex(Span<IndexWeight> pairs)
+        {
+            for (int i = 0; i < pairs.Length - 1; ++i)
+            {
+                bool sorted = true;
+
+                for (int j = 1; j < pairs.Length; ++j)
+                {
+                    var k = j - 1;
+
+                    var kk = pairs[k];
+                    var jj = pairs[j];
+
+                    if (kk.Index  < jj.Index) continue;
+                    if (kk.Index == jj.Index && kk.Weight > jj.Weight) continue;
+
+                    pairs[k] = jj;
+                    pairs[j] = kk;
+
+                    sorted = false;
+                }
+
+                if (sorted) return;
+            }
+        }
+
+        #endregion
+    }
+}

+ 5 - 52
src/SharpGLTF.Core/Transforms/MeshTransforms.cs

@@ -77,54 +77,7 @@ namespace SharpGLTF.Transforms
                 return;
             }
 
-            _Weights = Normalize(morphWeights);
-        }
-
-        /// <summary>
-        /// Increments all indices and adds a new Index[0] with a weight that makes the sum of all weights equal to 1
-        /// </summary>
-        /// <param name="r">A <see cref="SparseWeight8"/> object representing the weights of the morph targets.</param>
-        /// <returns>A <see cref="SparseWeight8"/> representing the morph target weights, compensated with the weight of the master values.</returns>
-        internal static SparseWeight8 Normalize(SparseWeight8 r)
-        {
-            int i = -1;
-            float w = float.MaxValue;
-            float ww = 0;
-
-            ww += r.Weight0; if (r.Weight0 < w) { i = 0; w = r.Weight0; }
-            ww += r.Weight1; if (r.Weight1 < w) { i = 1; w = r.Weight1; }
-            ww += r.Weight2; if (r.Weight2 < w) { i = 2; w = r.Weight2; }
-            ww += r.Weight3; if (r.Weight3 < w) { i = 3; w = r.Weight3; }
-            ww += r.Weight4; if (r.Weight4 < w) { i = 4; w = r.Weight4; }
-            ww += r.Weight5; if (r.Weight5 < w) { i = 5; w = r.Weight5; }
-            ww += r.Weight6; if (r.Weight6 < w) { i = 6; w = r.Weight6; }
-            ww += r.Weight7; if (r.Weight7 < w) { i = 7; w = r.Weight7; }
-
-            r.Index0 += 1;
-            r.Index1 += 1;
-            r.Index2 += 1;
-            r.Index3 += 1;
-            r.Index4 += 1;
-            r.Index5 += 1;
-            r.Index6 += 1;
-            r.Index7 += 1;
-
-            ww -= w;
-            var iw = 1 - ww;
-
-            switch (i)
-            {
-                case 0: r.Index0 = 0; r.Weight0 = iw; break;
-                case 1: r.Index1 = 0; r.Weight1 = iw; break;
-                case 2: r.Index2 = 0; r.Weight2 = iw; break;
-                case 3: r.Index3 = 0; r.Weight3 = iw; break;
-                case 4: r.Index4 = 0; r.Weight4 = iw; break;
-                case 5: r.Index5 = 0; r.Weight5 = iw; break;
-                case 6: r.Index6 = 0; r.Weight6 = iw; break;
-                case 7: r.Index7 = 0; r.Weight7 = iw; break;
-            }
-
-            return r;
+            _Weights = morphWeights.GetNormalizedWithComplement();
         }
 
         protected V3 MorphVectors(V3 value, V3[] morphTargets)
@@ -137,7 +90,7 @@ namespace SharpGLTF.Transforms
 
             if (_AbsoluteMorphTargets)
             {
-                foreach (var pair in _Weights.GetSparseWeights())
+                foreach (var pair in _Weights.GetNonZeroWeights())
                 {
                     var val = pair.Item1 == 0 ? value : morphTargets[pair.Item1 - 1];
                     p += val * pair.Item2;
@@ -145,7 +98,7 @@ namespace SharpGLTF.Transforms
             }
             else
             {
-                foreach (var pair in _Weights.GetSparseWeights())
+                foreach (var pair in _Weights.GetNonZeroWeights())
                 {
                     var val = pair.Item1 == 0 ? value : value + morphTargets[pair.Item1 - 1];
                     p += val * pair.Item2;
@@ -165,7 +118,7 @@ namespace SharpGLTF.Transforms
 
             if (_AbsoluteMorphTargets)
             {
-                foreach (var pair in _Weights.GetSparseWeights())
+                foreach (var pair in _Weights.GetNonZeroWeights())
                 {
                     var val = pair.Item1 == 0 ? value : morphTargets[pair.Item1 - 1];
                     p += val * pair.Item2;
@@ -173,7 +126,7 @@ namespace SharpGLTF.Transforms
             }
             else
             {
-                foreach (var pair in _Weights.GetSparseWeights())
+                foreach (var pair in _Weights.GetNonZeroWeights())
                 {
                     var val = pair.Item1 == 0 ? value : value + morphTargets[pair.Item1 - 1];
                     p += val * pair.Item2;

+ 280 - 254
src/SharpGLTF.Core/Transforms/SparseWeight8.cs

@@ -16,15 +16,27 @@ namespace SharpGLTF.Transforms
     /// - 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.
     /// </remarks>
     [System.Diagnostics.DebuggerDisplay("[{Index0}]={Weight0}  [{Index1}]={Weight1}  [{Index2}]={Weight2}  [{Index3}]={Weight3}  [{Index4}]={Weight4}  [{Index5}]={Weight5}  [{Index6}]={Weight6}  [{Index7}]={Weight7}")]
-    public struct SparseWeight8 : IReadOnlyList<float>
+    public readonly struct SparseWeight8 : IReadOnlyList<float>
     {
-        #region lifecycle
+        #region constructors
 
+        /// <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.
+        /// </summary>
+        /// <param name="weights">A sequence of weight values.</param>
+        /// <returns>A <see cref="SparseWeight8"/> instance.</returns>
         public static SparseWeight8 Create(params float[] weights)
         {
             return Create((IEnumerable<float>)weights);
         }
 
+        /// <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
+        /// </summary>
+        /// <param name="weights">A sequence of weight values.</param>
+        /// <returns>A <see cref="SparseWeight8"/> instance.</returns>
         public static SparseWeight8 Create(IEnumerable<float> weights)
         {
             if (weights == null) return default;
@@ -39,29 +51,37 @@ namespace SharpGLTF.Transforms
             return Create(indexedWeights);
         }
 
+        /// <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
+        /// </summary>
+        /// <param name="pairs">A sequence of indexed weight values.</param>
+        /// <returns>A <see cref="SparseWeight8"/> instance.</returns>
         public static SparseWeight8 Create(params (int, float)[] pairs)
         {
             if (pairs == null) return default;
 
-            Span<int> indices = stackalloc int[pairs.Length];
-            Span<float> weights = stackalloc float[pairs.Length];
+            Span<IndexWeight> sparse = stackalloc IndexWeight[pairs.Length];
 
             for (int i = 0; i < pairs.Length; ++i)
             {
-                indices[i] = pairs[i].Item1;
-                weights[i] = pairs[i].Item2;
+                sparse[i] = pairs[i];
             }
 
             if (pairs.Length > 8)
             {
-                BubbleSortByWeight(indices, weights, indices.Length);
-                indices = indices.Slice(0, 8);
-                weights = weights.Slice(0, 8);
+                IndexWeight.BubbleSortByWeight(sparse);
+                sparse = sparse.Slice(0, 8);
             }
 
-            return new SparseWeight8(indices, weights);
+            return new SparseWeight8(sparse);
         }
 
+        /// <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="wgt0123">The weights of indices 0 to 3.</param>
         public SparseWeight8(in Vector4 idx0123, in Vector4 wgt0123)
         {
             Index0 = (int)idx0123.X;
@@ -85,6 +105,13 @@ namespace SharpGLTF.Transforms
             Weight7 = 0;
         }
 
+        /// <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)
         {
             Index0 = (int)idx0123.X;
@@ -108,82 +135,120 @@ namespace SharpGLTF.Transforms
             Weight7 = wgt4567.W;
         }
 
-        private SparseWeight8(ReadOnlySpan<int> indices, ReadOnlySpan<float> weights)
+        private SparseWeight8(ReadOnlySpan<IndexWeight> pairs)
+        {
+            System.Diagnostics.Debug.Assert(pairs.Length <= 8, nameof(pairs));
+
+            this = default;
+
+            if (pairs.Length < 1) return;
+            this.Index0 = pairs[0].Index;
+            this.Weight0 = pairs[0].Weight;
+
+            if (pairs.Length < 2) return;
+            this.Index1 = pairs[1].Index;
+            this.Weight1 = pairs[1].Weight;
+
+            if (pairs.Length < 3) return;
+            this.Index2 = pairs[2].Index;
+            this.Weight2 = pairs[2].Weight;
+
+            if (pairs.Length < 4) return;
+            this.Index3 = pairs[3].Index;
+            this.Weight3 = pairs[3].Weight;
+
+            if (pairs.Length < 5) return;
+            this.Index4 = pairs[4].Index;
+            this.Weight4 = pairs[4].Weight;
+
+            if (pairs.Length < 6) return;
+            this.Index5 = pairs[5].Index;
+            this.Weight5 = pairs[5].Weight;
+
+            if (pairs.Length < 7) return;
+            this.Index6 = pairs[6].Index;
+            this.Weight6 = pairs[6].Weight;
+
+            if (pairs.Length < 8) return;
+            this.Index7 = pairs[7].Index;
+            this.Weight7 = pairs[7].Weight;
+        }
+
+        private SparseWeight8(in SparseWeight8 sparse, float scale)
         {
-            System.Diagnostics.Debug.Assert(indices.Length <= 8, nameof(indices));
-            System.Diagnostics.Debug.Assert(indices.Length == weights.Length, nameof(weights));
-
-            this.Index0 = indices.Length > 0 ? indices[0] : 0;
-            this.Index1 = indices.Length > 1 ? indices[1] : 0;
-            this.Index2 = indices.Length > 2 ? indices[2] : 0;
-            this.Index3 = indices.Length > 3 ? indices[3] : 0;
-            this.Index4 = indices.Length > 4 ? indices[4] : 0;
-            this.Index5 = indices.Length > 5 ? indices[5] : 0;
-            this.Index6 = indices.Length > 6 ? indices[6] : 0;
-            this.Index7 = indices.Length > 7 ? indices[7] : 0;
-
-            this.Weight0 = weights.Length > 0 ? weights[0] : 0;
-            this.Weight1 = weights.Length > 1 ? weights[1] : 0;
-            this.Weight2 = weights.Length > 2 ? weights[2] : 0;
-            this.Weight3 = weights.Length > 3 ? weights[3] : 0;
-            this.Weight4 = weights.Length > 4 ? weights[4] : 0;
-            this.Weight5 = weights.Length > 5 ? weights[5] : 0;
-            this.Weight6 = weights.Length > 6 ? weights[6] : 0;
-            this.Weight7 = weights.Length > 7 ? weights[7] : 0;
+            Index0 = sparse.Index0;
+            Index1 = sparse.Index1;
+            Index2 = sparse.Index2;
+            Index3 = sparse.Index3;
+            Index4 = sparse.Index4;
+            Index5 = sparse.Index5;
+            Index6 = sparse.Index6;
+            Index7 = sparse.Index7;
+            Weight0 = sparse.Weight0 * scale;
+            Weight1 = sparse.Weight1 * scale;
+            Weight2 = sparse.Weight2 * scale;
+            Weight3 = sparse.Weight3 * scale;
+            Weight4 = sparse.Weight4 * scale;
+            Weight5 = sparse.Weight5 * scale;
+            Weight6 = sparse.Weight6 * scale;
+            Weight7 = sparse.Weight7 * scale;
         }
 
         #endregion
 
         #region data
 
-        public int Index0;
-        public int Index1;
-        public int Index2;
-        public int Index3;
-        public int Index4;
-        public int Index5;
-        public int Index6;
-        public int Index7;
-
-        public float Weight0;
-        public float Weight1;
-        public float Weight2;
-        public float Weight3;
-        public float Weight4;
-        public float Weight5;
-        public float Weight6;
-        public float Weight7;
+        public readonly int Index0;
+        public readonly int Index1;
+        public readonly int Index2;
+        public readonly int Index3;
+        public readonly int Index4;
+        public readonly int Index5;
+        public readonly int Index6;
+        public readonly int Index7;
+
+        public readonly float Weight0;
+        public readonly float Weight1;
+        public readonly float Weight2;
+        public readonly float Weight3;
+        public readonly float Weight4;
+        public readonly float Weight5;
+        public readonly float Weight6;
+        public readonly float Weight7;
 
         public static bool AreWeightsEqual(in SparseWeight8 x, in SparseWeight8 y)
         {
-            Span<int>   indices = stackalloc int[16];
-            Span<float> xWeights = stackalloc float[16];
-            Span<float> yWeights = stackalloc float[16];
+            const int STACKSIZE = 8 * 2;
 
-            int c = 0;
-            c = CopyTo(x, indices, xWeights, c);
-            c = CopyTo(y, indices, yWeights, c);
+            Span<int>   indices = stackalloc int[STACKSIZE];
+            Span<float> xWeights = stackalloc float[STACKSIZE];
+            Span<float> yWeights = stackalloc float[STACKSIZE];
 
-            xWeights = xWeights.Slice(0, c);
-            yWeights = yWeights.Slice(0, c);
+            int offset = 0;
+            offset = IndexWeight.CopyTo(x, indices, xWeights, offset);
+            offset = IndexWeight.CopyTo(y, indices, yWeights, offset);
+
+            xWeights = xWeights.Slice(0, offset);
+            yWeights = yWeights.Slice(0, offset);
 
             return xWeights.SequenceEqual(yWeights);
         }
 
         public int GetWeightsHashCode()
         {
-            Span<int> indices = stackalloc int[8];
-            Span<float> weights = stackalloc float[8];
+            Span<IndexWeight> iw = stackalloc IndexWeight[8];
 
-            var c = CopyTo(this, indices, weights, 0);
+            var c = IndexWeight.CopyTo(this, iw);
 
-            BubbleSortByIndex(indices, weights, c);
+            iw = iw.Slice(0, c);
+
+            IndexWeight.BubbleSortByIndex(iw);
 
             int h = 0;
 
-            for (int i = 0; i < c; ++i)
+            for (int i = 0; i < iw.Length; ++i)
             {
-                h += indices[i].GetHashCode() ^ weights[i].GetHashCode();
+                h += iw[i].GetHashCode();
                 h *= 17;
             }
 
@@ -211,77 +276,100 @@ namespace SharpGLTF.Transforms
 
         #region API
 
+        /// <summary>
+        /// Returns a copy of this <see cref="SparseWeight8"/> where all the
+        /// indices have been reordered by weight in descending order.
+        /// </summary>
+        /// <param name="sparse">The <see cref="SparseWeight8"/> to get ordered.</param>
+        /// <returns>A weight ordered <see cref="SparseWeight8"/>.</returns>
         public static SparseWeight8 OrderedByWeight(in SparseWeight8 sparse)
         {
-            Span<int> indices = stackalloc int[8];
-            Span<float> weights = stackalloc float[8];
+            Span<IndexWeight> iw = stackalloc IndexWeight[8];
+
+            var c = IndexWeight.CopyTo(sparse, iw);
 
-            var c = CopyTo(sparse, indices, weights, 0);
-            BubbleSortByWeight(indices, weights, c);
+            iw = iw.Slice(0, c);
 
-            indices = indices.Slice(0, c);
-            weights = weights.Slice(0, c);
+            IndexWeight.BubbleSortByWeight(iw);
 
-            return new SparseWeight8(indices, weights);
+            return new SparseWeight8(iw);
         }
 
+        /// <summary>
+        /// Returns a copy of this <see cref="SparseWeight8"/> where all the
+        /// indices have been reordered by index in ascending order.
+        /// </summary>
+        /// <param name="sparse">The <see cref="SparseWeight8"/> to get ordered.</param>
+        /// <returns>An index ordered <see cref="SparseWeight8"/>.</returns>
         public static SparseWeight8 OrderedByIndex(in SparseWeight8 sparse)
         {
-            Span<int> indices = stackalloc int[8];
-            Span<float> weights = stackalloc float[8];
+            Span<IndexWeight> iw = stackalloc IndexWeight[8];
 
-            var c = CopyTo(sparse, indices, weights, 0);
-            BubbleSortByIndex(indices, weights, c);
+            var c = IndexWeight.CopyTo(sparse, iw);
 
-            indices = indices.Slice(0, c);
-            weights = weights.Slice(0, c);
+            iw = iw.Slice(0, c);
 
-            return new SparseWeight8(indices, weights);
+            IndexWeight.BubbleSortByIndex(iw);
+
+            return new SparseWeight8(iw);
         }
 
+        /// <summary>
+        /// Adds <paramref name="x"/> with <paramref name="y"/> element wise.
+        /// If there's more than 8 non zero result values, the 8 most representative values are taken.
+        /// </summary>
+        /// <param name="x">The first operand.</param>
+        /// <param name="y">The second operand.</param>
+        /// <returns>A new <see cref="SparseWeight8"/></returns>
         public static SparseWeight8 Add(in SparseWeight8 x, in SparseWeight8 y)
         {
             return _OperateLinear(x, y, (xx, yy) => xx + yy);
         }
 
+        /// <summary>
+        /// Subtracts <paramref name="y"/> from <paramref name="x"/> element wise.
+        /// If there's more than 8 non zero result values, the 8 most representative values are taken.
+        /// </summary>
+        /// <param name="x">The first operand.</param>
+        /// <param name="y">The second operand.</param>
+        /// <returns>A new <see cref="SparseWeight8"/></returns>
         public static SparseWeight8 Subtract(in SparseWeight8 x, in SparseWeight8 y)
         {
             return _OperateLinear(x, y, (xx, yy) => xx - yy);
         }
 
+        /// <summary>
+        /// Multiplies <paramref name="x"/> with <paramref name="y"/> element wise.
+        /// If there's more than 8 non zero result values, the 8 most representative values are taken.
+        /// </summary>
+        /// <param name="x">The first operand.</param>
+        /// <param name="y">The second operand.</param>
+        /// <returns>A new <see cref="SparseWeight8"/></returns>
         public static SparseWeight8 Multiply(in SparseWeight8 x, in SparseWeight8 y)
         {
             return _OperateLinear(x, y, (xx, yy) => xx * yy);
         }
 
+        /// <summary>
+        /// Multiplies <paramref name="x"/> with <paramref name="y"/> element wise.
+        /// If there's more than 8 non zero result values, the 8 most representative values are taken.
+        /// </summary>
+        /// <param name="x">The first operand.</param>
+        /// <param name="y">The second operand.</param>
+        /// <returns>A new <see cref="SparseWeight8"/></returns>
         public static SparseWeight8 Multiply(in SparseWeight8 x, Single y)
         {
-            return new SparseWeight8
-            {
-                Index0 = x.Index0,
-                Index1 = x.Index1,
-                Index2 = x.Index2,
-                Index3 = x.Index3,
-                Index4 = x.Index4,
-                Index5 = x.Index5,
-                Index6 = x.Index6,
-                Index7 = x.Index7,
-                Weight0 = x.Weight0 * y,
-                Weight1 = x.Weight1 * y,
-                Weight2 = x.Weight2 * y,
-                Weight3 = x.Weight3 * y,
-                Weight4 = x.Weight4 * y,
-                Weight5 = x.Weight5 * y,
-                Weight6 = x.Weight6 * y,
-                Weight7 = x.Weight7 * y
-            };
-        }
-
-        public static SparseWeight8 Divide(in SparseWeight8 x, in SparseWeight8 y)
-        {
-            return _OperateLinear(x, y, (xx, yy) => xx / yy);
+            return new SparseWeight8(x, y);
         }
 
+        /// <summary>
+        /// Interpolates Linearly <paramref name="x"/> with <paramref name="y"/> an <paramref name="amount"/>.
+        /// If there's more than 8 non zero result values, the 8 most representative values are taken.
+        /// </summary>
+        /// <param name="x">The first operand.</param>
+        /// <param name="y">The second operand.</param>
+        /// <param name="amount">The amount of <paramref name="y"/></param>
+        /// <returns>A new <see cref="SparseWeight8"/></returns>
         public static SparseWeight8 InterpolateLinear(in SparseWeight8 x, in SparseWeight8 y, float amount)
         {
             var xAmount = 1.0f - amount;
@@ -290,6 +378,16 @@ namespace SharpGLTF.Transforms
             return _OperateLinear(x, y, (xx, yy) => (xx * xAmount) + (yy * yAmount));
         }
 
+        /// <summary>
+        /// Interpolates (<paramref name="x"/> , <paramref name="xt"/>) with (<paramref name="y"/> , <paramref name="yt"/>) an <paramref name="amount"/>.
+        /// If there's more than 8 non zero result values, the 8 most representative values are taken.
+        /// </summary>
+        /// <param name="x">The first value operand.</param>
+        /// <param name="xt">The first tangent operand.</param>
+        /// <param name="y">The second value operand.</param>
+        /// <param name="yt">The second tangent operand.</param>
+        /// <param name="amount">The amount of <paramref name="y"/></param>
+        /// <returns>A new <see cref="SparseWeight8"/></returns>
         public static SparseWeight8 InterpolateCubic(in SparseWeight8 x, in SparseWeight8 xt, in SparseWeight8 y, in SparseWeight8 yt, float amount)
         {
             var basis = Animations.SamplerFactory.CreateHermitePointWeights(amount);
@@ -309,25 +407,6 @@ namespace SharpGLTF.Transforms
 
         IEnumerator IEnumerable.GetEnumerator() { return Expand(GetExpandedCount()).GetEnumerator(); }
 
-        public bool ContainsKey(int key)
-        {
-            return GetSparseWeights().Select(item => item.Item1).Contains(key);
-        }
-
-        public bool TryGetValue(int key, out float value)
-        {
-            if (key == Index0) { value = Weight0; return true; }
-            if (key == Index1) { value = Weight1; return true; }
-            if (key == Index2) { value = Weight2; return true; }
-            if (key == Index3) { value = Weight3; return true; }
-            if (key == Index4) { value = Weight4; return true; }
-            if (key == Index5) { value = Weight5; return true; }
-            if (key == Index6) { value = Weight6; return true; }
-            if (key == Index7) { value = Weight7; return true; }
-            value = 0;
-            return false;
-        }
-
         public override string ToString()
         {
             var sb = new StringBuilder();
@@ -347,46 +426,6 @@ namespace SharpGLTF.Transforms
 
         #region code
 
-        /// <summary>
-        /// Copies the current index-weight pairs to available slots in destination <paramref name="dstIndices"/> and <paramref name="dstWeights"/>
-        /// </summary>
-        /// <param name="src">The source <see cref="SparseWeight8"/>.</param>
-        /// <param name="dstIndices">The destination index array.</param>
-        /// <param name="dstWeights">The destination weight array.</param>
-        /// <param name="dstLength">The explicit length of both <paramref name="dstIndices"/> and <paramref name="dstWeights"/></param>
-        /// <returns>The new length of the destination arrays.</returns>
-        private static int CopyTo(in SparseWeight8 src, Span<int> dstIndices, Span<float> dstWeights, int dstLength)
-        {
-            System.Diagnostics.Debug.Assert(dstIndices.Length == dstWeights.Length);
-            System.Diagnostics.Debug.Assert(dstIndices.Length >= dstLength, $"{nameof(dstIndices)}.Length must be at least {nameof(dstLength)}");
-            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)
-            {
-                var pair = src.GetSparseWeight(i);
-                if (pair.Item2 == 0) continue;
-                var idx = dstIndices
-                    .Slice(0, dstLength)
-                    .IndexOf(pair.Item1);
-
-                if (idx < 0)
-                {
-                    // the index doesn't exist, insert it to the list.
-                    dstIndices[dstLength] = pair.Item1;
-                    dstWeights[dstLength] = pair.Item2;
-                    ++dstLength;
-                }
-                else
-                {
-                    // the index already exists, so we aggregate the weights
-                    dstWeights[idx] += pair.Item2;
-                }
-            }
-
-            return dstLength;
-        }
-
         /// <summary>
         /// Performs <paramref name="operationFunc"/> over all the elements of the operands.
         /// </summary>
@@ -396,32 +435,41 @@ namespace SharpGLTF.Transforms
         /// <returns>A new <see cref="SparseWeight8"/>.</returns>
         private static SparseWeight8 _OperateLinear(in SparseWeight8 x, in SparseWeight8 y, Func<float, float, float> operationFunc)
         {
+            const int STACKSIZE = 8 * 2;
+
             // prepare buffers in the stack
-            Span<int> indices = stackalloc int[16];
-            Span<float> xxx = stackalloc float[16];
-            Span<float> yyy = stackalloc float[16];
+            Span<int> indices = stackalloc int[STACKSIZE];
+            Span<float> xxx = stackalloc float[STACKSIZE];
+            Span<float> yyy = stackalloc float[STACKSIZE];
 
             // fill the buffers so all the elements are aligned by column
             int offset = 0;
-            offset = CopyTo(x, indices, xxx, offset);
-            offset = CopyTo(y, indices, yyy, offset);
+            offset = IndexWeight.CopyTo(x, indices, xxx, offset);
+            offset = IndexWeight.CopyTo(y, indices, yyy, offset);
 
             // perform operation element by element
+
+            int r = 0;
+            Span<IndexWeight> rrr = stackalloc IndexWeight[STACKSIZE];
+
             for (int i = 0; i < offset; ++i)
             {
-                xxx[i] = operationFunc(xxx[i], yyy[i]);
-            }
+                var ww = operationFunc(xxx[i], yyy[i]);
 
-            // sort results by relevance, so they can fit
-            // in a new structure in case there's more than
-            // 8 results
+                if (ww == 0) continue;
 
-            indices = indices.Slice(0, offset);
-            xxx = xxx.Slice(0, offset);
+                rrr[r++] = new IndexWeight(indices[i], ww);
+            }
 
-            BubbleSortByWeight(indices, xxx, offset);
+            rrr = rrr.Slice(0, r);
 
-            return new SparseWeight8(indices, xxx);
+            if (rrr.Length > 8)
+            {
+                IndexWeight.BubbleSortByWeight(rrr);
+                rrr = rrr.Slice(0, 8);
+            }
+
+            return new SparseWeight8(rrr);
         }
 
         /// <summary>
@@ -442,89 +490,48 @@ namespace SharpGLTF.Transforms
             Func<float, float, float, float, float> operationFunc
             )
         {
+            const int STACKSIZE = 8 * 4;
+
             // prepare buffers in the stack
-            Span<int> indices = stackalloc int[32];
-            Span<float> xxx = stackalloc float[32];
-            Span<float> yyy = stackalloc float[32];
-            Span<float> zzz = stackalloc float[32];
-            Span<float> www = stackalloc float[32];
+            Span<int> indices = stackalloc int[STACKSIZE];
+            Span<float> xxx = stackalloc float[STACKSIZE];
+            Span<float> yyy = stackalloc float[STACKSIZE];
+            Span<float> zzz = stackalloc float[STACKSIZE];
+            Span<float> www = stackalloc float[STACKSIZE];
 
             // fill the buffers so all the elements are aligned by column
             int offset = 0;
-            offset = CopyTo(x, indices, xxx, offset);
-            offset = CopyTo(y, indices, yyy, offset);
-            offset = CopyTo(z, indices, zzz, offset);
-            offset = CopyTo(w, indices, www, offset);
+            offset = IndexWeight.CopyTo(x, indices, xxx, offset);
+            offset = IndexWeight.CopyTo(y, indices, yyy, offset);
+            offset = IndexWeight.CopyTo(z, indices, zzz, offset);
+            offset = IndexWeight.CopyTo(w, indices, www, offset);
 
             // perform operation element by element
-            for (int i = 0; i < offset; ++i)
-            {
-                xxx[i] = operationFunc(xxx[i], yyy[i], zzz[i], www[i]);
-            }
-
-            // sort results by relevance, so they can fit
-            // in a new structure in case there's more than
-            // 8 results
-
-            indices = indices.Slice(0, offset);
-            xxx = xxx.Slice(0, offset);
-
-            BubbleSortByWeight(indices, xxx, offset);
-
-            return new SparseWeight8(indices, xxx);
-        }
 
-        private static void BubbleSortByWeight(Span<int> indices, Span<float> weights, int count = int.MaxValue)
-        {
-            System.Diagnostics.Debug.Assert(indices.Length == weights.Length);
-
-            count = Math.Min(indices.Length, count);
+            int r = 0;
+            Span<IndexWeight> rrr = stackalloc IndexWeight[STACKSIZE];
 
-            for (int i = 0; i < count - 1; ++i)
+            for (int i = 0; i < offset; ++i)
             {
-                for (int j = i + 1; j < count; ++j)
-                {
-                    if (weights[i] > weights[j]) continue;
-
-                    if (weights[i] == weights[j] && indices[i] < indices[j]) continue;
+                var ww = operationFunc(xxx[i], yyy[i], zzz[i], www[i]);
 
-                    var index = indices[i];
-                    indices[i] = indices[j];
-                    indices[j] = index;
+                if (ww == 0) continue;
 
-                    var weight = weights[i];
-                    weights[i] = weights[j];
-                    weights[j] = weight;
-                }
+                rrr[r++] = new IndexWeight(indices[i], ww);
             }
-        }
-
-        private static void BubbleSortByIndex(Span<int> indices, Span<float> weights, int count = int.MaxValue)
-        {
-            System.Diagnostics.Debug.Assert(indices.Length == weights.Length);
 
-            count = Math.Min(indices.Length, count);
+            rrr = rrr.Slice(0, r);
 
-            for (int i = 0; i < count - 1; ++i)
+            if (rrr.Length > 8)
             {
-                for (int j = i + 1; j < count; ++j)
-                {
-                    if (indices[i] < indices[j]) continue;
-
-                    if (indices[i] == indices[j] && weights[i] > weights[j]) continue;
-
-                    var index = indices[i];
-                    indices[i] = indices[j];
-                    indices[j] = index;
-
-                    var weight = weights[i];
-                    weights[i] = weights[j];
-                    weights[j] = weight;
-                }
+                IndexWeight.BubbleSortByWeight(rrr);
+                rrr = rrr.Slice(0, 8);
             }
+
+            return new SparseWeight8(rrr);
         }
 
-        internal IEnumerable<(int, float)> GetSparseWeights()
+        internal IEnumerable<(int, float)> GetNonZeroWeights()
         {
             if (Weight0 != 0) yield return (Index0, Weight0);
             if (Weight1 != 0) yield return (Index1, Weight1);
@@ -536,22 +543,6 @@ namespace SharpGLTF.Transforms
             if (Weight7 != 0) yield return (Index7, Weight7);
         }
 
-        private (int, float) GetSparseWeight(int sparseIndex)
-        {
-            switch (sparseIndex)
-            {
-                case 0: return (Index0, Weight0);
-                case 1: return (Index1, Weight1);
-                case 2: return (Index2, Weight2);
-                case 3: return (Index3, Weight3);
-                case 4: return (Index4, Weight4);
-                case 5: return (Index5, Weight5);
-                case 6: return (Index6, Weight6);
-                case 7: return (Index7, Weight7);
-                default: throw new ArgumentOutOfRangeException(nameof(sparseIndex));
-            }
-        }
-
         private float GetExpandedAt(int idx)
         {
             if (idx == Index0) return Weight0;
@@ -580,6 +571,41 @@ namespace SharpGLTF.Transforms
             return c;
         }
 
+        /// <summary>
+        /// Normalizes the current <see cref="SparseWeight8"/> by adding a complement weight
+        /// at index 0 that resolves <see cref="WeightSum"/> to 1.
+        /// </summary>
+        /// <returns>A new <see cref="SparseWeight8"/> with an extra weight.</returns>
+        internal SparseWeight8 GetNormalizedWithComplement()
+        {
+            Span<IndexWeight> weights = stackalloc IndexWeight[8 + 1];
+
+            var offset = IndexWeight.CopyTo(this, weights);
+
+            float ww = 0;
+
+            for (int i = 0; i < offset; ++i)
+            {
+                var w = weights[i].Weight;
+
+                ww += w;
+
+                weights[i] = new IndexWeight(weights[i].Index + 1, w);
+            }
+
+            weights[offset++] = new IndexWeight(0, 1 - ww);
+
+            weights = weights.Slice(0, offset);
+
+            if (offset > 8)
+            {
+                IndexWeight.BubbleSortByWeight(weights);
+                weights = weights.Slice(0, 8);
+            }
+
+            return new SparseWeight8(weights);
+        }
+
         #endregion
     }
 }

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

@@ -337,7 +337,7 @@ namespace SharpGLTF.Geometry
             for (int i = 0; i < Skinning.MaxBindings; ++i)
             {
                 var jw = Skinning.GetJointBinding(i);
-                if (!jw.Weight._IsReal() || jw.Weight < 0 || jw.Joint < 0) sb.Append($" ❌𝐉𝐖{i} {jw.Joint}:{jw.Weight}");
+                if (!jw.Item2._IsReal() || jw.Item2 < 0 || jw.Item1 < 0) sb.Append($" ❌𝐉𝐖{i} {jw.Item1}:{jw.Item2}");
             }
 
             return sb.ToString();

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

@@ -114,11 +114,11 @@ namespace SharpGLTF.Geometry.VertexTypes
             {
                 var pair = vertex.GetJointBinding(i);
 
-                Guard.MustBeGreaterThanOrEqualTo(pair.Joint, 0, $"Joint{i}");
-                Guard.IsTrue(pair.Weight._IsReal(), $"Weight{i}", "Values are not finite.");
-                if (pair.Weight == 0) Guard.IsTrue(pair.Joint == 0, "joints with weight zero must be set to zero");
+                Guard.MustBeGreaterThanOrEqualTo(pair.Item1, 0, $"Joint{i}");
+                Guard.IsTrue(pair.Item2._IsReal(), $"Weight{i}", "Values are not finite.");
+                if (pair.Item2 == 0) Guard.IsTrue(pair.Item1 == 0, "joints with weight zero must be set to zero");
 
-                weightsSum += pair.Weight;
+                weightsSum += pair.Item2;
             }
 
             // TODO: check that joints are unique
@@ -232,7 +232,7 @@ 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.SparseWeights);
+            var sparse = Transforms.SparseWeight8.OrderedByWeight(vertex.GetWeights());
 
             var sum = sparse.WeightSum;
             if (sum == 0) return default(TvS);

+ 16 - 17
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexEmpty.cs

@@ -18,30 +18,29 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public int MaxTextCoords => 0;
 
-        void IVertexMaterial.SetColor(int setIndex, Vector4 color) { }
+        void IVertexMaterial.SetColor(int index, Vector4 color) { throw new ArgumentOutOfRangeException(nameof(index)); }
 
-        void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord) { }
+        void IVertexMaterial.SetTexCoord(int index, Vector2 coord) { throw new ArgumentOutOfRangeException(nameof(index)); }
 
-        Vector4 IVertexMaterial.GetColor(int index) { throw new NotSupportedException(); }
+        Vector4 IVertexMaterial.GetColor(int index) { throw new ArgumentOutOfRangeException(nameof(index)); }
 
-        Vector2 IVertexMaterial.GetTexCoord(int index) { throw new NotSupportedException(); }
+        Vector2 IVertexMaterial.GetTexCoord(int index) { throw new ArgumentOutOfRangeException(nameof(index)); }
 
-        void IVertexSkinning.SetJointBinding(int index, int joint, float weight) { }
+        public SparseWeight8 GetWeights() { throw new NotSupportedException(); }
 
-        JointBinding IVertexSkinning.GetJointBinding(int index) { throw new NotSupportedException(); }
+        public void SetWeights(in SparseWeight8 weights) { throw new NotSupportedException(); }
 
-        public void SetWeights(in SparseWeight8 weights) { }
+        void IVertexSkinning.SetJointBinding(int index, int joint, float weight) { throw new ArgumentOutOfRangeException(nameof(index)); }
 
-        public IEnumerable<JointBinding> JointBindings => Enumerable.Empty<JointBinding>();
+        (int, float) IVertexSkinning.GetJointBinding(int index) { throw new ArgumentOutOfRangeException(nameof(index)); }
 
-        public Vector4 JointsLow => Vector4.Zero;
-
-        public Vector4 JointsHigh => Vector4.Zero;
-
-        public Vector4 WeightsLow => Vector4.Zero;
-
-        public Vector4 Weightshigh => Vector4.Zero;
-
-        public SparseWeight8 SparseWeights => default(SparseWeight8);
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        Vector4 IVertexSkinning.JointsLow => Vector4.Zero;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        Vector4 IVertexSkinning.JointsHigh => Vector4.Zero;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        Vector4 IVertexSkinning.WeightsLow => Vector4.Zero;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        Vector4 IVertexSkinning.Weightshigh => Vector4.Zero;
     }
 }

+ 105 - 316
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexSkinning.cs

@@ -6,87 +6,18 @@ using SharpGLTF.Transforms;
 
 namespace SharpGLTF.Geometry.VertexTypes
 {
-    /// <summary>
-    /// Represents a a Node Joint index and its weight in a skinning system.
-    /// </summary>
-    [System.Diagnostics.DebuggerDisplay("{Joint} = {Weight}")]
-    public struct JointBinding
-    {
-        #region constructors
-
-        public JointBinding(int joint, float weight)
-        {
-            this.Joint = joint;
-            this.Weight = weight;
-            if (Weight == 0) Joint = 0;
-        }
-
-        public static implicit operator JointBinding((int, float) jw)
-        {
-            return new JointBinding(jw.Item1, jw.Item2);
-        }
-
-        #endregion
-
-        #region data
-
-        public int Joint;
-        public float Weight;
-
-        private static readonly _WeightComparer _DefaultWeightComparer = new _WeightComparer();
-
-        #endregion
-
-        #region properties
-
-        public static IComparer<JointBinding> WeightComparer => _DefaultWeightComparer;
-
-        #endregion
-
-        #region API
-
-        public static IEnumerable<JointBinding> GetBindings(IVertexSkinning vs)
-        {
-            for (int i = 0; i < vs.MaxBindings; ++i)
-            {
-                var jw = vs.GetJointBinding(i);
-                if (jw.Weight != 0) yield return jw;
-            }
-        }
-
-        #endregion
-
-        #region types
-
-        private sealed class _WeightComparer : IComparer<JointBinding>
-        {
-            public int Compare(JointBinding x, JointBinding y)
-            {
-                var a = x.Weight.CompareTo(y.Weight);
-                if (a != 0) return a;
-
-                return x.Joint.CompareTo(y.Joint);
-            }
-        }
-
-        #endregion
-    }
-
     public interface IVertexSkinning
     {
         int MaxBindings { get; }
 
         void Validate();
 
-        JointBinding GetJointBinding(int index);
-
+        (int, float) GetJointBinding(int index);
         void SetJointBinding(int index, int joint, float weight);
 
         void SetWeights(in SparseWeight8 weights);
 
-        IEnumerable<JointBinding> JointBindings { get; }
-
-        SparseWeight8 SparseWeights { get; }
+        SparseWeight8 GetWeights();
 
         Vector4 JointsLow { get; }
         Vector4 JointsHigh { get; }
@@ -104,73 +35,23 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexJoints8x4(int jointIndex)
         {
-            Joints = new Vector4(jointIndex);
+            Joints = new Vector4(jointIndex, 0, 0, 0);
             Weights = Vector4.UnitX;
         }
 
-        public VertexJoints8x4(JointBinding a, JointBinding b)
-        {
-            Joints = new Vector4(a.Joint, b.Joint, 0, 0);
-            Weights = new Vector4(a.Weight, b.Weight, 0, 0);
-
-            InPlaceSort();
-        }
-
-        public VertexJoints8x4(JointBinding a, JointBinding b, JointBinding c)
-        {
-            Joints = new Vector4(a.Joint, b.Joint, c.Joint, 0);
-            Weights = new Vector4(a.Weight, b.Weight, c.Weight, 0);
-
-            InPlaceSort();
-        }
-
-        public VertexJoints8x4(JointBinding a, JointBinding b, JointBinding c, JointBinding d)
-        {
-            Joints = new Vector4(a.Joint, b.Joint, c.Joint, d.Joint);
-            Weights = new Vector4(a.Weight, b.Weight, c.Weight, d.Weight);
-
-            InPlaceSort();
-        }
-
         public VertexJoints8x4(params (int, float)[] bindings)
-        {
-            // var sparse = new Transforms.SparseWeight8(bindings);
-
-            Guard.NotNull(bindings, nameof(bindings));
-            Guard.MustBeBetweenOrEqualTo(bindings.Length, 1, 4, nameof(bindings));
-
-            Joints = Vector4.Zero;
-            Weights = Vector4.Zero;
-
-            for (int i = 0; i < bindings.Length; ++i)
-            {
-                this.SetJointBinding(i, bindings[i].Item1, bindings[i].Item2);
-            }
-        }
+            : this(SparseWeight8.Create(bindings)) { }
 
-        public VertexJoints8x4(in Transforms.SparseWeight8 weights)
+        public VertexJoints8x4(in SparseWeight8 weights)
         {
-            var w4 = Transforms.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
-                );
+            var w4 = 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);
 
             // renormalize
             var w = Vector4.Dot(Weights, Vector4.One);
-            if (w != 0) Weights /= w;
+            if (w != 0 && w != 1) Weights /= w;
         }
 
         #endregion
@@ -190,15 +71,13 @@ namespace SharpGLTF.Geometry.VertexTypes
         #region properties
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 JointsLow => this.Joints;
+        Vector4 IVertexSkinning.JointsLow => this.Joints;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 JointsHigh => Vector4.Zero;
+        Vector4 IVertexSkinning.JointsHigh => Vector4.Zero;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 WeightsLow => this.Weights;
+        Vector4 IVertexSkinning.WeightsLow => this.Weights;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 Weightshigh => Vector4.Zero;
-
-        public Transforms.SparseWeight8 SparseWeights => new Transforms.SparseWeight8(this.Joints, this.Weights);
+        Vector4 IVertexSkinning.Weightshigh => Vector4.Zero;
 
         #endregion
 
@@ -206,14 +85,18 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public void Validate() { FragmentPreprocessors.ValidateVertexSkinning(this); }
 
-        public JointBinding GetJointBinding(int index)
+        public SparseWeight8 GetWeights() { return new SparseWeight8(this.Joints, this.Weights); }
+
+        public void SetWeights(in SparseWeight8 weights) { this = new VertexJoints8x4(weights); }
+
+        public (int, float) GetJointBinding(int index)
         {
             switch (index)
             {
-                case 0: return new JointBinding((int)this.Joints.X, this.Weights.X);
-                case 1: return new JointBinding((int)this.Joints.Y, this.Weights.Y);
-                case 2: return new JointBinding((int)this.Joints.Z, this.Weights.Z);
-                case 3: return new JointBinding((int)this.Joints.W, this.Weights.W);
+                case 0: return ((int)this.Joints.X, this.Weights.X);
+                case 1: return ((int)this.Joints.Y, this.Weights.Y);
+                case 2: return ((int)this.Joints.Z, this.Weights.Z);
+                case 3: return ((int)this.Joints.W, this.Weights.W);
                 default: throw new ArgumentOutOfRangeException(nameof(index));
             }
         }
@@ -232,14 +115,10 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public void InPlaceSort()
         {
-            var sparse = new Transforms.SparseWeight8(this.Joints, this.Weights);
+            var sparse = new SparseWeight8(this.Joints, this.Weights);
             this = new VertexJoints8x4(sparse);
         }
 
-        public void SetWeights(in SparseWeight8 weights) { this = new VertexJoints8x4(weights); }
-
-        public IEnumerable<JointBinding> JointBindings => JointBinding.GetBindings(this);
-
         #endregion
     }
 
@@ -252,57 +131,23 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexJoints16x4(int jointIndex)
         {
-            Joints = new Vector4(jointIndex);
+            Joints = new Vector4(jointIndex, 0, 0, 0);
             Weights = Vector4.UnitX;
         }
 
-        public VertexJoints16x4(JointBinding a, JointBinding b)
-        {
-            Joints = new Vector4(a.Joint, b.Joint, 0, 0);
-            Weights = new Vector4(a.Weight, b.Weight, 0, 0);
-
-            InPlaceSort();
-        }
-
-        public VertexJoints16x4(JointBinding a, JointBinding b, JointBinding c)
-        {
-            Joints = new Vector4(a.Joint, b.Joint, c.Joint, 0);
-            Weights = new Vector4(a.Weight, b.Weight, c.Weight, 0);
-
-            InPlaceSort();
-        }
+        public VertexJoints16x4(params (int, float)[] bindings)
+            : this( SparseWeight8.Create(bindings) ) { }
 
-        public VertexJoints16x4(JointBinding a, JointBinding b, JointBinding c, JointBinding d)
+        public VertexJoints16x4(in SparseWeight8 weights)
         {
-            Joints = new Vector4(a.Joint, b.Joint, c.Joint, d.Joint);
-            Weights = new Vector4(a.Weight, b.Weight, c.Weight, d.Weight);
+            var w4 = SparseWeight8.OrderedByWeight(weights);
 
-            InPlaceSort();
-        }
-
-        public VertexJoints16x4(in Transforms.SparseWeight8 weights)
-        {
-            var w4 = Transforms.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(w4.Index0, w4.Index1, w4.Index2, w4.Index3);
+            Weights = new Vector4(w4.Weight0, w4.Weight1, w4.Weight2, w4.Weight3);
 
             // renormalize
             var w = Vector4.Dot(Weights, Vector4.One);
-            if (w != 0) Weights /= w;
+            if (w != 0 && w != 1) Weights /= w;
         }
 
         #endregion
@@ -322,15 +167,13 @@ namespace SharpGLTF.Geometry.VertexTypes
         #region properties
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 JointsLow => this.Joints;
+        Vector4 IVertexSkinning.JointsLow => this.Joints;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 JointsHigh => Vector4.Zero;
+        Vector4 IVertexSkinning.JointsHigh => Vector4.Zero;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 WeightsLow => this.Weights;
+        Vector4 IVertexSkinning.WeightsLow => this.Weights;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 Weightshigh => Vector4.Zero;
-
-        public SparseWeight8 SparseWeights => new SparseWeight8(this.Joints, this.Weights);
+        Vector4 IVertexSkinning.Weightshigh => Vector4.Zero;
 
         #endregion
 
@@ -338,14 +181,18 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public void Validate() { FragmentPreprocessors.ValidateVertexSkinning(this); }
 
-        public JointBinding GetJointBinding(int index)
+        public SparseWeight8 GetWeights() { return new SparseWeight8(this.Joints, this.Weights); }
+
+        public void SetWeights(in SparseWeight8 weights) { this = new VertexJoints16x4(weights); }
+
+        public (int, float) GetJointBinding(int index)
         {
             switch (index)
             {
-                case 0: return new JointBinding((int)this.Joints.X, this.Weights.X);
-                case 1: return new JointBinding((int)this.Joints.Y, this.Weights.Y);
-                case 2: return new JointBinding((int)this.Joints.Z, this.Weights.Z);
-                case 3: return new JointBinding((int)this.Joints.W, this.Weights.W);
+                case 0: return ((int)this.Joints.X, this.Weights.X);
+                case 1: return ((int)this.Joints.Y, this.Weights.Y);
+                case 2: return ((int)this.Joints.Z, this.Weights.Z);
+                case 3: return ((int)this.Joints.W, this.Weights.W);
                 default: throw new ArgumentOutOfRangeException(nameof(index));
             }
         }
@@ -362,16 +209,12 @@ namespace SharpGLTF.Geometry.VertexTypes
             }
         }
 
-        public void SetWeights(in SparseWeight8 weights) { this = new VertexJoints16x4(weights); }
-
         public void InPlaceSort()
         {
-            var sparse = new Transforms.SparseWeight8(this.Joints, this.Weights);
+            var sparse = new SparseWeight8(this.Joints, this.Weights);
             this = new VertexJoints16x4(sparse);
         }
 
-        public IEnumerable<JointBinding> JointBindings => JointBinding.GetBindings(this);
-
         #endregion
     }
 
@@ -384,53 +227,27 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexJoints8x8(int jointIndex)
         {
-            Joints0 = new Vector4(jointIndex);
-            Joints1 = new Vector4(jointIndex);
+            Joints0 = new Vector4(jointIndex, 0, 0, 0);
+            Joints1 = Vector4.Zero;
             Weights0 = Vector4.UnitX;
             Weights1 = Vector4.Zero;
         }
 
-        public VertexJoints8x8(int jointIndex1, int jointIndex2)
-        {
-            Joints0 = new Vector4(jointIndex1, jointIndex2, 0, 0);
-            Joints1 = Vector4.Zero;
-            Weights0 = new Vector4(0.5f, 0.5f, 0, 0);
-            Weights1 = Vector4.Zero;
-        }
+        public VertexJoints8x8(params (int, float)[] bindings)
+            : this(SparseWeight8.Create(bindings)) { }
 
         public VertexJoints8x8(in SparseWeight8 weights)
         {
-            Joints0 = new Vector4
-                (
-                weights.Index0,
-                weights.Index1,
-                weights.Index2,
-                weights.Index3
-                );
-
-            Joints1 = new Vector4
-                (
-                weights.Index4,
-                weights.Index5,
-                weights.Index6,
-                weights.Index7
-                );
-
-            Weights0 = new Vector4
-                (
-                weights.Weight0,
-                weights.Weight1,
-                weights.Weight2,
-                weights.Weight3
-                );
-
-            Weights1 = new Vector4
-                (
-                weights.Weight4,
-                weights.Weight5,
-                weights.Weight6,
-                weights.Weight7
-                );
+            var w8 = 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);
+
+            // renormalize
+            var w = Vector4.Dot(Weights0, Vector4.One) + Vector4.Dot(Weights1, Vector4.One);
+            if (w != 0 && w != 1) { Weights0 /= w; Weights1 /= w; }
         }
 
         #endregion
@@ -456,13 +273,13 @@ namespace SharpGLTF.Geometry.VertexTypes
         #region properties
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 JointsLow => this.Joints0;
+        Vector4 IVertexSkinning.JointsLow => this.Joints0;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 JointsHigh => this.Joints1;
+        Vector4 IVertexSkinning.JointsHigh => this.Joints1;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 WeightsLow => this.Weights0;
+        Vector4 IVertexSkinning.WeightsLow => this.Weights0;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 Weightshigh => this.Joints1;
+        Vector4 IVertexSkinning.Weightshigh => this.Joints1;
 
         public SparseWeight8 SparseWeights => new SparseWeight8(this.Joints0, this.Joints1, this.Weights0, this.Weights1);
 
@@ -472,20 +289,22 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public void Validate() { FragmentPreprocessors.ValidateVertexSkinning(this); }
 
+        public SparseWeight8 GetWeights() { return new SparseWeight8(this.Joints0, this.Joints1, this.Weights0, this.Weights1); }
+
         public void SetWeights(in SparseWeight8 weights) { this = new VertexJoints8x8(weights); }
 
-        public JointBinding GetJointBinding(int index)
+        public (int, float) GetJointBinding(int index)
         {
             switch (index)
             {
-                case 0: return new JointBinding((int)this.Joints0.X, this.Weights0.X);
-                case 1: return new JointBinding((int)this.Joints0.Y, this.Weights0.Y);
-                case 2: return new JointBinding((int)this.Joints0.Z, this.Weights0.Z);
-                case 3: return new JointBinding((int)this.Joints0.W, this.Weights0.W);
-                case 4: return new JointBinding((int)this.Joints1.X, this.Weights1.X);
-                case 5: return new JointBinding((int)this.Joints1.Y, this.Weights1.Y);
-                case 6: return new JointBinding((int)this.Joints1.Z, this.Weights1.Z);
-                case 7: return new JointBinding((int)this.Joints1.W, this.Weights1.W);
+                case 0: return ((int)this.Joints0.X, this.Weights0.X);
+                case 1: return ((int)this.Joints0.Y, this.Weights0.Y);
+                case 2: return ((int)this.Joints0.Z, this.Weights0.Z);
+                case 3: return ((int)this.Joints0.W, this.Weights0.W);
+                case 4: return ((int)this.Joints1.X, this.Weights1.X);
+                case 5: return ((int)this.Joints1.Y, this.Weights1.Y);
+                case 6: return ((int)this.Joints1.Z, this.Weights1.Z);
+                case 7: return ((int)this.Joints1.W, this.Weights1.W);
                 default: throw new ArgumentOutOfRangeException(nameof(index));
             }
         }
@@ -506,8 +325,6 @@ namespace SharpGLTF.Geometry.VertexTypes
             }
         }
 
-        public IEnumerable<JointBinding> JointBindings => JointBinding.GetBindings(this);
-
         #endregion
     }
 
@@ -520,53 +337,27 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexJoints16x8(int jointIndex)
         {
-            Joints0 = new Vector4(jointIndex);
-            Joints1 = new Vector4(jointIndex);
+            Joints0 = new Vector4(jointIndex, 0, 0, 0);
+            Joints1 = Vector4.Zero;
             Weights0 = Vector4.UnitX;
             Weights1 = Vector4.Zero;
         }
 
-        public VertexJoints16x8(int jointIndex1, int jointIndex2)
-        {
-            Joints0 = new Vector4(jointIndex1, jointIndex2, 0, 0);
-            Joints1 = Vector4.Zero;
-            Weights0 = new Vector4(0.5f, 0.5f, 0, 0);
-            Weights1 = Vector4.Zero;
-        }
+        public VertexJoints16x8(params (int, float)[] bindings)
+            : this(SparseWeight8.Create(bindings)) { }
 
-        public VertexJoints16x8(in Transforms.SparseWeight8 weights)
+        public VertexJoints16x8(in SparseWeight8 weights)
         {
-            Joints0 = new Vector4
-                (
-                weights.Index0,
-                weights.Index1,
-                weights.Index2,
-                weights.Index3
-                );
-
-            Joints1 = new Vector4
-                (
-                weights.Index4,
-                weights.Index5,
-                weights.Index6,
-                weights.Index7
-                );
-
-            Weights0 = new Vector4
-                (
-                weights.Weight0,
-                weights.Weight1,
-                weights.Weight2,
-                weights.Weight3
-                );
-
-            Weights1 = new Vector4
-                (
-                weights.Weight4,
-                weights.Weight5,
-                weights.Weight6,
-                weights.Weight7
-                );
+            var w8 = 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);
+
+            // renormalize
+            var w = Vector4.Dot(Weights0, Vector4.One) + Vector4.Dot(Weights1, Vector4.One);
+            if (w != 0 && w != 1) { Weights0 /= w; Weights1 /= w; }
         }
 
         #endregion
@@ -592,15 +383,13 @@ namespace SharpGLTF.Geometry.VertexTypes
         #region properties
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 JointsLow => this.Joints0;
+        Vector4 IVertexSkinning.JointsLow => this.Joints0;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 JointsHigh => this.Joints1;
+        Vector4 IVertexSkinning.JointsHigh => this.Joints1;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 WeightsLow => this.Weights0;
+        Vector4 IVertexSkinning.WeightsLow => this.Weights0;
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public Vector4 Weightshigh => this.Joints1;
-
-        public SparseWeight8 SparseWeights => new SparseWeight8(this.Joints0, this.Joints1, this.Weights0, this.Weights1);
+        Vector4 IVertexSkinning.Weightshigh => this.Joints1;
 
         #endregion
 
@@ -608,20 +397,22 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public void Validate() { FragmentPreprocessors.ValidateVertexSkinning(this); }
 
+        public SparseWeight8 GetWeights() { return new SparseWeight8(this.Joints0, this.Joints1, this.Weights0, this.Weights1); }
+
         public void SetWeights(in SparseWeight8 weights) { this = new VertexJoints16x8(weights); }
 
-        public JointBinding GetJointBinding(int index)
+        public (int, float) GetJointBinding(int index)
         {
             switch (index)
             {
-                case 0: return new JointBinding((int)this.Joints0.X, this.Weights0.X);
-                case 1: return new JointBinding((int)this.Joints0.Y, this.Weights0.Y);
-                case 2: return new JointBinding((int)this.Joints0.Z, this.Weights0.Z);
-                case 3: return new JointBinding((int)this.Joints0.W, this.Weights0.W);
-                case 4: return new JointBinding((int)this.Joints1.X, this.Weights1.X);
-                case 5: return new JointBinding((int)this.Joints1.Y, this.Weights1.Y);
-                case 6: return new JointBinding((int)this.Joints1.Z, this.Weights1.Z);
-                case 7: return new JointBinding((int)this.Joints1.W, this.Weights1.W);
+                case 0: return ((int)this.Joints0.X, this.Weights0.X);
+                case 1: return ((int)this.Joints0.Y, this.Weights0.Y);
+                case 2: return ((int)this.Joints0.Z, this.Weights0.Z);
+                case 3: return ((int)this.Joints0.W, this.Weights0.W);
+                case 4: return ((int)this.Joints1.X, this.Weights1.X);
+                case 5: return ((int)this.Joints1.Y, this.Weights1.Y);
+                case 6: return ((int)this.Joints1.Z, this.Weights1.Z);
+                case 7: return ((int)this.Joints1.W, this.Weights1.W);
                 default: throw new ArgumentOutOfRangeException(nameof(index));
             }
         }
@@ -642,8 +433,6 @@ namespace SharpGLTF.Geometry.VertexTypes
             }
         }
 
-        public IEnumerable<JointBinding> JointBindings => JointBinding.GetBindings(this);
-
         #endregion
     }
 }

+ 3 - 10
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexUtils.cs

@@ -5,8 +5,6 @@ using System.Numerics;
 
 using SharpGLTF.Memory;
 
-using JOINTWEIGHT = System.Collections.Generic.KeyValuePair<int, float>;
-
 namespace SharpGLTF.Geometry.VertexTypes
 {
     static class VertexUtils
@@ -174,16 +172,11 @@ namespace SharpGLTF.Geometry.VertexTypes
         {
             if (src.GetType() == typeof(TvS)) return (TvS)src;
 
-            var dst = default(TvS);
-
-            var sparse = Transforms.SparseWeight8.OrderedByWeight(src.SparseWeights);
+            var sparse = src.MaxBindings > 0 ? src.GetWeights() : default;
 
-            var sum = sparse.WeightSum;
-            if (sum == 0) return default(TvS);
-
-            sparse = Transforms.SparseWeight8.Multiply(sparse, 1.0f / sum);
+            var dst = default(TvS);
 
-            dst.SetWeights(sparse);
+            if (dst.MaxBindings > 0) dst.SetWeights(sparse);
 
             return dst;
         }

+ 15 - 14
tests/SharpGLTF.Tests/Geometry/VertexTypes/VertexSkinningTests.cs

@@ -14,25 +14,26 @@ namespace SharpGLTF.Geometry.VertexTypes
         public void TestVertexSkinningDowngradeFrom8To4Joints()
         {
             // vertex with 5 bindings
-            var v8 = new VertexJoints8x8();
-            v8.SetJointBinding(0, 1, 0.2f);
-            v8.SetJointBinding(1, 2, 0.15f);
-            v8.SetJointBinding(2, 3, 0.25f);
-            v8.SetJointBinding(3, 4, 0.10f);
-            v8.SetJointBinding(4, 5, 0.30f);
+            var v8 = new VertexJoints8x8
+                (
+                (1, 0.20f),
+                (2, 0.15f),
+                (3, 0.25f),
+                (4, 0.10f),
+                (5, 0.30f)
+                );
 
             // we downgrade to 4 bindings; remaining bindings should be interpolated to keep weighting 1.
             var v4 = v8.ConvertToSkinning<VertexJoints8x4>();
 
-            Assert.AreEqual(5, v4.GetJointBinding(0).Joint);
-            Assert.AreEqual(3, v4.GetJointBinding(1).Joint);
-            Assert.AreEqual(1, v4.GetJointBinding(2).Joint);
-            Assert.AreEqual(2, v4.GetJointBinding(3).Joint);
+            var sparse = v4.GetWeights();
 
-            Assert.AreEqual(0.333333f, v4.GetJointBinding(0).Weight, 0.0001f);
-            Assert.AreEqual(0.277777f, v4.GetJointBinding(1).Weight, 0.0001f);
-            Assert.AreEqual(0.222222f, v4.GetJointBinding(2).Weight, 0.0001f);
-            Assert.AreEqual(0.166666f, v4.GetJointBinding(3).Weight, 0.0001f);
+            Assert.AreEqual(1, sparse.WeightSum, 0.00001f);
+
+            Assert.AreEqual(0.333333f, sparse[5], 0.00001f);
+            Assert.AreEqual(0.277777f, sparse[3], 0.00001f);
+            Assert.AreEqual(0.222222f, sparse[1], 0.00001f);
+            Assert.AreEqual(0.166666f, sparse[2], 0.00001f);
         }
     }
 }

+ 29 - 2
tests/SharpGLTF.Tests/Transforms/SparseWeight8Tests.cs

@@ -42,7 +42,7 @@ namespace SharpGLTF.Transforms
             Assert.AreEqual(0.1f, sparse1.Weight2);
             Assert.AreEqual(0, sparse1.Weight3);
 
-            var sparse1Nrm = MorphTransform.Normalize(sparse1);
+            var sparse1Nrm = sparse1.GetNormalizedWithComplement();
             Assert.AreEqual(7, sparse1Nrm.Index0);
             Assert.AreEqual(6, sparse1Nrm.Index1);
             Assert.AreEqual(11, sparse1Nrm.Index2);
@@ -63,7 +63,7 @@ namespace SharpGLTF.Transforms
         }
 
         [Test]
-        public void TestSparseOrdering()
+        public void TestSparseOrdering1()
         {
             var array1 = new float[] { 0.2f, 0.15f, 0.25f, 0.10f, 0.30f };
 
@@ -78,6 +78,23 @@ namespace SharpGLTF.Transforms
             CheckIndexOrdered(s5byWeights);
         }
 
+        [Test]
+        public void TestSparseOrdering2()
+        {
+            var expanded = new float[] { 0,0,1,0,2,0,3,4,5,0,6,0,7,0,6,0,9,0,11 };
+
+            var sparse = SparseWeight8.Create(expanded);
+
+            Assert.AreEqual(11, sparse[18]);
+            Assert.AreEqual(9, sparse[16]);
+            Assert.AreEqual(7, sparse[12]);
+            Assert.AreEqual(6, sparse[10]);
+            Assert.AreEqual(6, sparse[14]);
+            Assert.AreEqual(5, sparse[8]);
+            Assert.AreEqual(4, sparse[7]);
+            Assert.AreEqual(3, sparse[6]);
+        }
+
         static void CheckWeightOrdered(SparseWeight8 sparse)
         {
             Assert.GreaterOrEqual(sparse.Weight0, sparse.Weight1);
@@ -107,6 +124,9 @@ namespace SharpGLTF.Transforms
 
             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)));
+
+            // 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))));
         }
 
         [Test]
@@ -143,6 +163,13 @@ namespace SharpGLTF.Transforms
             var a = SparseWeight8.Create(0, 0, 0.2f, 0, 0, 0, 1);
             var b = SparseWeight8.Create(1, 1, 0.4f, 0, 0, 1, 0);
             var t = SparseWeight8.Subtract(b, a);
+            Assert.AreEqual(1, t[0]);
+            Assert.AreEqual(1, t[1]);
+            Assert.AreEqual(0.2f, t[2]);
+            Assert.AreEqual(0, t[3]);
+            Assert.AreEqual(0, t[4]);
+            Assert.AreEqual(1, t[5]);
+            Assert.AreEqual(-1, t[6]);
 
             var lr = SparseWeight8.InterpolateLinear(a, b, 0.4f);
             var cr = SparseWeight8.InterpolateCubic(a, t, b, t, 0.4f);