Browse Source

Improved Extras support.
bug fixes, tests and documentation.

vpenades 4 years ago
parent
commit
58d94a9778
54 changed files with 1468 additions and 935 deletions
  1. 1 1
      build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj
  2. 1 1
      examples/SharpGLTF.Plotly/PlotlyToolkit.cs
  3. 13 0
      src/Shared/_Extensions.cs
  4. 110 110
      src/SharpGLTF.Core/Diagnostics/DebugViews.cs
  5. 164 164
      src/SharpGLTF.Core/Diagnostics/DebuggerDisplay.cs
  6. 115 13
      src/SharpGLTF.Core/IO/JsonContent.Impl.cs
  7. 48 15
      src/SharpGLTF.Core/IO/JsonContent.cs
  8. 1 1
      src/SharpGLTF.Core/Memory/MemoryAccessor.cs
  9. 26 18
      src/SharpGLTF.Core/Memory/MemoryImage.cs
  10. 3 1
      src/SharpGLTF.Core/Runtime/AnimationTrackInfo.cs
  11. 6 6
      src/SharpGLTF.Core/Runtime/ArmatureTemplate.cs
  12. 5 6
      src/SharpGLTF.Core/Runtime/MeshDecoder.Schema2.cs
  13. 7 7
      src/SharpGLTF.Core/Runtime/MeshDecoder.cs
  14. 2 0
      src/SharpGLTF.Core/Runtime/NodeInstance.cs
  15. 6 1
      src/SharpGLTF.Core/Runtime/NodeTemplate.cs
  16. 28 0
      src/SharpGLTF.Core/Runtime/RuntimeOptions.cs
  17. 29 3
      src/SharpGLTF.Core/Runtime/SceneTemplate.cs
  18. 3 3
      src/SharpGLTF.Core/Schema2/Generated/ext.ModelLightsPunctual.g.cs
  19. 2 2
      src/SharpGLTF.Core/Schema2/gltf.Accessors.cs
  20. 2 2
      src/SharpGLTF.Core/Schema2/gltf.BufferView.cs
  21. 1 4
      src/SharpGLTF.Core/Schema2/gltf.ExtraProperties.cs
  22. 1 1
      src/SharpGLTF.Core/Schema2/gltf.Images.cs
  23. 9 0
      src/SharpGLTF.Core/Schema2/gltf.LogicalChildOfRoot.cs
  24. 14 2
      src/SharpGLTF.Core/Schema2/gltf.MaterialChannel.cs
  25. 1 1
      src/SharpGLTF.Core/Schema2/gltf.Mesh.cs
  26. 1 1
      src/SharpGLTF.Core/Schema2/gltf.MeshPrimitive.cs
  27. 1 4
      src/SharpGLTF.Core/Schema2/gltf.TextureInfo.cs
  28. 1 1
      src/SharpGLTF.Core/SharpGLTF.Core.csproj
  29. 1 1
      src/SharpGLTF.Core/Transforms/Matrix4x4Double.cs
  30. 3 3
      src/SharpGLTF.Toolkit/Animations/CurveFactory.cs
  31. 89 0
      src/SharpGLTF.Toolkit/BaseBuilder.cs
  32. 117 117
      src/SharpGLTF.Toolkit/Diagnostics/DebugViews.cs
  33. 57 63
      src/SharpGLTF.Toolkit/Geometry/MeshBuilder.cs
  34. 56 14
      src/SharpGLTF.Toolkit/Geometry/MorphTargetBuilder.cs
  35. 4 11
      src/SharpGLTF.Toolkit/Geometry/Packed/PackedMeshBuilder.cs
  36. 28 18
      src/SharpGLTF.Toolkit/Geometry/PrimitiveBuilder.cs
  37. 9 2
      src/SharpGLTF.Toolkit/Geometry/VertexTypes/FragmentPreprocessors.cs
  38. 8 12
      src/SharpGLTF.Toolkit/Materials/ChannelBuilder.cs
  39. 99 0
      src/SharpGLTF.Toolkit/Materials/ImageBuilder.cs
  40. 11 24
      src/SharpGLTF.Toolkit/Materials/MaterialBuilder.cs
  41. 34 33
      src/SharpGLTF.Toolkit/Materials/TextureBuilder.cs
  42. 22 15
      src/SharpGLTF.Toolkit/Scenes/CameraBuilder.cs
  43. 4 6
      src/SharpGLTF.Toolkit/Scenes/LightBuilder.cs
  44. 8 19
      src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs
  45. 21 16
      src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs
  46. 28 40
      src/SharpGLTF.Toolkit/Scenes/SceneBuilder.cs
  47. 33 15
      src/SharpGLTF.Toolkit/Schema2/MaterialExtensions.cs
  48. 2 2
      src/SharpGLTF.Toolkit/Schema2/SceneExtensions.cs
  49. 1 1
      src/SharpGLTF.Toolkit/SharpGLTF.Toolkit.csproj
  50. 1 1
      tests/SharpGLTF.NUnit/SharpGLTF.NUnit.csproj
  51. 83 39
      tests/SharpGLTF.Tests/IO/JsonContentTests.cs
  52. 2 2
      tests/SharpGLTF.Tests/Runtime/SceneTemplateTests.cs
  53. 1 1
      tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSampleTests.cs
  54. 145 112
      tests/SharpGLTF.Toolkit.Tests/Materials/ContentSharingTests.cs

+ 1 - 1
build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj

@@ -8,7 +8,7 @@
 
   <ItemGroup>
     <PackageReference Include="LibGit2Sharp" Version="0.26.2" />    
-    <PackageReference Include="NJsonSchema.CodeGeneration.CSharp" Version="10.3.2" />
+    <PackageReference Include="NJsonSchema.CodeGeneration.CSharp" Version="10.3.5" />
   </ItemGroup>
 
 </Project>

+ 1 - 1
examples/SharpGLTF.Plotly/PlotlyToolkit.cs

