Browse Source

Updated nugets.
Added a new MemoryImage object to handle image blobs.
WIP support for Quantized geometry extension.
Refactored NUnit tests.

Vicente Penades 5 years ago
parent
commit
b788d6e860
53 changed files with 1256 additions and 1063 deletions
  1. 10 3
      SharpGLTF.sln
  2. 2 3
      build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj
  3. 1 1
      examples/SharpGLTF.Runtime.MonoGame/MonoGameModelTemplate.cs
  4. 52 45
      src/Shared/_Extensions.cs
  5. 3 3
      src/SharpGLTF.Core/Debug/DebuggerDisplay.cs
  6. 5 23
      src/SharpGLTF.Core/Memory/ColorArray.cs
  7. 22 150
      src/SharpGLTF.Core/Memory/FloatingArrays.cs
  8. 5 23
      src/SharpGLTF.Core/Memory/IntegerArrays.cs
  9. 230 0
      src/SharpGLTF.Core/Memory/MemoryAccessor.Validation.cs
  10. 145 88
      src/SharpGLTF.Core/Memory/MemoryAccessor.cs
  11. 12 10
      src/SharpGLTF.Core/Memory/MemoryImage.cs
  12. 1 1
      src/SharpGLTF.Core/Memory/SparseArrays.cs
  13. 1 1
      src/SharpGLTF.Core/Schema2/gltf.AccessorSparse.cs
  14. 60 26
      src/SharpGLTF.Core/Schema2/gltf.Accessors.cs
  15. 4 3
      src/SharpGLTF.Core/Schema2/gltf.BufferView.cs
  16. 6 6
      src/SharpGLTF.Core/Schema2/gltf.Images.cs
  17. 20 62
      src/SharpGLTF.Core/Schema2/gltf.MeshPrimitive.cs
  18. 1 1
      src/SharpGLTF.Core/Schema2/gltf.Textures.cs
  19. 60 49
      src/SharpGLTF.Core/Schema2/khr.lights.cs
  20. 1 2
      src/SharpGLTF.Core/SharpGLTF.Core.csproj
  21. 1 1
      src/SharpGLTF.Core/Validation/ValidationContext.cs
  22. 4 2
      src/SharpGLTF.Toolkit/Geometry/PackedBuffer.cs
  23. 26 0
      src/SharpGLTF.Toolkit/Geometry/PackedEncoding.cs
  24. 10 7
      src/SharpGLTF.Toolkit/Geometry/PackedMeshBuilder.cs
  25. 11 9
      src/SharpGLTF.Toolkit/Geometry/PackedPrimitiveBuilder.cs
  26. 1 1
      src/SharpGLTF.Toolkit/Geometry/VertexBuilder.cs
  27. 9 7
      src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexMaterial.cs
  28. 8 6
      src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexSkinning.cs
  29. 38 63
      src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexUtils.cs
  30. 1 1
      src/SharpGLTF.Toolkit/IO/WavefrontWriter.cs
  31. 7 7
      src/SharpGLTF.Toolkit/Materials/TextureBuilder.cs
  32. 24 8
      src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs
  33. 1 1
      src/SharpGLTF.Toolkit/Schema2/LightExtensions.cs
  34. 2 2
      src/SharpGLTF.Toolkit/Schema2/MaterialExtensions.cs
  35. 4 10
      src/SharpGLTF.Toolkit/Schema2/MeshExtensions.cs
  36. 1 1
      tests/SharpGLTF.NUnit/DumpAssemblyAPI.cs
  37. 93 0
      tests/SharpGLTF.NUnit/NUnitGltfUtils.cs
  38. 68 0
      tests/SharpGLTF.NUnit/NUnitUtils.cs
  39. 86 29
      tests/SharpGLTF.NUnit/NumericsAssert.cs
  40. 107 0
      tests/SharpGLTF.NUnit/NumericsUtils.cs
  41. 0 0
      tests/SharpGLTF.NUnit/Plotting.cs
  42. 0 0
      tests/SharpGLTF.NUnit/Reports.cs
  43. 30 0
      tests/SharpGLTF.NUnit/SharpGLTF.NUnit.csproj
  44. 42 0
      tests/SharpGLTF.NUnit/ShortcutUtils.cs
  45. 1 1
      tests/SharpGLTF.NUnit/gltf_validator.cs
  46. 1 1
      tests/SharpGLTF.Tests/AnimationSamplingTests.cs
  47. 3 3
      tests/SharpGLTF.Tests/Memory/MemoryAccessorTests.cs
  48. 14 4
      tests/SharpGLTF.Tests/Runtime/SceneTemplateTests.cs
  49. 6 3
      tests/SharpGLTF.Tests/Scenes/SceneBuilderTests.cs
  50. 7 4
      tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSampleTests.cs
  51. 2 4
      tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSpecialModelsTest.cs
  52. 7 98
      tests/SharpGLTF.Tests/SharpGLTF.Tests.csproj
  53. 0 290
      tests/SharpGLTF.Tests/Utils.cs

+ 10 - 3
SharpGLTF.sln

@@ -1,7 +1,7 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.28307.329
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29709.97
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{29566B60-311D-42A0-9E8D-C48DECDD587F}"
 	ProjectSection(SolutionItems) = preProject
@@ -36,7 +36,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGLTF.Runtime.MonoGame"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGameScene", "examples\MonoGameScene\MonoGameScene.csproj", "{894781CA-F508-43AE-8526-6AA6B6EDF613}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpGLTF.DownloadTestFiles", "tests\SharpGLTF.DownloadTestFiles\SharpGLTF.DownloadTestFiles.csproj", "{7CC20DF6-14B5-4C1C-B4FC-151E97AED4F4}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGLTF.DownloadTestFiles", "tests\SharpGLTF.DownloadTestFiles\SharpGLTF.DownloadTestFiles.csproj", "{7CC20DF6-14B5-4C1C-B4FC-151E97AED4F4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpGLTF.NUnit", "tests\SharpGLTF.NUnit\SharpGLTF.NUnit.csproj", "{7A5EAF7E-D6A6-4861-9488-F98E4AA00A3A}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -84,6 +86,10 @@ Global
 		{7CC20DF6-14B5-4C1C-B4FC-151E97AED4F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{7CC20DF6-14B5-4C1C-B4FC-151E97AED4F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{7CC20DF6-14B5-4C1C-B4FC-151E97AED4F4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7A5EAF7E-D6A6-4861-9488-F98E4AA00A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7A5EAF7E-D6A6-4861-9488-F98E4AA00A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7A5EAF7E-D6A6-4861-9488-F98E4AA00A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7A5EAF7E-D6A6-4861-9488-F98E4AA00A3A}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -99,6 +105,7 @@ Global
 		{6C7B3CD8-21D0-447E-9034-8F72057F2ED7} = {83E7E49D-8A28-45E8-9DBD-1F3AEDEF3E42}
 		{894781CA-F508-43AE-8526-6AA6B6EDF613} = {83E7E49D-8A28-45E8-9DBD-1F3AEDEF3E42}
 		{7CC20DF6-14B5-4C1C-B4FC-151E97AED4F4} = {0CBF510D-D836-40BA-95EC-E93FDBB90632}
+		{7A5EAF7E-D6A6-4861-9488-F98E4AA00A3A} = {0CBF510D-D836-40BA-95EC-E93FDBB90632}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {1D7BBAD9-834C-4981-AC96-0AA5226FC43F}

+ 2 - 3
build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj

@@ -7,9 +7,8 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="LibGit2Sharp" Version="0.26.2" />
-    <PackageReference Include="NJsonSchema.CodeGeneration" Version="10.1.2" />
-    <PackageReference Include="NJsonSchema.CodeGeneration.CSharp" Version="10.1.2" />
+    <PackageReference Include="LibGit2Sharp" Version="0.26.2" />    
+    <PackageReference Include="NJsonSchema.CodeGeneration.CSharp" Version="10.1.5" />
   </ItemGroup>
 
 </Project>

+ 1 - 1
examples/SharpGLTF.Runtime.MonoGame/MonoGameModelTemplate.cs

@@ -13,7 +13,7 @@ namespace SharpGLTF.Runtime
 
         public static MonoGameDeviceContent<MonoGameModelTemplate> LoadDeviceModel(GraphicsDevice device, string filePath)
         {
-            var model = Schema2.ModelRoot.Load(filePath);
+            var model = Schema2.ModelRoot.Load(filePath, Validation.ValidationMode.TryFix);
 
             return CreateDeviceModel(device, model);
         }

+ 52 - 45
src/Shared/_Extensions.cs

@@ -13,6 +13,23 @@ namespace SharpGLTF
     /// </summary>
     static class _Extensions
     {
+        #region constants
+
+        // constants from: https://github.com/KhronosGroup/glTF-Validator/blob/master/lib/src/errors.dart
+
+        private const float unitLengthThresholdVec3 = 0.00674f;
+        private const float unitLengthThresholdVec4 = 0.00769f;
+
+        // This value is slightly greater
+        // than the maximum error from unsigned 8-bit quantization
+        // 1..2 elements - 0 * step
+        // 3..4 elements - 1 * step
+        // 5..6 elements - 2 * step
+        // ...
+        private const float unitSumThresholdStep = 0.0039216f;
+
+        #endregion
+
         #region private numerics extensions
 
         internal static bool IsMultipleOf(this int value, int mult)
@@ -20,13 +37,6 @@ namespace SharpGLTF
             return (value % mult) == 0;
         }
 
-        internal static int PaddingSize(this int size, int mult)
-        {
-            var rest = size % mult;
-
-            return rest == 0 ? 0 : mult - rest;
-        }
-
         internal static int WordPadded(this int length)
         {
             var padding = length & 3;
@@ -36,36 +46,36 @@ namespace SharpGLTF
 
         internal static bool _IsFinite(this float value)
         {
-            return !(float.IsNaN(value) | float.IsInfinity(value));
+            return !(float.IsNaN(value) || float.IsInfinity(value));
         }
 
         internal static bool _IsFinite(this Vector2 v)
         {
-            return v.X._IsFinite() & v.Y._IsFinite();
+            return v.X._IsFinite() && v.Y._IsFinite();
         }
 
         internal static bool _IsFinite(this Vector3 v)
         {
-            return v.X._IsFinite() & v.Y._IsFinite() & v.Z._IsFinite();
+            return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite();
         }
 
         internal static bool _IsFinite(this Vector4 v)
         {
-            return v.X._IsFinite() & v.Y._IsFinite() & v.Z._IsFinite() & v.W._IsFinite();
+            return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite() && v.W._IsFinite();
         }
 
         internal static bool _IsFinite(this Matrix4x4 v)
         {
-            if (!(v.M11._IsFinite() & v.M12._IsFinite() & v.M13._IsFinite() & v.M14._IsFinite())) return false;
-            if (!(v.M21._IsFinite() & v.M22._IsFinite() & v.M23._IsFinite() & v.M24._IsFinite())) return false;
-            if (!(v.M31._IsFinite() & v.M32._IsFinite() & v.M33._IsFinite() & v.M34._IsFinite())) return false;
-            if (!(v.M41._IsFinite() & v.M42._IsFinite() & v.M43._IsFinite() & v.M44._IsFinite())) return false;
+            if (!(v.M11._IsFinite() && v.M12._IsFinite() && v.M13._IsFinite() && v.M14._IsFinite())) return false;
+            if (!(v.M21._IsFinite() && v.M22._IsFinite() && v.M23._IsFinite() && v.M24._IsFinite())) return false;
+            if (!(v.M31._IsFinite() && v.M32._IsFinite() && v.M33._IsFinite() && v.M34._IsFinite())) return false;
+            if (!(v.M41._IsFinite() && v.M42._IsFinite() && v.M43._IsFinite() && v.M44._IsFinite())) return false;
             return true;
         }
 
         internal static bool _IsFinite(this Quaternion v)
         {
-            return v.X._IsFinite() & v.Y._IsFinite() & v.Z._IsFinite() & v.W._IsFinite();
+            return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite() && v.W._IsFinite();
         }
 
         internal static Vector3 WithLength(this Vector3 v, float len)
@@ -73,16 +83,25 @@ namespace SharpGLTF
             return Vector3.Normalize(v) * len;
         }
 
-        internal static Quaternion AsQuaternion(this Vector4 v)
+        internal static Boolean IsNormalized(this Vector3 normal)
         {
-            return new Quaternion(v.X, v.Y, v.Z, v.W);
+            if (!normal._IsFinite()) return false;
+
+            return Math.Abs(normal.Length() - 1) <= unitLengthThresholdVec3;
         }
 
         internal static Boolean IsNormalized(this Quaternion q)
         {
             // As per: https://github.com/KhronosGroup/glTF-Validator/issues/33 , quaternions need to be normalized.
 
-            return Math.Abs(1.0 - q.Length()) > 0.000005;
+            if (!q._IsFinite()) return false;
+
+            return Math.Abs(q.Length() - 1) <= 0.000005;
+        }
+
+        internal static Quaternion AsQuaternion(this Vector4 v)
+        {
+            return new Quaternion(v.X, v.Y, v.Z, v.W);
         }
 
         internal static Quaternion Sanitized(this Quaternion q)
@@ -114,18 +133,17 @@ namespace SharpGLTF
             return (value - r) == Vector4.Zero;
         }
 
+        /*
         internal static void Validate(this Vector3 vector, string msg)
         {
             if (!vector._IsFinite()) throw new NotFiniteNumberException($"{msg} is invalid.");
-        }
+        }*/
 
         internal static void ValidateNormal(this Vector3 normal, string msg)
         {
             if (!normal._IsFinite()) throw new NotFiniteNumberException($"{msg} is invalid.");
 
-            var len = normal.Length();
-
-            if (len < 0.99f || len > 1.01f) throw new ArithmeticException($"{msg} is not unit length.");
+            if (!normal.IsNormalized()) throw new ArithmeticException($"{msg} is not unit length.");
         }
 
         internal static void ValidateTangent(this Vector4 tangent, string msg)
@@ -135,36 +153,22 @@ namespace SharpGLTF
             new Vector3(tangent.X, tangent.Y, tangent.Z).ValidateNormal(msg);
         }
 
-        internal static bool IsValidNormal(this Vector3 normal)
-        {
-            if (!normal._IsFinite()) return false;
-
-            var len = normal.Length();
-
-            if (len < 0.99f || len > 1.01f) return false;
-
-            return true;
-        }
-
         internal static Vector3 SanitizeNormal(this Vector3 normal)
         {
-            var isn = normal._IsFinite() && normal.LengthSquared() > 0;
-            return isn ? Vector3.Normalize(normal) : Vector3.UnitZ;
+            return normal.IsNormalized() ? normal : Vector3.Normalize(normal);
         }
 
         internal static bool IsValidTangent(this Vector4 tangent)
         {
             if (tangent.W != 1 && tangent.W != -1) return false;
 
-            return new Vector3(tangent.X, tangent.Y, tangent.Z).IsValidNormal();
+            return new Vector3(tangent.X, tangent.Y, tangent.Z).IsNormalized();
         }
 
         internal static Vector4 SanitizeTangent(this Vector4 tangent)
         {
-            var n = new Vector3(tangent.X, tangent.Y, tangent.Z);
+            var n = new Vector3(tangent.X, tangent.Y, tangent.Z).SanitizeNormal();
             var s = float.IsNaN(tangent.W) ? 1 : tangent.W;
-            var isn = n._IsFinite() && n.LengthSquared() > 0;
-            n = isn ? Vector3.Normalize(n) : Vector3.UnitX;
             return new Vector4(n, s > 0 ? 1 : -1);
         }
 
@@ -474,17 +478,17 @@ namespace SharpGLTF
             }
         }
 
-        public static IEnumerable<(int, int)> GetLinesIndices(this PrimitiveType ptype, int vertexCount)
+        public static IEnumerable<(int A, int B)> GetLinesIndices(this PrimitiveType ptype, int vertexCount)
         {
             return ptype.GetLinesIndices(Enumerable.Range(0, vertexCount).Select(item => (UInt32)item));
         }
 
-        public static IEnumerable<(int, int, int)> GetTrianglesIndices(this PrimitiveType ptype, int vertexCount)
+        public static IEnumerable<(int A, int B, int C)> GetTrianglesIndices(this PrimitiveType ptype, int vertexCount)
         {
             return ptype.GetTrianglesIndices(Enumerable.Range(0, vertexCount).Select(item => (UInt32)item));
         }
 
-        public static IEnumerable<(int, int)> GetLinesIndices(this PrimitiveType ptype, IEnumerable<UInt32> sourceIndices)
+        public static IEnumerable<(int A, int B)> GetLinesIndices(this PrimitiveType ptype, IEnumerable<UInt32> sourceIndices)
         {
             switch (ptype)
             {
@@ -510,7 +514,7 @@ namespace SharpGLTF
             }
         }
 
-        public static IEnumerable<(int, int, int)> GetTrianglesIndices(this PrimitiveType ptype, IEnumerable<UInt32> sourceIndices)
+        public static IEnumerable<(int A, int B, int C)> GetTrianglesIndices(this PrimitiveType ptype, IEnumerable<UInt32> sourceIndices)
         {
             switch (ptype)
             {
@@ -621,7 +625,10 @@ namespace SharpGLTF
 
             if (content.Length.IsMultipleOf(4)) return content;
 
-            var paddedContent = new Byte[content.Length + content.Length.PaddingSize(4)];
+            var rest = content.Length % 4;
+            rest = rest == 0 ? 0 : 4 - rest;
+
+            var paddedContent = new Byte[content.Length + rest];
             content.CopyTo(paddedContent, 0);
             return paddedContent;
         }

+ 3 - 3
src/SharpGLTF.Core/Debug/DebuggerDisplay.cs

@@ -27,7 +27,7 @@ namespace SharpGLTF.Debug
             return attributeName;
         }
 