@@ -18,7 +18,7 @@ namespace SharpGLTF
         {
             // create an instantiable scene.
             var sceneInstance = Runtime.SceneTemplate
-                .Create(srcScene, false)
+                .Create(srcScene)
                 .CreateInstance();
 
             // set the node animations for our scene instance-

+ 13 - 0
src/Shared/_Extensions.cs

@@ -192,6 +192,19 @@ namespace SharpGLTF
 
         #region linq
 
+        public static bool AreSameReference<T>(this (T x, T y) refs, out bool result)
+            where T : class
+        {
+            #pragma warning disable IDE0041 // Use 'is null' check
+            if (Object.ReferenceEquals(refs.x, refs.y)) { result = true; return true; }
+            if (Object.ReferenceEquals(refs.x, null)) { result = false; return true; }
+            if (Object.ReferenceEquals(refs.y, null)) { result = false; return true; }
+            #pragma warning restore IDE0041 // Use 'is null' check
+
+            result = false;
+            return false;
+        }
+
         internal static int GetContentHashCode<T>(this IEnumerable<T> collection, int count = int.MaxValue)
         {
             if (collection == null) return 0;

+ 110 - 110
src/SharpGLTF.Core/Debug/DebugViews.cs → src/SharpGLTF.Core/Diagnostics/DebugViews.cs

@@ -1,110 +1,110 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Numerics;
-using System.Text;
-
-namespace SharpGLTF.Debug
-{
-    internal sealed class _CollectionDebugProxy<T>
-    {
-        // https://referencesource.microsoft.com/#mscorlib/system/collections/generic/debugview.cs,29
-
-        public _CollectionDebugProxy(ICollection<T> collection)
-        {
-            _Collection = collection ?? throw new ArgumentNullException(nameof(collection));
-        }
-
-        private readonly ICollection<T> _Collection;
-
-        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
-        public T[] Items
-        {
-            get
-            {
-                T[] items = new T[_Collection.Count];
-                _Collection.CopyTo(items, 0);
-                return items;
-            }
-        }
-    }
-
-    internal sealed class _BufferViewDebugProxy
-    {
-        public _BufferViewDebugProxy(Schema2.BufferView value) { _Value = value; }
-
-        public int LogicalIndex => _Value.LogicalIndex;
-
-        private readonly Schema2.BufferView _Value;
-
-        public int ByteStride => _Value.ByteStride;
-
-        public int ByteLength => _Value.Content.Count;
-
-        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
-        public Schema2.Accessor[] Accessors => _Value.FindAccessors().ToArray();
-    }
-
-    internal sealed class _AccessorDebugProxy
-    {
-        public _AccessorDebugProxy(Schema2.Accessor value) { _Value = value; }
-
-        private readonly Schema2.Accessor _Value;
-
-        public String Identity => $"Accessor[{_Value.LogicalIndex}] {_Value.Name}";
-
-        public Schema2.BufferView Source => _Value.SourceBufferView;
-
-        public (Schema2.DimensionType Dimensions, Schema2.EncodingType Encoding, bool Normalized) Format => (_Value.Dimensions, _Value.Encoding, _Value.Normalized);
-
-        public Object[] Items
-        {
-            get
-            {
-                if (_Value.Dimensions == Schema2.DimensionType.SCALAR) return _Value.AsScalarArray().Cast<Object>().ToArray();
-                if (_Value.Dimensions == Schema2.DimensionType.VEC2) return _Value.AsVector2Array().Cast<Object>().ToArray();
-                if (_Value.Dimensions == Schema2.DimensionType.VEC3) return _Value.AsVector3Array().Cast<Object>().ToArray();
-                if (_Value.Dimensions == Schema2.DimensionType.VEC4) return _Value.AsVector4Array().Cast<Object>().ToArray();
-                if (_Value.Dimensions == Schema2.DimensionType.MAT4) return _Value.AsMatrix4x4Array().Cast<Object>().ToArray();
-
-                var itemByteSz = _Value.Format.ByteSize;
-                var byteStride = Math.Max(_Value.SourceBufferView.ByteStride, itemByteSz);
-                var items = new ArraySegment<Byte>[_Value.Count];
-
-                var buffer = _Value.SourceBufferView.Content.Slice(_Value.ByteOffset, _Value.Count * byteStride);
-
-                for (int i = 0; i < items.Length; ++i )
-                {
-                    items[i] = buffer.Slice(i * byteStride, itemByteSz);
-                }
-
-                return items.Cast<Object>().ToArray();
-            }
-        }
-    }
-
-    internal sealed class _MeshDebugProxy
-    {
-        public _MeshDebugProxy(Schema2.Mesh value) { _Value = value; }
-
-        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private readonly Schema2.Mesh _Value;
-
-        public String Name => _Value.Name;
-
-        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
-        public Schema2.MeshPrimitive[] Primitives => _Value.Primitives.ToArray();
-    }
-
-    internal sealed class _Matrix4x4DoubleProxy
-    {
-        public _Matrix4x4DoubleProxy(Transforms.Matrix4x4Double value) { _Value = value; }
-
-        private Transforms.Matrix4x4Double _Value;
-
-        public (Double X, Double Y, Double Z, Double W) Row1 => (_Value.M11, _Value.M12, _Value.M13, _Value.M14);
-        public (Double X, Double Y, Double Z, Double W) Row2 => (_Value.M21, _Value.M22, _Value.M23, _Value.M24);
-        public (Double X, Double Y, Double Z, Double W) Row3 => (_Value.M31, _Value.M32, _Value.M33, _Value.M34);
-        public (Double X, Double Y, Double Z, Double W) Row4 => (_Value.M41, _Value.M42, _Value.M43, _Value.M44);
-    }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.Diagnostics
+{
+    internal sealed class _CollectionDebugProxy<T>
+    {
+        // https://referencesource.microsoft.com/#mscorlib/system/collections/generic/debugview.cs,29
+
+        public _CollectionDebugProxy(ICollection<T> collection)
+        {
+            _Collection = collection ?? throw new ArgumentNullException(nameof(collection));
+        }
+
+        private readonly ICollection<T> _Collection;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+        public T[] Items
+        {
+            get
+            {
+                T[] items = new T[_Collection.Count];
+                _Collection.CopyTo(items, 0);
+                return items;
+            }
+        }
+    }
+
+    internal sealed class _BufferViewDebugProxy
+    {
+        public _BufferViewDebugProxy(Schema2.BufferView value) { _Value = value; }
+
+        public int LogicalIndex => _Value.LogicalIndex;
+
+        private readonly Schema2.BufferView _Value;
+
+        public int ByteStride => _Value.ByteStride;
+
+        public int ByteLength => _Value.Content.Count;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+        public Schema2.Accessor[] Accessors => _Value.FindAccessors().ToArray();
+    }
+
+    internal sealed class _AccessorDebugProxy
+    {
+        public _AccessorDebugProxy(Schema2.Accessor value) { _Value = value; }
+
+        private readonly Schema2.Accessor _Value;
+
+        public String Identity => $"Accessor[{_Value.LogicalIndex}] {_Value.Name}";
+
+        public Schema2.BufferView Source => _Value.SourceBufferView;
+
+        public (Schema2.DimensionType Dimensions, Schema2.EncodingType Encoding, bool Normalized) Format => (_Value.Dimensions, _Value.Encoding, _Value.Normalized);
+
+        public Object[] Items
+        {
+            get
+            {
+                if (_Value.Dimensions == Schema2.DimensionType.SCALAR) return _Value.AsScalarArray().Cast<Object>().ToArray();
+                if (_Value.Dimensions == Schema2.DimensionType.VEC2) return _Value.AsVector2Array().Cast<Object>().ToArray();
+                if (_Value.Dimensions == Schema2.DimensionType.VEC3) return _Value.AsVector3Array().Cast<Object>().ToArray();
+                if (_Value.Dimensions == Schema2.DimensionType.VEC4) return _Value.AsVector4Array().Cast<Object>().ToArray();
+                if (_Value.Dimensions == Schema2.DimensionType.MAT4) return _Value.AsMatrix4x4Array().Cast<Object>().ToArray();
+
+                var itemByteSz = _Value.Format.ByteSize;
+                var byteStride = Math.Max(_Value.SourceBufferView.ByteStride, itemByteSz);
+                var items = new ArraySegment<Byte>[_Value.Count];
+
+                var buffer = _Value.SourceBufferView.Content.Slice(_Value.ByteOffset, _Value.Count * byteStride);
+
+                for (int i = 0; i < items.Length; ++i )
+                {
+                    items[i] = buffer.Slice(i * byteStride, itemByteSz);
+                }
+
+                return items.Cast<Object>().ToArray();
+            }
+        }
+    }
+
+    internal sealed class _MeshDebugProxy
+    {
+        public _MeshDebugProxy(Schema2.Mesh value) { _Value = value; }
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly Schema2.Mesh _Value;
+
+        public String Name => _Value.Name;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+        public Schema2.MeshPrimitive[] Primitives => _Value.Primitives.ToArray();
+    }
+
+    internal sealed class _Matrix4x4DoubleProxy
+    {
+        public _Matrix4x4DoubleProxy(Transforms.Matrix4x4Double value) { _Value = value; }
+
+        private Transforms.Matrix4x4Double _Value;
+
+        public (Double X, Double Y, Double Z, Double W) Row1 => (_Value.M11, _Value.M12, _Value.M13, _Value.M14);
+        public (Double X, Double Y, Double Z, Double W) Row2 => (_Value.M21, _Value.M22, _Value.M23, _Value.M24);
+        public (Double X, Double Y, Double Z, Double W) Row3 => (_Value.M31, _Value.M32, _Value.M33, _Value.M34);
+        public (Double X, Double Y, Double Z, Double W) Row4 => (_Value.M41, _Value.M42, _Value.M43, _Value.M44);
+    }
+}

+ 164 - 164
src/SharpGLTF.Core/Debug/DebuggerDisplay.cs → src/SharpGLTF.Core/Diagnostics/DebuggerDisplay.cs

@@ -1,164 +1,164 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-using SharpGLTF.Schema2;
-
-namespace SharpGLTF.Debug
-{
-    static class DebuggerDisplay
-    {
-        internal static string GetAttributeShortName(string attributeName)
-        {
-            if (attributeName == "POSITION") return "𝐏";
-            if (attributeName == "NORMAL") return "𝚴";
-            if (attributeName == "TANGENT") return "𝚻";
-            if (attributeName == "COLOR_0") return "𝐂₀";
-            if (attributeName == "COLOR_1") return "𝐂₁";
-            if (attributeName == "TEXCOORD_0") return "𝐔𝐕₀";
-            if (attributeName == "TEXCOORD_1") return "𝐔𝐕₁";
-
-            if (attributeName == "JOINTS_0") return "𝐉₀";
-            if (attributeName == "JOINTS_1") return "𝐉₁";
-
-            if (attributeName == "WEIGHTS_0") return "𝐖₀";
-            if (attributeName == "WEIGHTS_1") return "𝐖₁";
-            return attributeName;
-        }
-
-        public static String ToReport(this Memory.MemoryAccessInfo minfo)
-        {
-            var txt = GetAttributeShortName(minfo.Name);
-            if (minfo.ByteOffset != 0) txt += $" Offs:{minfo.ByteOffset}ᴮʸᵗᵉˢ";
-            if (minfo.ByteStride != 0) txt += $" Strd:{minfo.ByteStride}ᴮʸᵗᵉˢ";
-            txt += $" {minfo.Encoding.ToDebugString(minfo.Dimensions, minfo.Normalized)}[{minfo.ItemsCount}]";
-
-            return txt;
-        }
-
-        public static string ToReport(this BufferView bv)
-        {
-            var path = string.Empty;
-
-            if (bv.IsVertexBuffer) path += " VertexView";
-            else if (bv.IsIndexBuffer) path += " IndexView";
-            else path += " BufferView";
-
-            var content = bv.Content;
-
-            path += $"[{bv.LogicalIndex}ᴵᵈˣ]";
-            path += $"[{content.Count}ᴮʸᵗᵉˢ]";
-
-            if (bv.ByteStride > 0) path += $" Stride:{bv.ByteStride}ᴮʸᵗᵉˢ";
-
-            return path;
-        }
-
-        public static string ToReportShort(this Accessor accessor)
-        {
-            return $"{accessor.Encoding.ToDebugString(accessor.Dimensions, accessor.Normalized)}[{accessor.Count}ᴵᵗᵉᵐˢ]";
-        }
-
-        public static string ToReportLong(this Accessor accessor)
-        {
-            var path = string.Empty;
-
-            var bv = accessor.SourceBufferView;
-
-            if (bv.IsVertexBuffer) path += "VertexBuffer";
-            else if (bv.IsIndexBuffer) path += "IndexBuffer";
-            else path += "BufferView";
-            path += $"[{bv.LogicalIndex}ᴵᵈˣ] ⇨";
-
-            path += $" Accessor[{accessor.LogicalIndex}ᴵᵈˣ] Offset:{accessor.ByteOffset}ᴮʸᵗᵉˢ ⇨";
-
-            path += $" {accessor.Encoding.ToDebugString(accessor.Dimensions, accessor.Normalized)}[{accessor.Count}ᴵᵗᵉᵐˢ]";
-
-            if (accessor.IsSparse) path += " SPARSE";
-
-            return path;
-        }
-
-        public static string ToReport(this MeshPrimitive prim, string txt)
-        {
-            // gather vertex attribute information
-
-            var vcounts = prim.VertexAccessors.Values
-                .Select(item => item.Count)
-                .Distinct();
-
-            var vcount = vcounts.First();
-
-            if (vcounts.Count() > 1)
-            {
-                var vAccessors = prim.VertexAccessors
-                    .OrderBy(item => item.Key, Memory.MemoryAccessInfo.NameComparer)
-                    .Select(item => $"{GetAttributeShortName(item.Key)}={item.Value.ToReportShort()}")
-                    .ToList();
-
-                txt += $" Vrts: {String.Join(" ", vAccessors)} ⚠️Vertex Count mismatch⚠️";
-            }
-            else
-            {
-                string toShort(string name, Accessor accessor)
-                {
-                    name = GetAttributeShortName(name);
-                    var t = accessor.Encoding.ToDebugString(accessor.Dimensions, accessor.Normalized);
-                    return $"{name}.{t}";
-                }
-
-                var vAccessors = prim.VertexAccessors
-                    .OrderBy(item => item.Key, Memory.MemoryAccessInfo.NameComparer)
-                    .Select(item => toShort(item.Key, item.Value))
-                    .ToList();
-
-                txt += $" Vrts: ( {String.Join(" ", vAccessors)} )[{vcount}]";
-            }
-
-            // gather index attribute information
-
-            var indices = prim.IndexAccessor?.AsIndicesArray();
-            var pcount = 0;
-
-            switch (prim.DrawPrimitiveType)
-            {
-                case PrimitiveType.POINTS:
-                    pcount = vcount;
-                    break;
-                case PrimitiveType.LINES:
-                case PrimitiveType.LINE_LOOP:
-                case PrimitiveType.LINE_STRIP:
-                    pcount = indices.HasValue ? prim.DrawPrimitiveType.GetLinesIndices(indices.Value).Count() : prim.DrawPrimitiveType.GetLinesIndices(vcount).Count();
-                    break;
-                case PrimitiveType.TRIANGLES:
-                case PrimitiveType.TRIANGLE_FAN:
-                case PrimitiveType.TRIANGLE_STRIP:
-                    pcount = indices.HasValue ? prim.DrawPrimitiveType.GetTrianglesIndices(indices.Value).Count() : prim.DrawPrimitiveType.GetTrianglesIndices(vcount).Count();
-                    break;
-            }
-
-            var culture = System.Globalization.CultureInfo.CurrentCulture;
-
-            var primName = culture.TextInfo.ToTitleCase(prim.DrawPrimitiveType.ToString().ToLower(culture));
-            txt += $" {primName}[{pcount}]";
-
-            // gather morph attributes information
-
-            if (prim.MorphTargetsCount > 0)
-            {
-                txt += $" MorphTargets[{prim.MorphTargetsCount}]";
-            }
-
-            // materials
-
-            if (prim.Material != null)
-            {
-                if (string.IsNullOrWhiteSpace(prim.Material.Name)) txt += $" Material[{prim.Material.LogicalIndex}]";
-                else txt += "Material " + "\"" + prim.Material.Name + "\"";
-            }
-
-            return txt;
-        }
-    }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using SharpGLTF.Schema2;
+
+namespace SharpGLTF.Diagnostics
+{
+    static class DebuggerDisplay
+    {
+        internal static string GetAttributeShortName(string attributeName)
+        {
+            if (attributeName == "POSITION") return "𝐏";
+            if (attributeName == "NORMAL") return "𝚴";
+            if (attributeName == "TANGENT") return "𝚻";
+            if (attributeName == "COLOR_0") return "𝐂₀";
+            if (attributeName == "COLOR_1") return "𝐂₁";
+            if (attributeName == "TEXCOORD_0") return "𝐔𝐕₀";
+            if (attributeName == "TEXCOORD_1") return "𝐔𝐕₁";
+
+            if (attributeName == "JOINTS_0") return "𝐉₀";
+            if (attributeName == "JOINTS_1") return "𝐉₁";
+
+            if (attributeName == "WEIGHTS_0") return "𝐖₀";
+            if (attributeName == "WEIGHTS_1") return "𝐖₁";
+            return attributeName;
+        }
+
+        public static String ToReport(this Memory.MemoryAccessInfo minfo)
+        {
+            var txt = GetAttributeShortName(minfo.Name);
+            if (minfo.ByteOffset != 0) txt += $" Offs:{minfo.ByteOffset}ᴮʸᵗᵉˢ";
+            if (minfo.ByteStride != 0) txt += $" Strd:{minfo.ByteStride}ᴮʸᵗᵉˢ";
+            txt += $" {minfo.Encoding.ToDebugString(minfo.Dimensions, minfo.Normalized)}[{minfo.ItemsCount}]";
+
+            return txt;
+        }
+
+        public static string ToReport(this BufferView bv)
+        {
+            var path = string.Empty;
+
+            if (bv.IsVertexBuffer) path += " VertexView";
+            else if (bv.IsIndexBuffer) path += " IndexView";
+            else path += " BufferView";
+
+            var content = bv.Content;
+
+            path += $"[{bv.LogicalIndex}ᴵᵈˣ]";
+            path += $"[{content.Count}ᴮʸᵗᵉˢ]";
+
+            if (bv.ByteStride > 0) path += $" Stride:{bv.ByteStride}ᴮʸᵗᵉˢ";
+
+            return path;
+        }
+
+        public static string ToReportShort(this Accessor accessor)
+        {
+            return $"{accessor.Encoding.ToDebugString(accessor.Dimensions, accessor.Normalized)}[{accessor.Count}ᴵᵗᵉᵐˢ]";
+        }
+
+        public static string ToReportLong(this Accessor accessor)
+        {
+            var path = string.Empty;
+
+            var bv = accessor.SourceBufferView;
+
+            if (bv.IsVertexBuffer) path += "VertexBuffer";
+            else if (bv.IsIndexBuffer) path += "IndexBuffer";
+            else path += "BufferView";
+            path += $"[{bv.LogicalIndex}ᴵᵈˣ] ⇨";
+
+            path += $" Accessor[{accessor.LogicalIndex}ᴵᵈˣ] Offset:{accessor.ByteOffset}ᴮʸᵗᵉˢ ⇨";
+
+            path += $" {accessor.Encoding.ToDebugString(accessor.Dimensions, accessor.Normalized)}[{accessor.Count}ᴵᵗᵉᵐˢ]";
+
+            if (accessor.IsSparse) path += " SPARSE";
+
+            return path;
+        }
+
+        public static string ToReport(this MeshPrimitive prim, string txt)
+        {
+            // gather vertex attribute information
+
+            var vcounts = prim.VertexAccessors.Values
+                .Select(item => item.Count)
+                .Distinct();
+
+            var vcount = vcounts.First();
+
+            if (vcounts.Count() > 1)
+            {
+                var vAccessors = prim.VertexAccessors
+                    .OrderBy(item => item.Key, Memory.MemoryAccessInfo.NameComparer)
+                    .Select(item => $"{GetAttributeShortName(item.Key)}={item.Value.ToReportShort()}")
+                    .ToList();
+
+                txt += $" Vrts: {String.Join(" ", vAccessors)} ⚠️Vertex Count mismatch⚠️";
+            }
+            else
+            {
+                string toShort(string name, Accessor accessor)
+                {
+                    name = GetAttributeShortName(name);
+                    var t = accessor.Encoding.ToDebugString(accessor.Dimensions, accessor.Normalized);
+                    return $"{name}.{t}";
+                }
+
+                var vAccessors = prim.VertexAccessors
+                    .OrderBy(item => item.Key, Memory.MemoryAccessInfo.NameComparer)
+                    .Select(item => toShort(item.Key, item.Value))
+                    .ToList();
+
+                txt += $" Vrts: ( {String.Join(" ", vAccessors)} )[{vcount}]";
+            }
+
+            // gather index attribute information
+
+            var indices = prim.IndexAccessor?.AsIndicesArray();
+            var pcount = 0;
+
+            switch (prim.DrawPrimitiveType)
+            {
+                case PrimitiveType.POINTS:
+                    pcount = vcount;
+                    break;
+                case PrimitiveType.LINES:
+                case PrimitiveType.LINE_LOOP:
+                case PrimitiveType.LINE_STRIP:
+                    pcount = indices.HasValue ? prim.DrawPrimitiveType.GetLinesIndices(indices.Value).Count() : prim.DrawPrimitiveType.GetLinesIndices(vcount).Count();
+                    break;
+                case PrimitiveType.TRIANGLES:
+                case PrimitiveType.TRIANGLE_FAN:
+                case PrimitiveType.TRIANGLE_STRIP:
+                    pcount = indices.HasValue ? prim.DrawPrimitiveType.GetTrianglesIndices(indices.Value).Count() : prim.DrawPrimitiveType.GetTrianglesIndices(vcount).Count();
+                    break;
+            }
+
+            var culture = System.Globalization.CultureInfo.CurrentCulture;
+
+            var primName = culture.TextInfo.ToTitleCase(prim.DrawPrimitiveType.ToString().ToLower(culture));
+            txt += $" {primName}[{pcount}]";
+
+            // gather morph attributes information
+
+            if (prim.MorphTargetsCount > 0)
+            {
+                txt += $" MorphTargets[{prim.MorphTargetsCount}]";
+            }
+
+            // materials
+
+            if (prim.Material != null)
+            {
+                if (string.IsNullOrWhiteSpace(prim.Material.Name)) txt += $" Material[{prim.Material.LogicalIndex}]";
+                else txt += "Material " + "\"" + prim.Material.Name + "\"";
+            }
+
+            return txt;
+        }
+    }
+}

+ 115 - 13
src/SharpGLTF.Core/IO/JsonContent.Impl.cs

@@ -174,6 +174,92 @@ namespace SharpGLTF.IO
 
             return true;
         }
+
+        public static bool AreEqualByContent(Object x, Object y, float precission)
+        {
+            #pragma warning disable IDE0041 // Use 'is null' check
+            if (Object.ReferenceEquals(x, y)) return true;
+            if (Object.ReferenceEquals(x, null)) return false;
+            if (Object.ReferenceEquals(y, null)) return false;
+            #pragma warning restore IDE0041 // Use 'is null' check
+
+            if (x is IConvertible xval && y is IConvertible yval)
+            {
+                return AreEqual(xval, yval, precission);
+            }
+
+            if (x is IReadOnlyList<object> xarr && y is IReadOnlyList<object> yarr)
+            {
+                if (xarr.Count != yarr.Count) return false;
+                int c = xarr.Count;
+
+                for (int i = 0; i < c; ++i)
+                {
+                    if (!AreEqualByContent(xarr[i], yarr[i], precission)) return false;
+                }
+
+                return true;
+            }
+
+            if (x is IReadOnlyDictionary<string, object> xdic && y is IReadOnlyDictionary<string, object> ydic)
+            {
+                if (xdic.Count != ydic.Count) return false;
+
+                foreach (var key in xdic.Keys)
+                {
+                    if (!xdic.TryGetValue(key, out Object xdval)) return false;
+                    if (!ydic.TryGetValue(key, out Object ydval)) return false;
+
+                    if (!AreEqualByContent(xdval, ydval, precission)) return false;
+                }
+
+                return true;
+            }
+
+            bool isValidType(Object z)
+            {
+                if (z is IConvertible) return true;
+                if (z is IReadOnlyList<Object>) return true;
+                if (z is IReadOnlyDictionary<string, object>) return true;
+                return false;
+            }
+
+            if (!isValidType(x)) throw new ArgumentException($"Invalid type: {x.GetType()}", nameof(x));
+            if (!isValidType(y)) throw new ArgumentException($"Invalid type: {y.GetType()}", nameof(y));
+
+            return false;
+        }
+
+        private static bool AreEqual(IConvertible x, IConvertible y, float precission)
+        {
+            var xc = x.GetTypeCode();
+            var yc = y.GetTypeCode();
+
+            if (xc == TypeCode.Decimal || yc == TypeCode.Decimal)
+            {
+                var xf = y.ToDecimal(CULTURE.InvariantCulture);
+                var yf = y.ToDecimal(CULTURE.InvariantCulture);
+                return Math.Abs((double)(xf - yf)) < precission;
+            }
+
+            if (xc == TypeCode.Double || yc == TypeCode.Double)
+            {
+                var xf = y.ToDouble(CULTURE.InvariantCulture);
+                var yf = y.ToDouble(CULTURE.InvariantCulture);
+                return Math.Abs(xf - yf) < precission;
+            }
+
+            if (xc == TypeCode.Single || yc == TypeCode.Single)
+            {
+                var xf = y.ToSingle(CULTURE.InvariantCulture);
+                var yf = y.ToSingle(CULTURE.InvariantCulture);
+                return Math.Abs(xf - yf) < precission;
+            }
+
+            if (xc == yc) return Object.Equals(x, y);
+
+            return false;
+        }
     }
 
     /// <summary>
@@ -185,8 +271,8 @@ namespace SharpGLTF.IO
 
         public static bool TryCreate(Object value, out _JsonArray obj)
         {
-            if (value is IConvertible _) { obj = default; return false; }
-            if (value is IDictionary _) { obj = default; return false; }
+            if (value is IConvertible) { obj = default; return false; }
+            if (value is IDictionary) { obj = default; return false; }
             if (value is IEnumerable collection) { obj = _From(collection); return true; }
 
             obj = default;
@@ -208,9 +294,19 @@ namespace SharpGLTF.IO
             }
         }
 
-        private static _JsonArray _From(IEnumerable collection) { return new _JsonArray(collection); }
+        private static _JsonArray _From(IEnumerable collection)
+        {
+            return new _JsonArray(TryClone(collection));
+        }
+
+        private _JsonArray(Array array)
+        {
+            _Array = array;
+        }
+
+        public object Clone() { return _From(this); }
 
-        private _JsonArray(IEnumerable collection)
+        private static Array TryClone(IEnumerable collection)
         {
             // 1st pass: determine element type and collection size
 
@@ -231,7 +327,11 @@ namespace SharpGLTF.IO
                     continue;
                 }
 
-                if (!elementType.IsAssignableFrom(item.GetType())) throw new ArgumentException($"{nameof(collection)}[{count}] is invalid type.", nameof(collection));
+                // subsequent types must match elementType.
+                if (!elementType.IsAssignableFrom(item.GetType()))
+                {
+                    throw new ArgumentException($"{nameof(collection)}[{count}] is invalid type.", nameof(collection));
+                }
             }
 
             if (elementType.IsGenericType && elementType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>))
@@ -241,24 +341,26 @@ namespace SharpGLTF.IO
 
             int contentType = 0;
             if (elementType == typeof(IConvertible)) contentType = 1;
-            if (contentType == 0 && elementType == typeof(IDictionary)) contentType = 3;
-            if (contentType == 0 && elementType.IsAssignableFrom(typeof(IEnumerable))) contentType = 2;
+            if (contentType == 0 && typeof(IDictionary).IsAssignableFrom(elementType)) contentType = 3;
+            if (contentType == 0 && typeof(IEnumerable).IsAssignableFrom(elementType)) contentType = 2;
+
+            Array container = null;
 
             switch (contentType)
             {
-                case 1: _Array = Array.CreateInstance(typeof(IConvertible), count); break;
-                case 2: _Array = Array.CreateInstance(typeof(_JsonArray), count); break;
-                case 3: _Array = Array.CreateInstance(typeof(_JsonObject), count); break;
+                case 1: container = Array.CreateInstance(typeof(IConvertible), count); break;
+                case 2: container = Array.CreateInstance(typeof(_JsonArray), count); break;
+                case 3: container = Array.CreateInstance(typeof(_JsonObject), count); break;
                 default: throw new NotImplementedException();
             }
 
             // 2nd pass: convert and assign items.
 
             int idx = 0;
-            foreach (var item in collection) _Array.SetValue(_JsonStaticUtils.Serialize(item), idx++);
-        }
+            foreach (var item in collection) container.SetValue(_JsonStaticUtils.Serialize(item), idx++);
 
-        public object Clone() { return _From(this); }
+            return container;
+        }
 
         #endregion
 

+ 48 - 15
src/SharpGLTF.Core/IO/JsonContent.cs

@@ -12,14 +12,27 @@ namespace SharpGLTF.IO
     /// Represents an inmutable json object stored in memory.
     /// </summary>
     /// <remarks>
-    /// Valid values can be:
-    /// - <see cref="IConvertible"/> for literal values.
-    /// - <see cref="IReadOnlyList{Object}"/> for arrays.
-    /// - <see cref="IReadOnlyDictionary{String, Object}"/> for objects.
+    /// The data structure is stored in memory as a DOM, using standard objects and collections.<br/>
+    /// Use <see cref="Serialize(object, JSONOPTIONS)"/> and <see cref="Deserialize{T}(JSONOPTIONS)"/> to convert to your types.<br/>
+    /// Use <see cref="Parse(JsonDocument)"/> and <see cref="ToJson(JSONOPTIONS)"/> to convert from/to raw json text.<br/>
     /// </remarks>
     [System.ComponentModel.ImmutableObject(true)]
+    [System.Diagnostics.DebuggerDisplay("{ToDebuggerDisplay(),nq}")]
     public readonly struct JsonContent
     {
+        #region debug
+
+        private string ToDebuggerDisplay()
+        {
+            if (_Content == null) return null;
+
+            var options = new JSONOPTIONS();
+            options.WriteIndented = true;
+            return ToJson(options);
+        }
+
+        #endregion
+
         #region constructors
 
         public static implicit operator JsonContent(Boolean value) { return new JsonContent(value); }
@@ -59,25 +72,40 @@ namespace SharpGLTF.IO
 
         #region data
 
-        /// <summary>
-        /// The dynamic json structure, where it can be any of this:
-        /// - A <see cref="IConvertible"/> object.
-        /// - A non empty <see cref="IReadOnlyList{Object}"/> object.
-        /// - A non empty <see cref="IReadOnlyDictionary{String, Object}"/> object.
-        /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly Object _Content;
 
-        // It is tempting to add Equality support, but it's problematic because these reasons:
-        // - It's not clear how to compare in-memory floating point values against deserialized string values.
-        // - Serialization roundtrip is not well supported in older NetFramework versions; this is specially
-        // apparent when using System.Text.JSon in NetCore and Net471, where NetCore is roundtrip safe, and
-        // NetFramework is not.
+        /// <summary>
+        /// Compares two <see cref="JsonContent"/> objects for equality.
+        /// </summary>
+        /// <param name="a">The first object to compare.</param>
+        /// <param name="b">The second object to compare.</param>
+        /// <param name="precission">The precission threshold when comparing floating point values.</param>
+        /// <returns>true if the objects are considered equal</returns>
+        /// <remarks>
+        /// - Comparing json structures is tricky because the values are typeless, so when we parse a json DOM
+        /// into memory we don't know which should be the right type to use for comparison.
+        /// - Also, System.Text.JSon is roundtrip safe when used in Net Core, but it is not when used in
+        /// Net Framework, so depending on the framework we use, floating point roundtrips will behave differently.
+        /// </remarks>
+        public static bool AreEqualByContent(JsonContent a, JsonContent b, float precission)
+        {
+            return _JsonStaticUtils.AreEqualByContent(a._Content, b._Content, precission);
+        }
 
         #endregion
 
         #region properties
 
+        /// <summary>
+        /// Gets the dynamic json structure.
+        /// </summary>
+        /// <remarks>
+        /// The possible value types can be:<br/>
+        /// - An <see cref="IConvertible"/> object.<br/>
+        /// - A non empty <see cref="IReadOnlyList{Object}"/> object.<br/>
+        /// - A non empty <see cref="IReadOnlyDictionary{String, Object}"/> object.
+        /// </remarks>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Collapsed)]
         public Object Content => _Content;
 
@@ -139,6 +167,11 @@ namespace SharpGLTF.IO
             return _JsonStaticUtils.Deserialize(_Content, type, options);
         }
 
+        public T Deserialize<T>(JSONOPTIONS options = null)
+        {
+            return (T)_JsonStaticUtils.Deserialize(_Content, typeof(T), options);
+        }
+
         #endregion
 
         #region static API

+ 1 - 1
src/SharpGLTF.Core/Memory/MemoryAccessor.cs

@@ -20,7 +20,7 @@ namespace SharpGLTF.Memory
 
         internal string _GetDebuggerDisplay()
         {
-            return Debug.DebuggerDisplay.ToReport(this);
+            return Diagnostics.DebuggerDisplay.ToReport(this);
         }
 
         #endregion

+ 26 - 18
src/SharpGLTF.Core/Memory/MemoryImage.cs

@@ -10,9 +10,27 @@ namespace SharpGLTF.Memory
     /// <summary>
     /// Represents an image file stored as an in-memory byte array
     /// </summary>
-    [System.Diagnostics.DebuggerDisplay("{DisplayText,nq}")]
+    [System.Diagnostics.DebuggerDisplay("{ToDebuggerDisplay(),nq}")]
     public readonly struct MemoryImage : IEquatable<MemoryImage>
     {
+        #region debug
+
+        public string ToDebuggerDisplay()
+        {
+            if (!string.IsNullOrWhiteSpace(_SourcePathHint)) return System.IO.Path.GetFileName(_SourcePathHint);
+
+            if (IsEmpty) return "Empty";
+            if (!_IsImage(_Image)) return $"Unknown {_Image.Count}ᴮʸᵗᵉˢ";
+            if (IsJpg) return $"JPG {_Image.Count}ᴮʸᵗᵉˢ";
+            if (IsPng) return $"PNG {_Image.Count}ᴮʸᵗᵉˢ";
+            if (IsDds) return $"DDS {_Image.Count}ᴮʸᵗᵉˢ";
+            if (IsWebp) return $"WEBP {_Image.Count}ᴮʸᵗᵉˢ";
+            if (IsKtx2) return $"KTX2 {_Image.Count}ᴮʸᵗᵉˢ";
+            return "Undefined";
+        }
+
+        #endregion
+
         #region constants
 
         const string EMBEDDED_OCTET_STREAM = "data:application/octet-stream";
@@ -161,8 +179,14 @@ namespace SharpGLTF.Memory
         public ReadOnlyMemory<Byte> Content => _Image;
 
         /// <summary>
-        /// Gets the source path of this image, or null if the image cannot be tracked to a file path (as it is the case of embedded images)
+        /// Gets the source path of this image, or null.<br/>
+        /// ⚠️ DO NOT USE AS AN IMAGE ID ⚠️
         /// </summary>
+        /// <remarks>
+        /// Not all images are expected to have a source path.<br/>
+        /// Specifically images embedded in a GLB file or encoded with BASE64
+        /// will not have any source path at all.
+        /// </remarks>
         public string SourcePath => _SourcePathHint;
 
         /// <summary>
@@ -234,22 +258,6 @@ namespace SharpGLTF.Memory
             }
         }
 
-        public string DisplayText
-        {
-            get {
-                if (!string.IsNullOrWhiteSpace(_SourcePathHint)) return System.IO.Path.GetFileName(_SourcePathHint);
-
-                if (IsEmpty) return "Empty";
-                if (!_IsImage(_Image)) return $"Unknown {_Image.Count}ᴮʸᵗᵉˢ";
-                if (IsJpg) return $"JPG {_Image.Count}ᴮʸᵗᵉˢ";
-                if (IsPng) return $"PNG {_Image.Count}ᴮʸᵗᵉˢ";
-                if (IsDds) return $"DDS {_Image.Count}ᴮʸᵗᵉˢ";
-                if (IsWebp) return $"WEBP {_Image.Count}ᴮʸᵗᵉˢ";
-                if (IsKtx2) return $"KTX2 {_Image.Count}ᴮʸᵗᵉˢ";
-                return "Undefined";
-            }
-        }
-
         #endregion
 
         #region API

+ 3 - 1
src/SharpGLTF.Core/Runtime/AnimationTrackInfo.cs

@@ -6,13 +6,15 @@ namespace SharpGLTF.Runtime
 {
     public class AnimationTrackInfo
     {
-        public AnimationTrackInfo(string name, float duration)
+        internal AnimationTrackInfo(string name, Object extras, float duration)
         {
             Name = name;
+            Extras = extras;
             Duration = duration;
         }
 
         public string Name { get; private set; }
+        public Object Extras { get; private set; }
         public float Duration { get; private set; }
     }
 }

+ 6 - 6
src/SharpGLTF.Core/Runtime/ArmatureTemplate.cs

@@ -11,8 +11,8 @@ namespace SharpGLTF.Runtime
     /// /// <remarks>
     /// Only the nodes used by a given <see cref="Schema2.Scene"/> will be copied.
     /// Also, nodes will be reordered so children nodes always come after their parents (for fast evaluation),