-        public static String ToReport(this Memory.MemoryEncoding minfo)
+        public static String ToReport(this Memory.MemoryAccessInfo minfo)
         {
             var txt = GetAttributeShortName(minfo.Name);
             if (minfo.ByteOffset != 0) txt += $" Offs:{minfo.ByteOffset}ᴮʸᵗᵉˢ";
@@ -93,7 +93,7 @@ namespace SharpGLTF.Debug
             if (vcounts.Count() > 1)
             {
                 var vAccessors = prim.VertexAccessors
-                    .OrderBy(item => item.Key, Memory.MemoryEncoding.NameComparer)
+                    .OrderBy(item => item.Key, Memory.MemoryAccessInfo.NameComparer)
                     .Select(item => $"{GetAttributeShortName(item.Key)}={item.Value.ToReportShort()}")
                     .ToList();
 
@@ -109,7 +109,7 @@ namespace SharpGLTF.Debug
                 }
 
                 var vAccessors = prim.VertexAccessors
-                    .OrderBy(item => item.Key, Memory.MemoryEncoding.NameComparer)
+                    .OrderBy(item => item.Key, Memory.MemoryAccessInfo.NameComparer)
                     .Select(item => toShort(item.Key, item.Value))
                     .ToList();
 

+ 5 - 23
src/SharpGLTF.Core/Memory/ColorArray.cs

@@ -4,7 +4,7 @@ using System.Numerics;
 using System.Collections;
 using System.Linq;
 
-using BYTES = System.ArraySegment<byte>;
+using BYTES = System.Memory<byte>;
 
 using ENCODING = SharpGLTF.Schema2.EncodingType;
 
@@ -14,28 +14,10 @@ namespace SharpGLTF.Memory
     /// Wraps an encoded <see cref="BYTES"/> and exposes it as an array of <see cref="Vector4"/> values.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("Color4[{Count}]")]
-    public struct ColorArray : IList<Vector4>, IReadOnlyList<Vector4>
+    public readonly struct ColorArray : IList<Vector4>, IReadOnlyList<Vector4>
     {
         #region constructors
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ColorArray"/> struct.
-        /// </summary>
-        /// <param name="source">The array to wrap.</param>
-        /// <param name="byteOffset">The zero-based index of the first <see cref="Byte"/> in <paramref name="source"/>.</param>
-        /// <param name="itemsCount">The number of <see cref="Vector4"/> items in <paramref name="source"/>.</param>
-        /// <param name="byteStride">
-        /// The byte stride between elements.
-        /// If the value is zero, the size of the item is used instead.
-        /// </param>
-        /// <param name="dimensions">The number of elements per item. Currently only values 3 and 4 are supported.</param>
-        /// <param name="encoding">A value of <see cref="ENCODING"/>.</param>
-        /// <param name="normalized">True if values are normalized.</param>
-        /// <param name="defaultW">If <paramref name="dimensions"/> is 3, the W values are filled with this value</param>
-        public ColorArray(Byte[] source, int byteOffset, int itemsCount, int byteStride, int dimensions = 4, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false, Single defaultW = 1)
-            : this(new BYTES(source), byteOffset, itemsCount, byteStride, dimensions, encoding, normalized, defaultW)
-        { }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="ColorArray"/> struct.
         /// </summary>
@@ -81,15 +63,15 @@ namespace SharpGLTF.Memory
         #region data
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private FloatingAccessor _Accessor;
+        private readonly FloatingAccessor _Accessor;
 
         private readonly int _Dimensions;
 
+        private readonly float _DefaultW;
+
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         private Vector4[] _DebugItems => this.ToArray();
 
-        private readonly float _DefaultW;
-
         #endregion
 
         #region API

+ 22 - 150
src/SharpGLTF.Core/Memory/FloatingArrays.cs

@@ -4,7 +4,7 @@ using System.Numerics;
 using System.Collections;
 using System.Linq;
 
-using BYTES = System.ArraySegment<byte>;
+using BYTES = System.Memory<byte>;
 
 using ENCODING = SharpGLTF.Schema2.EncodingType;
 
@@ -13,7 +13,7 @@ namespace SharpGLTF.Memory
     /// <summary>
     /// Wraps an encoded <see cref="BYTES"/> and exposes it as an array of strided <see cref="Single"/> values.
     /// </summary>
-    struct FloatingAccessor
+    readonly struct FloatingAccessor
     {
         private const string ERR_UNSUPPORTEDENCODING = "Unsupported encoding.";
 
@@ -28,10 +28,10 @@ namespace SharpGLTF.Memory
             this._Setter = null;
             this._ByteStride = Math.Max(byteStride, enclen * dimensions);
             this._EncodedLen = enclen;
-            this._ItemCount = this._Data.Count / this._ByteStride;
+            this._ItemCount = this._Data.Length / this._ByteStride;
 
-            // strided buffers usually have room for an extra item
-            if ((_Data.Count % _ByteStride) >= enclen * dimensions) ++_ItemCount;
+            // strided buffers require 4 byte word padding.
+            if ((_Data.Length % _ByteStride) >= enclen * dimensions) ++_ItemCount;
 
             _ItemCount = Math.Min(itemsCount, _ItemCount);
 
@@ -158,13 +158,13 @@ namespace SharpGLTF.Memory
         private T _GetValue<T>(int byteOffset)
             where T : unmanaged
         {
-            return System.Runtime.InteropServices.MemoryMarshal.Read<T>(_Data.AsSpan(byteOffset));
+            return System.Runtime.InteropServices.MemoryMarshal.Read<T>(_Data.Span.Slice(byteOffset));
         }
 
         private void _SetValue<T>(int byteOffset, T value)
             where T : unmanaged
         {
-            System.Runtime.InteropServices.MemoryMarshal.Write<T>(_Data.AsSpan(byteOffset), ref value);
+            System.Runtime.InteropServices.MemoryMarshal.Write<T>(_Data.Span.Slice(byteOffset), ref value);
         }
 
         #endregion
@@ -189,7 +189,7 @@ namespace SharpGLTF.Memory
 
         #region API
 
-        public int ByteLength => _Data.Count;
+        public int ByteLength => _Data.Length;
 
         public int Count => _ItemCount;
 
@@ -220,23 +220,10 @@ namespace SharpGLTF.Memory
     /// Wraps an encoded <see cref="BYTES"/> and exposes it as an <see cref="IList{single}"/>.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("Float[{Count}]")]
-    public struct ScalarArray : IList<Single>, IReadOnlyList<Single>
+    public readonly struct ScalarArray : IList<Single>, IReadOnlyList<Single>
     {
         #region constructors
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ScalarArray"/> struct.
-        /// </summary>
-        /// <param name="source">The array range to wrap.</param>
-        /// <param name="byteStride">
-        /// The byte stride between elements.
-        /// If the value is zero, the size of the item is used instead.
-        /// </param>
-        /// <param name="encoding">A value of <see cref="ENCODING"/>.</param>
-        /// <param name="normalized">True if values are normalized.</param>
-        public ScalarArray(Byte[] source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="ScalarArray"/> struct.
         /// </summary>
@@ -250,21 +237,6 @@ namespace SharpGLTF.Memory
         public ScalarArray(BYTES source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
             : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ScalarArray"/> struct.
-        /// </summary>
-        /// <param name="source">The array to wrap.</param>
-        /// <param name="byteOffset">The zero-based index of the first <see cref="Byte"/> in <paramref name="source"/>.</param>
-        /// <param name="itemsCount">The number of <see cref="Single"/> items in <paramref name="source"/>.</param>
-        /// <param name="byteStride">
-        /// The byte stride between elements.
-        /// If the value is zero, the size of the item is used instead.
-        /// </param>
-        /// <param name="encoding">A value of <see cref="ENCODING"/>.</param>
-        /// <param name="normalized">True if values are normalized.</param>
-        public ScalarArray(Byte[] source, int byteOffset, int itemsCount, int byteStride, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(new BYTES(source), byteOffset, itemsCount, byteStride, encoding, normalized) { }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="ScalarArray"/> struct.
         /// </summary>
@@ -287,7 +259,7 @@ namespace SharpGLTF.Memory
         #region data
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private FloatingAccessor _Accessor;
+        private readonly FloatingAccessor _Accessor;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         private Single[] _DebugItems => this.ToArray();
@@ -344,23 +316,10 @@ namespace SharpGLTF.Memory
     /// Wraps an encoded <see cref="BYTES"/> and exposes it as an <see cref="IList{Vector2}"/>.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("Vector2[{Count}]")]
-    public struct Vector2Array : IList<Vector2>, IReadOnlyList<Vector2>
+    public readonly struct Vector2Array : IList<Vector2>, IReadOnlyList<Vector2>
     {
         #region constructors
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Vector2Array"/> struct.
-        /// </summary>
-        /// <param name="source">The array range to wrap.</param>
-        /// <param name="byteStride">
-        /// The byte stride between elements.
-        /// If the value is zero, the size of the item is used instead.
-        /// </param>
-        /// <param name="encoding">A value of <see cref="ENCODING"/>.</param>
-        /// <param name="normalized">True if values are normalized.</param>
-        public Vector2Array(Byte[] source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="Vector2Array"/> struct.
         /// </summary>
@@ -374,21 +333,6 @@ namespace SharpGLTF.Memory
         public Vector2Array(BYTES source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
             : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Vector2Array"/> struct.
-        /// </summary>
-        /// <param name="source">The array range to wrap.</param>
-        /// <param name="byteOffset">The zero-based index of the first <see cref="Byte"/> in <paramref name="source"/>.</param>
-        /// <param name="itemsCount">>The number of <see cref="Vector2"/> items in <paramref name="source"/>.</param>
-        /// <param name="byteStride">
-        /// The byte stride between elements.
-        /// If the value is zero, the size of the item is used instead.
-        /// </param>
-        /// <param name="encoding">A value of <see cref="ENCODING"/>.</param>
-        /// <param name="normalized">True if values are normalized.</param>
-        public Vector2Array(Byte[] source, int byteOffset, int itemsCount, int byteStride, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(new BYTES(source), byteOffset, itemsCount, byteStride, encoding, normalized) { }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="Vector2Array"/> struct.
         /// </summary>
@@ -411,7 +355,7 @@ namespace SharpGLTF.Memory
         #region data
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private FloatingAccessor _Accessor;
+        private readonly FloatingAccessor _Accessor;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         private Vector2[] _DebugItems => this.ToArray();
@@ -476,23 +420,10 @@ namespace SharpGLTF.Memory
     /// Wraps an encoded <see cref="BYTES"/> and exposes it as an <see cref="IList{Vector3}"/>.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("Vector3[{Count}]")]
-    public struct Vector3Array : IList<Vector3>, IReadOnlyList<Vector3>
+    public readonly struct Vector3Array : IList<Vector3>, IReadOnlyList<Vector3>
     {
         #region constructors
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Vector3Array"/> struct.
-        /// </summary>
-        /// <param name="source">The array range to wrap.</param>
-        /// <param name="byteStride">
-        /// The byte stride between elements.
-        /// If the value is zero, the size of the item is used instead.
-        /// </param>
-        /// <param name="encoding">A value of <see cref="ENCODING"/>.</param>
-        /// <param name="normalized">True if values are normalized.</param>
-        public Vector3Array(Byte[] source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="Vector3Array"/> struct.
         /// </summary>
@@ -506,21 +437,6 @@ namespace SharpGLTF.Memory
         public Vector3Array(BYTES source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
             : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Vector3Array"/> struct.
-        /// </summary>
-        /// <param name="source">The array to wrap.</param>
-        /// <param name="byteOffset">The zero-based index of the first <see cref="Byte"/> in <paramref name="source"/>.</param>
-        /// <param name="itemsCount">The number of <see cref="Vector3"/> items in <paramref name="source"/>.</param>
-        /// <param name="byteStride">
-        /// The byte stride between elements.
-        /// If the value is zero, the size of the item is used instead.
-        /// </param>
-        /// <param name="encoding">A value of <see cref="ENCODING"/>.</param>
-        /// <param name="normalized">True if values are normalized.</param>
-        public Vector3Array(Byte[] source, int byteOffset, int itemsCount, int byteStride, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(new BYTES(source), byteOffset, itemsCount, byteStride, encoding, normalized) { }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="Vector3Array"/> struct.
         /// </summary>
@@ -543,7 +459,7 @@ namespace SharpGLTF.Memory
         #region data
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private FloatingAccessor _Accessor;
+        private readonly FloatingAccessor _Accessor;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         private Vector3[] _DebugItems => this.ToArray();
@@ -609,23 +525,10 @@ namespace SharpGLTF.Memory
     /// Wraps an encoded <see cref="BYTES"/> and exposes it as an <see cref="IList{Vector4}"/>.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("Vector4[{Count}]")]
-    public struct Vector4Array : IList<Vector4>, IReadOnlyList<Vector4>
+    public readonly struct Vector4Array : IList<Vector4>, IReadOnlyList<Vector4>
     {
         #region constructors
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Vector4Array"/> struct.
-        /// </summary>
-        /// <param name="source">The array range to wrap.</param>
-        /// <param name="byteStride">
-        /// The byte stride between elements.
-        /// If the value is zero, the size of the item is used instead.
-        /// </param>
-        /// <param name="encoding">A value of <see cref="ENCODING"/>.</param>
-        /// <param name="normalized">True if values are normalized.</param>
-        public Vector4Array(Byte[] source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="Vector4Array"/> struct.
         /// </summary>
@@ -639,21 +542,6 @@ namespace SharpGLTF.Memory
         public Vector4Array(BYTES source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
             : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Vector4Array"/> struct.
-        /// </summary>
-        /// <param name="source">The array to wrap.</param>
-        /// <param name="byteOffset">The zero-based index of the first <see cref="Byte"/> in <paramref name="source"/>.</param>
-        /// <param name="itemsCount">The number of <see cref="Vector3"/> items in <paramref name="source"/>.</param>
-        /// <param name="byteStride">
-        /// The byte stride between elements.
-        /// If the value is zero, the size of the item is used instead.
-        /// </param>
-        /// <param name="encoding">A value of <see cref="ENCODING"/>.</param>
-        /// <param name="normalized">True if values are normalized.</param>
-        public Vector4Array(Byte[] source, int byteOffset, int itemsCount, int byteStride, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(new BYTES(source), byteOffset, itemsCount, byteStride, encoding, normalized) { }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="Vector4Array"/> struct.
         /// </summary>
@@ -676,7 +564,7 @@ namespace SharpGLTF.Memory
         #region data
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private FloatingAccessor _Accessor;
+        private readonly FloatingAccessor _Accessor;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         private Vector4[] _DebugItems => this.ToArray();
@@ -743,19 +631,13 @@ namespace SharpGLTF.Memory
     /// Wraps an encoded <see cref="BYTES"/> and exposes it as an <see cref="IList{Quaternion}"/>.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("Quaternion[{Count}]")]
-    public struct QuaternionArray : IList<Quaternion>, IReadOnlyList<Quaternion>
+    public readonly struct QuaternionArray : IList<Quaternion>, IReadOnlyList<Quaternion>
     {
         #region constructors
 
-        public QuaternionArray(Byte[] source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
-
         public QuaternionArray(BYTES source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
             : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
 
-        public QuaternionArray(Byte[] source, int byteOffset, int itemsCount, int byteStride, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(new BYTES(source), byteOffset, itemsCount, byteStride, encoding, normalized) { }
-
         public QuaternionArray(BYTES source, int byteOffset, int itemsCount, int byteStride, ENCODING encoding, Boolean normalized)
         {
             _Accessor = new FloatingAccessor(source, byteOffset, itemsCount, byteStride, 4, encoding, normalized);
@@ -766,7 +648,7 @@ namespace SharpGLTF.Memory
         #region data
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private FloatingAccessor _Accessor;
+        private readonly FloatingAccessor _Accessor;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         private Quaternion[] _DebugItems => this.ToArray();
@@ -833,19 +715,13 @@ namespace SharpGLTF.Memory
     /// Wraps an encoded <see cref="BYTES"/> and exposes it as an <see cref="IList{Matrix4x4}"/>.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("Matrix4x4[{Count}]")]
-    public struct Matrix4x4Array : IList<Matrix4x4>, IReadOnlyList<Matrix4x4>
+    public readonly struct Matrix4x4Array : IList<Matrix4x4>, IReadOnlyList<Matrix4x4>
     {
         #region constructors
 
-        public Matrix4x4Array(byte[] source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
-
         public Matrix4x4Array(BYTES source, int byteStride = 0, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
             : this(source, 0, int.MaxValue, byteStride, encoding, normalized) { }
 
-        public Matrix4x4Array(Byte[] source, int byteOffset, int itemsCount, int byteStride, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(new BYTES(source), byteOffset, itemsCount, byteStride, encoding, normalized) { }
-
         public Matrix4x4Array(BYTES source, int byteOffset, int itemsCount, int byteStride, ENCODING encoding, Boolean normalized)
         {
             _Accessor = new FloatingAccessor(source, byteOffset, itemsCount, byteStride, 16, encoding, normalized);
@@ -856,7 +732,7 @@ namespace SharpGLTF.Memory
         #region data
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private FloatingAccessor _Accessor;
+        private readonly FloatingAccessor _Accessor;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         private Matrix4x4[] _DebugItems => this.ToArray();
@@ -941,13 +817,9 @@ namespace SharpGLTF.Memory
     /// Wraps an encoded <see cref="BYTES"/> and exposes it as an IList{Single[]}/>.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("Float[][{Count}]")]
-    public struct MultiArray : IList<Single[]>, IReadOnlyList<Single[]>
+    public readonly struct MultiArray : IList<Single[]>, IReadOnlyList<Single[]>
     {
         #region constructors
-
-        public MultiArray(Byte[] source, int byteOffset, int itemsCount, int byteStride, int dimensions, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
-            : this(new BYTES(source), byteOffset, itemsCount, byteStride, dimensions, encoding, normalized) { }
-
         public MultiArray(BYTES source, int byteOffset, int itemsCount, int byteStride, int dimensions, ENCODING encoding, Boolean normalized)
         {
             _Dimensions = dimensions;
@@ -962,7 +834,7 @@ namespace SharpGLTF.Memory
         private readonly int _Dimensions;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private FloatingAccessor _Accessor;
+        private readonly FloatingAccessor _Accessor;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         private Single[][] _DebugItems => this.ToArray();

+ 5 - 23
src/SharpGLTF.Core/Memory/IntegerArrays.cs

@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using System.Collections;
 using System.Linq;
 
-using BYTES = System.ArraySegment<byte>;
+using BYTES = System.Memory<byte>;
 
 using ENCODING = SharpGLTF.Schema2.IndexEncodingType;
 
@@ -13,18 +13,10 @@ namespace SharpGLTF.Memory
     /// Wraps an encoded <see cref="BYTES"/> and exposes it as an <see cref="IList{UInt32}"/>.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("Integer[{Count}]")]
-    public struct IntegerArray : IList<UInt32>, IReadOnlyList<UInt32>
+    public readonly struct IntegerArray : IList<UInt32>, IReadOnlyList<UInt32>
     {
         #region constructors
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="IntegerArray"/> struct.
-        /// </summary>
-        /// <param name="source">The array range to wrap.</param>
-        /// <param name="encoding">Byte encoding.</param>
-        public IntegerArray(Byte[] source, ENCODING encoding = ENCODING.UNSIGNED_INT)
-            : this(source, 0, int.MaxValue, encoding) { }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="IntegerArray"/> struct.
         /// </summary>
@@ -33,16 +25,6 @@ namespace SharpGLTF.Memory
         public IntegerArray(BYTES source, ENCODING encoding = ENCODING.UNSIGNED_INT)
             : this(source, 0, int.MaxValue, encoding) { }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="IntegerArray"/> struct.
-        /// </summary>
-        /// <param name="source">The array to wrap.</param>
-        /// <param name="byteOffset">The zero-based index of the first <see cref="Byte"/> in <paramref name="source"/>.</param>
-        /// <param name="itemsCount">The number of <see cref="UInt32"/> items in <paramref name="source"/>.</param>
-        /// <param name="encoding">Byte encoding.</param>
-        public IntegerArray(Byte[] source, int byteOffset, int itemsCount, ENCODING encoding)
-            : this(new BYTES(source), byteOffset, itemsCount, encoding) { }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="IntegerArray"/> struct.
         /// </summary>
@@ -95,13 +77,13 @@ namespace SharpGLTF.Memory
         private T _GetValue<T>(int index)
             where T : unmanaged
         {
-            return System.Runtime.InteropServices.MemoryMarshal.Cast<Byte, T>(_Data)[index];
+            return System.Runtime.InteropServices.MemoryMarshal.Cast<Byte, T>(_Data.Span)[index];
         }
 
         private void _SetValue<T>(int index, T value)
             where T : unmanaged
         {
-            System.Runtime.InteropServices.MemoryMarshal.Cast<Byte, T>(_Data)[index] = value;
+            System.Runtime.InteropServices.MemoryMarshal.Cast<Byte, T>(_Data.Span)[index] = value;
         }
 
         #endregion
@@ -135,7 +117,7 @@ namespace SharpGLTF.Memory
         /// Gets the number of elements in the range delimited by the <see cref="IntegerArray"/>
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        public int Count => _Data.Count / _ByteStride;
+        public int Count => _Data.Length / _ByteStride;
 
         bool ICollection<UInt32>.IsReadOnly => false;
 

+ 230 - 0
src/SharpGLTF.Core/Memory/MemoryAccessor.Validation.cs

@@ -0,0 +1,230 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using DIMENSIONS = SharpGLTF.Schema2.DimensionType;
+using ENCODING = SharpGLTF.Schema2.EncodingType;
+
+namespace SharpGLTF.Memory
+{
+    partial class MemoryAccessor
+    {
+        #region helpers
+
+        public static void SanitizeVertexAttributes(MemoryAccessor[] vertexAccessors)
+        {
+            System.Diagnostics.Debug.Assert(vertexAccessors.All(item => !string.IsNullOrWhiteSpace(item.Attribute.Name)), nameof(vertexAccessors));
+
+            // https://github.com/KhronosGroup/glTF/pull/1749
+
+            var weights0 = vertexAccessors.FirstOrDefault(item => item.Attribute.Name == "WEIGHTS_0");
+            var weights1 = vertexAccessors.FirstOrDefault(item => item.Attribute.Name == "WEIGHTS_1");
+            SanitizeWeightsSum(weights0, weights1);
+        }
+
+        #endregion
+
+        #region sanitize weights sum
+
+        public static void SanitizeWeightsSum(MemoryAccessor weights0, MemoryAccessor weights1)
+        {
+            if (weights1 == null)
+            {
+                if (weights0 == null) return;
+
+                foreach (var item in weights0.GetItemsAsRawBytes())
+                {
+                    _SanitizeWeightSum(item, weights0.Attribute.Encoding);
+                }
+
+                return;
+            }
+
+            if (weights0 == null) return;
+
+            var len = weights0.Attribute.ItemByteLength;
+            Span<Byte> dst = stackalloc byte[len * 2];
+
+            var zip = weights0.GetItemsAsRawBytes().Zip(weights1.GetItemsAsRawBytes(), (a, b) => (a, b));
+
+            foreach (var (a, b) in zip)
+            {
+                a.AsSpan().CopyTo(dst);
+                b.AsSpan().CopyTo(dst.Slice(len));
+
+                if (_SanitizeWeightSum(dst, weights0.Attribute.Encoding))
+                {
+                    dst.Slice(0, len).CopyTo(a);
+                    dst.Slice(len, len).CopyTo(b);
+                }
+            }
+        }
+
+        private static bool _SanitizeWeightSum(Span<byte> dst, ENCODING encoding)
+        {
+            if (encoding == ENCODING.UNSIGNED_BYTE)
+            {
+                var weights = dst;
+
+                int r = 0;
+                for (int j = 0; j < weights.Length; ++j) r += weights[j];
+                if (r == 255) return false;
+
+                weights[0] += (Byte)(255 - r);
+
+                return true;
+            }
+
+            if (encoding == ENCODING.UNSIGNED_SHORT)
+            {
+                var weights = System.Runtime.InteropServices.MemoryMarshal.Cast<Byte, UInt16>(dst);
+
+                int r = 0;
+                for (int j = 0; j < weights.Length; ++j) r += weights[j];
+
+                if (r == 65535) return false;
+
+                weights[0] += (Byte)(65535 - r);
+
+                return true;
+            }
+
+            if (encoding == ENCODING.FLOAT)
+            {
+                var weights = System.Runtime.InteropServices.MemoryMarshal.Cast<Byte, Single>(dst);
+
+                float nonZero = 0;
+                float sum = 0;
+
+                for (int j = 0; j < weights.Length; ++j)
+                {
+                    var w = weights[j];
+
+                    if (float.IsNaN(w)) return false;
+                    if (w < 0 || w > 1) return false;
+
+                    if (w > 0)
+                    {
+                        sum += w;
+                        nonZero += 1;
+                    }
+                }
+
+                float err = 2e-7f * nonZero;
+
+                if (Math.Abs(sum - 1) <= err) return false;
+
+                for (int j = 0; j < weights.Length; ++j)
+                {
+                    weights[j] /= sum;
+                }
+
+                return true;
+            }
+
+            return false;
+        }
+
+        #endregion
+
+        #region validate weights sum
+
+        public static void ValidateWeightsSum(Validation.ValidationContext result, MemoryAccessor weights0, MemoryAccessor weights1)
+        {
+            int idx = 0;
+
+            if (weights1 == null)
+            {
+                if (weights0 == null) return;
+
+                foreach (var item in weights0.GetItemsAsRawBytes())
+                {
+                    if (!_CheckWeightSum(item, weights0.Attribute.Encoding))
+                    {
+                        result.AddDataError($"Weight Sum invalid at Index {idx}");
+                    }
+
+                    ++idx;
+                }
+
+                return;
+            }
+
+            if (weights0 == null)
+            {
+                result.AddLinkError("");
+                return;
+            }
+
+            var len = weights0.Attribute.ItemByteLength;
+            Span<Byte> dst = stackalloc byte[len * 2];
+
+            var zip = weights0.GetItemsAsRawBytes().Zip(weights1.GetItemsAsRawBytes(), (a, b) => (a, b));
+
+            foreach (var (a, b) in zip)
+            {
+                a.AsSpan().CopyTo(dst);
+                b.AsSpan().CopyTo(dst.Slice(len));
+
+                if (!_CheckWeightSum(dst, weights0.Attribute.Encoding))
+                {
+                    result.AddDataError($"Weight Sum invalid at Index {idx}");
+                }
+
+                ++idx;
+            }
+        }
+
+        private static bool _CheckWeightSum(Span<byte> dst, ENCODING encoding)
+        {
+            if (encoding == ENCODING.UNSIGNED_BYTE)
+            {
+                var weights = dst;
+
+                int r = 0;
+                for (int j = 0; j < weights.Length; ++j) r += weights[j];
+                return r == 255;
+            }
+
+            if (encoding == ENCODING.UNSIGNED_SHORT)
+            {
+                var weights = System.Runtime.InteropServices.MemoryMarshal.Cast<Byte, UInt16>(dst);
+
+                int r = 0;
+                for (int j = 0; j < weights.Length; ++j) r += weights[j];
+                return r == 65535;
+            }
+
+            if (encoding == ENCODING.FLOAT)
+            {
+                var weights = System.Runtime.InteropServices.MemoryMarshal.Cast<Byte, Single>(dst);
+
+                float nonZero = 0;
+                float sum = 0;
+
+                for (int j = 0; j < weights.Length; ++j)
+                {
+                    var w = weights[j];
+
+                    if (float.IsNaN(w)) return false;
+                    if (w < 0 || w > 1) return false;
+
+                    if (w > 0)
+                    {
+                        sum += w;
+                        nonZero += 1;
+                    }
+                }
+
+                float err = 2e-7f * nonZero;
+
+                return Math.Abs(sum - 1) <= err;
+            }
+
+            return false;
+        }
+
+        #endregion
+    }
+}

+ 145 - 88
src/SharpGLTF.Core/Memory/MemoryAccessor.cs

@@ -6,13 +6,15 @@ using System.Numerics;
 using DIMENSIONS = SharpGLTF.Schema2.DimensionType;
 using ENCODING = SharpGLTF.Schema2.EncodingType;
 
+using BYTES = System.ArraySegment<System.Byte>;
+
 namespace SharpGLTF.Memory
 {
     /// <summary>
-    /// Defines the memory encoding pattern for an arbitrary <see cref="ArraySegment{Byte}"/>.
+    /// Defines the memory encoding pattern for an arbitrary <see cref="BYTES"/>.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
-    public struct MemoryEncoding
+    public struct MemoryAccessInfo
     {
         #region debug
 
@@ -25,42 +27,42 @@ namespace SharpGLTF.Memory
 
         #region constructor
 
-        public static MemoryEncoding[] Create(params string[] attributes)
+        public static MemoryAccessInfo[] Create(params string[] attributes)
         {
             return attributes.Select(item => CreateDefaultElement(item)).ToArray();
         }
 
-        public static MemoryEncoding CreateDefaultElement(string attribute)
+        public static MemoryAccessInfo CreateDefaultElement(string attribute)
         {
             switch (attribute)
             {
-                case "INDEX": return new MemoryEncoding("INDEX", 0, 0, 0, DIMENSIONS.SCALAR, ENCODING.UNSIGNED_INT, false);
+                case "INDEX": return new MemoryAccessInfo("INDEX", 0, 0, 0, DIMENSIONS.SCALAR, ENCODING.UNSIGNED_INT, false);
 
-                case "POSITION": return new MemoryEncoding("POSITION", 0, 0, 0, DIMENSIONS.VEC3);
-                case "NORMAL": return new MemoryEncoding("NORMAL", 0, 0, 0, DIMENSIONS.VEC3);
-                case "TANGENT": return new MemoryEncoding("TANGENT", 0, 0, 0, DIMENSIONS.VEC4);
+                case "POSITION": return new MemoryAccessInfo("POSITION", 0, 0, 0, DIMENSIONS.VEC3);
+                case "NORMAL": return new MemoryAccessInfo("NORMAL", 0, 0, 0, DIMENSIONS.VEC3);
+                case "TANGENT": return new MemoryAccessInfo("TANGENT", 0, 0, 0, DIMENSIONS.VEC4);
 
-                case "TEXCOORD_0": return new MemoryEncoding("TEXCOORD_0", 0, 0, 0, DIMENSIONS.VEC2);
-                case "TEXCOORD_1": return new MemoryEncoding("TEXCOORD_1", 0, 0, 0, DIMENSIONS.VEC2);
-                case "TEXCOORD_2": return new MemoryEncoding("TEXCOORD_2", 0, 0, 0, DIMENSIONS.VEC2);
-                case "TEXCOORD_3": return new MemoryEncoding("TEXCOORD_3", 0, 0, 0, DIMENSIONS.VEC2);
+                case "TEXCOORD_0": return new MemoryAccessInfo("TEXCOORD_0", 0, 0, 0, DIMENSIONS.VEC2);
+                case "TEXCOORD_1": return new MemoryAccessInfo("TEXCOORD_1", 0, 0, 0, DIMENSIONS.VEC2);
+                case "TEXCOORD_2": return new MemoryAccessInfo("TEXCOORD_2", 0, 0, 0, DIMENSIONS.VEC2);
+                case "TEXCOORD_3": return new MemoryAccessInfo("TEXCOORD_3", 0, 0, 0, DIMENSIONS.VEC2);
 
-                case "COLOR_0": return new MemoryEncoding("COLOR_0", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
-                case "COLOR_1": return new MemoryEncoding("COLOR_1", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
-                case "COLOR_2": return new MemoryEncoding("COLOR_2", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
-                case "COLOR_3": return new MemoryEncoding("COLOR_3", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
+                case "COLOR_0": return new MemoryAccessInfo("COLOR_0", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
+                case "COLOR_1": return new MemoryAccessInfo("COLOR_1", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
+                case "COLOR_2": return new MemoryAccessInfo("COLOR_2", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
+                case "COLOR_3": return new MemoryAccessInfo("COLOR_3", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
 
-                case "JOINTS_0": return new MemoryEncoding("JOINTS_0", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE);
-                case "JOINTS_1": return new MemoryEncoding("JOINTS_1", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE);
+                case "JOINTS_0": return new MemoryAccessInfo("JOINTS_0", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE);
+                case "JOINTS_1": return new MemoryAccessInfo("JOINTS_1", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE);
 
-                case "WEIGHTS_0": return new MemoryEncoding("WEIGHTS_0", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
-                case "WEIGHTS_1": return new MemoryEncoding("WEIGHTS_1", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
+                case "WEIGHTS_0": return new MemoryAccessInfo("WEIGHTS_0", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
+                case "WEIGHTS_1": return new MemoryAccessInfo("WEIGHTS_1", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
             }
 
             throw new NotImplementedException();
         }
 
-        public MemoryEncoding(string name, int byteOffset, int itemsCount, int byteStride, DIMENSIONS dimensions, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
+        public MemoryAccessInfo(string name, int byteOffset, int itemsCount, int byteStride, DIMENSIONS dimensions, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
         {
             this.Name = name;
             this.ByteOffset = byteOffset;
@@ -71,7 +73,7 @@ namespace SharpGLTF.Memory
             this.Normalized = normalized;
         }
 
-        public MemoryEncoding Slice(int itemStart, int itemCount)
+        public MemoryAccessInfo Slice(int itemStart, int itemCount)
         {
             var stride = _GetRowByteLength();
 
@@ -86,12 +88,39 @@ namespace SharpGLTF.Memory
 
         #region data
 
+        /// <summary>
+        /// If set, it can be used to identify the data with an attribute name: POSITION, NORMAL, etc
+        /// </summary>
         public String Name;
+
+        /// <summary>
+        /// number of bytes to advance to the beginning of the first item.
+        /// </summary>
         public int ByteOffset;
+
+        /// <summary>
+        /// Total number of items
+        /// </summary>
         public int ItemsCount;
+
+        /// <summary>
+        /// number of bytes to advance to the beginning of the next item
+        /// </summary>
         public int ByteStride;
+
+        /// <summary>
+        /// number of sub-elements of each item.
+        /// </summary>
         public DIMENSIONS Dimensions;
+
+        /// <summary>
+        /// byte encoding of sub-elements of each item.
+        /// </summary>
         public ENCODING Encoding;
+
+        /// <summary>
+        /// normalization of sub-elements of each item.
+        /// </summary>
         public Boolean Normalized;
 
         #endregion
@@ -103,6 +132,8 @@ namespace SharpGLTF.Memory
         /// </summary>
         public int PaddedByteLength => _GetRowByteLength();
 
+        public int ItemByteLength => _GetItemByteLength();
+
         public Boolean IsValidVertexAttribute
         {
             get
@@ -142,7 +173,7 @@ namespace SharpGLTF.Memory
 
         #region API
 
-        private int _GetRowByteLength()
+        private int _GetItemByteLength()
         {
             var xlen = Encoding.ByteLength();
 
@@ -152,10 +183,15 @@ namespace SharpGLTF.Memory
                 xlen = xlen.WordPadded();
             }
 
-            return Math.Max(ByteStride, xlen);
+            return xlen;
+        }
+
+        private int _GetRowByteLength()
+        {
+            return Math.Max(ByteStride, _GetItemByteLength());
         }
 
-        public static int SetInterleavedInfo(MemoryEncoding[] attributes, int byteOffset, int itemsCount)
+        public static int SetInterleavedInfo(MemoryAccessInfo[] attributes, int byteOffset, int itemsCount)
         {
             Guard.NotNull(attributes, nameof(attributes));
 
@@ -186,11 +222,11 @@ namespace SharpGLTF.Memory
             return byteStride;
         }
 
-        public static MemoryEncoding[] Slice(MemoryEncoding[] attributes, int start, int count)
+        public static MemoryAccessInfo[] Slice(MemoryAccessInfo[] attributes, int start, int count)
         {
             Guard.NotNull(attributes, nameof(attributes));
 
-            var dst = new MemoryEncoding[attributes.Length];
+            var dst = new MemoryAccessInfo[attributes.Length];
 
             for (int i = 0; i < dst.Length; ++i)
             {
@@ -250,22 +286,31 @@ namespace SharpGLTF.Memory
     }
 
     /// <summary>
-    /// Wraps a <see cref="ArraySegment{Byte}"/> decoding it and exposing its content as arrays of different types.
+    /// Wraps a <see cref="BYTES"/> decoding it and exposing its content as arrays of different types.
     /// </summary>
-    [System.Diagnostics.DebuggerDisplay("{Attribute._GetDebuggerDisplay(),nq}")]
-    public sealed class MemoryAccessor
+    [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
+    public sealed partial class MemoryAccessor
     {
+        #region debug
+
+        internal string _GetDebuggerDisplay()
+        {
+            return _Slicer._GetDebuggerDisplay();
+        }
+
+        #endregion
+
         #region constructor
 
-        public MemoryAccessor(ArraySegment<Byte> data, MemoryEncoding info)
+        public MemoryAccessor(BYTES data, MemoryAccessInfo info)
         {
-            this._Encoding = info;
+            this._Slicer = info;
             this._Data = data;
         }
 
-        public MemoryAccessor(MemoryEncoding info)
+        public MemoryAccessor(MemoryAccessInfo info)
         {
-            this._Encoding = info;
+            this._Slicer = info;
             this._Data = default;
         }
 
@@ -273,10 +318,10 @@ namespace SharpGLTF.Memory
         {
             Guard.NotNull(bottom, nameof(bottom));
             Guard.NotNull(topValues, nameof(topValues));
-            Guard.IsTrue(bottom._Encoding.Dimensions == topValues._Encoding.Dimensions, nameof(topValues));
-            Guard.IsTrue(topKeys.Count <= bottom._Encoding.ItemsCount, nameof(topKeys));
-            Guard.IsTrue(topKeys.Count == topValues._Encoding.ItemsCount, nameof(topValues));
-            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Encoding.ItemsCount), nameof(topKeys));
+            Guard.IsTrue(bottom._Slicer.Dimensions == topValues._Slicer.Dimensions, nameof(topValues));
+            Guard.IsTrue(topKeys.Count <= bottom._Slicer.ItemsCount, nameof(topKeys));
+            Guard.IsTrue(topKeys.Count == topValues._Slicer.ItemsCount, nameof(topValues));
+            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Slicer.ItemsCount), nameof(topKeys));
 
             return new SparseArray<Single>(bottom.AsScalarArray(), topValues.AsScalarArray(), topKeys);
         }
@@ -285,10 +330,10 @@ namespace SharpGLTF.Memory
         {
             Guard.NotNull(bottom, nameof(bottom));
             Guard.NotNull(topValues, nameof(topValues));
-            Guard.IsTrue(bottom._Encoding.Dimensions == topValues._Encoding.Dimensions, nameof(topValues));
-            Guard.IsTrue(topKeys.Count <= bottom._Encoding.ItemsCount, nameof(topKeys));
-            Guard.IsTrue(topKeys.Count == topValues._Encoding.ItemsCount, nameof(topValues));
-            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Encoding.ItemsCount), nameof(topKeys));
+            Guard.IsTrue(bottom._Slicer.Dimensions == topValues._Slicer.Dimensions, nameof(topValues));
+            Guard.IsTrue(topKeys.Count <= bottom._Slicer.ItemsCount, nameof(topKeys));
+            Guard.IsTrue(topKeys.Count == topValues._Slicer.ItemsCount, nameof(topValues));
+            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Slicer.ItemsCount), nameof(topKeys));
 
             return new SparseArray<Vector2>(bottom.AsVector2Array(), topValues.AsVector2Array(), topKeys);
         }
@@ -297,10 +342,10 @@ namespace SharpGLTF.Memory
         {
             Guard.NotNull(bottom, nameof(bottom));
             Guard.NotNull(topValues, nameof(topValues));
-            Guard.IsTrue(bottom._Encoding.Dimensions == topValues._Encoding.Dimensions, nameof(topValues));
-            Guard.IsTrue(topKeys.Count <= bottom._Encoding.ItemsCount, nameof(topKeys));
-            Guard.IsTrue(topKeys.Count == topValues._Encoding.ItemsCount, nameof(topValues));
-            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Encoding.ItemsCount), nameof(topKeys));
+            Guard.IsTrue(bottom._Slicer.Dimensions == topValues._Slicer.Dimensions, nameof(topValues));
+            Guard.IsTrue(topKeys.Count <= bottom._Slicer.ItemsCount, nameof(topKeys));
+            Guard.IsTrue(topKeys.Count == topValues._Slicer.ItemsCount, nameof(topValues));
+            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Slicer.ItemsCount), nameof(topKeys));
 
             return new SparseArray<Vector3>(bottom.AsVector3Array(), topValues.AsVector3Array(), topKeys);
         }
@@ -309,10 +354,10 @@ namespace SharpGLTF.Memory
         {
             Guard.NotNull(bottom, nameof(bottom));
             Guard.NotNull(topValues, nameof(topValues));
-            Guard.IsTrue(bottom._Encoding.Dimensions == topValues._Encoding.Dimensions, nameof(topValues));
-            Guard.IsTrue(topKeys.Count <= bottom._Encoding.ItemsCount, nameof(topKeys));
-            Guard.IsTrue(topKeys.Count == topValues._Encoding.ItemsCount, nameof(topValues));
-            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Encoding.ItemsCount), nameof(topKeys));
+            Guard.IsTrue(bottom._Slicer.Dimensions == topValues._Slicer.Dimensions, nameof(topValues));
+            Guard.IsTrue(topKeys.Count <= bottom._Slicer.ItemsCount, nameof(topKeys));
+            Guard.IsTrue(topKeys.Count == topValues._Slicer.ItemsCount, nameof(topValues));
+            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Slicer.ItemsCount), nameof(topKeys));
 
             return new SparseArray<Vector4>(bottom.AsVector4Array(), topValues.AsVector4Array(), topKeys);
         }
@@ -321,10 +366,10 @@ namespace SharpGLTF.Memory
         {
             Guard.NotNull(bottom, nameof(bottom));
             Guard.NotNull(topValues, nameof(topValues));
-            Guard.IsTrue(bottom._Encoding.Dimensions == topValues._Encoding.Dimensions, nameof(topValues));
-            Guard.IsTrue(topKeys.Count <= bottom._Encoding.ItemsCount, nameof(topKeys));
-            Guard.IsTrue(topKeys.Count == topValues._Encoding.ItemsCount, nameof(topValues));
-            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Encoding.ItemsCount), nameof(topKeys));
+            Guard.IsTrue(bottom._Slicer.Dimensions == topValues._Slicer.Dimensions, nameof(topValues));
+            Guard.IsTrue(topKeys.Count <= bottom._Slicer.ItemsCount, nameof(topKeys));
+            Guard.IsTrue(topKeys.Count == topValues._Slicer.ItemsCount, nameof(topValues));
+            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Slicer.ItemsCount), nameof(topKeys));
 
             return new SparseArray<Vector4>(bottom.AsColorArray(defaultW), topValues.AsColorArray(defaultW), topKeys);
         }
@@ -334,91 +379,103 @@ namespace SharpGLTF.Memory
         #region data
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private MemoryEncoding _Encoding;
+        private MemoryAccessInfo _Slicer;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private ArraySegment<Byte> _Data;
+        private BYTES _Data;
 
         #endregion
 
         #region properties
 
-        public MemoryEncoding Attribute => _Encoding;
+        public MemoryAccessInfo Attribute => _Slicer;
 
-        public ArraySegment<Byte> Data => _Data;
+        public BYTES Data => _Data;
 
         #endregion
 
         #region API
 
-        public void Update(ArraySegment<Byte> data, MemoryEncoding info)
+        public void Update(BYTES data, MemoryAccessInfo encoding)
         {
-            this._Encoding = info;
+            this._Slicer = encoding;
             this._Data = data;
         }
 
         public IntegerArray AsIntegerArray()
         {
-            Guard.IsTrue(_Encoding.IsValidIndexer, nameof(_Encoding));
-            Guard.IsTrue(_Encoding.Dimensions == DIMENSIONS.SCALAR, nameof(_Encoding));
-            return new IntegerArray(_Data, _Encoding.ByteOffset, _Encoding.ItemsCount, _Encoding.Encoding.ToIndex());
+            Guard.IsTrue(_Slicer.IsValidIndexer, nameof(_Slicer));
+            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.SCALAR, nameof(_Slicer));
+            return new IntegerArray(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.Encoding.ToIndex());
         }
 
         public ScalarArray AsScalarArray()
         {
-            Guard.IsTrue(_Encoding.IsValidVertexAttribute, nameof(_Encoding));
-            Guard.IsTrue(_Encoding.Dimensions == DIMENSIONS.SCALAR, nameof(_Encoding));
-            return new ScalarArray(_Data, _Encoding.ByteOffset, _Encoding.ItemsCount, _Encoding.ByteStride, _Encoding.Encoding, _Encoding.Normalized);
+            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
+            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.SCALAR, nameof(_Slicer));
+            return new ScalarArray(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
         }
 
         public Vector2Array AsVector2Array()
         {
-            Guard.IsTrue(_Encoding.IsValidVertexAttribute, nameof(_Encoding));
-            Guard.IsTrue(_Encoding.Dimensions == DIMENSIONS.VEC2, nameof(_Encoding));
-            return new Vector2Array(_Data, _Encoding.ByteOffset, _Encoding.ItemsCount, _Encoding.ByteStride, _Encoding.Encoding, _Encoding.Normalized);
+            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
+            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.VEC2, nameof(_Slicer));
+            return new Vector2Array(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
         }
 
         public Vector3Array AsVector3Array()
         {
-            Guard.IsTrue(_Encoding.IsValidVertexAttribute, nameof(_Encoding));
-            Guard.IsTrue(_Encoding.Dimensions == DIMENSIONS.VEC3, nameof(_Encoding));
-            return new Vector3Array(_Data, _Encoding.ByteOffset, _Encoding.ItemsCount, _Encoding.ByteStride, _Encoding.Encoding, _Encoding.Normalized);
+            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
+            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.VEC3, nameof(_Slicer));
+            return new Vector3Array(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
         }
 
         public Vector4Array AsVector4Array()
         {
-            Guard.IsTrue(_Encoding.IsValidVertexAttribute, nameof(_Encoding));
-            Guard.IsTrue(_Encoding.Dimensions == DIMENSIONS.VEC4, nameof(_Encoding));
-            return new Vector4Array(_Data, _Encoding.ByteOffset, _Encoding.ItemsCount, _Encoding.ByteStride, _Encoding.Encoding, _Encoding.Normalized);
+            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
+            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.VEC4, nameof(_Slicer));
+            return new Vector4Array(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
         }
 
         public ColorArray AsColorArray(Single defaultW = 1)
         {
-            Guard.IsTrue(_Encoding.IsValidVertexAttribute, nameof(_Encoding));
-            Guard.IsTrue(_Encoding.Dimensions == DIMENSIONS.VEC3 || _Encoding.Dimensions == DIMENSIONS.VEC4, nameof(_Encoding));
-            return new ColorArray(_Data, _Encoding.ByteOffset, _Encoding.ItemsCount, _Encoding.ByteStride, _Encoding.Dimensions.DimCount(), _Encoding.Encoding, _Encoding.Normalized, defaultW);
+            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
+            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.VEC3 || _Slicer.Dimensions == DIMENSIONS.VEC4, nameof(_Slicer));
+            return new ColorArray(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Dimensions.DimCount(), _Slicer.Encoding, _Slicer.Normalized, defaultW);
         }
 
         public QuaternionArray AsQuaternionArray()
         {
-            Guard.IsTrue(_Encoding.IsValidVertexAttribute, nameof(_Encoding));
-            Guard.IsTrue(_Encoding.Dimensions == DIMENSIONS.VEC4, nameof(_Encoding));
-            return new QuaternionArray(_Data, _Encoding.ByteOffset, _Encoding.ItemsCount, _Encoding.ByteStride, _Encoding.Encoding, _Encoding.Normalized);
+            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
+            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.VEC4, nameof(_Slicer));
+            return new QuaternionArray(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
         }
 
         public Matrix4x4Array AsMatrix4x4Array()
         {
-            Guard.IsTrue(_Encoding.IsValidVertexAttribute, nameof(_Encoding));
-            Guard.IsTrue(_Encoding.Dimensions == DIMENSIONS.MAT4, nameof(_Encoding));
-            return new Matrix4x4Array(_Data, _Encoding.ByteOffset, _Encoding.ItemsCount, _Encoding.ByteStride, _Encoding.Encoding, _Encoding.Normalized);
+            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
+            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.MAT4, nameof(_Slicer));
+            return new Matrix4x4Array(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
         }
 
         public MultiArray AsMultiArray(int dimensions)
         {
-            Guard.IsTrue(_Encoding.IsValidVertexAttribute, nameof(_Encoding));
-            Guard.IsTrue(_Encoding.Dimensions == DIMENSIONS.SCALAR, nameof(_Encoding));
-            Guard.IsTrue(_Encoding.ByteStride == 0, nameof(_Encoding));
-            return new MultiArray(_Data, _Encoding.ByteOffset, _Encoding.ItemsCount, _Encoding.ByteStride, dimensions, _Encoding.Encoding, _Encoding.Normalized);
+            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
+            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.SCALAR, nameof(_Slicer));
+            Guard.IsTrue(_Slicer.ByteStride == 0, nameof(_Slicer));
+            return new MultiArray(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, dimensions, _Slicer.Encoding, _Slicer.Normalized);
+        }
+
+        public IEnumerable<BYTES> GetItemsAsRawBytes()
+        {
+            var rowOffset = this._Slicer.ByteOffset;
+            var rowStride = this._Slicer.PaddedByteLength;
+            var itemSize = this._Slicer.Dimensions.DimCount() * _Slicer.Encoding.ByteLength();
+
+            for (int i = 0; i < this._Slicer.ItemsCount; ++i)
+            {
+                yield return this._Data.Slice((i * rowStride) + rowOffset, itemSize);
+            }
         }
 
         #endregion

+ 12 - 10
src/SharpGLTF.Core/Memory/InMemoryImage.cs → src/SharpGLTF.Core/Memory/MemoryImage.cs

@@ -2,12 +2,14 @@
 using System.Collections.Generic;
 using System.Text;
 
+using BYTES = System.ArraySegment<System.Byte>;
+
 namespace SharpGLTF.Memory
 {
     /// <summary>
     /// Represents an image file stored as an in-memory byte array
     /// </summary>
-    public readonly struct InMemoryImage
+    public readonly struct MemoryImage
     {
         #region constants
 
@@ -27,31 +29,31 @@ namespace SharpGLTF.Memory
 
         internal static Byte[] DefaultPngImage => Convert.FromBase64String(DEFAULT_PNG_IMAGE);
 
-        public static InMemoryImage Empty => default;
+        public static MemoryImage Empty => default;
 
         #endregion
 
         #region constructor
 
-        public static implicit operator InMemoryImage(ArraySegment<Byte> image) { return new InMemoryImage(image); }
+        public static implicit operator MemoryImage(BYTES image) { return new MemoryImage(image); }
 
-        public static implicit operator InMemoryImage(Byte[] image) { return new InMemoryImage(image); }
+        public static implicit operator MemoryImage(Byte[] image) { return new MemoryImage(image); }
 
-        public InMemoryImage(ArraySegment<Byte> image) { _Image = image; }
+        public MemoryImage(BYTES image) { _Image = image; }
 
-        public InMemoryImage(Byte[] image) { _Image = image == null ? default : new ArraySegment<byte>(image); }
+        public MemoryImage(Byte[] image) { _Image = image == null ? default : new BYTES(image); }
 
-        public InMemoryImage(string filePath)
+        public MemoryImage(string filePath)
         {
             var data = System.IO.File.ReadAllBytes(filePath);
-            _Image = new ArraySegment<byte>(data);
+            _Image = new BYTES(data);
         }
 
         #endregion
 
         #region data
 
-        private readonly ArraySegment<Byte> _Image;
+        private readonly BYTES _Image;
 
         #endregion
 
@@ -153,7 +155,7 @@ namespace SharpGLTF.Memory
         /// Gets the internal buffer.
         /// </summary>
         /// <returns>An array buffer.</returns>
-        public ArraySegment<Byte> GetBuffer() { return _Image; }
+        public BYTES GetBuffer() { return _Image; }
 
         /// <summary>
         /// Tries to parse a Mime64 string to a Byte array.

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

@@ -10,7 +10,7 @@ namespace SharpGLTF.Memory
     /// </summary>
     /// <typeparam name="T">An unmanage structure type.</typeparam>
     [System.Diagnostics.DebuggerDisplay("Sparse {typeof(T).Name} Accessor {Count}")]
-    public struct SparseArray<T> : IList<T>, IReadOnlyList<T>
+    public readonly struct SparseArray<T> : IList<T>, IReadOnlyList<T>
         where T : unmanaged
     {
         #region lifecycle

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

@@ -150,7 +150,7 @@ namespace SharpGLTF.Schema2
         internal Memory.MemoryAccessor _GetMemoryAccessor(ROOT root, int count, Accessor baseAccessor)
         {
             var view = root.LogicalBufferViews[this._bufferView];
-            var info = new Memory.MemoryEncoding(null, this._byteOffset ?? 0, count, view.ByteStride, baseAccessor.Dimensions, baseAccessor.Encoding, baseAccessor.Normalized);
+            var info = new Memory.MemoryAccessInfo(null, this._byteOffset ?? 0, count, view.ByteStride, baseAccessor.Dimensions, baseAccessor.Encoding, baseAccessor.Normalized);
             return new Memory.MemoryAccessor(view.Content, info);
         }
 

+ 60 - 26
src/SharpGLTF.Core/Schema2/gltf.Accessors.cs

@@ -1,8 +1,11 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Numerics;
-
 using SharpGLTF.Memory;
+using SharpGLTF.Validation;
+
+using VALIDATIONCTX = SharpGLTF.Validation.ValidationContext;
 
 namespace SharpGLTF.Schema2
 {
@@ -90,10 +93,10 @@ namespace SharpGLTF.Schema2
 
         #region API
 
-        internal MemoryAccessor _GetMemoryAccessor()
+        internal MemoryAccessor _GetMemoryAccessor(string name = null)
         {
             var view = SourceBufferView;
-            var info = new MemoryEncoding(null, ByteOffset, Count, view.ByteStride, Dimensions, Encoding, Normalized);
+            var info = new MemoryAccessInfo(name, ByteOffset, Count, view.ByteStride, Dimensions, Encoding, Normalized);
             return new MemoryAccessor(view.Content, info);
         }
 
@@ -334,7 +337,7 @@ namespace SharpGLTF.Schema2
 
         #region Validation
 
-        protected override void OnValidateReferences(Validation.ValidationContext result)
+        protected override void OnValidateReferences(VALIDATIONCTX result)
         {
             base.OnValidateReferences(result);
 
@@ -347,7 +350,7 @@ namespace SharpGLTF.Schema2
             _sparse?.ValidateReferences(result);
         }
 
-        protected override void OnValidate(Validation.ValidationContext result)
+        protected override void OnValidate(VALIDATIONCTX result)
         {
             base.OnValidate(result);
 
@@ -361,7 +364,7 @@ namespace SharpGLTF.Schema2
             // using this accessor to validate the data.
         }
 
-        private void ValidateBounds(Validation.ValidationContext result)
+        private void ValidateBounds(VALIDATIONCTX result)
         {
             result = result.GetContext(this);
 
@@ -405,7 +408,7 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        internal void ValidateIndices(Validation.ValidationContext result, uint vertexCount, PrimitiveType drawingType)
+        internal void ValidateIndices(VALIDATIONCTX result, uint vertexCount, PrimitiveType drawingType)
         {
             result = result.GetContext(this);
 
@@ -426,7 +429,34 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        internal void ValidatePositions(Validation.ValidationContext result)
+        internal static void ValidateVertexAttributes(VALIDATIONCTX result, IReadOnlyDictionary<string, Accessor> attributes, int skinsMaxJointCount)
+        {
+            if (result.TryFix)
+            {
+                foreach(var kvp in attributes.Where(item => item.Key != "POSITION"))
+                {
+                    // remove unnecessary bounds
+                    kvp.Value._min.Clear();
+                    kvp.Value._max.Clear();
+                }
+            }
+
+            if (attributes.TryGetValue("POSITION", out Accessor positions)) positions._ValidatePositions(result);
+            else result.AddSemanticWarning("No POSITION attribute found.");
+
+            if (attributes.TryGetValue("NORMAL", out Accessor normals)) normals._ValidateNormals(result);
+            if (attributes.TryGetValue("TANGENT", out Accessor tangents)) tangents._ValidateTangents(result);
+            if (normals == null && tangents != null) result.AddSemanticWarning("TANGENT", "attribute without NORMAL found.");
+
+            if (attributes.TryGetValue("JOINTS_0", out Accessor joints0)) joints0._ValidateJoints(result, "JOINTS_0", skinsMaxJointCount);
+            if (attributes.TryGetValue("JOINTS_1", out Accessor joints1)) joints0._ValidateJoints(result, "JOINTS_1", skinsMaxJointCount);
+
+            attributes.TryGetValue("WEIGHTS_0", out Accessor weights0);
+            attributes.TryGetValue("WEIGHTS_1", out Accessor weights1);
+            _ValidateWeights(result, weights0, weights1);
+        }
+
+        private void _ValidatePositions(VALIDATIONCTX result)
         {
             result = result.GetContext(this);
 
@@ -446,7 +476,7 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        internal void ValidateNormals(Validation.ValidationContext result)
+        private void _ValidateNormals(VALIDATIONCTX result)
         {
             result = result.GetContext(this);
 
@@ -474,7 +504,7 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        internal void ValidateTangents(Validation.ValidationContext result)
+        private void _ValidateTangents(VALIDATIONCTX result)
         {
             result = result.GetContext(this);
 
@@ -507,7 +537,7 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        internal void ValidateJoints(Validation.ValidationContext result, string attributeName)
+        private void _ValidateJoints(VALIDATIONCTX result, string attributeName, int skinsMaxJointCount)
         {
             result = result.GetContext(this);
 
@@ -520,30 +550,34 @@ namespace SharpGLTF.Schema2
 
             for (int i = 0; i < joints.Count; ++i)
             {
-                result.CheckIsFinite(i, joints[i]);
+                var jidx = joints[i];
+
+                result.CheckIsFinite(i, jidx);
+                result.CheckIsInRange(i, jidx, 0, skinsMaxJointCount);
             }
         }
 
-        internal void ValidateWeights(Validation.ValidationContext result, int jwset)
+        private static void _ValidateWeights(VALIDATIONCTX result, Accessor weights0, Accessor weights1)
+        {
+            weights0?._ValidateWeights(result);
+            weights1?._ValidateWeights(result);
+
+            var memory0 = weights0?._GetMemoryAccessor("WEIGHTS_0");
+            var memory1 = weights1?._GetMemoryAccessor("WEIGHTS_1");
+
+            MemoryAccessor.ValidateWeightsSum(result, memory0, memory1);
+        }
+
+        private void _ValidateWeights(VALIDATIONCTX result)
         {
             result = result.GetContext(this);
 
             SourceBufferView.ValidateBufferUsageGPU(result, BufferMode.ARRAY_BUFFER);
             result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.UNSIGNED_BYTE, EncodingType.UNSIGNED_SHORT, EncodingType.FLOAT);
             result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC4);
-
-            var weights = this.AsVector4Array();
-
-            for (int i = 0; i < weights.Count; ++i)
-            {
-                result.CheckIsInRange(i, weights[i], 0, 1);
-
-                // theoretically, the sum of all the weights should give 1, ASSUMING there's only one weight set.
-                // but in practice, that seems not to be true.
-            }
         }
 
-        internal void ValidateMatrices(Validation.ValidationContext result)
+        internal void ValidateMatrices(VALIDATIONCTX result)
         {
             result = result.GetContext(this);
 
@@ -558,13 +592,13 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        internal void ValidateAnimationInput(Validation.ValidationContext result)
+        internal void ValidateAnimationInput(VALIDATIONCTX result)
         {
             SourceBufferView.ValidateBufferUsagePlainData(result);
             result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR);
         }
 
-        internal void ValidateAnimationOutput(Validation.ValidationContext result)
+        internal void ValidateAnimationOutput(VALIDATIONCTX result)
         {
             SourceBufferView.ValidateBufferUsagePlainData(result);
             result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR, DimensionType.VEC3, DimensionType.VEC4);

+ 4 - 3
src/SharpGLTF.Core/Schema2/gltf.BufferView.cs

@@ -200,6 +200,8 @@ namespace SharpGLTF.Schema2
 
             if (bv.IsIndexBuffer)
             {
+                if (bv._byteStride.HasValue) result.AddSemanticError("bufferView: Invalid ByteStride.");
+
                 if (dim != DimensionType.SCALAR) result.AddLinkError(("BufferView", bv.LogicalIndex), $"is an IndexBuffer, but accessor dimensions is: {dim}");
 
                 // TODO: these could by fixed by replacing BYTE by UBYTE, SHORT by USHORT, etc
@@ -213,8 +215,6 @@ namespace SharpGLTF.Schema2
             {
                 if (bv.ByteStride < elementByteSize) result.AddLinkError("ElementByteSize", $"Referenced bufferView's byteStride value {bv.ByteStride} is less than accessor element's length {elementByteSize}.");
 
-                // "Accessor's total byteOffset {0} isn't a multiple of componentType length {1}.";
-
                 return;
             }
 
@@ -268,13 +268,14 @@ namespace SharpGLTF.Schema2
 
         internal void ValidateBufferUsagePlainData(Validation.ValidationContext result)
         {
+            /*
             if (this._byteStride.HasValue)
             {
                 if (result.TryFixLinkOrError("BufferView", "Unexpected ByteStride found. Expected null"))
                 {
                     this._byteStride = null;
                 }
-            }
+            }*/
 
             result = result.GetContext(this);
 

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

@@ -47,7 +47,7 @@ namespace SharpGLTF.Schema2
         /// <summary>
         /// Returns the in-memory representation of the image file.
         /// </summary>
-        public Memory.InMemoryImage MemoryImage => new Memory.InMemoryImage(GetImageContent());
+        public Memory.MemoryImage MemoryImage => new Memory.MemoryImage(GetImageContent());
 
         /// <summary>
         /// Gets a value indicating whether the contained image is a PNG image.
@@ -147,7 +147,7 @@ namespace SharpGLTF.Schema2
         {
             Guard.NotNull(content, nameof(content));
 
-            var imimg = new Memory.InMemoryImage(content);
+            var imimg = new Memory.MemoryImage(content);
             if (!imimg.IsValid) throw new ArgumentException($"{nameof(content)} must be a PNG, JPG, DDS or WEBP image", nameof(content));
 
             string imageType = imimg.MimeType;
@@ -183,7 +183,7 @@ namespace SharpGLTF.Schema2
         {
             if (String.IsNullOrWhiteSpace(_uri)) return;
 
-            var data = Memory.InMemoryImage.TryParseBytes(_uri);
+            var data = Memory.MemoryImage.TryParseBytes(_uri);
 
             if (data == null)
             {
@@ -218,7 +218,7 @@ namespace SharpGLTF.Schema2
         {
             if (_SatelliteImageContent == null) { _WriteAsBufferView(); return; }
 
-            var imimg = new Memory.InMemoryImage(_SatelliteImageContent);
+            var imimg = new Memory.MemoryImage(_SatelliteImageContent);
             _mimeType = imimg.MimeType;
             _uri = imimg.ToMime64();
         }
@@ -238,7 +238,7 @@ namespace SharpGLTF.Schema2
 
             _mimeType = null;
 
-            var imimg = new Memory.InMemoryImage(_SatelliteImageContent);
+            var imimg = new Memory.MemoryImage(_SatelliteImageContent);
             if (!imimg.IsValid) throw new InvalidOperationException();
 
             _uri = System.IO.Path.ChangeExtension(satelliteUri, imimg.FileExtension);
@@ -319,7 +319,7 @@ namespace SharpGLTF.Schema2
         public Image UseImage(BYTES imageContent)
         {
             Guard.NotNullOrEmpty(imageContent, nameof(imageContent));
-            Guard.IsTrue(new Memory.InMemoryImage(imageContent).IsValid, nameof(imageContent), $"{nameof(imageContent)} must be a valid image byte stream.");
+            Guard.IsTrue(new Memory.MemoryImage(imageContent).IsValid, nameof(imageContent), $"{nameof(imageContent)} must be a valid image byte stream.");
 
             foreach (var img in this.LogicalImages)
             {

+ 20 - 62
src/SharpGLTF.Core/Schema2/gltf.MeshPrimitive.cs

@@ -151,7 +151,7 @@ namespace SharpGLTF.Schema2
 
         public Memory.MemoryAccessor GetVertices(string attributeKey)
         {
-            return GetVertexAccessor(attributeKey)._GetMemoryAccessor();
+            return GetVertexAccessor(attributeKey)._GetMemoryAccessor(attributeKey);
         }
 
         #endregion
@@ -323,7 +323,7 @@ namespace SharpGLTF.Schema2
                 if (incompatibleMode) result.AddLinkWarning("Indices", $"Number of vertices or indices({IndexAccessor.Count}) is not compatible with used drawing mode('{this.DrawPrimitiveType}').");
             }
 
-            // check attributes accessors
+            // check vertex attributes accessors
 
             foreach (var group in this.VertexAccessors.Values.GroupBy(item => item.SourceBufferView))
             {
@@ -357,11 +357,22 @@ namespace SharpGLTF.Schema2
                 }
             }
 
+            if (DrawPrimitiveType == PrimitiveType.POINTS)
+            {
+                if (this.VertexAccessors.Keys.Contains("NORMAL")) result.AddSemanticWarning("NORMAL", $"attribute defined for {DrawPrimitiveType} rendering mode.");
+                if (this.VertexAccessors.Keys.Contains("TANGENT")) result.AddSemanticWarning("TANGENT", $"attribute defined for {DrawPrimitiveType} rendering mode.");
+            }
+
             // check vertex attributes
 
-            _ValidatePositions(result);
-            _ValidateNormals(result);
-            _ValidateTangents(result);
+            if (result.TryFix)
+            {
+                var vattributes = this.VertexAccessors
+                .Select(item => item.Value._GetMemoryAccessor(item.Key))
+                .ToArray();
+
+                Memory.MemoryAccessor.SanitizeVertexAttributes(vattributes);
+            }
 
             // find skins using this mesh primitive:
 
@@ -370,65 +381,12 @@ namespace SharpGLTF.Schema2
                 .LogicalNodes
                 .Where(item => item.Mesh == this.LogicalParent)
                 .Select(item => item.Skin)
-                .ToList();
-
-            _ValidateJoints(result, skins, "JOINTS_0");
-            _ValidateJoints(result, skins, "JOINTS_1");
-        }
+                .Where(item => item != null)
+                .Select(item => item.JointsCount);
 
-        private void _ValidatePositions(Validation.ValidationContext result)
-        {
-            var positions = GetVertexAccessor("POSITION");
-            if (positions == null)
-            {
-                result.AddSemanticWarning("No POSITION attribute found.");
-                return;
-            }
-
-            positions.ValidatePositions(result);
-        }
-
-        private void _ValidateNormals(Validation.ValidationContext result)
-        {
-            var normals = GetVertexAccessor("NORMAL");
-            if (normals == null) return;
+            var maxJoints = skins.Any() ? skins.Max() : 0;
 
-            normals.ValidateNormals(result);
-        }
-
-        private void _ValidateTangents(Validation.ValidationContext result)
-        {
-            var tangents = GetVertexAccessor("TANGENT");
-            if (tangents == null) return;
-
-            if (GetVertexAccessor("NORMAL") == null) result.AddSemanticWarning("TANGENT", "attribute without NORMAL found.");
-            if (DrawPrimitiveType == PrimitiveType.POINTS) result.AddSemanticWarning("TANGENT", "attribute defined for POINTS rendering mode.");
-
-            tangents.ValidateTangents(result);
-        }
-
-        private void _ValidateJoints(Validation.ValidationContext result, IEnumerable<Skin> skins, string attributeName)
-        {
-            var joints = GetVertexAccessor(attributeName);
-
-            if (joints == null) return;
-
-            joints.ValidateJoints(result, attributeName);
-
-            int max = 0;
-            foreach (var jjjj in joints.AsVector4Array())
-            {
-                max = Math.Max(max, (int)jjjj.X);
-                max = Math.Max(max, (int)jjjj.Y);
-                max = Math.Max(max, (int)jjjj.Z);
-                max = Math.Max(max, (int)jjjj.W);
-            }
-
-            foreach (var skin in skins.Where(item => item != null))
-            {
-                var skinJoints = new int[skin.JointsCount];
-                result.CheckArrayIndexAccess(attributeName, max, skinJoints);
-            }
+            Accessor.ValidateVertexAttributes(result, this.VertexAccessors, maxJoints);
         }
 
         #endregion

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

@@ -89,7 +89,7 @@ namespace SharpGLTF.Schema2
 
             if (primaryImage.IsDds || primaryImage.IsWebp)
             {
-                var fallback = LogicalParent.UseImage(Memory.InMemoryImage.DefaultPngImage.Slice(0));
+                var fallback = LogicalParent.UseImage(Memory.MemoryImage.DefaultPngImage.Slice(0));
                 SetImages(primaryImage, fallback);
             }
             else

+ 60 - 49
src/SharpGLTF.Core/Schema2/khr.lights.cs

@@ -48,6 +48,12 @@ namespace SharpGLTF.Schema2
     [System.Diagnostics.DebuggerDisplay("{LightType} {Color} {Intensity} {Range}")]
     public sealed partial class PunctualLight
     {
+        #region constants
+
+        private const Double _rangeDefault = Double.PositiveInfinity;
+
+        #endregion
+
         #region lifecycle
 
         internal PunctualLight() { }
@@ -94,7 +100,7 @@ namespace SharpGLTF.Schema2
         /// to have reached zero. Supported only for point and spot lights. Must be > 0.
         /// When undefined, range is assumed to be infinite.
         /// </param>
-        public void SetColor(Vector3 color, float intensity = 1, float range = 0)
+        public void SetColor(Vector3 color, float intensity = 1, float range = float.PositiveInfinity)
         {
             this.Color = color;
             this.Intensity = intensity;
@@ -163,8 +169,13 @@ namespace SharpGLTF.Schema2
         /// </summary>
         public Single Range
         {
-            get => (Single)_range.AsValue(0);
-            set => _range = LightType == PunctualLightType.Directional ? 0 : ((double)value).AsNullable(0, _rangeMinimum, float.MaxValue);
+            get => (Single)_range.AsValue(_rangeDefault);
+            set
+            {
+                if (LightType == PunctualLightType.Directional) { _range = null; return; }
+
+                _range = ((double)value).AsNullable(_rangeDefault, _rangeMinimum + 2e-07, float.MaxValue);
+            }
         }
 
         #endregion
@@ -185,6 +196,52 @@ namespace SharpGLTF.Schema2
         }
     }
 
+    partial class KHR_lights_punctualnodeextension
+    {
+        internal KHR_lights_punctualnodeextension(Node node)
+        {
+        }
+
+        public int LightIndex
+        {
+            get => _light;
+            set => _light = value;
+        }
+    }
+
+    partial class Node
+    {
+        /// <summary>
+        /// Gets or sets the <see cref="Schema2.PunctualLight"/> of this <see cref="Node"/>.
+        /// </summary>
+        /// <remarks>
+        /// This is part of <see href="https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual"/> extension.
+        /// </remarks>
+        public PunctualLight PunctualLight
+        {
+            get
+            {
+                var ext = this.GetExtension<KHR_lights_punctualnodeextension>();
+                if (ext == null) return null;
+
+                return this.LogicalParent.LogicalPunctualLights[ext.LightIndex];
+            }
+            set
+            {
+                if (value == null) { this.RemoveExtensions<KHR_lights_punctualnodeextension>(); return; }
+
+                Guard.MustShareLogicalParent(this, value, nameof(value));
+
+                this.UsingExtension(typeof(KHR_lights_punctualnodeextension));
+
+                var ext = new KHR_lights_punctualnodeextension(this);
+                ext.LightIndex = value.LogicalIndex;
+
+                this.SetExtension(ext);
+            }
+        }
+    }
+
     partial class ModelRoot
     {
         /// <summary>
@@ -236,50 +293,4 @@ namespace SharpGLTF.Schema2
             return ext.CreateLight(name, lightType);
         }
     }
-
-    partial class KHR_lights_punctualnodeextension
-    {
-        internal KHR_lights_punctualnodeextension(Node node)
-        {
-        }
-
-        public int LightIndex
-        {
-            get => _light;
-            set => _light = value;
-        }
-    }
-
-    partial class Node
-    {
-        /// <summary>
-        /// Gets or sets the <see cref="Schema2.PunctualLight"/> of this <see cref="Node"/>.
-        /// </summary>
-        /// <remarks>
-        /// This is part of <see href="https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual"/> extension.
-        /// </remarks>
-        public PunctualLight PunctualLight
-        {
-            get
-            {
-                var ext = this.GetExtension<KHR_lights_punctualnodeextension>();
-                if (ext == null) return null;
-
-                return this.LogicalParent.LogicalPunctualLights[ext.LightIndex];
-            }
-            set
-            {
-                if (value == null) { this.RemoveExtensions<KHR_lights_punctualnodeextension>(); return; }
-
-                Guard.MustShareLogicalParent(this, value, nameof(value));
-
-                this.UsingExtension(typeof(KHR_lights_punctualnodeextension));
-
-                var ext = new KHR_lights_punctualnodeextension(this);
-                ext.LightIndex = value.LogicalIndex;
-
-                this.SetExtension(ext);
-            }
-        }
-    }
 }

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

@@ -23,8 +23,7 @@
     </AssemblyAttribute>    
   </ItemGroup>  
   
-  <ItemGroup>        
-    <PackageReference Include="System.Memory" Version="4.5.3" />
+  <ItemGroup>
     <PackageReference Include="System.Text.Json" Version="4.7.0" />
   </ItemGroup>
 

+ 1 - 1
src/SharpGLTF.Core/Validation/ValidationContext.cs

@@ -251,7 +251,7 @@ namespace SharpGLTF.Validation
         {
             if (!value.HasValue) return false;
             if (!CheckIsFinite(location, value)) return false;
-            if (value.Value.IsValidNormal()) return false;
+            if (value.Value.IsNormalized()) return false;
 
             return TryFixDataOrError(location, $"is not of unit length: {value.Value.Length()}.");
         }

+ 4 - 2
src/SharpGLTF.Toolkit/Geometry/PackedBuffer.cs

@@ -4,6 +4,8 @@ using System.Linq;
 using System.Text;
 using SharpGLTF.Memory;
 
+using BYTES = System.ArraySegment<byte>;
+
 namespace SharpGLTF.Geometry
 {
     class PackedBuffer
@@ -51,7 +53,7 @@ namespace SharpGLTF.Geometry
 
             int offset = 0;
 
-            var dstOffsets = new Dictionary<ArraySegment<Byte>, int>();
+            var dstOffsets = new Dictionary<BYTES, int>();
 
             foreach (var src in srcBuffers)
             {
@@ -63,7 +65,7 @@ namespace SharpGLTF.Geometry
                 offset += src.Count;
             }
 
-            var dstBuffer = new ArraySegment<Byte>(array);
+            var dstBuffer = new BYTES(array);
 
             foreach (var a in _Accessors)
             {

+ 26 - 0
src/SharpGLTF.Toolkit/Geometry/PackedEncoding.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using ENCODING = SharpGLTF.Schema2.EncodingType;
+
+namespace SharpGLTF.Geometry
+{
+
+    class PackedEncoding
+    {
+        public ENCODING? JointsEncoding;
+        public ENCODING? WeightsEncoding;
+
+        public void AdjustJointEncoding<TVertex>(IReadOnlyList<TVertex> vertices)
+            where TVertex : IVertexBuilder
+        {
+            if (JointsEncoding.HasValue) return;
+
+            var indices = vertices.Select(item => item.GetSkinning().GetWeights().MaxIndex);
+            var maxIndex = indices.Any() ? indices.Max() : 0;
+            JointsEncoding = maxIndex < 256 ? ENCODING.UNSIGNED_BYTE : ENCODING.UNSIGNED_SHORT;
+        }
+    }
+}

+ 10 - 7
src/SharpGLTF.Toolkit/Geometry/PackedMeshBuilder.cs

@@ -21,9 +21,9 @@ namespace SharpGLTF.Geometry
         /// ensuring that the resources are shared across all meshes.
         /// </summary>
         /// <param name="meshBuilders">A collection of <see cref="IMeshBuilder{TMaterial}"/> meshes.</param>
-        /// <param name="prefferStrided">true to create strided vertex buffers.</param>
+        /// <param name="settings">Mesh packaging settings.</param>
         /// <returns>A collectio of <see cref="PackedMeshBuilder{TMaterial}"/> meshes.</returns>
-        internal static IEnumerable<PackedMeshBuilder<TMaterial>> CreatePackedMeshes(IEnumerable<IMeshBuilder<TMaterial>> meshBuilders, bool prefferStrided)
+        internal static IEnumerable<PackedMeshBuilder<TMaterial>> CreatePackedMeshes(IEnumerable<IMeshBuilder<TMaterial>> meshBuilders, Scenes.SceneBuilderSchema2Settings settings)
         {
             try
             {
@@ -34,7 +34,10 @@ namespace SharpGLTF.Geometry
                 throw new ArgumentException(ex.Message, nameof(meshBuilders), ex);
             }
 
-            var jointEncoding = meshBuilders.GetOptimalJointEncoding();
+            var vertexEncodings = new PackedEncoding();
+            vertexEncodings.JointsEncoding = meshBuilders.GetOptimalJointEncoding();
+            vertexEncodings.WeightsEncoding = settings.CompactVertexWeights ? EncodingType.UNSIGNED_SHORT : EncodingType.FLOAT;
+
             var indexEncoding = meshBuilders.GetOptimalIndexEncoding();
 
             foreach (var srcMesh in meshBuilders)
@@ -47,14 +50,14 @@ namespace SharpGLTF.Geometry
 
                     var dstPrim = dstMesh.AddPrimitive(srcPrim.Material, srcPrim.VerticesPerPrimitive);
 
-                    bool useStrided = prefferStrided;
+                    bool useStrided = settings.UseStridedBuffers;
                     if (srcPrim.MorphTargets.Count > 0) useStrided = false; // if the primitive has morphing, it is better not to use strided vertex buffers.
 
-                    if (useStrided) dstPrim.SetStridedVertices(srcPrim, jointEncoding);
-                    else dstPrim.SetStreamedVertices(srcPrim, jointEncoding);
+                    if (useStrided) dstPrim.SetStridedVertices(srcPrim, vertexEncodings);
+                    else dstPrim.SetStreamedVertices(srcPrim, vertexEncodings);
 
                     dstPrim.SetIndices(srcPrim, indexEncoding);
-                    dstPrim.SetMorphTargets(srcPrim);
+                    dstPrim.SetMorphTargets(srcPrim, vertexEncodings);
                 }
 
                 yield return dstMesh;

+ 11 - 9
src/SharpGLTF.Toolkit/Geometry/PackedPrimitiveBuilder.cs

@@ -37,11 +37,11 @@ namespace SharpGLTF.Geometry
 
         #region API
 
-        public void SetStridedVertices(IPrimitiveReader<TMaterial> srcPrim, EncodingType encoding)
+        public void SetStridedVertices(IPrimitiveReader<TMaterial> srcPrim, PackedEncoding vertexEncoding)
         {
             Guard.NotNull(srcPrim, nameof(srcPrim));
 
-            var vAccessors = VertexTypes.VertexUtils.CreateVertexMemoryAccessors(srcPrim.Vertices, encoding);
+            var vAccessors = VertexTypes.VertexUtils.CreateVertexMemoryAccessors(srcPrim.Vertices, vertexEncoding);
 
             Guard.NotNull(vAccessors, nameof(srcPrim));
 
@@ -49,12 +49,12 @@ namespace SharpGLTF.Geometry
             _VertexAccessors = vAccessors;
         }
 
-        public void SetStreamedVertices(IPrimitiveReader<TMaterial> srcPrim, EncodingType encoding)
+        public void SetStreamedVertices(IPrimitiveReader<TMaterial> srcPrim, PackedEncoding vertexEncoding)
         {
             Guard.NotNull(srcPrim, nameof(srcPrim));
 
             var attributeNames = VertexTypes.VertexUtils
-                        .GetVertexAttributes(srcPrim.Vertices[0], srcPrim.Vertices.Count, encoding)
+                        .GetVertexAttributes(srcPrim.Vertices[0], srcPrim.Vertices.Count, vertexEncoding)
                         .Select(item => item.Name)
                         .ToList();
 
@@ -64,13 +64,15 @@ namespace SharpGLTF.Geometry
 
             foreach (var an in attributeNames)
             {
-                var vAccessor = VertexTypes.VertexUtils.CreateVertexMemoryAccessor(srcPrim.Vertices, an, encoding);
+                var vAccessor = VertexTypes.VertexUtils.CreateVertexMemoryAccessor(srcPrim.Vertices, an, vertexEncoding);
                 if (vAccessor == null) continue;
 
                 vAccessors.Add(vAccessor);
             }
 
             _VertexAccessors = vAccessors.ToArray();
+
+            Memory.MemoryAccessor.SanitizeVertexAttributes(_VertexAccessors);
         }
 
         public void SetIndices(IPrimitiveReader<TMaterial> srcPrim, EncodingType encoding)
@@ -85,7 +87,7 @@ namespace SharpGLTF.Geometry
             _IndexAccessors = iAccessor;
         }
 
-        public void SetMorphTargets(IPrimitiveReader<TMaterial> srcPrim)
+        public void SetMorphTargets(IPrimitiveReader<TMaterial> srcPrim, PackedEncoding vertexEncodings)
         {
             bool hasPositions = _VertexAccessors.Any(item => item.Attribute.Name == "POSITION");
             bool hasNormals = _VertexAccessors.Any(item => item.Attribute.Name == "NORMAL");
@@ -97,11 +99,11 @@ namespace SharpGLTF.Geometry
             {
                 var mtv = srcPrim.MorphTargets[i].GetMorphTargetVertices(srcPrim.Vertices.Count);
 
-                var pAccessor = VertexTypes.VertexUtils.CreateVertexMemoryAccessor(mtv, "POSITIONDELTA", EncodingType.UNSIGNED_SHORT);
+                var pAccessor = VertexTypes.VertexUtils.CreateVertexMemoryAccessor(mtv, "POSITIONDELTA", vertexEncodings);
 
-                var nAccessor = !hasNormals ? null : VertexTypes.VertexUtils.CreateVertexMemoryAccessor(mtv, "NORMALDELTA", EncodingType.UNSIGNED_SHORT);
+                var nAccessor = !hasNormals ? null : VertexTypes.VertexUtils.CreateVertexMemoryAccessor(mtv, "NORMALDELTA", vertexEncodings);
 
-                var tAccessor = !hasTangents ? null : VertexTypes.VertexUtils.CreateVertexMemoryAccessor(mtv, "TANGENTDELTA", EncodingType.UNSIGNED_SHORT);
+                var tAccessor = !hasTangents ? null : VertexTypes.VertexUtils.CreateVertexMemoryAccessor(mtv, "TANGENTDELTA", vertexEncodings);
 
                 AddMorphTarget(pAccessor, nAccessor, tAccessor);
             }

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

@@ -103,7 +103,7 @@ namespace SharpGLTF.Geometry
 
             if (Geometry.TryGetNormal(out Vector3 n))
             {
-                if (!n.IsValidNormal()) sb.Append($" ❌𝚴:{n}");
+                if (!n.IsNormalized()) sb.Append($" ❌𝚴:{n}");
             }
 
             if (Geometry.TryGetTangent(out Vector4 t))

+ 9 - 7
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexMaterial.cs

@@ -3,6 +3,8 @@ using System.Collections.Generic;
 using System.Numerics;
 using System.Text;
 
+using ENCODING = SharpGLTF.Schema2.EncodingType;
+
 namespace SharpGLTF.Geometry.VertexTypes
 {
     public interface IVertexMaterial
@@ -57,7 +59,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         #region data
 
-        [VertexAttribute("COLOR_0", Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        [VertexAttribute("COLOR_0", ENCODING.UNSIGNED_BYTE, true)]
         public Vector4 Color;
 
         public int MaxColors => 1;
@@ -127,10 +129,10 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         #region data
 
-        [VertexAttribute("COLOR_0", Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        [VertexAttribute("COLOR_0", ENCODING.UNSIGNED_BYTE, true)]
         public Vector4 Color0;
 
-        [VertexAttribute("COLOR_1", Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        [VertexAttribute("COLOR_1", ENCODING.UNSIGNED_BYTE, true)]
         public Vector4 Color1;
 
         public int MaxColors => 2;
@@ -348,7 +350,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         #region data
 
-        [VertexAttribute("COLOR_0", Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        [VertexAttribute("COLOR_0", ENCODING.UNSIGNED_BYTE, true)]
         public Vector4 Color;
 
         [VertexAttribute("TEXCOORD_0")]
@@ -424,7 +426,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         #region data
 
-        [VertexAttribute("COLOR_0", Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        [VertexAttribute("COLOR_0", ENCODING.UNSIGNED_BYTE, true)]
         public Vector4 Color;
 
         [VertexAttribute("TEXCOORD_0")]
@@ -513,10 +515,10 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         #region data
 
-        [VertexAttribute("COLOR_0", Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        [VertexAttribute("COLOR_0", ENCODING.UNSIGNED_BYTE, true)]
         public Vector4 Color0;
 
-        [VertexAttribute("COLOR_1", Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        [VertexAttribute("COLOR_1", ENCODING.UNSIGNED_BYTE, true)]
         public Vector4 Color1;
 
         [VertexAttribute("TEXCOORD_0")]

+ 8 - 6
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexSkinning.cs

@@ -4,6 +4,8 @@ using System.Numerics;
 using System.Text;
 using SharpGLTF.Transforms;
 
+using ENCODING = SharpGLTF.Schema2.EncodingType;
+
 namespace SharpGLTF.Geometry.VertexTypes
 {
     public interface IVertexSkinning
@@ -58,10 +60,10 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         #region data
 
-        [VertexAttribute("JOINTS_0", Schema2.EncodingType.UNSIGNED_SHORT, false)]
+        [VertexAttribute("JOINTS_0", ENCODING.UNSIGNED_SHORT, false)]
         public Vector4 Joints;
 
-        [VertexAttribute("WEIGHTS_0", Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        [VertexAttribute("WEIGHTS_0")]
         public Vector4 Weights;
 
         public int MaxBindings => 4;
@@ -158,16 +160,16 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         #region data
 
-        [VertexAttribute("JOINTS_0", Schema2.EncodingType.UNSIGNED_SHORT, false)]
+        [VertexAttribute("JOINTS_0", ENCODING.UNSIGNED_SHORT, false)]
         public Vector4 Joints0;
 
-        [VertexAttribute("JOINTS_1", Schema2.EncodingType.UNSIGNED_SHORT, false)]
+        [VertexAttribute("JOINTS_1", ENCODING.UNSIGNED_SHORT, false)]
         public Vector4 Joints1;
 
-        [VertexAttribute("WEIGHTS_0", Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        [VertexAttribute("WEIGHTS_0")]
         public Vector4 Weights0;
 
-        [VertexAttribute("WEIGHTS_1", Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        [VertexAttribute("WEIGHTS_1")]
         public Vector4 Weights1;
 
         public int MaxBindings => 8;

+ 38 - 63
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexUtils.cs

@@ -4,6 +4,8 @@ using System.Linq;
 using System.Numerics;
 
 using SharpGLTF.Memory;
+using DIMENSIONS = SharpGLTF.Schema2.DimensionType;
+using ENCODING = SharpGLTF.Schema2.EncodingType;
 
 namespace SharpGLTF.Geometry.VertexTypes
 {
@@ -99,44 +101,6 @@ namespace SharpGLTF.Geometry.VertexTypes
             return vtype.MakeGenericType(tvg, tvm, tvs);
         }
 
-        public static bool SanitizeVertex<TvG>(this TvG inVertex, out TvG outVertex)
-            where TvG : struct, IVertexGeometry
-        {
-            outVertex = inVertex;
-
-            var p = inVertex.GetPosition();
-
-            if (!p._IsFinite()) return false;
-
-            if (inVertex.TryGetNormal(out Vector3 n))
-            {
-                if (!n._IsFinite()) return false;
-                if (n == Vector3.Zero) n = p;
-                if (n == Vector3.Zero) return false;
-
-                var l = n.Length();
-                if (l < 0.99f || l > 0.01f) outVertex.SetNormal(Vector3.Normalize(n));
-            }
-
-            if (inVertex.TryGetTangent(out Vector4 tw))
-            {
-                if (!tw._IsFinite()) return false;
-
-                var t = new Vector3(tw.X, tw.Y, tw.Z);
-                if (t == Vector3.Zero) return false;
-
-                if (tw.W > 0) tw.W = 1;
-                if (tw.W < 0) tw.W = -1;
-
-                var l = t.Length();
-                if (l < 0.99f || l > 0.01f) t = Vector3.Normalize(t);
-
-                outVertex.SetTangent(new Vector4(t, tw.W));
-            }
-
-            return true;
-        }
-
         public static TvP ConvertToGeometry<TvP>(this IVertexGeometry src)
             where TvP : struct, IVertexGeometry
         {
@@ -207,13 +171,15 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         #region memory buffers API
 
-        public static MemoryAccessor CreateVertexMemoryAccessor<TVertex>(this IReadOnlyList<TVertex> vertices, string attributeName, Schema2.EncodingType jointEncoding)
+        public static MemoryAccessor CreateVertexMemoryAccessor<TVertex>(this IReadOnlyList<TVertex> vertices, string attributeName, PackedEncoding vertexEncoding)
             where TVertex : IVertexBuilder
         {
             if (vertices == null || vertices.Count == 0) return null;
 
+            vertexEncoding.AdjustJointEncoding(vertices);
+
             // determine the vertex attributes from the first vertex.
-            var attributes = GetVertexAttributes(vertices[0], vertices.Count, jointEncoding);
+            var attributes = GetVertexAttributes(vertices[0], vertices.Count, vertexEncoding);
 
             var attribute = attributes.FirstOrDefault(item => item.Name == attributeName);
             if (attribute.Name == null) return null;
@@ -231,20 +197,22 @@ namespace SharpGLTF.Geometry.VertexTypes
             return accessor;
         }
 
-        public static MemoryAccessor[] CreateVertexMemoryAccessors<TVertex>(this IReadOnlyList<TVertex> vertices, Schema2.EncodingType jointEncoding)
+        public static MemoryAccessor[] CreateVertexMemoryAccessors<TVertex>(this IReadOnlyList<TVertex> vertices, PackedEncoding vertexEncoding)
             where TVertex : IVertexBuilder
         {
             if (vertices == null || vertices.Count == 0) return null;
 
+            vertexEncoding.AdjustJointEncoding(vertices);
+
             // determine the vertex attributes from the first vertex.
-            var attributes = GetVertexAttributes(vertices[0], vertices.Count, jointEncoding);
+            var attributes = GetVertexAttributes(vertices[0], vertices.Count, vertexEncoding);
 
             // create a buffer
             int byteStride = attributes[0].ByteStride;
             var vbuffer = new ArraySegment<byte>(new Byte[byteStride * vertices.Count]);
 
             // fill the buffer with the vertex attributes.
-            var accessors = MemoryEncoding
+            var accessors = MemoryAccessInfo
                 .Slice(attributes, 0, vertices.Count)
                 .Select(item => new MemoryAccessor(vbuffer, item))
                 .ToArray();
@@ -254,6 +222,8 @@ namespace SharpGLTF.Geometry.VertexTypes
                 accessor.FillAccessor(vertices);
             }
 
+            MemoryAccessor.SanitizeVertexAttributes(accessors);
+
             return accessors;
         }
 
@@ -262,20 +232,20 @@ namespace SharpGLTF.Geometry.VertexTypes
         {
             var columnFunc = _GetVertexBuilderAttributeFunc(dstAccessor.Attribute.Name);
 
-            if (dstAccessor.Attribute.Dimensions == Schema2.DimensionType.SCALAR) dstAccessor.AsScalarArray().Fill(srcVertices._GetColumn<TVertex, Single>(columnFunc));
-            if (dstAccessor.Attribute.Dimensions == Schema2.DimensionType.VEC2) dstAccessor.AsVector2Array().Fill(srcVertices._GetColumn<TVertex, Vector2>(columnFunc));
-            if (dstAccessor.Attribute.Dimensions == Schema2.DimensionType.VEC3) dstAccessor.AsVector3Array().Fill(srcVertices._GetColumn<TVertex, Vector3>(columnFunc));
-            if (dstAccessor.Attribute.Dimensions == Schema2.DimensionType.VEC4) dstAccessor.AsVector4Array().Fill(srcVertices._GetColumn<TVertex, Vector4>(columnFunc));
+            if (dstAccessor.Attribute.Dimensions == DIMENSIONS.SCALAR) dstAccessor.AsScalarArray().Fill(srcVertices._GetColumn<TVertex, Single>(columnFunc));
+            if (dstAccessor.Attribute.Dimensions == DIMENSIONS.VEC2) dstAccessor.AsVector2Array().Fill(srcVertices._GetColumn<TVertex, Vector2>(columnFunc));
+            if (dstAccessor.Attribute.Dimensions == DIMENSIONS.VEC3) dstAccessor.AsVector3Array().Fill(srcVertices._GetColumn<TVertex, Vector3>(columnFunc));
+            if (dstAccessor.Attribute.Dimensions == DIMENSIONS.VEC4) dstAccessor.AsVector4Array().Fill(srcVertices._GetColumn<TVertex, Vector4>(columnFunc));
         }
 
-        public static MemoryAccessor CreateIndexMemoryAccessor(this IReadOnlyList<Int32> indices, Schema2.EncodingType encoding)
+        public static MemoryAccessor CreateIndexMemoryAccessor(this IReadOnlyList<Int32> indices, ENCODING indexEncoding)
         {
             if (indices == null || indices.Count == 0) return null;
 
-            var attribute = new MemoryEncoding("INDEX", 0, indices.Count, 0, Schema2.DimensionType.SCALAR, encoding);
+            var attribute = new MemoryAccessInfo("INDEX", 0, indices.Count, 0, DIMENSIONS.SCALAR, indexEncoding);
 
             // create buffer
-            var ibytes = new Byte[encoding.ByteLength() * indices.Count];
+            var ibytes = new Byte[indexEncoding.ByteLength() * indices.Count];
             var ibuffer = new ArraySegment<byte>(ibytes);
 
             // fill the buffer with indices.
@@ -286,13 +256,13 @@ namespace SharpGLTF.Geometry.VertexTypes
             return accessor;
         }
 
-        public static MemoryEncoding[] GetVertexAttributes(this IVertexBuilder firstVertex, int vertexCount, Schema2.EncodingType jointEncoding)
+        public static MemoryAccessInfo[] GetVertexAttributes(this IVertexBuilder firstVertex, int vertexCount, PackedEncoding vertexEncoding)
         {
             var tvg = firstVertex.GetGeometry().GetType();
             var tvm = firstVertex.GetMaterial().GetType();
             var tvs = firstVertex.GetSkinning().GetType();
 
-            var attributes = new List<MemoryEncoding>();
+            var attributes = new List<MemoryAccessInfo>();
 
             foreach (var finfo in tvg.GetFields())
             {
@@ -312,7 +282,12 @@ namespace SharpGLTF.Geometry.VertexTypes
                 if (attribute.HasValue)
                 {
                     var a = attribute.Value;
-                    if (a.Name.StartsWith("JOINTS_", StringComparison.OrdinalIgnoreCase)) a.Encoding = jointEncoding;
+                    if (a.Name.StartsWith("JOINTS_", StringComparison.OrdinalIgnoreCase)) a.Encoding = vertexEncoding.JointsEncoding.Value;
+                    if (a.Name.StartsWith("WEIGHTS_", StringComparison.OrdinalIgnoreCase))
+                    {
+                        a.Encoding = vertexEncoding.WeightsEncoding.Value;
+                        if (a.Encoding != ENCODING.FLOAT) a.Normalized = true;
+                    }
 
                     attributes.Add(a);
                 }
@@ -320,12 +295,12 @@ namespace SharpGLTF.Geometry.VertexTypes
 
             var array = attributes.ToArray();
 
-            MemoryEncoding.SetInterleavedInfo(array, 0, vertexCount);
+            MemoryAccessInfo.SetInterleavedInfo(array, 0, vertexCount);
 
             return array;
         }
 
-        private static MemoryEncoding? _GetMemoryAccessInfo(System.Reflection.FieldInfo finfo)
+        private static MemoryAccessInfo? _GetMemoryAccessInfo(System.Reflection.FieldInfo finfo)
         {
             var attribute = finfo.GetCustomAttributes(true)
                     .OfType<VertexAttributeAttribute>()
@@ -333,18 +308,18 @@ namespace SharpGLTF.Geometry.VertexTypes
 
             if (attribute == null) return null;
 
-            var dimensions = (Schema2.DimensionType?)null;
+            var dimensions = (DIMENSIONS?)null;
 
-            if (finfo.FieldType == typeof(Single)) dimensions = Schema2.DimensionType.SCALAR;
-            if (finfo.FieldType == typeof(Vector2)) dimensions = Schema2.DimensionType.VEC2;
-            if (finfo.FieldType == typeof(Vector3)) dimensions = Schema2.DimensionType.VEC3;
-            if (finfo.FieldType == typeof(Vector4)) dimensions = Schema2.DimensionType.VEC4;
-            if (finfo.FieldType == typeof(Quaternion)) dimensions = Schema2.DimensionType.VEC4;
-            if (finfo.FieldType == typeof(Matrix4x4)) dimensions = Schema2.DimensionType.MAT4;
+            if (finfo.FieldType == typeof(Single)) dimensions = DIMENSIONS.SCALAR;
+            if (finfo.FieldType == typeof(Vector2)) dimensions = DIMENSIONS.VEC2;
+            if (finfo.FieldType == typeof(Vector3)) dimensions = DIMENSIONS.VEC3;
+            if (finfo.FieldType == typeof(Vector4)) dimensions = DIMENSIONS.VEC4;
+            if (finfo.FieldType == typeof(Quaternion)) dimensions = DIMENSIONS.VEC4;
+            if (finfo.FieldType == typeof(Matrix4x4)) dimensions = DIMENSIONS.MAT4;
 
             if (dimensions == null) throw new ArgumentException($"invalid type {finfo.FieldType}");
 
-            return new MemoryEncoding(attribute.Name, 0, 0, 0, dimensions.Value, attribute.Encoding, attribute.Normalized);
+            return new MemoryAccessInfo(attribute.Name, 0, 0, 0, dimensions.Value, attribute.Encoding, attribute.Normalized);
         }
 
         private static Func<IVertexBuilder, Object> _GetVertexBuilderAttributeFunc(string attributeName)

+ 1 - 1
src/SharpGLTF.Toolkit/IO/WavefrontWriter.cs

@@ -105,7 +105,7 @@ namespace SharpGLTF.IO
 
             foreach (var img in images)
             {
-                var imimg = new Memory.InMemoryImage(img);
+                var imimg = new Memory.MemoryImage(img);
 
                 var imgName = firstImg ? baseName : $"{baseName}_{files.Count}.{imimg.FileExtension}";
 

+ 7 - 7
src/SharpGLTF.Toolkit/Materials/TextureBuilder.cs

@@ -123,9 +123,9 @@ namespace SharpGLTF.Materials
         /// Gets or sets the default image bytes to use by this <see cref="TextureBuilder"/>,
         /// Supported formats are: PNG, JPG, DDS and WEBP
         /// </summary>
-        public Memory.InMemoryImage PrimaryImage
+        public Memory.MemoryImage PrimaryImage
         {
-            get => new Memory.InMemoryImage(_PrimaryImageContent);
+            get => new Memory.MemoryImage(_PrimaryImageContent);
             set => WithPrimaryImage(value.GetBuffer());
         }
 
@@ -133,9 +133,9 @@ namespace SharpGLTF.Materials
         /// Gets or sets the fallback image bytes to use by this <see cref="TextureBuilder"/>,
         /// Supported formats are: PNG, JPG.
         /// </summary>
-        public Memory.InMemoryImage FallbackImage
+        public Memory.MemoryImage FallbackImage
         {
-            get => new Memory.InMemoryImage(_FallbackImageContent);
+            get => new Memory.MemoryImage(_FallbackImageContent);
             set => WithFallbackImage(value.GetBuffer());
         }
 
@@ -169,7 +169,7 @@ namespace SharpGLTF.Materials
         public TextureBuilder WithImage(string imagePath) { return WithPrimaryImage(imagePath); }
 
         [Obsolete("Use WithPrimaryImage instead,")]
-        public TextureBuilder WithImage(Memory.InMemoryImage image) { return WithPrimaryImage(image); }
+        public TextureBuilder WithImage(Memory.MemoryImage image) { return WithPrimaryImage(image); }
 
         public TextureBuilder WithPrimaryImage(string imagePath)
         {
@@ -180,7 +180,7 @@ namespace SharpGLTF.Materials
             return WithPrimaryImage(primary);
         }
 
-        public TextureBuilder WithPrimaryImage(Memory.InMemoryImage image)
+        public TextureBuilder WithPrimaryImage(Memory.MemoryImage image)
         {
             if (!image.IsEmpty)
             {
@@ -204,7 +204,7 @@ namespace SharpGLTF.Materials
             return WithFallbackImage(primary);
         }
 
-        public TextureBuilder WithFallbackImage(Memory.InMemoryImage image)
+        public TextureBuilder WithFallbackImage(Memory.MemoryImage image)
         {
             if (!image.IsEmpty)
             {

+ 24 - 8
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs

@@ -31,7 +31,7 @@ namespace SharpGLTF.Scenes
 
         public Node GetNode(NodeBuilder key) { return key == null ? null : _Nodes.TryGetValue(key, out Node val) ? val : null; }
 
-        public void AddGeometryResources(ModelRoot root, IEnumerable<SceneBuilder> srcScenes, bool useStridedBuffers)
+        public void AddGeometryResources(ModelRoot root, IEnumerable<SceneBuilder> srcScenes, SceneBuilderSchema2Settings settings)
         {
             // gather all unique MeshBuilders
 
@@ -63,7 +63,7 @@ namespace SharpGLTF.Scenes
 
             // create a Schema2.Mesh for every MeshBuilder.
 
-            var dstMeshes = root.CreateMeshes(mat => _Materials[mat], useStridedBuffers, srcMeshes);
+            var dstMeshes = root.CreateMeshes(mat => _Materials[mat], settings, srcMeshes);
 
             for (int i = 0; i < srcMeshes.Length; ++i)
             {
@@ -157,6 +157,19 @@ namespace SharpGLTF.Scenes
         #endregion
     }
 
+    public struct SceneBuilderSchema2Settings
+    {
+        public static SceneBuilderSchema2Settings Default => new SceneBuilderSchema2Settings
+        {
+            UseStridedBuffers = true,
+            CompactVertexWeights = false
+        };
+
+        public bool UseStridedBuffers;
+
+        public bool CompactVertexWeights;
+    }
+
     public partial class SceneBuilder : IConvertibleToGltf2
     {
         #region from SceneBuilder to Schema2
@@ -167,14 +180,14 @@ namespace SharpGLTF.Scenes
         /// <param name="srcScenes">A collection of scenes</param>
         /// <param name="useStridedBuffers">True to generate strided vertex buffers whenever possible.</param>
         /// <returns>A new <see cref="ModelRoot"/> instance.</returns>
-        public static ModelRoot ToSchema2(IEnumerable<SceneBuilder> srcScenes, bool useStridedBuffers = true)
+        public static ModelRoot ToSchema2(IEnumerable<SceneBuilder> srcScenes, SceneBuilderSchema2Settings settings)
         {
             Guard.NotNull(srcScenes, nameof(srcScenes));
 
             var context = new Schema2SceneBuilder();
 
             var dstModel = ModelRoot.CreateModel();
-            context.AddGeometryResources(dstModel, srcScenes, useStridedBuffers);
+            context.AddGeometryResources(dstModel, srcScenes, settings);
 
             foreach (var srcScene in srcScenes)
             {
@@ -191,7 +204,10 @@ namespace SharpGLTF.Scenes
         [Obsolete("Use ToGltf2")]
         public ModelRoot ToSchema2(bool useStridedBuffers = true)
         {
-            return ToGltf2(useStridedBuffers);
+            var settings = SceneBuilderSchema2Settings.Default;
+            settings.UseStridedBuffers = useStridedBuffers;
+
+            return ToGltf2(settings);
         }
 
         /// <summary>
@@ -199,12 +215,12 @@ namespace SharpGLTF.Scenes
         /// </summary>
         /// <param name="useStridedBuffers">True to generate strided vertex buffers whenever possible.</param>
         /// <returns>A new <see cref="ModelRoot"/> instance.</returns>
-        public ModelRoot ToGltf2(bool useStridedBuffers = true)
+        public ModelRoot ToGltf2(SceneBuilderSchema2Settings settings)
         {
             var context = new Schema2SceneBuilder();
 
             var dstModel = ModelRoot.CreateModel();
-            context.AddGeometryResources(dstModel, new[] { this }, useStridedBuffers);
+            context.AddGeometryResources(dstModel, new[] { this }, settings);
 
             var dstScene = dstModel.UseScene(0);
 
@@ -219,7 +235,7 @@ namespace SharpGLTF.Scenes
 
         public ModelRoot ToGltf2()
         {
-            return ToGltf2(true);
+            return ToGltf2(SceneBuilderSchema2Settings.Default);
         }
 
         #endregion

+ 1 - 1
src/SharpGLTF.Toolkit/Schema2/LightExtensions.cs

@@ -44,7 +44,7 @@ namespace SharpGLTF.Schema2
         /// When undefined, range is assumed to be infinite.
         /// </param>
         /// <returns>This <see cref="PunctualLight"/> instance.</returns>
-        public static PunctualLight WithColor(this PunctualLight light, Vector3 color, float intensity = 1, float range = 0)
+        public static PunctualLight WithColor(this PunctualLight light, Vector3 color, float intensity = 1, float range = float.PositiveInfinity)
         {
             Guard.NotNull(light, nameof(light));
 

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

@@ -298,8 +298,8 @@ namespace SharpGLTF.Schema2
                 dstChannel.Texture.WithTransform(srcXform.Offset, srcXform.Scale, srcXform.Rotation, srcXform.TextureCoordinateOverride);
             }
 
-            dstChannel.Texture.PrimaryImage = srcChannel.Texture.PrimaryImage?.MemoryImage ?? Memory.InMemoryImage.Empty;
-            dstChannel.Texture.FallbackImage = srcChannel.Texture.FallbackImage?.MemoryImage ?? Memory.InMemoryImage.Empty;
+            dstChannel.Texture.PrimaryImage = srcChannel.Texture.PrimaryImage?.MemoryImage ?? Memory.MemoryImage.Empty;
+            dstChannel.Texture.FallbackImage = srcChannel.Texture.FallbackImage?.MemoryImage ?? Memory.MemoryImage.Empty;
         }
 
         public static void CopyTo(this Materials.MaterialBuilder srcMaterial, Material dstMaterial)

+ 4 - 10
src/SharpGLTF.Toolkit/Schema2/MeshExtensions.cs

@@ -57,10 +57,10 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(materialConverter, nameof(materialConverter));
             Guard.NotNull(meshBuilders, nameof(meshBuilders));
 
-            return root.CreateMeshes(materialConverter, true, meshBuilders);
+            return root.CreateMeshes(materialConverter, Scenes.SceneBuilderSchema2Settings.Default, meshBuilders);
         }
 
-        public static IReadOnlyList<Mesh> CreateMeshes<TMaterial>(this ModelRoot root, Converter<TMaterial, Material> materialConverter, bool strided, params IMeshBuilder<TMaterial>[] meshBuilders)
+        public static IReadOnlyList<Mesh> CreateMeshes<TMaterial>(this ModelRoot root, Converter<TMaterial, Material> materialConverter, Scenes.SceneBuilderSchema2Settings settings, params IMeshBuilder<TMaterial>[] meshBuilders)
         {
             Guard.NotNull(root, nameof(root));
             Guard.NotNull(materialConverter, nameof(materialConverter));
@@ -80,7 +80,7 @@ namespace SharpGLTF.Schema2
             // create Schema2.Mesh collections for every gathered group.
 
             var srcMeshes = PackedMeshBuilder<TMaterial>
-                .CreatePackedMeshes(meshBuilders, strided)
+                .CreatePackedMeshes(meshBuilders, settings)
                 .ToList();
 
             PackedMeshBuilder<TMaterial>.MergeBuffers(srcMeshes);
@@ -263,13 +263,7 @@ namespace SharpGLTF.Schema2
         public static MeshPrimitive WithVertexAccessors<TVertex>(this MeshPrimitive primitive, IReadOnlyList<TVertex> vertices)
             where TVertex : IVertexBuilder
         {
-            var indices = vertices.Select(item => item.GetSkinning().GetWeights().MaxIndex);
-
-            var maxIndex = indices.Any() ? indices.Max() : 0;
-
-            var encoding = maxIndex < 256 ? Schema2.EncodingType.UNSIGNED_BYTE : EncodingType.UNSIGNED_SHORT;
-
-            var memAccessors = VertexUtils.CreateVertexMemoryAccessors(vertices, encoding);
+            var memAccessors = VertexUtils.CreateVertexMemoryAccessors(vertices, new PackedEncoding());
 
             return primitive.WithVertexAccessors(memAccessors);
         }

+ 1 - 1
tests/SharpGLTF.Tests/DumpAssemblyAPI.cs → tests/SharpGLTF.NUnit/DumpAssemblyAPI.cs

@@ -11,7 +11,7 @@ namespace SharpGLTF
     /// <summary>
     /// Utility class to dump the public API of an assembly
     /// </summary>
-    static class DumpAssemblyAPI
+    public static class DumpAssemblyAPI
     {
         // https://www.hanselman.com/blog/ManagingChangeWithNETAssemblyDiffTools.aspx
 

+ 93 - 0
tests/SharpGLTF.NUnit/NUnitGltfUtils.cs

@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+using NUnit.Framework;
+
+namespace SharpGLTF
+{
+    public static class NUnitGltfUtils
+    {
+        public static void AttachGltfValidatorLinks(this TestContext context)
+        {
+            context.AttachLink("🌍 Khronos Validator", "http://github.khronos.org/glTF-Validator/");
+            context.AttachLink("🌍 BabylonJS Sandbox", "https://sandbox.babylonjs.com/");
+            context.AttachLink("🌍 Don McCurdy Sandbox", "https://gltf-viewer.donmccurdy.com/");
+            context.AttachLink("🌍 VirtualGIS Cesium Sandbox", "https://www.virtualgis.io/gltfviewer/");
+        }        
+
+        public static void AttachToCurrentTest(this Scenes.SceneBuilder scene, string fileName)
+        {
+            var model = scene.ToGltf2();
+
+            model.AttachToCurrentTest(fileName);
+        }
+
+        public static void AttachToCurrentTest(this Schema2.ModelRoot model, string fileName, Schema2.Animation animation, float time)
+        {
+            fileName = fileName.Replace(" ", "_");
+
+            // find the output path for the current test
+            fileName = TestContext.CurrentContext.GetAttachmentPath(fileName, true);
+
+            Schema2.Schema2Toolkit.SaveAsWavefront(model, fileName, animation, time);
+
+            // Attach the saved file to the current test
+            TestContext.AddTestAttachment(fileName);
+        }
+
+        public static void AttachToCurrentTest<TvG, TvM, TvS>(this Geometry.MeshBuilder<TvG, TvM, TvS> mesh, string fileName)
+            where TvG : struct, Geometry.VertexTypes.IVertexGeometry
+            where TvM : struct, Geometry.VertexTypes.IVertexMaterial
+            where TvS : struct, Geometry.VertexTypes.IVertexSkinning
+        {
+            var gl2model = Schema2.ModelRoot.CreateModel();
+
+            var gl2mesh = Schema2.Schema2Toolkit.CreateMeshes(gl2model, mesh).First();
+
+            var node = gl2model.UseScene(0).CreateNode();
+            node.Mesh = gl2mesh;
+
+            gl2model.AttachToCurrentTest(fileName);
+        }
+
+        public static void AttachToCurrentTest(this Schema2.ModelRoot model, string fileName)
+        {
+            // find the output path for the current test
+            fileName = TestContext.CurrentContext.GetAttachmentPath(fileName, true);
+
+            if (fileName.ToLower().EndsWith(".glb"))
+            {
+                model.SaveGLB(fileName);
+            }
+            else if (fileName.ToLower().EndsWith(".gltf"))
+            {
+                model.Save(fileName, new Schema2.WriteSettings { JsonIndented = true });
+            }
+            else if (fileName.ToLower().EndsWith(".obj"))
+            {
+                fileName = fileName.Replace(" ", "_");
+                Schema2.Schema2Toolkit.SaveAsWavefront(model, fileName);
+            }
+
+            // Attach the saved file to the current test
+            TestContext.AddTestAttachment(fileName);
+
+            if (fileName.ToLower().EndsWith(".obj")) return;
+
+            var report = gltf_validator.ValidateFile(fileName);
+            if (report == null) return;
+
+            if (report.HasErrors || report.HasWarnings)
+            {
+                TestContext.WriteLine(report.ToString());
+            }
+
+            Assert.IsFalse(report.HasErrors);
+        }
+    }
+
+    
+}

+ 68 - 0
tests/SharpGLTF.NUnit/NUnitUtils.cs

@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using NUnit.Framework;
+
+namespace SharpGLTF
+{
+    public static class NUnitUtils
+    {
+        public static string ToShortDisplayPath(this string path)
+        {
+            var dir = System.IO.Path.GetDirectoryName(path);
+            var fxt = System.IO.Path.GetFileName(path);
+
+            const int maxdir = 12;
+
+            if (dir.Length > maxdir)
+            {
+                dir = "..." + dir.Substring(dir.Length - maxdir);
+            }
+
+            return System.IO.Path.Combine(dir, fxt);
+        }
+
+        public static string GetAttachmentPath(this TestContext context, string fileName, bool ensureDirectoryExists = false)
+        {
+            var path = System.IO.Path.Combine(context.TestDirectory, "TestResults", $"{context.Test.ID}");
+            var dir = path;
+
+            if (!string.IsNullOrWhiteSpace(fileName))
+            {
+                if (System.IO.Path.IsPathRooted(fileName)) throw new ArgumentException(nameof(fileName), "path must be a relative path");
+                path = System.IO.Path.Combine(path, fileName);
+
+                dir = System.IO.Path.GetDirectoryName(path);
+            }
+
+            System.IO.Directory.CreateDirectory(dir);
+
+            return path;
+        }
+
+        public static void AttachText(this TestContext context, string fileName, string[] lines)
+        {
+            fileName = context.GetAttachmentPath(fileName, true);
+
+            System.IO.File.WriteAllLines(fileName, lines.ToArray());
+
+            TestContext.AddTestAttachment(fileName);
+        }
+
+        public static void AttachShowDirLink(this TestContext context)
+        {
+            context.AttachLink("📂 Show Directory", context.GetAttachmentPath(string.Empty));
+        }
+
+        public static void AttachLink(this TestContext context, string linkPath, string targetPath)
+        {
+            linkPath = context.GetAttachmentPath(linkPath);
+
+            linkPath = ShortcutUtils.CreateLink(linkPath, targetPath);
+
+            TestContext.AddTestAttachment(linkPath);
+        }
+    }
+}

+ 86 - 29
tests/SharpGLTF.Tests/NumericsAssert.cs → tests/SharpGLTF.NUnit/NumericsAssert.cs

@@ -9,6 +9,13 @@ namespace SharpGLTF
 {
     public static class NumericsAssert
     {
+        public static double UnitError(this Vector3 v) { return v.LengthError(1); }
+        
+        public static double LengthError(this Vector3 v, double expectedLength)
+        {
+            return Math.Abs(Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z) - expectedLength);
+        }
+
         public static void IsFinite(Single value, string message = null)
         {
             // Assert.IsTrue(float.IsFinite(value), message);
@@ -60,6 +67,18 @@ namespace SharpGLTF
             IsFinite(plane.D, "D");
         }
 
+        public static void IsFinite(Matrix3x2 matrix)
+        {
+            IsFinite(matrix.M11, "M11");
+            IsFinite(matrix.M12, "M12");            
+
+            IsFinite(matrix.M21, "M21");
+            IsFinite(matrix.M22, "M22");
+            
+            IsFinite(matrix.M31, "M31");
+            IsFinite(matrix.M32, "M32");
+        }
+
         public static void IsFinite(Matrix4x4 matrix)
         {
             IsFinite(matrix.M11, "M11");
@@ -87,7 +106,7 @@ namespace SharpGLTF
         {
             Assert.AreEqual(0, (double)BigInteger.Abs(actual - expected), tolerance);
         }
-
+        
         public static void AreEqual(Vector2 expected, Vector2 actual, double tolerance = 0)
         {
             Assert.AreEqual(expected.X, actual.X, tolerance, "X");
@@ -117,27 +136,33 @@ namespace SharpGLTF
             Assert.AreEqual(expected.W, actual.W, tolerance, "W");
         }
 
-        public static void AreEqual(Matrix4x4 expected, Matrix4x4 actual, double delta = 0)
+        public static void AreEqual(Matrix4x4 expected, Matrix4x4 actual, double tolerance = 0)
         {
-            Assert.AreEqual(expected.M11, actual.M11, delta, "M11");
-            Assert.AreEqual(expected.M12, actual.M12, delta, "M12");
-            Assert.AreEqual(expected.M13, actual.M13, delta, "M13");
-            Assert.AreEqual(expected.M14, actual.M14, delta, "M14");
+            Assert.AreEqual(expected.M11, actual.M11, tolerance, "M11");
+            Assert.AreEqual(expected.M12, actual.M12, tolerance, "M12");
+            Assert.AreEqual(expected.M13, actual.M13, tolerance, "M13");
+            Assert.AreEqual(expected.M14, actual.M14, tolerance, "M14");
 
-            Assert.AreEqual(expected.M21, actual.M21, delta, "M21");
-            Assert.AreEqual(expected.M22, actual.M22, delta, "M22");
-            Assert.AreEqual(expected.M23, actual.M23, delta, "M23");
-            Assert.AreEqual(expected.M24, actual.M24, delta, "M24");
+            Assert.AreEqual(expected.M21, actual.M21, tolerance, "M21");
+            Assert.AreEqual(expected.M22, actual.M22, tolerance, "M22");
+            Assert.AreEqual(expected.M23, actual.M23, tolerance, "M23");
+            Assert.AreEqual(expected.M24, actual.M24, tolerance, "M24");
 
-            Assert.AreEqual(expected.M31, actual.M31, delta, "M31");
-            Assert.AreEqual(expected.M32, actual.M32, delta, "M32");
-            Assert.AreEqual(expected.M33, actual.M33, delta, "M33");
-            Assert.AreEqual(expected.M34, actual.M34, delta, "M34");
+            Assert.AreEqual(expected.M31, actual.M31, tolerance, "M31");
+            Assert.AreEqual(expected.M32, actual.M32, tolerance, "M32");
+            Assert.AreEqual(expected.M33, actual.M33, tolerance, "M33");
+            Assert.AreEqual(expected.M34, actual.M34, tolerance, "M34");
 
-            Assert.AreEqual(expected.M41, actual.M41, delta, "M41");
-            Assert.AreEqual(expected.M42, actual.M42, delta, "M42");
-            Assert.AreEqual(expected.M43, actual.M43, delta, "M43");
-            Assert.AreEqual(expected.M44, actual.M44, delta, "M44");
+            Assert.AreEqual(expected.M41, actual.M41, tolerance, "M41");
+            Assert.AreEqual(expected.M42, actual.M42, tolerance, "M42");
+            Assert.AreEqual(expected.M43, actual.M43, tolerance, "M43");
+            Assert.AreEqual(expected.M44, actual.M44, tolerance, "M44");
+        }
+
+        public static void IsInvertible(Matrix3x2 matrix)
+        {
+            IsFinite(matrix);
+            Assert.IsTrue(Matrix3x2.Invert(matrix, out Matrix3x2 inverted));
         }
 
         public static void IsInvertible(Matrix4x4 matrix)
@@ -164,30 +189,62 @@ namespace SharpGLTF
             Assert.AreEqual(0, Vector3.Dot(cy, cz), tolerance);
         }
 
-        public static void IsNormalized(Vector2 actual, double delta = 0)
+        public static void Length(Vector2 actual, double length, double tolerance = 0)
         {
             IsFinite(actual);
-            AreEqual(Vector2.Normalize(actual), actual, delta);
+
+            length = Math.Abs(actual.Length() - length);
+
+            Assert.AreEqual(0, length, tolerance);
         }
 
-        public static void IsNormalized(Vector3 actual, double delta = 0)
+        public static void Length(Vector3 actual, double length, double tolerance = 0)
         {
             IsFinite(actual);
-            AreEqual(Vector3.Normalize(actual), actual, delta);
+
+            length = Math.Abs(actual.Length() - length);
+
+            Assert.AreEqual(0, length, tolerance);
         }
 
-        public static void IsNormalized(Vector4 actual, double delta = 0)
+        public static void Length(Vector4 actual, double length, double tolerance = 0)
         {
             IsFinite(actual);
-            AreEqual(Vector4.Normalize(actual), actual, delta);
+
+            length = Math.Abs(actual.Length() - length);
+
+            Assert.AreEqual(0, length, tolerance);
         }
 
-        public static void IsNormalized(Quaternion actual, double delta = 0)
+        public static void Length(Quaternion actual, double length, double tolerance = 0)
         {
             IsFinite(actual);
-            AreEqual(Quaternion.Normalize(actual), actual, delta);
+
+            length = Math.Abs(actual.Length() - length);
+
+            Assert.AreEqual(0, length, tolerance);
+        }
+
+        public static void IsNormalized(Vector2 actual, double tolerance = 0)
+        {
+            Length(actual, 1, tolerance);
         }
 
+        public static void IsNormalized(Vector3 actual, double tolerance = 0)
+        {
+            Length(actual, 1, tolerance);
+        }
+
+        public static void IsNormalized(Vector4 actual, double tolerance = 0)
+        {
+            Length(actual, 1, tolerance);
+        }
+
+        public static void IsNormalized(Quaternion actual, double tolerance = 0)
+        {
+            Length(actual, 1, tolerance);
+        }
+        
         public static void InRange(BigInteger value, BigInteger min, BigInteger max)
         {
             GreaterOrEqual(value, min);
@@ -318,21 +375,21 @@ namespace SharpGLTF
 
         public static void AngleLessOrEqual(Vector2 a, Vector2 b, double radians)
         {
-            var angle = VectorsUtils.GetAngle(a, b);
+            var angle = (a, b).GetAngle();
 
             Assert.LessOrEqual(angle, radians, "Angle");
         }
 
         public static void AngleLessOrEqual(Vector3 a, Vector3 b, double radians)
         {
-            var angle = VectorsUtils.GetAngle(a, b);
+            var angle = (a, b).GetAngle();
 
             Assert.LessOrEqual(angle, radians, "Angle");
         }
 
         public static void AngleLessOrEqual(Quaternion a, Quaternion b, double radians)
         {
-            var angle = VectorsUtils.GetAngle(a, b);
+            var angle = (a, b).GetAngle();
 
             Assert.LessOrEqual(angle, radians, "Angle");
         }

+ 107 - 0
tests/SharpGLTF.NUnit/NumericsUtils.cs

@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF
+{
+    public static class VectorsUtils
+    {
+        public static bool IsFinite(this float value)
+        {
+            return !float.IsNaN(value) && !float.IsInfinity(value);
+        }
+
+        public static Single NextSingle(this Random rnd)
+        {
+            return (Single)rnd.NextDouble();
+        }
+
+        public static Vector2 NextVector2(this Random rnd)
+        {
+            return new Vector2(rnd.NextSingle(), rnd.NextSingle());
+        }
+
+        public static Vector3 NextVector3(this Random rnd)
+        {
+            return new Vector3(rnd.NextSingle(), rnd.NextSingle(), rnd.NextSingle());
+        }
+
+        public static Vector4 NextVector4(this Random rnd)
+        {
+            return new Vector4(rnd.NextSingle(), rnd.NextSingle(), rnd.NextSingle(), rnd.NextSingle());
+        }
+
+        public static float GetAngle(this (Quaternion a, Quaternion b) pair)
+        {
+            var w = Quaternion.Concatenate(pair.b, Quaternion.Inverse(pair.a)).W;
+
+            if (w < -1) w = -1;
+            if (w > 1) w = 1;
+
+            return (float)Math.Acos(w) * 2;
+        }
+
+        public static float GetAngle(this (Vector3 a, Vector3 b) pair)
+        {
+            var a = Vector3.Normalize(pair.a);
+            var b = Vector3.Normalize(pair.b);
+
+            var c = Vector3.Dot(a, b);
+            if (c > 1) c = 1;
+            if (c < -1) c = -1;
+
+            return (float)Math.Acos(c);
+        }
+
+        public static float GetAngle(this (Vector2 a, Vector2 b) pair)
+        {
+            var a = Vector2.Normalize(pair.a);
+            var b = Vector2.Normalize(pair.b);
+
+            var c = Vector2.Dot(a, b);
+            if (c > 1) c = 1;
+            if (c < -1) c = -1;
+
+            return (float)Math.Acos(c);
+        }
+
+        public static (Vector3, Vector3) GetBounds(this IEnumerable<Vector3> collection)
+        {
+            var min = new Vector3(float.MaxValue);
+            var max = new Vector3(float.MinValue);
+
+            foreach (var v in collection)
+            {
+                min = Vector3.Min(v, min);
+                max = Vector3.Max(v, max);
+            }
+
+            return (min, max);
+        }
+
+        public static Vector3 GetMin(this IEnumerable<Vector3> collection)
+        {
+            var min = new Vector3(float.MaxValue);
+
+            foreach (var v in collection)
+            {
+                min = Vector3.Min(v, min);
+            }
+
+            return min;
+        }
+
+        public static Vector3 GetMax(this IEnumerable<Vector3> collection)
+        {
+            var max = new Vector3(float.MinValue);
+
+            foreach (var v in collection)
+            {
+                max = Vector3.Max(v, max);
+            }
+
+            return max;
+        }
+    }
+}

+ 0 - 0
tests/SharpGLTF.Tests/Plotting.cs → tests/SharpGLTF.NUnit/Plotting.cs


+ 0 - 0
tests/SharpGLTF.Tests/Reports.cs → tests/SharpGLTF.NUnit/Reports.cs


+ 30 - 0
tests/SharpGLTF.NUnit/SharpGLTF.NUnit.csproj

@@ -0,0 +1,30 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  
+  <PropertyGroup>
+    <TargetFrameworks>netstandard2.0</TargetFrameworks>
+    <IsPackable>false</IsPackable>
+    <RootNamespace>SharpGLTF</RootNamespace>
+    <LangVersion>latest</LangVersion>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Content Include="..\..\tools\linux64\gltf_validator" Link="gltf_validator">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="..\..\tools\win64\gltf_validator.exe" Link="gltf_validator.exe">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+
+  <ItemGroup>    
+    <PackageReference Include="nunit" Version="3.12.0" />    
+    <PackageReference Include="PLplot" Version="5.13.7" />
+    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\SharpGLTF.Toolkit\SharpGLTF.Toolkit.csproj" />
+    <ProjectReference Include="..\..\src\SharpGLTF.Core\SharpGLTF.Core.csproj" />
+  </ItemGroup>
+
+</Project>

+ 42 - 0
tests/SharpGLTF.NUnit/ShortcutUtils.cs

@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF
+{
+    public static class ShortcutUtils
+    {
+        public static string CreateLink(string localLinkPath, string targetPath)
+        {
+            if (string.IsNullOrWhiteSpace(localLinkPath)) throw new ArgumentNullException(nameof(localLinkPath));
+            if (string.IsNullOrWhiteSpace(targetPath)) throw new ArgumentNullException(nameof(targetPath));
+
+            if (!Uri.TryCreate(targetPath, UriKind.Absolute, out Uri uri)) throw new UriFormatException(nameof(targetPath));
+
+            var sb = new StringBuilder();
+
+            sb.AppendLine("[{000214A0-0000-0000-C000-000000000046}]");
+            sb.AppendLine("Prop3=19,11");
+            sb.AppendLine("[InternetShortcut]");
+            sb.AppendLine("IDList=");
+            sb.AppendLine($"URL={uri.AbsoluteUri}");
+
+            if (uri.IsFile)
+            {
+                sb.AppendLine("IconIndex=1");
+                string icon = targetPath.Replace('\\', '/');
+                sb.AppendLine("IconFile=" + icon);
+            }
+            else
+            {
+                sb.AppendLine("IconIndex=0");
+            }
+
+            localLinkPath = System.IO.Path.ChangeExtension(localLinkPath, ".url");
+
+            System.IO.File.WriteAllText(localLinkPath, sb.ToString());
+
+            return localLinkPath;
+        }
+    }
+}

+ 1 - 1
tests/SharpGLTF.Tests/gltf_validator.cs → tests/SharpGLTF.NUnit/gltf_validator.cs

@@ -12,7 +12,7 @@ namespace SharpGLTF
     /// <remarks>
     /// LINUX execution path has not been tested!
     /// </remarks>
-    static class gltf_validator
+    public static class gltf_validator
     {
         static gltf_validator()
         {

+ 1 - 1
tests/SharpGLTF.Tests/AnimationSamplingTests.cs

@@ -218,7 +218,7 @@ namespace SharpGLTF
                 NumericsAssert.AngleLessOrEqual(sq, hq, 0.22f);
 
                 // diff
-                var a = VectorsUtils.GetAngle(sq, hq) * 180.0f / 3.141592f;
+                var a = (sq, hq).GetAngle() * 180.0f / 3.141592f;
                 angles.Add(new Vector2(amount, a));                          
             }
 

+ 3 - 3
tests/SharpGLTF.Tests/Memory/MemoryAccessorTests.cs

@@ -13,14 +13,14 @@ namespace SharpGLTF.Memory
         [Test]
         public void CreateInterleaved1()
         {
-            var pos = MemoryEncoding.CreateDefaultElement("POSITION");
-            var nrm = MemoryEncoding.CreateDefaultElement("NORMAL");
+            var pos = MemoryAccessInfo.CreateDefaultElement("POSITION");
+            var nrm = MemoryAccessInfo.CreateDefaultElement("NORMAL");
 
             var attributes = new[] { pos, nrm };
 
             const int baseOffset = 8;
 
-            var byteStride = MemoryEncoding.SetInterleavedInfo(attributes, baseOffset, 5);
+            var byteStride = MemoryAccessInfo.SetInterleavedInfo(attributes, baseOffset, 5);
 
             pos = attributes[0];
             nrm = attributes[1];

+ 14 - 4
tests/SharpGLTF.Tests/Runtime/SceneTemplateTests.cs

@@ -17,12 +17,22 @@ namespace SharpGLTF.Runtime
             // there's any reference from the template to the source model
             // that prevents the source model to be garbage collected.
 
-            var path = TestFiles.GetSampleModelsPaths()
-                            .FirstOrDefault(item => item.Contains("CesiumMan.glb"));
+            (SceneTemplate, WeakReference<Schema2.ModelRoot>) scopedLoad()
+            {
+                var path = TestFiles.GetSampleModelsPaths()
+                                .FirstOrDefault(item => item.Contains("BrainStem.glb"));
 
-            var result = LoadModelTemplate(path);            
+                var result = LoadModelTemplate(path);
 
-            GC.Collect(0);
+                GC.Collect();
+                GC.WaitForFullGCComplete();
+
+                return result;
+            }
+
+            var result = scopedLoad();
+
+            GC.Collect();
             GC.WaitForFullGCComplete();
 
             Assert.IsFalse(result.Item2.TryGetTarget(out Schema2.ModelRoot model));

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

@@ -517,7 +517,7 @@ namespace SharpGLTF.Scenes
         [TestCase("BrainStem.glb")]
         [TestCase("CesiumMan.glb")]
         [TestCase("GearboxAssy.glb")]
-        [TestCase("Monster.glb")]
+        // [TestCase("Monster.glb")]
         [TestCase("OrientationTest.glb")]
         [TestCase("RiggedFigure.glb")]
         [TestCase("RiggedSimple.glb")]
@@ -530,7 +530,7 @@ namespace SharpGLTF.Scenes
                 .GetSampleModelsPaths()
                 .FirstOrDefault(item => item.Contains(path));
 
-            var srcModel = Schema2.ModelRoot.Load(path);
+            var srcModel = Schema2.ModelRoot.Load(path, Validation.ValidationMode.TryFix);
             Assert.NotNull(srcModel);
 
             // perform roundtrip
@@ -538,7 +538,10 @@ namespace SharpGLTF.Scenes
             var srcScene = Schema2Toolkit.ToSceneBuilder(srcModel.DefaultScene);            
 
             var rowModel = srcScene.ToGltf2();
-            var colModel = srcScene.ToGltf2(false);
+
+            var settings = SceneBuilderSchema2Settings.Default;
+            settings.UseStridedBuffers = false;
+            var colModel = srcScene.ToGltf2(settings);
 
             var rowScene = Schema2Toolkit.ToSceneBuilder(rowModel.DefaultScene);
             var colScene = Schema2Toolkit.ToSceneBuilder(colModel.DefaultScene);

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

@@ -26,7 +26,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
         #region helpers
 
-        private static void _LoadModel(string f)
+        private static void _LoadModel(string f, bool tryFix = false)
         {
             var perf = System.Diagnostics.Stopwatch.StartNew();
 
@@ -34,7 +34,9 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
             try
             {
-                model = ModelRoot.Load(f);
+                var settings = tryFix ? Validation.ValidationMode.TryFix : Validation.ValidationMode.Strict;
+
+                model = ModelRoot.Load(f, settings);
                 Assert.NotNull(model);
             }
             catch (Exception ex)
@@ -115,7 +117,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
             {
                 TestContext.Progress.WriteLine(f);
 
-                _LoadModel(f);
+                _LoadModel(f, true);
             }
         }
 
@@ -247,8 +249,9 @@ namespace SharpGLTF.Schema2.LoadAndSave
         [TestCase("AnimatedMorphCube.glb")]
         [TestCase("AnimatedMorphSphere.glb")]
         [TestCase("CesiumMan.glb")]
-        [TestCase("Monster.glb")]
+        //[TestCase("Monster.glb")] // temporarily removed from khronos repo
         [TestCase("BrainStem.glb")]
+        [TestCase("Fox.glb")]
         public void LoadModelsWithAnimations(string path)
         {
             TestContext.CurrentContext.AttachShowDirLink();

+ 2 - 4
tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSpecialModelsTest.cs

@@ -26,9 +26,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
         public void LoadWithCustomImageLoader()
         {
-            TestContext.CurrentContext.AttachShowDirLink();
-
-            
+            TestContext.CurrentContext.AttachShowDirLink();            
 
             // load Polly model
             var model = ModelRoot.Load(TestFiles.GetPollyFileModelPath());
@@ -40,7 +38,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
             TestContext.CurrentContext.AttachShowDirLink();
 
             // load Polly model
-            var model = ModelRoot.Load(TestFiles.GetPollyFileModelPath());
+            var model = ModelRoot.Load(TestFiles.GetPollyFileModelPath(), Validation.ValidationMode.TryFix);
 
             Assert.NotNull(model);
 

+ 7 - 98
tests/SharpGLTF.Tests/SharpGLTF.Tests.csproj

@@ -1,119 +1,28 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>netcoreapp2.1;net471</TargetFrameworks>
-
+    <TargetFrameworks>netcoreapp3;net471</TargetFrameworks>
     <IsPackable>false</IsPackable>
-
     <RootNamespace>SharpGLTF</RootNamespace>
-
     <LangVersion>latest</LangVersion>
-  </PropertyGroup>  
-
-  <ItemGroup>
-    <None Remove="Assets\SpecialCases\mouse.glb" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <Content Include="Assets\SpecialCases\mouse.glb">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
+  </PropertyGroup> 
 
-  <ItemGroup>
-    <None Include="..\..\tools\linux64\gltf_validator" Link="gltf_validator" />
-    <None Include="..\..\tools\win64\gltf_validator.exe" Link="gltf_validator.exe" />
+  <ItemGroup>    
+    <ProjectReference Include="..\SharpGLTF.NUnit\SharpGLTF.NUnit.csproj" />
   </ItemGroup>
 
   <ItemGroup>    
-    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />    
-    <PackageReference Include="nunit" Version="3.12.0" />
-    <PackageReference Include="NUnit3TestAdapter" Version="3.16.0">
+    <PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
-    <PackageReference Include="PLplot" Version="5.13.7" />    
-  </ItemGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\..\src\SharpGLTF.Toolkit\SharpGLTF.Toolkit.csproj" />
-    <ProjectReference Include="..\..\src\SharpGLTF.Core\SharpGLTF.Core.csproj" />
   </ItemGroup>
 
   <ItemGroup>
-    <None Update="Assets\API.Core.1.0.0-alpha0005.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Core.1.0.0-alpha0006.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Core.1.0.0-alpha0007.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Core.1.0.0-alpha0008.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Core.1.0.0-alpha0009.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Core.1.0.0-alpha0010.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Core.1.0.0-alpha0011.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Toolkit.1.0.0-alpha0005.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Toolkit.1.0.0-alpha0006.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Toolkit.1.0.0-alpha0007.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Toolkit.1.0.0-alpha0008.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Toolkit.1.0.0-alpha0009.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Toolkit.1.0.0-alpha0010.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Toolkit.1.0.0-alpha0011.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\API.Toolkit.1.0.0-alpha011.txt">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\Invalid_Json.gltf">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\shannon-dxt5.dds">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\shannon.jpg">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\shannon.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\shannon.webp">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\SpecialCases\batched.glb">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\SpecialCases\body_id.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\SpecialCases\shrekshao.glb">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Update="Assets\Texture1.jpg">
+    <None Update="Assets\**">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
+    </None>    
   </ItemGroup>
 
 </Project>

+ 0 - 290
tests/SharpGLTF.Tests/Utils.cs

@@ -1,290 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Numerics;
-using System.Text;
-
-using NUnit.Framework;
-
-namespace SharpGLTF
-{
-    static class NUnitUtils
-    {
-        public static string ToShortDisplayPath(this string path)
-        {
-            var dir = System.IO.Path.GetDirectoryName(path);
-            var fxt = System.IO.Path.GetFileName(path);
-
-            const int maxdir = 12;
-
-            if (dir.Length > maxdir)
-            {
-                dir = "..." + dir.Substring(dir.Length - maxdir);
-            }
-
-            return System.IO.Path.Combine(dir, fxt);
-        }        
-
-        public static string GetAttachmentPath(this TestContext context, string fileName, bool ensureDirectoryExists = false)
-        {
-            var path = System.IO.Path.Combine(context.TestDirectory, "TestResults", $"{context.Test.ID}");
-            var dir = path;
-
-            if (!string.IsNullOrWhiteSpace(fileName))
-            {
-                if (System.IO.Path.IsPathRooted(fileName)) throw new ArgumentException(nameof(fileName), "path must be a relative path");
-                path = System.IO.Path.Combine(path, fileName);
-
-                dir = System.IO.Path.GetDirectoryName(path);
-            }
-
-            System.IO.Directory.CreateDirectory(dir);
-
-            return path;
-        }
-
-        public static void AttachText(this TestContext context, string fileName, string[] lines)
-        {
-            fileName = context.GetAttachmentPath(fileName, true);
-
-            System.IO.File.WriteAllLines(fileName, lines.ToArray());
-
-            TestContext.AddTestAttachment(fileName);
-        }
-
-        public static void AttachShowDirLink(this TestContext context)
-        {
-            context.AttachLink("📂 Show Directory", context.GetAttachmentPath(string.Empty));
-        }
-
-        public static void AttachLink(this TestContext context, string linkPath, string targetPath)
-        {
-            linkPath = context.GetAttachmentPath(linkPath);
-
-            linkPath = ShortcutUtils.CreateLink(linkPath, targetPath);
-
-            TestContext.AddTestAttachment(linkPath);
-        }        
-    }
-
-    static class ShortcutUtils
-    {
-        public static string CreateLink(string localLinkPath, string targetPath)
-        {
-            if (string.IsNullOrWhiteSpace(localLinkPath)) throw new ArgumentNullException(nameof(localLinkPath));
-            if (string.IsNullOrWhiteSpace(targetPath)) throw new ArgumentNullException(nameof(targetPath));
-
-            if (!Uri.TryCreate(targetPath, UriKind.Absolute, out Uri uri)) throw new UriFormatException(nameof(targetPath));
-
-            var sb = new StringBuilder();
-
-            sb.AppendLine("[{000214A0-0000-0000-C000-000000000046}]");
-            sb.AppendLine("Prop3=19,11");
-            sb.AppendLine("[InternetShortcut]");
-            sb.AppendLine("IDList=");
-            sb.AppendLine($"URL={uri.AbsoluteUri}");
-
-            if (uri.IsFile)
-            {
-                sb.AppendLine("IconIndex=1");
-                string icon = targetPath.Replace('\\', '/');
-                sb.AppendLine("IconFile=" + icon);
-            }
-            else
-            {
-                sb.AppendLine("IconIndex=0");
-            }
-
-            localLinkPath = System.IO.Path.ChangeExtension(localLinkPath, ".url");
-
-            System.IO.File.WriteAllText(localLinkPath, sb.ToString());
-
-            return localLinkPath;
-        }
-    }
-
-    static class NUnitGltfUtils
-    {
-        public static void AttachGltfValidatorLinks(this TestContext context)
-        {
-            context.AttachLink("🌍 Khronos Validator", "http://github.khronos.org/glTF-Validator/");
-            context.AttachLink("🌍 BabylonJS Sandbox", "https://sandbox.babylonjs.com/");
-            context.AttachLink("🌍 Don McCurdy Sandbox", "https://gltf-viewer.donmccurdy.com/");
-            context.AttachLink("🌍 VirtualGIS Cesium Sandbox", "https://www.virtualgis.io/gltfviewer/");
-        }
-
-        
-
-        public static void AttachToCurrentTest(this Scenes.SceneBuilder scene, string fileName)
-        {
-            var model = scene.ToGltf2();
-
-            model.AttachToCurrentTest(fileName);
-        }
-
-        public static void AttachToCurrentTest(this Schema2.ModelRoot model, string fileName, Schema2.Animation animation, float time)
-        {
-            fileName = fileName.Replace(" ", "_");
-
-            // find the output path for the current test
-            fileName = TestContext.CurrentContext.GetAttachmentPath(fileName, true);
-
-            Schema2.Schema2Toolkit.SaveAsWavefront(model, fileName, animation, time);
-
-            // Attach the saved file to the current test
-            TestContext.AddTestAttachment(fileName);
-        }
-
-        public static void AttachToCurrentTest<TvG, TvM, TvS>(this Geometry.MeshBuilder<TvG, TvM, TvS> mesh, string fileName)
-            where TvG : struct, Geometry.VertexTypes.IVertexGeometry
-            where TvM : struct, Geometry.VertexTypes.IVertexMaterial
-            where TvS : struct, Geometry.VertexTypes.IVertexSkinning
-        {
-            var gl2model = Schema2.ModelRoot.CreateModel();
-
-            var gl2mesh = Schema2.Schema2Toolkit.CreateMeshes(gl2model, mesh).First();
-
-            var node = gl2model.UseScene(0).CreateNode();
-            node.Mesh = gl2mesh;
-
-            gl2model.AttachToCurrentTest(fileName);
-        }
-
-        public static void AttachToCurrentTest(this Schema2.ModelRoot model, string fileName)
-        {
-            // find the output path for the current test
-            fileName = TestContext.CurrentContext.GetAttachmentPath(fileName, true);
-
-            if (fileName.ToLower().EndsWith(".glb"))
-            {
-                model.SaveGLB(fileName);
-            }
-            else if (fileName.ToLower().EndsWith(".gltf"))
-            {
-                model.Save(fileName, new Schema2.WriteSettings { JsonIndented = true });
-            }
-            else if (fileName.ToLower().EndsWith(".obj"))
-            {
-                fileName = fileName.Replace(" ", "_");
-                Schema2.Schema2Toolkit.SaveAsWavefront(model, fileName);
-            }
-
-            // Attach the saved file to the current test
-            TestContext.AddTestAttachment(fileName);
-
-            if (fileName.ToLower().EndsWith(".obj")) return;
-
-            var report = gltf_validator.ValidateFile(fileName);
-            if (report == null) return;
-
-            if (report.HasErrors || report.HasWarnings)
-            {
-                TestContext.WriteLine(report.ToString());
-            }
-
-            Assert.IsFalse(report.HasErrors);
-        }
-    }
-
-    static class VectorsUtils
-    {
-        public static bool IsFinite(this float value)
-        {
-            return !float.IsNaN(value) && !float.IsInfinity(value);
-        }
-
-        public static Single NextSingle(this Random rnd)
-        {
-            return (Single)rnd.NextDouble();
-        }
-
-        public static Vector2 NextVector2(this Random rnd)
-        {
-            return new Vector2(rnd.NextSingle(), rnd.NextSingle());
-        }
-
-        public static Vector3 NextVector3(this Random rnd)
-        {
-            return new Vector3(rnd.NextSingle(), rnd.NextSingle(), rnd.NextSingle());
-        }
-
-        public static Vector4 NextVector4(this Random rnd)
-        {
-            return new Vector4(rnd.NextSingle(), rnd.NextSingle(), rnd.NextSingle(), rnd.NextSingle());
-        }
-
-        public static float GetAngle(Quaternion a, Quaternion b)
-        {
-            var w = Quaternion.Concatenate(b, Quaternion.Inverse(a)).W;
-
-            if (w < -1) w = -1;
-            if (w > 1) w = 1;
-
-            return (float)Math.Acos(w) * 2;
-        }
-
-        public static float GetAngle(Vector3 a, Vector3 b)
-        {
-            a = Vector3.Normalize(a);
-            b = Vector3.Normalize(b);
-
-            var c = Vector3.Dot(a, b);
-            if (c > 1) c = 1;
-            if (c < -1) c = -1;
-
-            return (float)Math.Acos(c);
-        }
-
-        public static float GetAngle(Vector2 a, Vector2 b)
-        {
-            a = Vector2.Normalize(a);
-            b = Vector2.Normalize(b);
-
-            var c = Vector2.Dot(a, b);
-            if (c > 1) c = 1;
-            if (c < -1) c = -1;
-
-            return (float)Math.Acos(c);
-        }
-
-        public static (Vector3, Vector3) GetBounds(this IEnumerable<Vector3> collection)
-        {
-            var min = new Vector3(float.MaxValue);
-            var max = new Vector3(float.MinValue);
-
-            foreach (var v in collection)
-            {
-                min = Vector3.Min(v, min);
-                max = Vector3.Max(v, max);
-            }
-
-            return (min, max);
-        }
-
-        public static Vector3 GetMin(this IEnumerable<Vector3> collection)
-        {
-            var min = new Vector3(float.MaxValue);            
-
-            foreach (var v in collection)
-            {
-                min = Vector3.Min(v, min);                
-            }
-
-            return min;
-        }
-
-        public static Vector3 GetMax(this IEnumerable<Vector3> collection)
-        {            
-            var max = new Vector3(float.MinValue);
-
-            foreach (var v in collection)
-            {
-                max = Vector3.Max(v, max);
-            }
-
-            return max;
-        }
-    }
-
-    
-}