-    /// so it's important to keek in mind that <see cref="NodeTemplate"/> indices will differ from those
-    /// in <see cref="Schema2.Scene"/>.
+    /// so it's important to keep in mind that <see cref="NodeTemplate"/> indices will differ from those
+    /// in <see cref="Schema2.Scene.VisualChildren"/>.
     /// </remarks>
     class ArmatureTemplate
     {
@@ -22,9 +22,9 @@ namespace SharpGLTF.Runtime
         /// Creates a new <see cref="ArmatureTemplate"/> based on the nodes of <see cref="Schema2.Scene"/>.
         /// </summary>
         /// <param name="srcScene">The source <see cref="Schema2.Scene"/> from where to take the nodes.</param>
-        /// <param name="isolateMemory">True if we want to copy the source data instead of share it.</param>
+        /// <param name="options">Custom processing options, or null.</param>
         /// <returns>A new <see cref="ArmatureTemplate"/> instance.</returns>
-        public static ArmatureTemplate Create(Schema2.Scene srcScene, bool isolateMemory)
+        internal static ArmatureTemplate Create(Schema2.Scene srcScene, RuntimeOptions options)
         {
             Guard.NotNull(srcScene, nameof(srcScene));
 
@@ -56,14 +56,14 @@ namespace SharpGLTF.Runtime
                     .Select(n => indexSolver(n))
                     .ToArray();
 
-                dstNodes[nidx] = new NodeTemplate(srcNode.Key, pidx, cidx, isolateMemory);
+                dstNodes[nidx] = new NodeTemplate(srcNode.Key, pidx, cidx, options);
             }
 
             // gather animation durations.
 
             var dstTracks = srcScene.LogicalParent
                 .LogicalAnimations
-                .Select(item => new AnimationTrackInfo(item.Name, item.Duration))
+                .Select(item => new AnimationTrackInfo(item.Name, RuntimeOptions.ConvertExtras(item, options), item.Duration))
                 .ToArray();
 
             return new ArmatureTemplate(dstNodes, dstTracks);

+ 5 - 6
src/SharpGLTF.Core/Runtime/MeshDecoder.Schema2.cs

@@ -19,11 +19,12 @@ namespace SharpGLTF.Runtime
     {
         #region lifecycle
 
-        public _MeshDecoder(Schema2.Mesh srcMesh)
+        public _MeshDecoder(Schema2.Mesh srcMesh, RuntimeOptions options)
         {
             Guard.NotNull(srcMesh, nameof(srcMesh));
 
             _Name = srcMesh.Name;
+            _Extras = RuntimeOptions.ConvertExtras(srcMesh, options);
 
             _LogicalIndex = srcMesh.LogicalIndex;
 
@@ -31,8 +32,6 @@ namespace SharpGLTF.Runtime
                 .Primitives
                 .Select(item => new _MeshPrimitiveDecoder<TMaterial>(item))
                 .ToArray();
-
-            _Extras = srcMesh.Extras;
         }
 
         #endregion
@@ -40,19 +39,19 @@ namespace SharpGLTF.Runtime
         #region data
 
         private readonly string _Name;
+        private readonly Object _Extras;
+
         private readonly int _LogicalIndex;
         private readonly _MeshPrimitiveDecoder<TMaterial>[] _Primitives;
 
-        private readonly Object _Extras;
-
         #endregion
 
         #region properties
 
         public string Name => _Name;
+        public Object Extras => _Extras;
         public int LogicalIndex => _LogicalIndex;
         public IReadOnlyList<IMeshPrimitiveDecoder<TMaterial>> Primitives => _Primitives;
-        public Object Extras => _Extras;
 
         #endregion
 

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

@@ -13,9 +13,9 @@ namespace SharpGLTF.Runtime
         where TMaterial : class
     {
         string Name { get; }
+        Object Extras { get; }
         int LogicalIndex { get; }
         IReadOnlyList<IMeshPrimitiveDecoder<TMaterial>> Primitives { get; }
-        Object Extras { get; }
     }
 
     public interface IMeshPrimitiveDecoder
@@ -96,21 +96,21 @@ namespace SharpGLTF.Runtime
     /// </summary>
     public static class MeshDecoder
     {
-        public static IMeshDecoder<Schema2.Material> Decode(this Schema2.Mesh mesh)
+        public static IMeshDecoder<Schema2.Material> Decode(this Schema2.Mesh mesh, RuntimeOptions options = null)
         {
             if (mesh == null) return null;
 
-            var meshDecoder = new _MeshDecoder<Schema2.Material>(mesh);
+            var meshDecoder = new _MeshDecoder<Schema2.Material>(mesh, options);
 
             meshDecoder.GenerateNormalsAndTangents();
 
             return meshDecoder;
         }
 
-        public static IMeshDecoder<Schema2.Material>[] Decode(this IReadOnlyList<Schema2.Mesh> meshes)
+        public static IMeshDecoder<Schema2.Material>[] Decode(this IReadOnlyList<Schema2.Mesh> meshes, RuntimeOptions options = null)
         {
             Guard.NotNull(meshes, nameof(meshes));
-            return meshes.Select(item => item.Decode()).ToArray();
+            return meshes.Select(item => item.Decode(options)).ToArray();
         }
 
         public static XYZ GetPosition(this IMeshPrimitiveDecoder primitive, int idx, Transforms.IGeometryTransform xform)
@@ -157,7 +157,7 @@ namespace SharpGLTF.Runtime
             Guard.NotNull(scene, nameof(scene));
 
             var decodedMeshes = scene.LogicalParent.LogicalMeshes.Decode();
-            var sceneTemplate = SceneTemplate.Create(scene, false);
+            var sceneTemplate = SceneTemplate.Create(scene);
             var sceneInstance = sceneTemplate.CreateInstance();
             var armatureInst = sceneInstance.Armature;
 
@@ -192,7 +192,7 @@ namespace SharpGLTF.Runtime
             Guard.NotNull(scene, nameof(scene));
 
             var decodedMeshes = scene.LogicalParent.LogicalMeshes.Decode();
-            var sceneTemplate = SceneTemplate.Create(scene, false);
+            var sceneTemplate = SceneTemplate.Create(scene);
             var sceneInstance = sceneTemplate.CreateInstance();
             var armatureInst = sceneInstance.Armature;
 

+ 2 - 0
src/SharpGLTF.Core/Runtime/NodeInstance.cs

@@ -40,6 +40,8 @@ namespace SharpGLTF.Runtime
 
         public String Name => _Template.Name;
 
+        public Object Extras => _Template.Extras;
+
         public NodeInstance VisualParent => _Parent;
 
         public SparseWeight8 MorphWeights

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

@@ -14,7 +14,7 @@ namespace SharpGLTF.Runtime
     {
         #region lifecycle
 
-        internal NodeTemplate(Schema2.Node srcNode, int parentIdx, int[] childIndices, bool isolateMemory)
+        internal NodeTemplate(Schema2.Node srcNode, int parentIdx, int[] childIndices, RuntimeOptions options)
         {
             _LogicalSourceIndex = srcNode.LogicalIndex;
 
@@ -22,6 +22,7 @@ namespace SharpGLTF.Runtime
             _ChildIndices = childIndices;
 
             Name = srcNode.Name;
+            Extras = RuntimeOptions.ConvertExtras(srcNode, options);
 
             _LocalMatrix = srcNode.LocalMatrix;
             _LocalTransform = srcNode.LocalTransform;
@@ -33,6 +34,8 @@ namespace SharpGLTF.Runtime
             var mw = Transforms.SparseWeight8.Create(srcNode.MorphWeights);
             _Morphing = new AnimatableProperty<Transforms.SparseWeight8>(mw);
 
+            var isolateMemory = options?.IsolateMemory ?? false;
+
             foreach (var anim in srcNode.LogicalParent.LogicalAnimations)
             {
                 var index = anim.LogicalIndex;
@@ -85,6 +88,8 @@ namespace SharpGLTF.Runtime
 
         public string Name { get; set; }
 
+        public Object Extras { get; set; }
+
         /// <summary>
         /// Gets the index of the source <see cref="Schema2.Node"/> in <see cref="Schema2.ModelRoot.LogicalNodes"/>
         /// </summary>

+ 28 - 0
src/SharpGLTF.Core/Runtime/RuntimeOptions.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF.Runtime
+{
+    public class RuntimeOptions
+    {
+        /// <summary>
+        /// True if we want to copy buffers data instead of sharing it.
+        /// </summary>
+        public bool IsolateMemory { get; set; }
+
+        /// <summary>
+        /// Custom extras converter.
+        /// </summary>
+        public Converter<Schema2.ExtraProperties, Object> ExtrasConverterCallback { get; set; }
+
+        internal static Object ConvertExtras(Schema2.ExtraProperties source, RuntimeOptions options)
+        {
+            if (source.Extras.Content == null) return null;
+
+            var callback = options?.ExtrasConverterCallback;
+
+            return callback != null ? callback(source) : source.Extras.DeepClone();
+        }
+    }
+}

+ 29 - 3
src/SharpGLTF.Core/Runtime/SceneTemplate.cs

@@ -21,11 +21,31 @@ namespace SharpGLTF.Runtime
         /// <param name="srcScene">The source <see cref="Schema2.Scene"/> to templateize.</param>
         /// <param name="isolateMemory">True if we want to copy data instead of sharing it.</param>
         /// <returns>A new <see cref="SceneTemplate"/> instance.</returns>
+        [Obsolete("Use Create(Schema2.Scene srcScene, RuntimeOptions options)")]
         public static SceneTemplate Create(Schema2.Scene srcScene, bool isolateMemory)
+        {
+            RuntimeOptions options = null;
+
+            if (isolateMemory)
+            {
+                options = new RuntimeOptions();
+                options.IsolateMemory = true;
+            }
+
+            return Create(srcScene, options);
+        }
+
+        /// <summary>
+        /// Creates a new <see cref="SceneTemplate"/> from a given <see cref="Schema2.Scene"/>.
+        /// </summary>
+        /// <param name="srcScene">The source <see cref="Schema2.Scene"/> to templateize.</param>
+        /// <param name="options">Custom processing options, or null.</param>
+        /// <returns>A new <see cref="SceneTemplate"/> instance.</returns>
+        public static SceneTemplate Create(Schema2.Scene srcScene, RuntimeOptions options = null)
         {
             Guard.NotNull(srcScene, nameof(srcScene));
 
-            var armature = ArmatureTemplate.Create(srcScene, isolateMemory);
+            var armature = ArmatureTemplate.Create(srcScene, options);
 
             // gather scene nodes.
 
@@ -58,12 +78,15 @@ namespace SharpGLTF.Runtime
                     (DrawableTemplate)new RigidDrawableTemplate(srcInstance, indexSolver);
             }
 
-            return new SceneTemplate(srcScene.Name, armature, drawables);
+            var extras = RuntimeOptions.ConvertExtras(srcScene, options);
+
+            return new SceneTemplate(srcScene.Name, extras, armature, drawables);
         }
 
-        private SceneTemplate(string name, ArmatureTemplate armature, DrawableTemplate[] drawables)
+        private SceneTemplate(string name, Object extras, ArmatureTemplate armature, DrawableTemplate[] drawables)
         {
             _Name = name;
+            _Extras = extras;
             _Armature = armature;
             _DrawableReferences = drawables;
         }
@@ -73,6 +96,7 @@ namespace SharpGLTF.Runtime
         #region data
 
         private readonly String _Name;
+        private readonly Object _Extras;
         private readonly ArmatureTemplate _Armature;
         private readonly DrawableTemplate[] _DrawableReferences;
 
@@ -82,6 +106,8 @@ namespace SharpGLTF.Runtime
 
         public String Name => _Name;
 
+        public Object Extras => _Extras;
+
         /// <summary>
         /// Gets the unique indices of <see cref="Schema2.Mesh"/> instances in <see cref="Schema2.ModelRoot.LogicalMeshes"/>
         /// </summary>

+ 3 - 3
src/SharpGLTF.Core/Schema2/Generated/ext.ModelLightsPunctual.g.cs

@@ -32,12 +32,12 @@ namespace SharpGLTF.Schema2
 	
 		private const Double _innerConeAngleDefault = 0;
 		private const Double _innerConeAngleMinimum = 0;
-		private const Double _innerConeAngleMaximum = 1.5707963267949;
+		private const Double _innerConeAngleMaximum = 1.5707963267948966;
 		private Double? _innerConeAngle = _innerConeAngleDefault;
 		
-		private const Double _outerConeAngleDefault = 0.785398163397448;
+		private const Double _outerConeAngleDefault = 0.7853981633974483;
 		private const Double _outerConeAngleMinimum = 0;
-		private const Double _outerConeAngleMaximum = 1.5707963267949;
+		private const Double _outerConeAngleMaximum = 1.5707963267948966;
 		private Double? _outerConeAngle = _outerConeAngleDefault;
 		
 	

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

@@ -12,14 +12,14 @@ namespace SharpGLTF.Schema2
     // https://github.com/KhronosGroup/glTF/issues/827#issuecomment-277537204
 
     [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
-    [System.Diagnostics.DebuggerTypeProxy(typeof(Debug._AccessorDebugProxy))]
+    [System.Diagnostics.DebuggerTypeProxy(typeof(Diagnostics._AccessorDebugProxy))]
     public sealed partial class Accessor
     {
         #region debug
 
         internal string _GetDebuggerDisplay()
         {
-            return Debug.DebuggerDisplay.ToReportLong(this);
+            return Diagnostics.DebuggerDisplay.ToReportLong(this);
         }
 
         #endregion

+ 2 - 2
src/SharpGLTF.Core/Schema2/gltf.BufferView.cs

@@ -6,7 +6,7 @@ using BYTES = System.ArraySegment<byte>;
 
 namespace SharpGLTF.Schema2
 {
-    [System.Diagnostics.DebuggerTypeProxy(typeof(Debug._BufferViewDebugProxy))]
+    [System.Diagnostics.DebuggerTypeProxy(typeof(Diagnostics._BufferViewDebugProxy))]
     [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
     public sealed partial class BufferView
     {
@@ -14,7 +14,7 @@ namespace SharpGLTF.Schema2
 
         internal string _GetDebuggerDisplay()
         {
-            return Debug.DebuggerDisplay.ToReport(this);
+            return Diagnostics.DebuggerDisplay.ToReport(this);
         }
 
         #endregion

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

@@ -43,10 +43,7 @@ namespace SharpGLTF.Schema2
         public IReadOnlyCollection<JsonSerializable> Extensions => _extensions;
 
         /// <summary>
-        /// Gets the extras of this instance, where the value can be
-        /// an <see cref="IConvertible"/>,
-        /// a <see cref="IReadOnlyList{Object}"/> or
-        /// a <see cref="IReadOnlyDictionary{String, Object}"/>.
+        /// Gets or sets the extras content of this instance.
         /// </summary>
         public IO.JsonContent Extras
         {

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

@@ -14,7 +14,7 @@ namespace SharpGLTF.Schema2
 
         internal string _DebuggerDisplay()
         {
-            return $"Image[{LogicalIndex}] {Name} = {Content.DisplayText}";
+            return $"Image[{LogicalIndex}] {Name} = {Content.ToDebuggerDisplay()}";
         }
 
         #endregion

+ 9 - 0
src/SharpGLTF.Core/Schema2/gltf.LogicalChildOfRoot.cs

@@ -13,6 +13,15 @@ namespace SharpGLTF.Schema2
     {
         #region properties
 
+        /// <summary>
+        /// Display text name, or null.<br/>⚠️ DO NOT USE AS AN OBJECT ID ⚠️
+        /// </summary>
+        /// <remarks>
+        /// glTF does not define any name ruling for object names.
+        /// This means that names can be null or non unique.
+        /// So don't use names for anything other than object name display.
+        /// Use lookup tables instead.
+        /// </remarks>
         public String Name
         {
             get => _name;

+ 14 - 2
src/SharpGLTF.Core/Schema2/gltf.MaterialChannel.cs

@@ -133,7 +133,7 @@ namespace SharpGLTF.Schema2
             return _Material.LogicalParent.LogicalTextures[texInfo._LogicalTextureIndex];
         }
 
-        public void SetTexture(
+        public Texture SetTexture(
             int texCoord,
             Image primaryImg,
             Image fallbackImg = null,
@@ -142,7 +142,7 @@ namespace SharpGLTF.Schema2
             TextureMipMapFilter min = TextureMipMapFilter.DEFAULT,
             TextureInterpolationFilter mag = TextureInterpolationFilter.DEFAULT)
         {
-            if (primaryImg == null) return; // in theory, we should completely remove the TextureInfo
+            if (primaryImg == null) return null; // in theory, we should completely remove the TextureInfo
 
             Guard.NotNull(_Material, nameof(_Material));
 
@@ -150,6 +150,8 @@ namespace SharpGLTF.Schema2
             var texture = _Material.LogicalParent.UseTexture(primaryImg, fallbackImg, sampler);
 
             SetTexture(texCoord, texture);
+
+            return texture;
         }
 
         public void SetTexture(int texSet, Texture tex)
@@ -165,6 +167,16 @@ namespace SharpGLTF.Schema2
             texInfo._LogicalTextureIndex = tex.LogicalIndex;
         }
 
+        private Texture TryGetTexture()
+        {
+            if (_TextureInfo == null) throw new InvalidOperationException();
+
+            var texInfo = _TextureInfo(false);
+            if (texInfo == null) return null;
+            if (texInfo._LogicalTextureIndex < 0) return null;
+            return _Material.LogicalParent.LogicalTextures[texInfo._LogicalTextureIndex];
+        }
+
         public void SetTransform(Vector2 offset, Vector2 scale, float rotation = 0, int? texCoordOverride = null)
         {
             if (_TextureInfo == null) throw new InvalidOperationException();

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

@@ -7,7 +7,7 @@ using SharpGLTF.Collections;
 namespace SharpGLTF.Schema2
 {
     [System.Diagnostics.DebuggerDisplay("{_DebuggerDisplay(),nq}")]
-    [System.Diagnostics.DebuggerTypeProxy(typeof(Debug._MeshDebugProxy))]
+    [System.Diagnostics.DebuggerTypeProxy(typeof(Diagnostics._MeshDebugProxy))]
     public sealed partial class Mesh
     {
         #region debug

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

@@ -15,7 +15,7 @@ namespace SharpGLTF.Schema2
         {
             var txt = $"Primitive[{this.LogicalIndex}]";
 
-            return Debug.DebuggerDisplay.ToReport(this, txt);
+            return Diagnostics.DebuggerDisplay.ToReport(this, txt);
         }
 
         #endregion

+ 1 - 4
src/SharpGLTF.Core/Schema2/gltf.TextureInfo.cs

@@ -23,10 +23,7 @@ namespace SharpGLTF.Schema2
             set => _texCoord = value.AsNullable(_texCoordDefault, _texCoordMinimum, int.MaxValue);
         }
 
-        public TextureTransform Transform
-        {
-            get => this.GetExtension<TextureTransform>();
-        }
+        public TextureTransform Transform => this.GetExtension<TextureTransform>();
 
         #endregion
 

+ 1 - 1
src/SharpGLTF.Core/SharpGLTF.Core.csproj

@@ -19,7 +19,7 @@
   <Import Project="..\Testing.props" />
   
   <ItemGroup>
-    <Compile Include="..\Shared\Guard.cs" Link="Debug\Guard.cs" />
+    <Compile Include="..\Shared\Guard.cs" Link="Diagnostics\Guard.cs" />
     <Compile Include="..\Shared\_Extensions.cs" Link="_Extensions.cs" />
   </ItemGroup>  
   

+ 1 - 1
src/SharpGLTF.Core/Transforms/Matrix4x4Double.cs

@@ -12,7 +12,7 @@ namespace SharpGLTF.Transforms
     // stripped from https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.cs
 
     [StructLayout(LayoutKind.Sequential)]
-    [DebuggerTypeProxy(typeof(Debug._Matrix4x4DoubleProxy))]
+    [DebuggerTypeProxy(typeof(Diagnostics._Matrix4x4DoubleProxy))]
     public struct Matrix4x4Double : IEquatable<Matrix4x4Double>
     {
         #region constants

+ 3 - 3
src/SharpGLTF.Toolkit/Animations/CurveFactory.cs

@@ -30,7 +30,7 @@ namespace SharpGLTF.Animations
         }
     }
 
-    [System.Diagnostics.DebuggerTypeProxy(typeof(Debug._CurveBuilderDebugProxyVector3))]
+    [System.Diagnostics.DebuggerTypeProxy(typeof(Diagnostics._CurveBuilderDebugProxyVector3))]
     sealed class Vector3CurveBuilder : CurveBuilder<Vector3>, ICurveSampler<Vector3>
     {
         #region lifecycle
@@ -85,7 +85,7 @@ namespace SharpGLTF.Animations
         #endregion
     }
 
-    [System.Diagnostics.DebuggerTypeProxy(typeof(Debug._CurveBuilderDebugProxyQuaternion))]
+    [System.Diagnostics.DebuggerTypeProxy(typeof(Diagnostics._CurveBuilderDebugProxyQuaternion))]
     sealed class QuaternionCurveBuilder : CurveBuilder<Quaternion>, ICurveSampler<Quaternion>
     {
         #region lifecycle
@@ -141,7 +141,7 @@ namespace SharpGLTF.Animations
         #endregion
     }
 
-    [System.Diagnostics.DebuggerTypeProxy(typeof(Debug._CurveBuilderDebugProxySparse))]
+    [System.Diagnostics.DebuggerTypeProxy(typeof(Diagnostics._CurveBuilderDebugProxySparse))]
     sealed class SparseCurveBuilder : CurveBuilder<SPARSE>, ICurveSampler<SPARSE>
     {
         #region lifecycle

+ 89 - 0
src/SharpGLTF.Toolkit/BaseBuilder.cs

@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF
+{
+    public abstract class BaseBuilder
+    {
+        #region lifecycle
+
+        public BaseBuilder() { }
+
+        public BaseBuilder(string name)
+        {
+            this.Name = name;
+        }
+
+        public BaseBuilder(string name, IO.JsonContent extras)
+        {
+            this.Name = name;
+            this.Extras = extras;
+        }
+
+        public BaseBuilder(BaseBuilder other)
+        {
+            this.Name = other.Name;
+            this.Extras = other.Extras.DeepClone();
+        }
+
+        #endregion
+
+        #region data
+
+        /// <summary>
+        /// Display text name, or null.<br/>⚠️ DO NOT USE AS AN OBJECT ID ⚠️
+        /// </summary>
+        /// <remarks>
+        /// glTF does not define any name ruling for object names.
+        /// This means that names can be null or non unique.
+        /// So don't use names for anything other than object name display.
+        /// Use lookup tables instead.
+        /// </remarks>
+        public string Name { get; set; }
+
+        public IO.JsonContent Extras { get; set; }
+
+        protected static int GetContentHashCode(BaseBuilder x)
+        {
+            return x?.Name?.GetHashCode() ?? 0;
+        }
+
+        protected static bool AreEqualByContent(BaseBuilder x, BaseBuilder y)
+        {
+            if ((x, y).AreSameReference(out bool areTheSame)) return areTheSame;
+
+            if (x.Name != y.Name) return false;
+
+            return IO.JsonContent.AreEqualByContent(x.Extras, y.Extras, 0.0001f);
+        }
+
+        #endregion
+
+        #region API
+
+        internal void SetNameAndExtrasFrom(BaseBuilder source)
+        {
+            this.Name = source.Name;
+            this.Extras = source.Extras.DeepClone();
+        }
+
+        internal void SetNameAndExtrasFrom(Schema2.LogicalChildOfRoot source)
+        {
+            this.Name = source.Name;
+            this.Extras = source.Extras.DeepClone();
+        }
+
+        /// <summary>
+        /// Copies the Name and Extras values to <paramref name="target"/> only if the values are defined.
+        /// </summary>
+        /// <param name="target">The target object</param>
+        internal void TryCopyNameAndExtrasTo(Schema2.LogicalChildOfRoot target)
+        {
+            if (this.Name != null) target.Name = this.Name;
+            if (this.Extras.Content != null) target.Extras = this.Extras.DeepClone();
+        }
+
+        #endregion
+    }
+}

+ 117 - 117
src/SharpGLTF.Toolkit/Debug/DebugViews.cs → src/SharpGLTF.Toolkit/Diagnostics/DebugViews.cs

@@ -1,118 +1,118 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Numerics;
-using System.Text;
-
-namespace SharpGLTF.Debug
-{
-    internal abstract class _CurveBuilderDebugProxy<T>
-        where T : struct
-    {
-        #region lifecycle
-
-        public _CurveBuilderDebugProxy(Animations.CurveBuilder<T> curve)
-        {
-            _Curve = curve;
-            _CreateItems(curve);
-        }
-
-        private void _CreateItems(Animations.CurveBuilder<T> curve)
-        {
-            Animations._CurveNode<T>? prev = null;
-
-            foreach (var kvp in curve._DebugKeys)
-            {
-                if (prev.HasValue)
-                {
-                    var d = prev.Value.Degree;
-
-                    switch (d)
-                    {
-                        case 0:
-
-                            break;
-
-                        case 1:
-                            _Items.Add(new _OutTangent { Tangent = GetTangent(prev.Value.Point, kvp.Value.Point) });
-                            break;
-
-                        case 3:
-                            _Items.Add(new _OutTangent { Tangent = prev.Value.OutgoingTangent });
-                            _Items.Add(new _InTangent { Tangent = kvp.Value.IncomingTangent });
-                            break;
-
-                        default:
-                            _Items.Add("ERROR: {d}");
-                            break;
-                    }
-                }
-
-                _Items.Add(new _Point { Key = kvp.Key, Point = kvp.Value.Point });
-
-                prev = kvp.Value;
-            }
-        }
-
-        #endregion
-
-        #region data
-
-        [System.Diagnostics.DebuggerDisplay("{Key} => {Point}")]
-        private struct _Point
-        {
-            public float Key;
-            public T Point;
-        }
-
-        [System.Diagnostics.DebuggerDisplay("               🡖 {Tangent}")]
-        private struct _OutTangent { public T Tangent; }
-
-        [System.Diagnostics.DebuggerDisplay("               🡗 {Tangent}")]
-        private struct _InTangent { public T Tangent; }
-
-        private readonly Animations.CurveBuilder<T> _Curve;
-        private readonly List<Object> _Items = new List<object>();
-
-        #endregion
-
-        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
-        public Object[] Items => _Items.ToArray();
-
-        protected abstract T GetTangent(T a, T b);
-    }
-
-    sealed class _CurveBuilderDebugProxyVector3 : _CurveBuilderDebugProxy<Vector3>
-    {
-        public _CurveBuilderDebugProxyVector3(Animations.CurveBuilder<Vector3> curve)
-            : base(curve) { }
-
-        protected override Vector3 GetTangent(Vector3 a, Vector3 b)
-        {
-            return b - a;
-        }
-    }
-
-    sealed class _CurveBuilderDebugProxyQuaternion : _CurveBuilderDebugProxy<Quaternion>
-    {
-        public _CurveBuilderDebugProxyQuaternion(Animations.CurveBuilder<Quaternion> curve)
-            : base(curve) { }
-
-        protected override Quaternion GetTangent(Quaternion a, Quaternion b)
-        {
-            return Animations.CurveSampler.CreateTangent(a, b);
-        }
-    }
-
-    sealed class _CurveBuilderDebugProxySparse : _CurveBuilderDebugProxy<Transforms.SparseWeight8>
-    {
-        public _CurveBuilderDebugProxySparse(Animations.CurveBuilder<Transforms.SparseWeight8> curve)
-            : base(curve) { }
-
-        protected override Transforms.SparseWeight8 GetTangent(Transforms.SparseWeight8 a, Transforms.SparseWeight8 b)
-        {
-            return Transforms.SparseWeight8.Subtract(b, a);
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.Diagnostics
+{
+    internal abstract class _CurveBuilderDebugProxy<T>
+        where T : struct
+    {
+        #region lifecycle
+
+        public _CurveBuilderDebugProxy(Animations.CurveBuilder<T> curve)
+        {
+            _Curve = curve;
+            _CreateItems(curve);
+        }
+
+        private void _CreateItems(Animations.CurveBuilder<T> curve)
+        {
+            Animations._CurveNode<T>? prev = null;
+
+            foreach (var kvp in curve._DebugKeys)
+            {
+                if (prev.HasValue)
+                {
+                    var d = prev.Value.Degree;
+
+                    switch (d)
+                    {
+                        case 0:
+
+                            break;
+
+                        case 1:
+                            _Items.Add(new _OutTangent { Tangent = GetTangent(prev.Value.Point, kvp.Value.Point) });
+                            break;
+
+                        case 3:
+                            _Items.Add(new _OutTangent { Tangent = prev.Value.OutgoingTangent });
+                            _Items.Add(new _InTangent { Tangent = kvp.Value.IncomingTangent });
+                            break;
+
+                        default:
+                            _Items.Add("ERROR: {d}");
+                            break;
+                    }
+                }
+
+                _Items.Add(new _Point { Key = kvp.Key, Point = kvp.Value.Point });
+
+                prev = kvp.Value;
+            }
+        }
+
+        #endregion
+
+        #region data
+
+        [System.Diagnostics.DebuggerDisplay("{Key} => {Point}")]
+        private struct _Point
+        {
+            public float Key;
+            public T Point;
+        }
+
+        [System.Diagnostics.DebuggerDisplay("               🡖 {Tangent}")]
+        private struct _OutTangent { public T Tangent; }
+
+        [System.Diagnostics.DebuggerDisplay("               🡗 {Tangent}")]
+        private struct _InTangent { public T Tangent; }
+
+        private readonly Animations.CurveBuilder<T> _Curve;
+        private readonly List<Object> _Items = new List<object>();
+
+        #endregion
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+        public Object[] Items => _Items.ToArray();
+
+        protected abstract T GetTangent(T a, T b);
+    }
+
+    sealed class _CurveBuilderDebugProxyVector3 : _CurveBuilderDebugProxy<Vector3>
+    {
+        public _CurveBuilderDebugProxyVector3(Animations.CurveBuilder<Vector3> curve)
+            : base(curve) { }
+
+        protected override Vector3 GetTangent(Vector3 a, Vector3 b)
+        {
+            return b - a;
+        }
+    }
+
+    sealed class _CurveBuilderDebugProxyQuaternion : _CurveBuilderDebugProxy<Quaternion>
+    {
+        public _CurveBuilderDebugProxyQuaternion(Animations.CurveBuilder<Quaternion> curve)
+            : base(curve) { }
+
+        protected override Quaternion GetTangent(Quaternion a, Quaternion b)
+        {
+            return Animations.CurveSampler.CreateTangent(a, b);
+        }
+    }
+
+    sealed class _CurveBuilderDebugProxySparse : _CurveBuilderDebugProxy<Transforms.SparseWeight8>
+    {
+        public _CurveBuilderDebugProxySparse(Animations.CurveBuilder<Transforms.SparseWeight8> curve)
+            : base(curve) { }
+
+        protected override Transforms.SparseWeight8 GetTangent(Transforms.SparseWeight8 a, Transforms.SparseWeight8 b)
+        {
+            return Transforms.SparseWeight8.Subtract(b, a);
+        }
+    }
 }

+ 57 - 63
src/SharpGLTF.Toolkit/Geometry/MeshBuilder.cs

@@ -14,30 +14,30 @@ namespace SharpGLTF.Geometry
     /// </summary>
     /// <typeparam name="TMaterial">The material type used by this <see cref="PrimitiveBuilder{TMaterial, TvP, TvM, TvS}"/> instance.</typeparam>
     /// <typeparam name="TvG">
-    /// The vertex fragment type with Position, Normal and Tangent.
-    /// Valid types are:
-    /// <see cref="VertexPosition"/>,
-    /// <see cref="VertexPositionNormal"/>,
-    /// <see cref="VertexPositionNormalTangent"/>.
+    /// 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/>
     /// </typeparam>
     /// <typeparam name="TvM">
-    /// The vertex fragment type with Colors and Texture Coordinates.
-    /// Valid types are:
-    /// <see cref="VertexEmpty"/>,
-    /// <see cref="VertexColor1"/>,
-    /// <see cref="VertexTexture1"/>,
-    /// <see cref="VertexColor1Texture1"/>.
-    /// <see cref="VertexColor1Texture2"/>.
-    /// <see cref="VertexColor2Texture2"/>.
+    /// 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="VertexColor2Texture2"/>.<br/>
     /// </typeparam>
     /// <typeparam name="TvS">
-    /// The vertex fragment type with Skin Joint Weights.
-    /// Valid types are:
-    /// <see cref="VertexEmpty"/>,
-    /// <see cref="VertexJoints4"/>,
-    /// <see cref="VertexJoints8"/>.
+    /// The vertex fragment type with Skin Joint Weights.<br/>
+    /// Valid types are:<br/>
+    /// - <see cref="VertexEmpty"/>,<br/>
+    /// - <see cref="VertexJoints4"/>,<br/>
+    /// - <see cref="VertexJoints8"/>.<br/>
     /// </typeparam>
-    public class MeshBuilder<TMaterial, TvG, TvM, TvS> : IMeshBuilder<TMaterial>
+    public class MeshBuilder<TMaterial, TvG, TvM, TvS> : BaseBuilder, IMeshBuilder<TMaterial>
         where TvG : struct, IVertexGeometry
         where TvM : struct, IVertexMaterial
         where TvS : struct, IVertexSkinning
@@ -45,9 +45,8 @@ namespace SharpGLTF.Geometry
         #region lifecycle
 
         public MeshBuilder(string name = null)
+            : base(name)
         {
-            this.Name = name;
-
             // this is the recomended preprocesor for release/production
             _VertexPreprocessor = new VertexPreprocessor<TvG, TvM, TvS>();
             _VertexPreprocessor.SetSanitizerPreprocessors();
@@ -64,11 +63,10 @@ namespace SharpGLTF.Geometry
         }
 
         private MeshBuilder(MeshBuilder<TMaterial, TvG, TvM, TvS> other, Func<TMaterial, TMaterial> materialCloneCallback = null)
+            : base(other)
         {
             Guard.NotNull(other, nameof(other));
 
-            this.Name = other.Name;
-            this.Extras = other.Extras.DeepClone();
             this._VertexPreprocessor = other._VertexPreprocessor;
 
             foreach (var kvp in other._Primitives)
@@ -108,10 +106,6 @@ namespace SharpGLTF.Geometry
 
         #region properties
 
-        public string Name { get; set; }
-
-        public IO.JsonContent Extras { get; set; }
-
         public VertexPreprocessor<TvG, TvM, TvS> VertexPreprocessor
         {
             get => _VertexPreprocessor;
@@ -242,28 +236,28 @@ namespace SharpGLTF.Geometry
     /// Represents an utility class to help build meshes by adding primitives associated with a given material.
     /// </summary>
     /// <typeparam name="TvG">
-    /// The vertex fragment type with Position, Normal and Tangent.
-    /// Valid types are:
-    /// <see cref="VertexPosition"/>,
-    /// <see cref="VertexPositionNormal"/>,
-    /// <see cref="VertexPositionNormalTangent"/>.
+    /// 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/>
     /// </typeparam>
     /// <typeparam name="TvM">
-    /// The vertex fragment type with Colors and Texture Coordinates.
-    /// Valid types are:
-    /// <see cref="VertexEmpty"/>,
-    /// <see cref="VertexColor1"/>,
-    /// <see cref="VertexTexture1"/>,
-    /// <see cref="VertexColor1Texture1"/>.
-    /// <see cref="VertexColor1Texture2"/>.
-    /// <see cref="VertexColor2Texture2"/>.
+    /// 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="VertexColor2Texture2"/>.<br/>
     /// </typeparam>
     /// <typeparam name="TvS">
-    /// The vertex fragment type with Skin Joint Weights.
-    /// Valid types are:
-    /// <see cref="VertexEmpty"/>,
-    /// <see cref="VertexJoints4"/>,
-    /// <see cref="VertexJoints8"/>.
+    /// The vertex fragment type with Skin Joint Weights.<br/>
+    /// Valid types are:<br/>
+    /// - <see cref="VertexEmpty"/>,<br/>
+    /// - <see cref="VertexJoints4"/>,<br/>
+    /// - <see cref="VertexJoints8"/>.<br/>
     /// </typeparam>
     public class MeshBuilder<TvG, TvM, TvS> : MeshBuilder<Materials.MaterialBuilder, TvG, TvM, TvS>
         where TvG : struct, IVertexGeometry
@@ -278,21 +272,21 @@ namespace SharpGLTF.Geometry
     /// Represents an utility class to help build meshes by adding primitives associated with a given material.
     /// </summary>
     /// <typeparam name="TvG">
-    /// The vertex fragment type with Position, Normal and Tangent.
-    /// Valid types are:
-    /// <see cref="VertexPosition"/>,
-    /// <see cref="VertexPositionNormal"/>,
-    /// <see cref="VertexPositionNormalTangent"/>.
+    /// 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/>
     /// </typeparam>
     /// <typeparam name="TvM">
-    /// The vertex fragment type with Colors and Texture Coordinates.
-    /// Valid types are:
-    /// <see cref="VertexEmpty"/>,
-    /// <see cref="VertexColor1"/>,
-    /// <see cref="VertexTexture1"/>,
-    /// <see cref="VertexColor1Texture1"/>.
-    /// <see cref="VertexColor1Texture2"/>.
-    /// <see cref="VertexColor2Texture2"/>.
+    /// 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="VertexColor2Texture2"/>.<br/>
     /// </typeparam>
     public class MeshBuilder<TvG, TvM> : MeshBuilder<Materials.MaterialBuilder, TvG, TvM, VertexEmpty>
         where TvG : struct, IVertexGeometry
@@ -306,11 +300,11 @@ namespace SharpGLTF.Geometry
     /// Represents an utility class to help build meshes by adding primitives associated with a given material.
     /// </summary>
     /// <typeparam name="TvG">
-    /// The vertex fragment type with Position, Normal and Tangent.
-    /// Valid types are:
-    /// <see cref="VertexPosition"/>,
-    /// <see cref="VertexPositionNormal"/>,
-    /// <see cref="VertexPositionNormalTangent"/>.
+    /// 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/>
     /// </typeparam>
     public class MeshBuilder<TvG> : MeshBuilder<Materials.MaterialBuilder, TvG, VertexEmpty, VertexEmpty>
         where TvG : struct, IVertexGeometry

+ 56 - 14
src/SharpGLTF.Toolkit/Geometry/MorphTargetBuilder.cs

@@ -31,6 +31,11 @@ namespace SharpGLTF.Geometry
         VertexGeometryDelta GetVertexDelta(int vertexIndex);
     }
 
+    /// <summary>
+    /// Represents the vertex deltas of a specific morph target.
+    /// <see cref="PrimitiveBuilder{TMaterial, TvG, TvM, TvS}._UseMorphTarget(int)"/>
+    /// </summary>
+    /// <typeparam name="TvG">The vertex fragment type with Position, Normal and Tangent.</typeparam>
     sealed class PrimitiveMorphTargetBuilder<TvG> : IPrimitiveMorphTargetReader
         where TvG : struct, IVertexGeometry
     {
@@ -160,20 +165,51 @@ namespace SharpGLTF.Geometry
         #endregion
     }
 
+    /// <summary>
+    /// Represents the vertex deltas of a specific morph target.
+    /// <see cref="IMeshBuilder{TMaterial}.UseMorphTarget(int)"/>
+    /// </summary>
     public interface IMorphTargetBuilder
     {
         IReadOnlyCollection<Vector3> Positions { get; }
         IReadOnlyCollection<IVertexGeometry> Vertices { get; }
         IReadOnlyList<IVertexGeometry> GetVertices(Vector3 position);
 
+        /// <summary>
+        /// Sets an absolute morph target.
+        /// </summary>
+        /// <param name="meshVertex">The base mesh vertex to morph.</param>
+        /// <param name="morphVertex">The morphed vertex.</param>
         void SetVertex(IVertexGeometry meshVertex, IVertexGeometry morphVertex);
-        void SetVertexDelta(Vector3 key, VertexGeometryDelta delta);
+
+        /// <summary>
+        /// Sets a relative morph target
+        /// </summary>
+        /// <param name="meshVertex">The base mesh vertex to morph.</param>
+        /// <param name="delta">The offset from <paramref name="meshVertex"/> to morph.</param>
         void SetVertexDelta(IVertexGeometry meshVertex, VertexGeometryDelta delta);
+
+        /// <summary>
+        /// Sets a relative morph target to all base mesh vertices matching <paramref name="meshPosition"/>.
+        /// </summary>
+        /// <param name="meshPosition">The base vertex position.</param>
+        /// <param name="delta">The offset to apply to each matching vertex found.</param>
+        void SetVertexDelta(Vector3 meshPosition, VertexGeometryDelta delta);
     }
 
     /// <summary>
-    /// Utility class to edit the Morph targets of a mesh.
+    /// Represents the vertex deltas of a specific morph target.
+    /// <see cref="MeshBuilder{TMaterial, TvG, TvM, TvS}.UseMorphTarget(int)"/>
     /// </summary>
+    /// <typeparam name="TMaterial">The material type used by the base mesh.</typeparam>
+    /// <typeparam name="TvG">The vertex geometry type used by the base mesh.</typeparam>
+    /// <typeparam name="TvS">The vertex skinning type used by the base mesh.</typeparam>
+    /// <typeparam name="TvM">The vertex material type used by the base mesh.</typeparam>
+    /// <remarks>
+    /// Morph targets are stored separately on each <see cref="PrimitiveBuilder{TMaterial, TvG, TvM, TvS}"/>,
+    /// so connecting vertices between two primitives might be duplicated. This means that when we set
+    /// a displaced vertex, we must be sure we do so for all instances we can find.
+    /// </remarks>
     public sealed class MorphTargetBuilder<TMaterial, TvG, TvS, TvM> : IMorphTargetBuilder
             where TvG : struct, IVertexGeometry
             where TvM : struct, IVertexMaterial
@@ -228,8 +264,6 @@ namespace SharpGLTF.Geometry
 
         public IReadOnlyCollection<TvG> Vertices => _Vertices.Keys;
 
-        IReadOnlyCollection<IVertexGeometry> IMorphTargetBuilder.Vertices => (IReadOnlyList<IVertexGeometry>)(IReadOnlyCollection<TvG>)_Vertices.Keys;
-
         #endregion
 
         #region API
@@ -239,11 +273,6 @@ namespace SharpGLTF.Geometry
             return _Positions.TryGetValue(position, out List<TvG> geos) ? (IReadOnlyList<TvG>)geos : Array.Empty<TvG>();
         }
 
-        IReadOnlyList<IVertexGeometry> IMorphTargetBuilder.GetVertices(Vector3 position)
-        {
-            return _Positions.TryGetValue(position, out List<TvG> geos) ? (IReadOnlyList<IVertexGeometry>)geos : Array.Empty<IVertexGeometry>();
-        }
-
         public void SetVertexDelta(Vector3 key, VertexGeometryDelta delta)
         {
             if (_Positions.TryGetValue(key, out List<TvG> geos))
@@ -265,11 +294,6 @@ namespace SharpGLTF.Geometry
             }
         }
 
-        void IMorphTargetBuilder.SetVertex(IVertexGeometry meshVertex, IVertexGeometry morphVertex)
-        {
-            SetVertex(meshVertex.ConvertToGeometry<TvG>(), morphVertex.ConvertToGeometry<TvG>());
-        }
-
         public void SetVertexDelta(TvG meshVertex, VertexGeometryDelta delta)
         {
             if (_Vertices.TryGetValue(meshVertex, out List<(PrimitiveBuilder<TMaterial, TvG, TvM, TvS>, int)> val))
@@ -283,6 +307,24 @@ namespace SharpGLTF.Geometry
             }
         }
 
+        #endregion
+
+        #region IMorphTargetBuilder
+
+        IReadOnlyCollection<IVertexGeometry> IMorphTargetBuilder.Vertices => (IReadOnlyList<IVertexGeometry>)(IReadOnlyCollection<TvG>)_Vertices.Keys;
+
+        IReadOnlyList<IVertexGeometry> IMorphTargetBuilder.GetVertices(Vector3 position)
+        {
+            return _Positions.TryGetValue(position, out List<TvG> geos)
+                ? (IReadOnlyList<IVertexGeometry>)geos
+                : Array.Empty<IVertexGeometry>();
+        }
+
+        void IMorphTargetBuilder.SetVertex(IVertexGeometry meshVertex, IVertexGeometry morphVertex)
+        {
+            SetVertex(meshVertex.ConvertToGeometry<TvG>(), morphVertex.ConvertToGeometry<TvG>());
+        }
+
         void IMorphTargetBuilder.SetVertexDelta(IVertexGeometry meshVertex, VertexGeometryDelta delta)
         {
             SetVertexDelta(meshVertex.ConvertToGeometry<TvG>(), delta);

+ 4 - 11
src/SharpGLTF.Toolkit/Geometry/Packed/PackedMeshBuilder.cs

@@ -12,7 +12,7 @@ namespace SharpGLTF.Geometry
     /// to <see cref="Schema2.Mesh"/>.
     /// </summary>
     /// <typeparam name="TMaterial">A material key to split primitives by material.</typeparam>
-    class PackedMeshBuilder<TMaterial>
+    class PackedMeshBuilder<TMaterial> : BaseBuilder
     {
         #region lifecycle
 
@@ -65,19 +65,12 @@ namespace SharpGLTF.Geometry
         }
 
         private PackedMeshBuilder(string name, IO.JsonContent extras)
-        {
-            _MeshName = name;
-            _MeshExtras = extras;
-        }
+            : base(name, extras) { }
 
         #endregion
 
         #region data
 
-        private readonly string _MeshName;
-
-        private readonly IO.JsonContent _MeshExtras;
-
         private readonly List<PackedPrimitiveBuilder<TMaterial>> _Primitives = new List<PackedPrimitiveBuilder<TMaterial>>();
 
         #endregion
@@ -96,8 +89,8 @@ namespace SharpGLTF.Geometry
         {
             if (_Primitives.Count == 0) return null;
 
-            var dstMesh = root.CreateMesh(_MeshName);
-            dstMesh.Extras = _MeshExtras.DeepClone();
+            var dstMesh = root.CreateMesh();
+            this.TryCopyNameAndExtrasTo(dstMesh);
 
             foreach (var p in _Primitives)
             {

+ 28 - 18
src/SharpGLTF.Toolkit/Geometry/PrimitiveBuilder.cs

@@ -14,28 +14,28 @@ namespace SharpGLTF.Geometry
     /// </summary>
     /// <typeparam name="TMaterial">The material type used by this <see cref="PrimitiveBuilder{TMaterial, TvP, TvM, TvS}"/> instance.</typeparam>
     /// <typeparam name="TvG">
-    /// The vertex fragment type with Position, Normal and Tangent.
-    /// Valid types are:
-    /// <see cref="VertexPosition"/>,
-    /// <see cref="VertexPositionNormal"/>,
-    /// <see cref="VertexPositionNormalTangent"/>.
+    /// 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/>
     /// </typeparam>
     /// <typeparam name="TvM">
-    /// The vertex fragment type with Colors and Texture Coordinates.
-    /// Valid types are:
-    /// <see cref="VertexEmpty"/>,
-    /// <see cref="VertexColor1"/>,
-    /// <see cref="VertexTexture1"/>,
-    /// <see cref="VertexColor1Texture1"/>.
-    /// <see cref="VertexColor1Texture2"/>.
-    /// <see cref="VertexColor2Texture2"/>.
+    /// 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="VertexColor2Texture2"/>.<br/>
     /// </typeparam>
     /// <typeparam name="TvS">
-    /// The vertex fragment type with Skin Joint Weights.
-    /// Valid types are:
-    /// <see cref="VertexEmpty"/>,
-    /// <see cref="VertexJoints4"/>,
-    /// <see cref="VertexJoints8"/>.
+    /// The vertex fragment type with Skin Joint Weights.<br/>
+    /// Valid types are:<br/>
+    /// - <see cref="VertexEmpty"/>,<br/>
+    /// - <see cref="VertexJoints4"/>,<br/>
+    /// - <see cref="VertexJoints8"/>.<br/>
     /// </typeparam>
     public abstract class PrimitiveBuilder<TMaterial, TvG, TvM, TvS> : IPrimitiveBuilder, IPrimitiveReader<TMaterial>
         where TvG : struct, IVertexGeometry
@@ -165,6 +165,16 @@ namespace SharpGLTF.Geometry
             _UseMorphTarget(morphTargetIndex).SetVertexDelta(vertexIndex, delta);
         }
 
+        /// <summary>
+        /// Checks if a vertex is already in the vertex buffer.
+        /// </summary>
+        /// <param name="vertex">The vertex to query.</param>
+        /// <returns>True if the vertex is already in.</returns>
+        public bool ContainsVertex(in VertexBuilder<TvG, TvM, TvS> vertex)
+        {
+            return _Vertices.IndexOf(vertex) >= 0;
+        }
+
         /// <summary>
         /// Adds a point.
         /// </summary>

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

@@ -108,6 +108,11 @@ namespace SharpGLTF.Geometry.VertexTypes
 
             float weightsSum = 0;
 
+            // The threshold for weights sum is defined as 2e-7 * weightCount.
+            // Refer to https://github.com/KhronosGroup/glTF-Validator/issues/132#issuecomment-578118301
+            // and to https://github.com/KhronosGroup/glTF/pull/1749
+            float threshold = 0;
+
             for (int i = 0; i < vertex.MaxBindings; ++i)
             {
                 var (index, weight) = vertex.GetJointBinding(i);
@@ -117,11 +122,13 @@ namespace SharpGLTF.Geometry.VertexTypes
                 if (weight == 0) Guard.IsTrue(index == 0, "joints with weight zero must be set to zero");
 
                 weightsSum += weight;
+
+                if (weight > 0) threshold += 2e-7f;
             }
 
-            // TODO: check that joints are unique
+            Guard.MustBeLessThan(Math.Abs(weightsSum-1), threshold, $"Weights must sum 1, but found {weightsSum}");
 
-            Guard.MustBeBetweenOrEqualTo(weightsSum, 0.99f, 1.01f, "Weights SUM");
+            // TODO: check that joints are unique
 
             return vertex;
         }

+ 8 - 12
src/SharpGLTF.Toolkit/Materials/ChannelBuilder.cs

@@ -50,10 +50,10 @@ namespace SharpGLTF.Materials
             }
 
             var tex = GetValidTexture();
-            if (tex != null)
+            if (tex?.PrimaryImage != null)
             {
                 if (hasParam) txt += " ×";
-                txt += $" {tex.PrimaryImage.DisplayText}";
+                txt += $" {tex.PrimaryImage.Content.ToDebuggerDisplay()}";
             }
 
             return txt;
@@ -91,19 +91,15 @@ namespace SharpGLTF.Materials
 
         public TextureBuilder Texture { get; private set; }
 
-        public static bool AreEqualByContent(ChannelBuilder a, ChannelBuilder b)
+        public static bool AreEqualByContent(ChannelBuilder x, ChannelBuilder y)
         {
-            #pragma warning disable IDE0041 // Use 'is null' check
-            if (Object.ReferenceEquals(a, b)) return true;
-            if (Object.ReferenceEquals(a, null)) return false;
-            if (Object.ReferenceEquals(b, null)) return false;
-            #pragma warning restore IDE0041 // Use 'is null' check
+            if ((x, y).AreSameReference(out bool areTheSame)) return areTheSame;
 
-            if (a._Key != b._Key) return false;
+            if (x._Key != y._Key) return false;
 
-            if (a.Parameter != b.Parameter) return false;
+            if (x.Parameter != y.Parameter) return false;
 
-            if (!TextureBuilder.AreEqualByContent(a.Texture, b.Texture)) return false;
+            if (!TextureBuilder.AreEqualByContent(x.Texture, y.Texture)) return false;
 
             return true;
         }
@@ -140,7 +136,7 @@ namespace SharpGLTF.Materials
         public TextureBuilder GetValidTexture()
         {
             if (Texture == null) return null;
-            if (Texture.PrimaryImage.IsEmpty) return null;
+            if (Texture.PrimaryImage == null) return null;
             return Texture;
         }
 

+ 99 - 0
src/SharpGLTF.Toolkit/Materials/ImageBuilder.cs

@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+using BYTES = System.ArraySegment<System.Byte>;
+using IMAGEFILE = SharpGLTF.Memory.MemoryImage;
+
+namespace SharpGLTF.Materials
+{
+    [System.Diagnostics.DebuggerDisplay("{_DebuggerDisplay(),nq}")]
+    public sealed class ImageBuilder : BaseBuilder
+    {
+        #region Debug
+
+        internal string _DebuggerDisplay()
+        {
+            var txt = "Image ";
+            if (!string.IsNullOrWhiteSpace(Name)) txt += $"{Name} ";
+            txt += Content.ToDebuggerDisplay();
+
+            return txt;
+        }
+
+        #endregion
+
+        #region lifecycle
+
+        public static implicit operator ImageBuilder(BYTES image) { return new IMAGEFILE(image); }
+
+        public static implicit operator ImageBuilder(Byte[] image) { return new IMAGEFILE(image); }
+
+        public static implicit operator ImageBuilder(string filePath) { return new IMAGEFILE(filePath); }
+
+        public static implicit operator ImageBuilder(IMAGEFILE content) { return From(content); }
+
+        public static ImageBuilder From(IMAGEFILE content, string name = null)
+        {
+            return content.IsEmpty ? null : new ImageBuilder(content, name, default);
+        }
+
+        public static ImageBuilder From(IMAGEFILE content, string name, IO.JsonContent extras)
+        {
+            return content.IsEmpty ? null : new ImageBuilder(content, name, extras);
+        }
+
+        private ImageBuilder(IMAGEFILE content, string name, IO.JsonContent extras)
+            : base(name, extras)
+        {
+            Content = content;
+        }
+
+        internal ImageBuilder Clone()
+        {
+            return new ImageBuilder(this);
+        }
+
+        private ImageBuilder(ImageBuilder other)
+            : base(other)
+        {
+            this.Content = other.Content;
+        }
+
+        #endregion
+
+        #region data
+        public IMAGEFILE Content { get; set; }
+
+        public static bool AreEqualByContent(ImageBuilder x, ImageBuilder y)
+        {
+            if ((x, y).AreSameReference(out bool areTheSame)) return areTheSame;
+
+            if (!BaseBuilder.AreEqualByContent(x, y)) return false;
+
+            if (!IMAGEFILE.AreEqual(x.Content, y.Content)) return false;
+
+            return true;
+        }
+
+        public static int GetContentHashCode(ImageBuilder x)
+        {
+            if (x == null) return 0;
+
+            var h = BaseBuilder.GetContentHashCode(x);
+
+            h ^= x.Content.GetHashCode();
+
+            return h;
+        }
+
+        #endregion
+
+        #region API
+
+        public static bool IsValid(ImageBuilder ib) { return ib != null && ib.Content.IsValid; }
+
+        #endregion
+
+    }
+}

+ 11 - 24
src/SharpGLTF.Toolkit/Materials/MaterialBuilder.cs

@@ -10,8 +10,11 @@ using IMAGEFILE = SharpGLTF.Memory.MemoryImage;
 
 namespace SharpGLTF.Materials
 {
+    /// <summary>
+    /// Represents the root object of a material instance structure.
+    /// </summary>
     [System.Diagnostics.DebuggerDisplay("{_DebuggerDisplay(),nq}")]
-    public class MaterialBuilder
+    public class MaterialBuilder : BaseBuilder
     {
         #region debug
 
@@ -67,23 +70,21 @@ namespace SharpGLTF.Materials
 
         #region lifecycle
 
-        public MaterialBuilder(string name = null)
-        {
-            Name = name;
-        }
-
         public static MaterialBuilder CreateDefault()
         {
             return new MaterialBuilder("Default");
         }
 
+        public MaterialBuilder(string name = null)
+            : base(name) { }
+
         public MaterialBuilder Clone() { return new MaterialBuilder(this); }
 
         public MaterialBuilder(MaterialBuilder other)
+            : base(other)
         {
             Guard.NotNull(other, nameof(other));
 
-            this.Name = other.Name;
             this.AlphaMode = other.AlphaMode;
             this.AlphaCutoff = other.AlphaCutoff;
             this.DoubleSided = other.DoubleSided;
@@ -114,11 +115,6 @@ namespace SharpGLTF.Materials
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private string _ShaderStyle = SHADERPBRMETALLICROUGHNESS;
 
-        /// <summary>
-        /// Gets or sets the name of this <see cref="MaterialBuilder"/> instance.
-        /// </summary>
-        public string Name { get; set; }
-
         public AlphaMode AlphaMode { get; set; } = AlphaMode.OPAQUE;
 
         public Single AlphaCutoff { get; set; } = 0.5f;
@@ -136,19 +132,10 @@ namespace SharpGLTF.Materials
 
         public static bool AreEqualByContent(MaterialBuilder x, MaterialBuilder y)
         {
-            #pragma warning disable IDE0041 // Use 'is null' check
-            if (Object.ReferenceEquals(x, y)) return true;
-            if (Object.ReferenceEquals(x, null)) return false;
-            if (Object.ReferenceEquals(y, null)) return false;
-            #pragma warning restore IDE0041 // Use 'is null' check
+            if ((x, y).AreSameReference(out bool areTheSame)) return areTheSame;
 
-            // Although .Name is not strictly a material property,
-            // it identifies a specific material during Runtime that
-            // might be relevant and needs to be preserved.
-            // If an author needs materials to be merged, it's better
-            // to keep the Name as null, or to use a common name like "Default".
+            if (!BaseBuilder.AreEqualByContent(x, y)) return false;
 
-            if (x.Name != y.Name) return false;
             if (x.AlphaMode != y.AlphaMode) return false;
             if (x.AlphaCutoff != y.AlphaCutoff) return false;
             if (x.DoubleSided != y.DoubleSided) return false;
@@ -178,7 +165,7 @@ namespace SharpGLTF.Materials
         {
             if (x == null) return 0;
 
-            var h = x.Name == null ? 0 : x.Name.GetHashCode();
+            var h = BaseBuilder.GetContentHashCode(x);
 
             h ^= x.AlphaMode.GetHashCode();
             h ^= x.AlphaCutoff.GetHashCode();

+ 34 - 33
src/SharpGLTF.Toolkit/Materials/TextureBuilder.cs

@@ -5,7 +5,6 @@ using System.Linq;
 using System.Numerics;
 using System.Text;
 
-using IMAGEFILE = SharpGLTF.Memory.MemoryImage;
 using TEXLERP = SharpGLTF.Schema2.TextureInterpolationFilter;
 using TEXMIPMAP = SharpGLTF.Schema2.TextureMipMapFilter;
 using TEXWRAP = SharpGLTF.Schema2.TextureWrapMode;
@@ -13,7 +12,7 @@ using TEXWRAP = SharpGLTF.Schema2.TextureWrapMode;
 namespace SharpGLTF.Materials
 {
     [System.Diagnostics.DebuggerDisplay("{_DebuggerDisplay(),nq}")]
-    public class TextureBuilder
+    public class TextureBuilder : BaseBuilder
     {
         #region Debug
 
@@ -28,8 +27,8 @@ namespace SharpGLTF.Materials
             if (WrapS != TEXWRAP.REPEAT) txt += $" {WrapS}↔";
             if (WrapT != TEXWRAP.REPEAT) txt += $" {WrapT}↕";
 
-            if (_PrimaryImageContent.IsValid) txt += $" {_PrimaryImageContent.DisplayText}";
-            if (_FallbackImageContent.IsValid) txt += $" => {_FallbackImageContent.DisplayText}";
+            if (_PrimaryImageContent != null) txt += $" {_PrimaryImageContent.Content.ToDebuggerDisplay()}";
+            if (_FallbackImageContent != null) txt += $" => {_FallbackImageContent.Content.ToDebuggerDisplay()}";
 
             return txt;
         }
@@ -53,10 +52,10 @@ namespace SharpGLTF.Materials
         private readonly ChannelBuilder _Parent;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private IMAGEFILE _PrimaryImageContent;
+        private ImageBuilder _PrimaryImageContent;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private IMAGEFILE _FallbackImageContent;
+        private ImageBuilder _FallbackImageContent;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private TextureTransformBuilder _Transform;
@@ -71,25 +70,23 @@ namespace SharpGLTF.Materials
 
         public TEXWRAP WrapT { get; set; } = TEXWRAP.REPEAT;
 
-        public static bool AreEqualByContent(TextureBuilder a, TextureBuilder b)
+        public static bool AreEqualByContent(TextureBuilder x, TextureBuilder y)
         {
-            #pragma warning disable IDE0041 // Use 'is null' check
-            if (Object.ReferenceEquals(a, b)) return true;
-            if (Object.ReferenceEquals(a, null)) return false;
-            if (Object.ReferenceEquals(b, null)) return false;
-            #pragma warning restore IDE0041 // Use 'is null' check
+            if ((x, y).AreSameReference(out bool areTheSame)) return areTheSame;
+
+            if (!BaseBuilder.AreEqualByContent(x, y)) return false;
 
-            if (a.CoordinateSet != b.CoordinateSet) return false;
+            if (x.CoordinateSet != y.CoordinateSet) return false;
 
-            if (a.MinFilter != b.MinFilter) return false;
-            if (a.MagFilter != b.MagFilter) return false;
-            if (a.WrapS != b.WrapS) return false;
-            if (a.WrapT != b.WrapT) return false;
+            if (x.MinFilter != y.MinFilter) return false;
+            if (x.MagFilter != y.MagFilter) return false;
+            if (x.WrapS != y.WrapS) return false;
+            if (x.WrapT != y.WrapT) return false;
 
-            if (!IMAGEFILE.AreEqual(a._PrimaryImageContent, b._PrimaryImageContent)) return false;
-            if (!IMAGEFILE.AreEqual(a._FallbackImageContent, b._FallbackImageContent)) return false;
+            if (!ImageBuilder.AreEqualByContent(x._PrimaryImageContent, y._PrimaryImageContent)) return false;
+            if (!ImageBuilder.AreEqualByContent(x._FallbackImageContent, y._FallbackImageContent)) return false;
 
-            if (!TextureTransformBuilder.AreEqualByContent(a._Transform, b._Transform)) return false;
+            if (!TextureTransformBuilder.AreEqualByContent(x._Transform, y._Transform)) return false;
 
             return true;
         }
@@ -98,14 +95,16 @@ namespace SharpGLTF.Materials
         {
             if (x == null) return 0;
 
-            var h = x.CoordinateSet.GetHashCode();
+            var h = BaseBuilder.GetContentHashCode(x);
+
+            h ^= x.CoordinateSet.GetHashCode();
             h ^= x.MinFilter.GetHashCode();
             h ^= x.MagFilter.GetHashCode();
             h ^= x.WrapS.GetHashCode();
             h ^= x.WrapT.GetHashCode();
 
-            h ^= x._PrimaryImageContent.GetHashCode();
-            h ^= x._FallbackImageContent.GetHashCode();
+            h ^= ImageBuilder.GetContentHashCode(x._PrimaryImageContent);
+            h ^= ImageBuilder.GetContentHashCode(x._FallbackImageContent);
 
             return h;
         }
@@ -118,7 +117,7 @@ namespace SharpGLTF.Materials
         /// Gets or sets the default image bytes to use by this <see cref="TextureBuilder"/>,
         /// Supported formats are: PNG, JPG, DDS, WEBP and KTX2
         /// </summary>
-        public IMAGEFILE PrimaryImage
+        public ImageBuilder PrimaryImage
         {
             get => _PrimaryImageContent;
             set => WithPrimaryImage(value);
@@ -128,7 +127,7 @@ namespace SharpGLTF.Materials
         /// Gets or sets the fallback image bytes to use by this <see cref="TextureBuilder"/>,
         /// Supported formats are: PNG, JPG.
         /// </summary>
-        public IMAGEFILE FallbackImage
+        public ImageBuilder FallbackImage
         {
             get => _FallbackImageContent;
             set => WithFallbackImage(value);
@@ -144,8 +143,10 @@ namespace SharpGLTF.Materials
 
         internal void CopyTo(TextureBuilder other)
         {
-            other._PrimaryImageContent = this._PrimaryImageContent;
-            other._FallbackImageContent = this._FallbackImageContent;
+            other.SetNameAndExtrasFrom(other);
+
+            other._PrimaryImageContent = this._PrimaryImageContent?.Clone();
+            other._FallbackImageContent = this._FallbackImageContent?.Clone();
 
             other.CoordinateSet = this.CoordinateSet;
 
@@ -160,11 +161,11 @@ namespace SharpGLTF.Materials
 
         public TextureBuilder WithCoordinateSet(int cset) { CoordinateSet = cset; return this; }
 
-        public TextureBuilder WithPrimaryImage(IMAGEFILE image)
+        public TextureBuilder WithPrimaryImage(ImageBuilder image)
         {
-            if (!image.IsEmpty)
+            if (image != null)
             {
-                Guard.IsTrue(image.IsValid, nameof(image), "Must be JPG, PNG, DDS, WEBP or KTX2");
+                Guard.IsTrue(ImageBuilder.IsValid(image), nameof(image), "Must be JPG, PNG, DDS, WEBP or KTX2");
             }
             else
             {
@@ -175,11 +176,11 @@ namespace SharpGLTF.Materials
             return this;
         }
 
-        public TextureBuilder WithFallbackImage(IMAGEFILE image)
+        public TextureBuilder WithFallbackImage(ImageBuilder image)
         {
-            if (!image.IsEmpty)
+            if (image != null)
             {
-                Guard.IsTrue(image.IsJpg || image.IsPng, nameof(image), "Must be JPG or PNG");
+                Guard.IsTrue(image.Content.IsJpg || image.Content.IsPng, nameof(image), "Must be JPG or PNG");
             }
             else
             {

+ 22 - 15
src/SharpGLTF.Toolkit/Scenes/CameraBuilder.cs

@@ -5,12 +5,25 @@ using System.Text;
 
 namespace SharpGLTF.Scenes
 {
-    public abstract class CameraBuilder
+    public abstract class CameraBuilder : BaseBuilder
     {
         #region lifecycle
 
         public abstract CameraBuilder Clone();
 
+        protected CameraBuilder(float znear, float zfar)
+        {
+            this.ZNear = znear;
+            this.ZFar = zfar;
+        }
+
+        protected CameraBuilder(CameraBuilder other)
+            : base(other)
+        {
+            this.ZNear = other.ZNear;
+            this.ZFar = other.ZFar;
+        }
+
         #endregion
 
         #region properties
@@ -41,19 +54,17 @@ namespace SharpGLTF.Scenes
             #region lifecycle
 
             public Orthographic(float xmag, float ymag, float znear, float zfar)
+                : base(znear, zfar)
             {
                 this.XMag = xmag;
                 this.YMag = ymag;
-                this.ZNear = znear;
-                this.ZFar = zfar;
             }
 
             internal Orthographic(Schema2.CameraOrthographic ortho)
+                : base(ortho.ZFar, ortho.ZFar)
             {
                 this.XMag = ortho.XMag;
                 this.YMag = ortho.YMag;
-                this.ZNear = ortho.ZNear;
-                this.ZFar = ortho.ZFar;
             }
 
             public override CameraBuilder Clone()
@@ -61,12 +72,11 @@ namespace SharpGLTF.Scenes
                 return new Orthographic(this);
             }
 
-            internal Orthographic(Orthographic ortho)
+            private Orthographic(Orthographic ortho)
+                : base(ortho)
             {
                 this.XMag = ortho.XMag;
                 this.YMag = ortho.YMag;
-                this.ZNear = ortho.ZNear;
-                this.ZFar = ortho.ZFar;
             }
 
             #endregion
@@ -97,19 +107,17 @@ namespace SharpGLTF.Scenes
             #region lifecycle
 
             public Perspective(float? aspectRatio, float fovy, float znear, float zfar = float.PositiveInfinity)
+                : base(znear, zfar)
             {
                 this.AspectRatio = aspectRatio;
                 this.VerticalFOV = fovy;
-                this.ZNear = znear;
-                this.ZFar = zfar;
             }
 
             internal Perspective(Schema2.CameraPerspective persp)
+                : base(persp.ZNear, persp.ZFar)
             {
                 this.AspectRatio = persp.AspectRatio;
                 this.VerticalFOV = persp.VerticalFOV;
-                this.ZNear = persp.ZNear;
-                this.ZFar = persp.ZFar;
             }
 
             public override CameraBuilder Clone()
@@ -117,12 +125,11 @@ namespace SharpGLTF.Scenes
                 return new Perspective(this);
             }
 
-            internal Perspective(Perspective persp)
+            private Perspective(Perspective persp)
+                : base(persp)
             {
                 this.AspectRatio = persp.AspectRatio;
                 this.VerticalFOV = persp.VerticalFOV;
-                this.ZNear = persp.ZNear;
-                this.ZFar = persp.ZFar;
             }
 
             #endregion

+ 4 - 6
src/SharpGLTF.Toolkit/Scenes/LightBuilder.cs

@@ -5,7 +5,7 @@ using System.Text;
 
 namespace SharpGLTF.Scenes
 {
-    public abstract class LightBuilder
+    public abstract class LightBuilder : BaseBuilder
     {
         #region lifecycle
 
@@ -13,6 +13,8 @@ namespace SharpGLTF.Scenes
         {
             Guard.NotNull(light, nameof(light));
 
+            this.SetNameAndExtrasFrom(light);
+
             this.Color = light.Color;
             this.Intensity = light.Intensity;
         }
@@ -20,6 +22,7 @@ namespace SharpGLTF.Scenes
         public abstract LightBuilder Clone();
 
         protected LightBuilder(LightBuilder other)
+            : base(other)
         {
             this.Color = other.Color;
             this.Intensity = other.Intensity;
@@ -47,8 +50,6 @@ namespace SharpGLTF.Scenes
 
         #region Nested types
 
-        #pragma warning disable CA1034 // Nested types should not be visible
-
         [System.Diagnostics.DebuggerDisplay("Directional")]
         public sealed class Directional : LightBuilder
         {
@@ -104,7 +105,6 @@ namespace SharpGLTF.Scenes
         }
 
         [System.Diagnostics.DebuggerDisplay("Spot")]
-
         public sealed class Spot : LightBuilder
         {
             #region lifecycle
@@ -156,8 +156,6 @@ namespace SharpGLTF.Scenes
             #endregion
         }
 
-        #pragma warning restore CA1034 // Nested types should not be visible
-
         #endregion
     }
 }

+ 8 - 19
src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs

@@ -13,7 +13,7 @@ namespace SharpGLTF.Scenes
     /// Defines a node object within an armature.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
-    public class NodeBuilder
+    public class NodeBuilder : BaseBuilder
     {
         #region debug
 
@@ -54,7 +54,11 @@ namespace SharpGLTF.Scenes
 
         public NodeBuilder() { }
 
-        public NodeBuilder(string name) { Name = name; }
+        public NodeBuilder(string name)
+            : base(name) { }
+
+        public NodeBuilder(string name, IO.JsonContent extras)
+            : base(name, extras) { }
 
         public Dictionary<NodeBuilder, NodeBuilder> DeepClone()
         {
@@ -69,16 +73,15 @@ namespace SharpGLTF.Scenes
         {
             var clone = new NodeBuilder();
 
+            clone.SetNameAndExtrasFrom(this);
+
             nodeMap[this] = clone;
 
-            clone.Name = this.Name;
             clone._Matrix = this._Matrix;
             clone._Scale = this._Scale?.Clone();
             clone._Rotation = this._Rotation?.Clone();
             clone._Translation = this._Translation?.Clone();
 
-            clone.Extras = this.Extras.DeepClone();
-
             foreach (var c in _Children)
             {
                 clone.AddNode(c.DeepClone(nodeMap));
@@ -102,14 +105,6 @@ namespace SharpGLTF.Scenes
 
         #endregion
 
-        #region properties
-
-        public String Name { get; set; }
-
-        public IO.JsonContent Extras { get; set; }
-
-        #endregion
-
         #region properties - hierarchy
         public NodeBuilder Parent => _Parent;
 
@@ -506,12 +501,6 @@ namespace SharpGLTF.Scenes
             return this;
         }
 
-        public NodeBuilder WithExtras(IO.JsonContent content)
-        {
-            Extras = content;
-            return this;
-        }
-
         #endregion
     }
 }

+ 21 - 16
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs

@@ -73,7 +73,7 @@ namespace SharpGLTF.Scenes
             // TODO: here we could check that every dstMesh has been correctly created.
         }
 
-        private void AddArmatureResources(Func<string, Node> nodeFactory, IEnumerable<SceneBuilder> srcScenes)
+        private void AddArmatureResources(Func<Node> nodeFactory, IEnumerable<SceneBuilder> srcScenes)
         {
             // ALIGNMENT ISSUE:
             // the toolkit builder is designed in a way that every instance can reuse the same node many times, even from different scenes.
@@ -97,9 +97,12 @@ namespace SharpGLTF.Scenes
             }
         }
 
-        private void CreateArmature(Func<string, Node> nodeFactory, NodeBuilder srcNode)
+        private void CreateArmature(Func<Node> nodeFactory, NodeBuilder srcNode)
         {
-            var dstNode = nodeFactory(srcNode.Name);
+            var dstNode = nodeFactory();
+
+            srcNode.TryCopyNameAndExtrasTo(dstNode);
+
             _Nodes[srcNode] = dstNode;
 
             if (srcNode.HasAnimations)
@@ -116,9 +119,7 @@ namespace SharpGLTF.Scenes
                 dstNode.LocalMatrix = srcNode.LocalMatrix;
             }
 
-            dstNode.Extras = srcNode.Extras.DeepClone();
-
-            foreach (var c in srcNode.VisualChildren) CreateArmature(dstNode.CreateNode, c);
+            foreach (var c in srcNode.VisualChildren) CreateArmature(() => dstNode.CreateNode(), c);
         }
 
         public static void SetMorphAnimation(Node dstNode, Animations.AnimatableProperty<Transforms.SparseWeight8> animation)
@@ -135,7 +136,7 @@ namespace SharpGLTF.Scenes
         public void AddScene(Scene dstScene, SceneBuilder srcScene)
         {
             _Nodes.Clear();
-            AddArmatureResources(dstScene.CreateNode, new[] { srcScene });
+            AddArmatureResources(() => dstScene.CreateNode(), new[] { srcScene });
 
             var schema2Instances = srcScene
                 .Instances
@@ -194,9 +195,7 @@ namespace SharpGLTF.Scenes
             foreach (var srcScene in srcScenes)
             {
                 var dstScene = dstModel.UseScene(dstModel.LogicalScenes.Count);
-
-                dstScene.Name = srcScene.Name;
-                dstScene.Extras = srcScene.Extras.DeepClone();
+                srcScene.TryCopyNameAndExtrasTo(dstScene);
 
                 context.AddScene(dstScene, srcScene);
             }
@@ -247,8 +246,7 @@ namespace SharpGLTF.Scenes
 
             var dstScene = new SceneBuilder();
 
-            dstScene.Name = srcScene.Name;
-            dstScene.Extras = srcScene.Extras.DeepClone();
+            dstScene.SetNameAndExtrasFrom(srcScene);
 
             // process mesh instances
             var srcMeshInstances = Node.Flatten(srcScene)
@@ -323,7 +321,11 @@ namespace SharpGLTF.Scenes
                 if (srcCam.Settings is CameraPerspective perspective) dstCam = new CameraBuilder.Perspective(perspective);
                 if (srcCam.Settings is CameraOrthographic orthographic) dstCam = new CameraBuilder.Orthographic(orthographic);
 
-                if (dstCam != null) dstScene.AddCamera(dstCam, dstNode);
+                if (dstCam != null)
+                {
+                    dstCam.SetNameAndExtrasFrom(srcCam);
+                    dstScene.AddCamera(dstCam, dstNode);
+                }
             }
         }
 
@@ -342,7 +344,11 @@ namespace SharpGLTF.Scenes
                 if (srcLight.LightType == PunctualLightType.Point) dstLight = new LightBuilder.Point(srcLight);
                 if (srcLight.LightType == PunctualLightType.Spot) dstLight = new LightBuilder.Spot(srcLight);
 
-                if (dstLight != null) dstScene.AddLight(dstLight, dstNode);
+                if (dstLight != null)
+                {
+                    dstLight.SetNameAndExtrasFrom(srcInstance);
+                    dstScene.AddLight(dstLight, dstNode);
+                }
             }
         }
 
@@ -351,8 +357,7 @@ namespace SharpGLTF.Scenes
             Guard.NotNull(srcNode, nameof(srcNode));
             Guard.NotNull(dstNode, nameof(dstNode));
 
-            dstNode.Name = srcNode.Name;
-            dstNode.Extras = srcNode.Extras.DeepClone();
+            dstNode.SetNameAndExtrasFrom(srcNode);
 
             dstNode.LocalTransform = srcNode.LocalTransform;
 

+ 28 - 40
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.cs

@@ -11,23 +11,19 @@ using MESHBUILDER = SharpGLTF.Geometry.IMeshBuilder<SharpGLTF.Materials.Material
 
 namespace SharpGLTF.Scenes
 {
-    [System.Diagnostics.DebuggerDisplay("Scene {_Name}")]
-    public partial class SceneBuilder
+    [System.Diagnostics.DebuggerDisplay("Scene {Name}")]
+    public partial class SceneBuilder : BaseBuilder
     {
         #region lifecycle
 
-        public SceneBuilder() { }
-
-        public SceneBuilder(string name)
-        {
-            _Name = name;
-        }
+        public SceneBuilder(string name = null)
+            : base(name) { }
 
         public SceneBuilder DeepClone(bool cloneArmatures = true)
         {
             var clone = new SceneBuilder();
 
-            clone._Name = this._Name;
+            clone.SetNameAndExtrasFrom(this);
 
             var nodeMap = new Dictionary<NodeBuilder, NodeBuilder>();
 
@@ -63,9 +59,6 @@ namespace SharpGLTF.Scenes
 
         #region data
 
-        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private String _Name;
-
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         internal readonly List<InstanceBuilder> _Instances = new List<InstanceBuilder>();
 
@@ -73,14 +66,6 @@ namespace SharpGLTF.Scenes
 
         #region properties
 
-        public String Name
-        {
-            get => _Name;
-            set => _Name = value;
-        }
-
-        public IO.JsonContent Extras { get; set; }
-
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         public IReadOnlyList<InstanceBuilder> Instances => _Instances;
 
@@ -238,19 +223,6 @@ namespace SharpGLTF.Scenes
             return instance;
         }
 
-        [Obsolete("It does not belong here.")]
-        public void RenameAllNodes(string namePrefix)
-        {
-            var allNodes = Instances
-                .Select(item => item.Content.GetArmatureRoot())
-                .Where(item => item != null)
-                .SelectMany(item => NodeBuilder.Flatten(item))
-                .Distinct()
-                .ToList();
-
-            NodeBuilder.Rename(allNodes, namePrefix);
-        }
-
         /// <summary>
         /// Gets all the unique armatures used by this <see cref="SceneBuilder"/>.
         /// </summary>
@@ -263,13 +235,23 @@ namespace SharpGLTF.Scenes
                 .ToList();
         }
 
+        /// <summary>
+        /// Applies a tranform the this <see cref="SceneBuilder"/>.
+        /// </summary>
+        /// <param name="basisTransform">The transform to apply.</param>
+        /// <param name="basisNodeName">The name of the dummy root node.</param>
+        /// <remarks>
+        /// In some circunstances, it's not possible to apply the <paramref name="basisTransform"/> to
+        /// the nodes in the scene. In this case a dummy node is created, and these nodes are made
+        /// children of this dummy node.
+        /// </remarks>
         public void ApplyBasisTransform(Matrix4x4 basisTransform, string basisNodeName = "BasisTransform")
         {
             // gather all root nodes:
             var rootNodes = this.FindArmatures();
 
             // find all the nodes that cannot be modified
-            bool isSensible(NodeBuilder node)
+            bool isExtrinsic(NodeBuilder node)
             {
                 if (node.Scale != null) return true;
                 if (node.Rotation != null) return true;
@@ -278,22 +260,22 @@ namespace SharpGLTF.Scenes
                 return false;
             }
 
-            var sensibleNodes = rootNodes
-                .Where(item => isSensible(item))
+            var extrinsicNodes = rootNodes
+                .Where(item => isExtrinsic(item))
                 .ToList();
 
             // find all the nodes that we can change their transform matrix safely.
             var intrinsicNodes = rootNodes
-                .Except(sensibleNodes)
+                .Except(extrinsicNodes)
                 .ToList();
 
-            // apply the transform to the nodes that are safe to change.
+            // apply the transform to the nodes that can be safely changed.
             foreach (var n in intrinsicNodes)
             {
                 n.LocalMatrix *= basisTransform;
             }
 
-            if (sensibleNodes.Count == 0) return;
+            if (extrinsicNodes.Count == 0) return;
 
             // create a proxy node to be used as the root for all sensible nodes.
             var basisNode = new NodeBuilder();
@@ -301,12 +283,18 @@ namespace SharpGLTF.Scenes
             basisNode.LocalMatrix = basisTransform;
 
             // assign all the sensible nodes to the basis node.
-            foreach (var n in sensibleNodes)
+            foreach (var n in extrinsicNodes)
             {
                 basisNode.AddNode(n);
             }
         }
 
+        /// <summary>
+        /// Copies the instances from <paramref name="scene"/> to this <see cref="SceneBuilder"/>
+        /// </summary>
+        /// <param name="scene">The source scene.</param>
+        /// <param name="sceneTransform">A transform to apply to <paramref name="scene"/> before addition.</param>
+        /// <returns>The instances copied from <paramref name="scene"/>.</returns>
         public IReadOnlyList<InstanceBuilder> AddScene(SceneBuilder scene, Matrix4x4 sceneTransform)
         {
             Guard.NotNull(scene, nameof(scene));

+ 33 - 15
src/SharpGLTF.Toolkit/Schema2/MaterialExtensions.cs

@@ -177,7 +177,7 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(root, nameof(root));
             Guard.NotNull(mb, nameof(mb));
 
-            var m = root.CreateMaterial(mb.Name);
+            var m = root.CreateMaterial();
 
             mb.CopyTo(m);
 
@@ -273,7 +273,8 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(srcMaterial, nameof(srcMaterial));
             Guard.NotNull(dstMaterial, nameof(dstMaterial));
 
-            dstMaterial.Name = srcMaterial.Name;
+            dstMaterial.SetNameAndExtrasFrom(srcMaterial);
+
             dstMaterial.AlphaMode = srcMaterial.Alpha.ToToolkit();
             dstMaterial.AlphaCutoff = srcMaterial.AlphaCutoff;
             dstMaterial.DoubleSided = srcMaterial.DoubleSided;
@@ -307,10 +308,11 @@ namespace SharpGLTF.Schema2
 
             dstChannel.Parameter = srcChannel.Parameter;
 
-            if (srcChannel.Texture == null) { return; }
-
+            if (srcChannel.Texture == null) return;
             if (dstChannel.Texture == null) dstChannel.UseTexture();
 
+            dstChannel.Texture.SetNameAndExtrasFrom(srcChannel.Texture);
+
             dstChannel.Texture.CoordinateSet = srcChannel.TextureCoordinate;
 
             if (srcChannel.TextureSampler != null)
@@ -328,8 +330,14 @@ namespace SharpGLTF.Schema2
                 dstChannel.Texture.WithTransform(srcXform.Offset, srcXform.Scale, srcXform.Rotation, srcXform.TextureCoordinateOverride);
             }
 
-            dstChannel.Texture.PrimaryImage = srcChannel.Texture.PrimaryImage?.Content ?? Memory.MemoryImage.Empty;
-            dstChannel.Texture.FallbackImage = srcChannel.Texture.FallbackImage?.Content ?? Memory.MemoryImage.Empty;
+            ImageBuilder _convert(Image src)
+            {
+                if (src == null) return null;
+                return ImageBuilder.From(src.Content, src.Name, src.Extras.DeepClone());
+            }
+
+            dstChannel.Texture.PrimaryImage = _convert(srcChannel.Texture.PrimaryImage);
+            dstChannel.Texture.FallbackImage = _convert(srcChannel.Texture.FallbackImage);
         }
 
         public static void CopyTo(this MaterialBuilder srcMaterial, Material dstMaterial)
@@ -339,6 +347,8 @@ namespace SharpGLTF.Schema2
 
             srcMaterial.ValidateForSchema2();
 
+            srcMaterial.TryCopyNameAndExtrasTo(dstMaterial);
+
             dstMaterial.Alpha = srcMaterial.AlphaMode.ToSchema2();
             dstMaterial.AlphaCutoff = srcMaterial.AlphaCutoff;
             dstMaterial.DoubleSided = srcMaterial.DoubleSided;
@@ -421,23 +431,31 @@ namespace SharpGLTF.Schema2
             Image primary = null;
             Image fallback = null;
 
-            if (srcTex.PrimaryImage.IsValid)
+            if (ImageBuilder.IsValid(srcTex.PrimaryImage))
             {
                 primary = dstChannel
-                .LogicalParent
-                .LogicalParent
-                .UseImageWithContent(srcTex.PrimaryImage);
+                    .LogicalParent
+                    .LogicalParent
+                    .UseImageWithContent(srcTex.PrimaryImage.Content);
+
+                srcTex.PrimaryImage.TryCopyNameAndExtrasTo(primary);
             }
 
-            if (srcTex.FallbackImage.IsValid)
+            if (primary == null) return;
+
+            if (ImageBuilder.IsValid(srcTex.FallbackImage))
             {
                 fallback = dstChannel
-                .LogicalParent
-                .LogicalParent
-                .UseImageWithContent(srcTex.FallbackImage);
+                    .LogicalParent
+                    .LogicalParent
+                    .UseImageWithContent(srcTex.FallbackImage.Content);
+
+                srcTex.FallbackImage.TryCopyNameAndExtrasTo(fallback);
             }
 
-            dstChannel.SetTexture(srcTex.CoordinateSet, primary, fallback, srcTex.WrapS, srcTex.WrapT, srcTex.MinFilter, srcTex.MagFilter);
+            var dstTex = dstChannel.SetTexture(srcTex.CoordinateSet, primary, fallback, srcTex.WrapS, srcTex.WrapT, srcTex.MinFilter, srcTex.MagFilter);
+
+            srcTex.TryCopyNameAndExtrasTo(dstTex);
 
             var srcXform = srcTex.Transform;
 

+ 2 - 2
src/SharpGLTF.Toolkit/Schema2/SceneExtensions.cs

@@ -214,7 +214,7 @@ namespace SharpGLTF.Schema2
             if (scene == null) return Enumerable.Empty<(IVertexBuilder, IVertexBuilder, IVertexBuilder, Material)>();
 
             var instance = Runtime.SceneTemplate
-                .Create(scene, false)
+                .Create(scene)
                 .CreateInstance();
 
             if (animation == null)
@@ -250,7 +250,7 @@ namespace SharpGLTF.Schema2
             if (scene == null) return Enumerable.Empty<(VertexBuilder<TvG, TvM, VertexEmpty>, VertexBuilder<TvG, TvM, VertexEmpty>, VertexBuilder<TvG, TvM, VertexEmpty>, Material)>();
 
             var instance = Runtime.SceneTemplate
-                .Create(scene, false)
+                .Create(scene)
                 .CreateInstance();
 
             if (animation == null)

+ 1 - 1
src/SharpGLTF.Toolkit/SharpGLTF.Toolkit.csproj

@@ -14,7 +14,7 @@
   <Import Project="..\Testing.props" />  
 
   <ItemGroup>
-    <Compile Include="..\Shared\Guard.cs" Link="Debug\Guard.cs" />
+    <Compile Include="..\Shared\Guard.cs" Link="Diagnostics\Guard.cs" />
     <Compile Include="..\Shared\_Extensions.cs" Link="_Extensions.cs" />
   </ItemGroup>
 

+ 1 - 1
tests/SharpGLTF.NUnit/SharpGLTF.NUnit.csproj

@@ -17,7 +17,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="nunit" Version="3.13.0" />
+    <PackageReference Include="nunit" Version="3.13.1" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
   </ItemGroup>
 

+ 83 - 39
tests/SharpGLTF.Tests/IO/JsonContentTests.cs

@@ -23,9 +23,64 @@ namespace SharpGLTF.IO
         public double DoublePI { get; set; }
 
         public List<int> Array1 { get; set; }
+        public List<float> Array2 { get; set; }
+
+        public List<int[]> Array3 { get; set; }
+
+        public List<_TestStructure2> Array4 { get; set; }
 
         public _TestStructure2 Dict1 { get; set; }
         public _TestStructure3 Dict2 { get; set; }
+        
+        public static Dictionary<string,object> CreateCompatibleDictionary()
+        {
+            var dict = new Dictionary<string, Object>();
+            dict["author"] = "me";
+            dict["integer1"] = 17;
+
+            dict["bool1"] = true;
+
+            dict["single1"] = 15.3f;
+            dict["single2"] = 1.1f;
+            dict["single3"] = -1.1f;
+            // dict["singlePI"] = (float)Math.PI; // Fails on .Net Framework 471
+
+            dict["double1"] = 15.3;
+            dict["double2"] = 1.1;
+            dict["double3"] = -1.1;
+            dict["doublePI"] = Math.PI;
+
+            dict["array1"] = new int[] { 1, 2, 3 };
+            dict["array2"] = new float[] { 1.1f, 2.2f, 3.3f };
+
+            dict["array3"] = new object[]
+            {
+                new int[] { 1,2,3 },
+                new int[] { 4,5,6 }
+            };
+
+            dict["array4"] = new object[]
+            {
+                new Dictionary<string, int> { ["a0"] = 5, ["a1"] = 6 },
+                new Dictionary<string, int> { ["a0"] = 7, ["a1"] = 8 }
+            };
+
+            dict["dict1"] = new Dictionary<string, int> { ["a0"] = 2, ["a1"] = 3 };
+            dict["dict2"] = new Dictionary<string, Object>
+            {
+                ["a"] = 16,
+                ["b"] = "delta",
+                ["c"] = new List<int>() { 4, 6, 7 },
+                ["d"] = new Dictionary<string, int> { ["a0"] = 1, ["a1"] = 2 }
+            };
+
+            if (!JsonContentTests.IsJsonRoundtripReady)
+            {
+                dict["array2"] = new float[] { 1, 2, 3 };
+            }
+
+            return dict;
+        }
     }
 
     struct _TestStructure2
@@ -47,19 +102,30 @@ namespace SharpGLTF.IO
     [Category("Core.IO")]
     public class JsonContentTests
     {
-        // when serializing a JsonContent object, it's important to take into account floating point values roundtrips.
-        // it seems that prior NetCore3.1, System.Text.JSon was not roundtrip proven, so some values might have some
-        // error margin when they complete a roundtrip.
+        public static bool IsJsonRoundtripReady
+        {
+            get
+            {
+                // when serializing a JsonContent object, it's important to take into account floating point values roundtrips.
+                // it seems that prior NetCore3.1, System.Text.JSon was not roundtrip ready, so some values might have some
+                // error margin when they complete a roundtrip.
 
-        // On newer, NetCore System.Text.Json versions, it seems to use "G9" and "G17" text formatting are used.
+                // On newer, NetCore System.Text.Json versions, it seems to use "G9" and "G17" text formatting are used.
 
-        // https://devblogs.microsoft.com/dotnet/floating-point-parsing-and-formatting-improvements-in-net-core-3-0/            
-        // https://github.com/dotnet/runtime/blob/76904319b41a1dd0823daaaaae6e56769ed19ed3/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs#L101
+                // https://devblogs.microsoft.com/dotnet/floating-point-parsing-and-formatting-improvements-in-net-core-3-0/            
+                // https://github.com/dotnet/runtime/blob/76904319b41a1dd0823daaaaae6e56769ed19ed3/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs#L101
 
-        // pull requests:
-        // https://github.com/dotnet/corefx/pull/40408
-        // https://github.com/dotnet/corefx/pull/38322
-        // https://github.com/dotnet/corefx/pull/32268
+                // pull requests:
+                // https://github.com/dotnet/corefx/pull/40408
+                // https://github.com/dotnet/corefx/pull/38322
+                // https://github.com/dotnet/corefx/pull/32268
+
+                var framework = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription;
+                return !framework.StartsWith(".NET Framework 4");
+            }
+        }
+
+        
 
         public static bool AreEqual(JsonContent a, JsonContent b)
         {
@@ -74,7 +140,11 @@ namespace SharpGLTF.IO
             var ajson = a.ToJson();
             var bjson = b.ToJson();
 
-            return ajson == bjson;
+            if (ajson != bjson) return false;
+
+            Assert.IsTrue(JsonContent.AreEqualByContent(a, b, 0.0001f));
+
+            return true;
         }
 
         [Test]
@@ -100,33 +170,7 @@ namespace SharpGLTF.IO
         [Test]
         public void CreateJsonContent()
         {
-            var dict = new Dictionary<string, Object>();
-            dict["author"] = "me";
-            dict["integer1"] = 17;
-
-            dict["bool1"] = true;
-
-            dict["single1"] = 15.3f;
-            dict["single2"] = 1.1f;
-            dict["single3"] = -1.1f;
-            // dict["singlePI"] = (float)Math.PI; // Fails on .Net Framework 471
-
-            dict["double1"] = 15.3;
-            dict["double2"] = 1.1;
-            dict["double3"] = -1.1;
-            dict["doublePI"] = Math.PI;
-
-            dict["array1"] = new int[] { 1, 2, 3 };
-            dict["dict1"] = new Dictionary<string, int> { ["a0"] = 2, ["a1"] = 3 };
-            dict["dict2"] = new Dictionary<string, Object>
-            {
-                ["a"] = 16,
-                ["b"] = "delta",
-                ["c"] = new List<int>() { 4, 6, 7 },
-                ["d"] = new Dictionary<string, int> { ["a0"] = 1, ["a1"] = 2 }
-            };            
-
-            JsonContent a = dict;
+            JsonContent a = _TestStructure.CreateCompatibleDictionary();
             
             // roundtrip to json
             var json = a.ToJson();
@@ -138,7 +182,7 @@ namespace SharpGLTF.IO
             var c = JsonContent.Serialize(x);
 
             Assert.IsTrue(AreEqual(a, b));
-            Assert.IsTrue(AreEqual(a, c));
+            Assert.IsTrue(AreEqual(a, c));            
 
             foreach (var dom in new[] { a, b, c})
             {

+ 2 - 2
tests/SharpGLTF.Tests/Runtime/SceneTemplateTests.cs

@@ -65,7 +65,7 @@ namespace SharpGLTF.Runtime
             var scene = model.DefaultScene;
             
             var decodedMeshes = scene.LogicalParent.LogicalMeshes.Decode();
-            var sceneTemplate = SceneTemplate.Create(scene, false);
+            var sceneTemplate = SceneTemplate.Create(scene);
             var sceneInstance = sceneTemplate.CreateInstance();
 
             var duration = sceneInstance.Armature.AnimationTracks[0].Duration;
@@ -108,7 +108,7 @@ namespace SharpGLTF.Runtime
 
             var (center, radius) = model.DefaultScene.EvaluateBoundingSphere(0.25f);           
             
-            var sceneTemplate = SceneTemplate.Create(model.DefaultScene, false);
+            var sceneTemplate = SceneTemplate.Create(model.DefaultScene);
             var sceneInstance = sceneTemplate.CreateInstance();
             sceneInstance.Armature.SetAnimationFrame(0, 0.1f);
 

+ 1 - 1
tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSampleTests.cs

@@ -324,7 +324,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
             // pos_master
 
             var instance = Runtime.SceneTemplate
-                .Create(model.DefaultScene, false)
+                .Create(model.DefaultScene)
                 .CreateInstance();
 
             var pvrt = node.Mesh.Primitives[0].GetVertexColumns();

+ 145 - 112
tests/SharpGLTF.Toolkit.Tests/Materials/ImageSharingTests.cs → tests/SharpGLTF.Toolkit.Tests/Materials/ContentSharingTests.cs

@@ -1,112 +1,145 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Numerics;
-using System.Text;
-
-using NUnit.Framework;
-
-using SharpGLTF.Geometry.Parametric;
-using SharpGLTF.Geometry.VertexTypes;
-using SharpGLTF.Scenes;
-using SharpGLTF.Schema2;
-
-namespace SharpGLTF.Materials
-{
-    [Category("Toolkit.Materials")]
-    public class ImageSharingTests
-    {
-        [Test]
-        public void WriteTwoModelsWithSharedTexture()
-        {
-            TestContext.CurrentContext.AttachShowDirLink();
-            TestContext.CurrentContext.AttachGltfValidatorLinks();
-
-            // get the texture from its original location and save it in our test directory.
-            var assetsPath = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Assets");
-            
-
-            var tex1Bytes = System.IO.File.ReadAllBytes(System.IO.Path.Combine(assetsPath, "shannon.png"));
-            var tex2Bytes = System.IO.File.ReadAllBytes(System.IO.Path.Combine(assetsPath, "Texture1.jpg"));
-
-            var tex1 = tex1Bytes.AttachToCurrentTest("shared-shannon.png");
-            var tex2 = tex2Bytes.AttachToCurrentTest("subdir\\shared-in-dir-Texture1.jpg");
-
-            // create a material using our shared texture
-            var material1 = new MaterialBuilder()                
-                .WithUnlitShader()
-                .WithBaseColor(tex1);
-
-            // create a material using our shared texture
-            var material2 = new MaterialBuilder()
-                .WithUnlitShader()
-                .WithBaseColor(tex2);
-
-            // create a simple cube mesh
-            var mesh1 = new Cube<MaterialBuilder>(material1).ToMesh(Matrix4x4.Identity);
-            var mesh2 = new Cube<MaterialBuilder>(material2).ToMesh(Matrix4x4.Identity);
-            var scene = new SceneBuilder();
-            scene.AddRigidMesh(mesh1, Matrix4x4.CreateTranslation(-2, 0, 0));
-            scene.AddRigidMesh(mesh2, Matrix4x4.CreateTranslation(2, 0, 0));
-
-            var gltf = scene.ToGltf2();
-
-            // define the texture sharing hook; this is a pretty naive approach, but it's good
-            // enough to demonstrate how it works.
-
-            string imageSharingHook(IO.WriteContext ctx, string uri, Memory.MemoryImage image)
-            {
-                Assert.IsTrue(new string[] { tex1, tex2 }.Contains(image.SourcePath) );
-
-                if (File.Exists(image.SourcePath))
-                {
-                    // image.SourcePath is an absolute path, we must make it relative to ctx.CurrentDirectory
-
-                    var currDir = ctx.CurrentDirectory.FullName + "\\";
-
-                    // if the shared texture can be reached by the model in its directory, reuse the texture.
-                    if (image.SourcePath.StartsWith(currDir, StringComparison.OrdinalIgnoreCase))
-                    {
-                        // we've found the shared texture!, return the uri relative to the model:
-                        return image.SourcePath.Substring(currDir.Length);
-                    }
-
-                    // TODO: Here we could also try to find a texture equivalent to MemoryImage in the
-                    // CurrentDirectory even if it has a different name, to minimize texture duplication.
-                }
-
-                // we were unable to reuse the shared texture,
-                // default to write our own texture.
-
-                image.SaveToFile(Path.Combine(ctx.CurrentDirectory.FullName, uri));
-
-                return uri;
-            }
-
-            var settings = new WriteSettings();            
-            settings.ImageWriting = ResourceWriteMode.SatelliteFile;
-            settings.ImageWriteCallback = imageSharingHook;
-
-            // save the model several times:           
-
-            var path1 = gltf.AttachToCurrentTest("model1.glb", settings);
-            var path2 = gltf.AttachToCurrentTest("model2.glb", settings);
-            var path3 = gltf.AttachToCurrentTest("model3.gltf", settings);
-
-            var satellites1 = ModelRoot.GetSatellitePaths(path1);
-            var satellites2 = ModelRoot.GetSatellitePaths(path2);
-            var satellites3 = ModelRoot.GetSatellitePaths(path3);
-
-            Assert.IsTrue(satellites1.Contains("shared-shannon.png"));
-            Assert.IsTrue(satellites1.Contains("subdir/shared-in-dir-Texture1.jpg"));
-
-            Assert.IsTrue(satellites2.Contains("shared-shannon.png"));
-            Assert.IsTrue(satellites2.Contains("subdir/shared-in-dir-Texture1.jpg"));
-
-            Assert.IsTrue(satellites3.Contains("shared-shannon.png"));
-            Assert.IsTrue(satellites3.Contains("subdir/shared-in-dir-Texture1.jpg"));
-        }
-
-    }
-}
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+using NUnit.Framework;
+
+using SharpGLTF.Geometry.Parametric;
+using SharpGLTF.Geometry.VertexTypes;
+using SharpGLTF.Scenes;
+using SharpGLTF.Schema2;
+
+namespace SharpGLTF.Materials
+{
+    [Category("Toolkit.Materials")]
+    public class ContentSharingTests
+    {
+        private static string AssetsPath = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Assets");
+
+        [Test]
+        public void TestMaterialBuilderEquality()
+        {
+            var tex1Bytes = System.IO.File.ReadAllBytes(System.IO.Path.Combine(AssetsPath, "shannon.png"));
+
+            // create a material using our shared texture
+            var material1 = new MaterialBuilder()
+                .WithUnlitShader()            
+                .WithBaseColor(tex1Bytes);
+
+            var material2 = material1.Clone();
+
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(material1, material2));
+
+            material2
+                .GetChannel(KnownChannel.BaseColor)
+                .Texture
+                .PrimaryImage
+                .Extras = IO.JsonContent.Serialize(new KeyValuePair<int, string>(1, "hello"));
+
+            var material3 = material2.Clone();
+
+            Assert.IsFalse(MaterialBuilder.AreEqualByContent(material1, material2));
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(material2, material3));
+
+            var kvp = material3.GetChannel(KnownChannel.BaseColor)
+                .Texture
+                .PrimaryImage
+                .Extras.Deserialize<KeyValuePair<int, string>>();
+
+            Assert.AreEqual(kvp.Key, 1);
+            Assert.AreEqual(kvp.Value, "hello");
+
+        }
+
+        [Test]
+        public void WriteTwoModelsWithSharedTexture()
+        {
+            TestContext.CurrentContext.AttachShowDirLink();
+            TestContext.CurrentContext.AttachGltfValidatorLinks();            
+
+            var tex1Bytes = System.IO.File.ReadAllBytes(System.IO.Path.Combine(AssetsPath, "shannon.png"));
+            var tex2Bytes = System.IO.File.ReadAllBytes(System.IO.Path.Combine(AssetsPath, "Texture1.jpg"));
+
+            var tex1 = tex1Bytes.AttachToCurrentTest("shared-shannon.png");
+            var tex2 = tex2Bytes.AttachToCurrentTest("subdir\\shared-in-dir-Texture1.jpg");
+
+            // create a material using our shared texture
+            var material1 = new MaterialBuilder()                
+                .WithUnlitShader()
+                .WithBaseColor(tex1);
+
+            // create a material using our shared texture
+            var material2 = new MaterialBuilder()
+                .WithUnlitShader()
+                .WithBaseColor(tex2);
+
+            // create a simple cube mesh
+            var mesh1 = new Cube<MaterialBuilder>(material1).ToMesh(Matrix4x4.Identity);
+            var mesh2 = new Cube<MaterialBuilder>(material2).ToMesh(Matrix4x4.Identity);
+            var scene = new SceneBuilder();
+            scene.AddRigidMesh(mesh1, Matrix4x4.CreateTranslation(-2, 0, 0));
+            scene.AddRigidMesh(mesh2, Matrix4x4.CreateTranslation(2, 0, 0));
+
+            var gltf = scene.ToGltf2();
+
+            // define the texture sharing hook; this is a pretty naive approach, but it's good
+            // enough to demonstrate how it works.
+
+            string imageSharingHook(IO.WriteContext ctx, string uri, Memory.MemoryImage image)
+            {
+                Assert.IsTrue(new string[] { tex1, tex2 }.Contains(image.SourcePath) );
+
+                if (File.Exists(image.SourcePath))
+                {
+                    // image.SourcePath is an absolute path, we must make it relative to ctx.CurrentDirectory
+
+                    var currDir = ctx.CurrentDirectory.FullName + "\\";
+
+                    // if the shared texture can be reached by the model in its directory, reuse the texture.
+                    if (image.SourcePath.StartsWith(currDir, StringComparison.OrdinalIgnoreCase))
+                    {
+                        // we've found the shared texture!, return the uri relative to the model:
+                        return image.SourcePath.Substring(currDir.Length);
+                    }
+
+                    // TODO: Here we could also try to find a texture equivalent to MemoryImage in the
+                    // CurrentDirectory even if it has a different name, to minimize texture duplication.
+                }
+
+                // we were unable to reuse the shared texture,
+                // default to write our own texture.
+
+                image.SaveToFile(Path.Combine(ctx.CurrentDirectory.FullName, uri));
+
+                return uri;
+            }
+
+            var settings = new WriteSettings();            
+            settings.ImageWriting = ResourceWriteMode.SatelliteFile;
+            settings.ImageWriteCallback = imageSharingHook;
+
+            // save the model several times:           
+
+            var path1 = gltf.AttachToCurrentTest("model1.glb", settings);
+            var path2 = gltf.AttachToCurrentTest("model2.glb", settings);
+            var path3 = gltf.AttachToCurrentTest("model3.gltf", settings);
+
+            var satellites1 = ModelRoot.GetSatellitePaths(path1);
+            var satellites2 = ModelRoot.GetSatellitePaths(path2);
+            var satellites3 = ModelRoot.GetSatellitePaths(path3);
+
+            Assert.IsTrue(satellites1.Contains("shared-shannon.png"));
+            Assert.IsTrue(satellites1.Contains("subdir/shared-in-dir-Texture1.jpg"));
+
+            Assert.IsTrue(satellites2.Contains("shared-shannon.png"));
+            Assert.IsTrue(satellites2.Contains("subdir/shared-in-dir-Texture1.jpg"));
+
+            Assert.IsTrue(satellites3.Contains("shared-shannon.png"));
+            Assert.IsTrue(satellites3.Contains("subdir/shared-in-dir-Texture1.jpg"));
+        }
+
+    }
+}