Browse Source

Added lots of Argument checks.
Improving scenes API.

Vicente Penades 6 years ago
parent
commit
a89058eae9
51 changed files with 874 additions and 469 deletions
  1. 2 2
      Analyzers.targets
  2. 2 0
      SharpGLTF.ruleset
  3. 7 7
      src/Shared/Guard.cs
  4. 14 1
      src/Shared/_Extensions.cs
  5. 17 1
      src/SharpGLTF.Core/Animations/SamplerFactory.cs
  6. 0 1
      src/SharpGLTF.Core/Collections/ChildrenCollection.cs
  7. 61 3
      src/SharpGLTF.Core/IO/JsonSerializable.cs
  8. 6 2
      src/SharpGLTF.Core/Memory/FloatingArrays.cs
  9. 4 4
      src/SharpGLTF.Core/Memory/IntegerArrays.cs
  10. 14 0
      src/SharpGLTF.Core/Memory/MemoryAccessor.cs
  11. 1 1
      src/SharpGLTF.Core/Memory/SparseArrays.cs
  12. 1 1
      src/SharpGLTF.Core/Schema2/gltf.Accessors.cs
  13. 1 0
      src/SharpGLTF.Core/Schema2/gltf.Animations.cs
  14. 2 0
      src/SharpGLTF.Core/Schema2/gltf.Buffer.cs
  15. 1 1
      src/SharpGLTF.Core/Schema2/gltf.BufferView.cs
  16. 3 0
      src/SharpGLTF.Core/Schema2/gltf.Camera.cs
  17. 3 0
      src/SharpGLTF.Core/Schema2/gltf.ExtraProperties.cs
  18. 1 1
      src/SharpGLTF.Core/Schema2/gltf.MaterialsFactory.cs
  19. 3 3
      src/SharpGLTF.Core/Schema2/gltf.MeshPrimitive.cs
  20. 3 3
      src/SharpGLTF.Core/Schema2/gltf.Node.cs
  21. 1 1
      src/SharpGLTF.Core/Schema2/gltf.Root.cs
  22. 4 0
      src/SharpGLTF.Core/Schema2/gltf.Serialization.Read.cs
  23. 1 0
      src/SharpGLTF.Core/Schema2/gltf.Serialization.Write.cs
  24. 1 1
      src/SharpGLTF.Core/Schema2/gltf.Skin.cs
  25. 6 3
      src/SharpGLTF.Core/Schema2/gltf.Textures.cs
  26. 1 1
      src/SharpGLTF.Core/Schema2/khr.lights.cs
  27. 1 2
      src/SharpGLTF.Core/Transforms/IndexWeight.cs
  28. 12 6
      src/SharpGLTF.Core/Transforms/MeshTransforms.cs
  29. 1 1
      src/SharpGLTF.Core/Transforms/SparseWeight8.cs
  30. 36 0
      src/SharpGLTF.Toolkit/Animations/AnimatableProperty.cs
  31. 13 3
      src/SharpGLTF.Toolkit/Animations/CurveFactory.cs
  32. 3 0
      src/SharpGLTF.Toolkit/Geometry/MeshBuilder.cs
  33. 2 2
      src/SharpGLTF.Toolkit/Geometry/PrimitiveBuilder.cs
  34. 1 1
      src/SharpGLTF.Toolkit/Geometry/VertexBuilder.cs
  35. 5 0
      src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexGeometry.cs
  36. 15 1
      src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexMaterial.cs
  37. 3 1
      src/SharpGLTF.Toolkit/IO/WavefrontWriter.cs
  38. 4 4
      src/SharpGLTF.Toolkit/Materials/MaterialBuilder.cs
  39. 31 4
      src/SharpGLTF.Toolkit/Materials/TextureBuilder.cs
  40. 47 0
      src/SharpGLTF.Toolkit/Scenes/Content.Schema2.cs
  41. 11 238
      src/SharpGLTF.Toolkit/Scenes/Content.cs
  42. 17 17
      src/SharpGLTF.Toolkit/Scenes/InstanceBuilder.cs
  43. 87 57
      src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs
  44. 17 4
      src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs
  45. 2 2
      src/SharpGLTF.Toolkit/Scenes/SceneBuilder.cs
  46. 87 0
      src/SharpGLTF.Toolkit/Scenes/Transformers.Schema2.cs
  47. 220 0
      src/SharpGLTF.Toolkit/Scenes/Transformers.cs
  48. 6 0
      src/SharpGLTF.Toolkit/Scenes/readme.md
  49. 4 0
      src/SharpGLTF.Toolkit/Schema2/LightExtensions.cs
  50. 88 88
      src/SharpGLTF.Toolkit/Schema2/MeshExtensions.cs
  51. 1 1
      src/SharpGLTF.Toolkit/Schema2/SceneExtensions.cs

+ 2 - 2
Analyzers.targets

@@ -6,8 +6,8 @@
   </PropertyGroup>
 
   <ItemGroup>    
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" PrivateAssets="all" />
-    <PackageReference Include="Microsoft.CodeQuality.Analyzers" Version="2.9.3" PrivateAssets="all" />
+    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" PrivateAssets="all" />
+    <PackageReference Include="Microsoft.CodeQuality.Analyzers" Version="2.9.4" PrivateAssets="all" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
   </ItemGroup>
 	

+ 2 - 0
SharpGLTF.ruleset

@@ -40,6 +40,7 @@
     <Rule Id="SA1649" Action="None" />
     <Rule Id="SA1652" Action="None" />
     <Rule Id="SA1605" Action="None" />
+    <Rule Id="SA1310" Action="Info" />
   </Rules>
   <Rules AnalyzerId="Microsoft.CodeQuality.CSharp.Analyzers" RuleNamespace="Microsoft.CodeQuality.CSharp.Analyzers">
     <Rule Id="CA1001" Action="Error" />
@@ -60,5 +61,6 @@
     <Rule Id="CA2216" Action="Error" />
     <Rule Id="CA2242" Action="Error" />
     <Rule Id="CA1303" Action="Info" />
+    <Rule Id="CA1308" Action="None" />
   </Rules>
 </RuleSet>

+ 7 - 7
src/Shared/Guard.cs

@@ -34,8 +34,8 @@ namespace SharpGLTF
 
             bool isDir = false;
 
-            isDir |= filePath.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString());
-            isDir |= filePath.EndsWith(System.IO.Path.AltDirectorySeparatorChar.ToString());
+            isDir |= filePath.EndsWith(new String(System.IO.Path.DirectorySeparatorChar, 1), StringComparison.Ordinal);
+            isDir |= filePath.EndsWith(new String(System.IO.Path.AltDirectorySeparatorChar, 1), StringComparison.Ordinal);
 
             if (!isDir) return;
 
@@ -167,15 +167,15 @@ namespace SharpGLTF
 
         public static void MustShareLogicalParent(Schema2.LogicalChildOfRoot a, Schema2.LogicalChildOfRoot b, string parameterName)
         {
-            MustShareLogicalParent(a?.LogicalParent, b, parameterName);
+            MustShareLogicalParent(a?.LogicalParent, nameof(a.LogicalParent), b, parameterName);
         }
 
-        public static void MustShareLogicalParent(Schema2.ModelRoot a, Schema2.LogicalChildOfRoot b, string parameterName)
+        public static void MustShareLogicalParent(Schema2.ModelRoot a, string aName, Schema2.LogicalChildOfRoot b, string bName)
         {
-            if (a is null) throw new ArgumentNullException("this");
-            if (b is null) throw new ArgumentNullException(parameterName);
+            if (a is null) throw new ArgumentNullException(aName);
+            if (b is null) throw new ArgumentNullException(bName);
 
-            if (a != b.LogicalParent) throw new ArgumentException("LogicalParent mismatch", parameterName);
+            if (a != b.LogicalParent) throw new ArgumentException("LogicalParent mismatch", bName);
         }
 
         #endregion

+ 14 - 1
src/Shared/_Extensions.cs

@@ -372,7 +372,20 @@ namespace SharpGLTF
 
             return false;
         }
-        
+
+        internal static bool _IsImage(this IReadOnlyList<Byte> image, string format)
+        {
+            if (string.IsNullOrWhiteSpace(format)) return image._IsImage();
+
+            if (format.EndsWith("png", StringComparison.OrdinalIgnoreCase)) return image._IsPngImage();
+            if (format.EndsWith("jpg", StringComparison.OrdinalIgnoreCase)) return image._IsJpgImage();
+            if (format.EndsWith("jpeg", StringComparison.OrdinalIgnoreCase)) return image._IsJpgImage();
+            if (format.EndsWith("dds", StringComparison.OrdinalIgnoreCase)) return image._IsDdsImage();
+            if (format.EndsWith("webp", StringComparison.OrdinalIgnoreCase)) return image._IsWebpImage();
+
+            return false;
+        }
+
         #endregion
 
         #region vertex & index accessors

+ 17 - 1
src/SharpGLTF.Core/Animations/SamplerFactory.cs

@@ -32,6 +32,10 @@ namespace SharpGLTF.Animations
 
         public static Single[] CreateTangent(Single[] fromValue, Single[] toValue, Single scale = 1)
         {
+            Guard.NotNull(fromValue, nameof(fromValue));
+            Guard.NotNull(toValue, nameof(toValue));
+            Guard.IsTrue(fromValue.Length == toValue.Length, nameof(toValue));
+
             var r = new float[fromValue.Length];
 
             for (int i = 0; i < r.Length; ++i)
@@ -123,6 +127,8 @@ namespace SharpGLTF.Animations
         /// <returns>Two consecutive <typeparamref name="T"/> values and a float amount to LERP amount.</returns>
         public static (T, T, float) FindPairContainingOffset<T>(this IEnumerable<(float, T)> sequence, float offset)
         {
+            Guard.NotNull(sequence, nameof(sequence));
+
             if (!sequence.Any()) return (default(T), default(T), 0);
 
             (float, T)? left = null;
@@ -175,6 +181,8 @@ namespace SharpGLTF.Animations
         /// <returns>Two consecutive offsets and a LERP amount.</returns>
         public static (float, float, float) FindPairContainingOffset(IEnumerable<float> sequence, float offset)
         {
+            Guard.NotNull(sequence, nameof(sequence));
+
             if (!sequence.Any()) return (0, 0, 0);
 
             float? left = null;
@@ -224,6 +232,9 @@ namespace SharpGLTF.Animations
 
         public static Single[] InterpolateLinear(Single[] start, Single[] end, Single amount)
         {
+            Guard.NotNull(start, nameof(start));
+            Guard.NotNull(end, nameof(end));
+
             var startW = 1 - amount;
             var endW = amount;
 
@@ -253,6 +264,11 @@ namespace SharpGLTF.Animations
 
         public static Single[] InterpolateCubic(Single[] start, Single[] outgoingTangent, Single[] end, Single[] incomingTangent, Single amount)
         {
+            Guard.NotNull(start, nameof(start));
+            Guard.NotNull(outgoingTangent, nameof(outgoingTangent));
+            Guard.NotNull(end, nameof(end));
+            Guard.NotNull(incomingTangent, nameof(incomingTangent));
+
             var hermite = CreateHermitePointWeights(amount);
 
             var result = new float[start.Length];
@@ -324,7 +340,7 @@ namespace SharpGLTF.Animations
 
             return new ArrayCubicSampler(collection);
         }
-        
+
         #endregion
     }
 }

+ 0 - 1
src/SharpGLTF.Core/Collections/ChildrenCollection.cs

@@ -8,7 +8,6 @@ namespace SharpGLTF.Collections
 {
     [System.Diagnostics.DebuggerDisplay("{Count}")]
     [System.Diagnostics.DebuggerTypeProxy(typeof(Debug._CollectionDebugProxy<>))]
-    [Serializable]
     sealed class ChildrenCollection<T, TParent> : IList<T>, IReadOnlyList<T>
         where T : class, IChildOf<TParent>
         where TParent : class

+ 61 - 3
src/SharpGLTF.Core/IO/JsonSerializable.cs

@@ -31,6 +31,8 @@ namespace SharpGLTF.IO
 
         internal void Serialize(JsonWriter writer)
         {
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WriteStartObject();
             SerializeProperties(writer);
             writer.WriteEndObject();
@@ -41,6 +43,9 @@ namespace SharpGLTF.IO
         protected static void SerializeProperty(JsonWriter writer, string name, Object value)
         {
             if (value == null) return;
+
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
             _Serialize(writer, value);
         }
@@ -49,6 +54,9 @@ namespace SharpGLTF.IO
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
+
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
             writer.WriteValue(value.Value);
         }
@@ -57,6 +65,9 @@ namespace SharpGLTF.IO
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
+
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
             writer.WriteValue(value.Value);
         }
@@ -65,6 +76,9 @@ namespace SharpGLTF.IO
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
+
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
             writer.WriteValue(value.Value);
         }
@@ -73,6 +87,9 @@ namespace SharpGLTF.IO
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
+
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
             writer.WriteValue(value.Value);
         }
@@ -81,6 +98,9 @@ namespace SharpGLTF.IO
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
+
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
             _Serialize(writer, value.Value);
         }
@@ -89,6 +109,9 @@ namespace SharpGLTF.IO
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
+
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
             _Serialize(writer, value.Value);
         }
@@ -97,6 +120,9 @@ namespace SharpGLTF.IO
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
+
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
             _Serialize(writer, value.Value);
         }
@@ -105,6 +131,9 @@ namespace SharpGLTF.IO
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
+
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
             _Serialize(writer, value.Value);
         }
@@ -113,6 +142,9 @@ namespace SharpGLTF.IO
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
+
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
             _Serialize(writer, value.Value);
         }
@@ -120,22 +152,26 @@ namespace SharpGLTF.IO
         protected static void SerializePropertyEnumValue<T>(JsonWriter writer, string name, T? value, T? defval = null)
             where T : struct
         {
-            if (!typeof(T).IsEnum) throw new ArgumentException(nameof(value));
+            Guard.IsTrue(typeof(T).IsEnum, nameof(T));
 
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value)) return;
 
+            Guard.NotNull(writer, nameof(writer));
+
             SerializeProperty(writer, name, (int)(Object)value);
         }
 
         protected static void SerializePropertyEnumSymbol<T>(JsonWriter writer, string name, T? value, T? defval = null)
             where T : struct
         {
-            if (!typeof(T).IsEnum) throw new ArgumentException(nameof(value));
+            Guard.IsTrue(typeof(T).IsEnum, nameof(T));
 
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value)) return;
 
+            Guard.NotNull(writer, nameof(writer));
+
             SerializeProperty(writer, name, Enum.GetName(typeof(T), value));
         }
 
@@ -143,6 +179,9 @@ namespace SharpGLTF.IO
             where T : JsonSerializable
         {
             if (value == null) return;
+
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
             _Serialize(writer, value);
         }
@@ -152,6 +191,8 @@ namespace SharpGLTF.IO
             if (collection == null) return;
             if (minItems.HasValue && collection.Count < minItems.Value) return;
 
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
 
             writer.WriteStartArray();
@@ -168,6 +209,8 @@ namespace SharpGLTF.IO
             if (collection == null) return;
             if (collection.Count < 1) return;
 
+            Guard.NotNull(writer, nameof(writer));
+
             writer.WritePropertyName(name);
 
             writer.WriteStartObject();
@@ -182,7 +225,8 @@ namespace SharpGLTF.IO
 
         private static void _Serialize(JsonWriter writer, Object value)
         {
-            if (value == null) throw new ArgumentNullException(nameof(value));
+            Guard.NotNull(writer, nameof(writer));
+            Guard.NotNull(value, nameof(value));
 
             System.Diagnostics.Debug.Assert(!value.GetType().IsEnum, "gltf schema does not define a typed way of serializing enums");
 
@@ -249,6 +293,8 @@ namespace SharpGLTF.IO
 
         internal void Deserialize(JsonReader reader)
         {
+            Guard.NotNull(reader, nameof(reader));
+
             if (reader.TokenType == JsonToken.PropertyName) reader.Read();
 
             if (reader.TokenType == JsonToken.StartObject)
@@ -275,6 +321,8 @@ namespace SharpGLTF.IO
 
         protected static Object DeserializeUnknownObject(JsonReader reader)
         {
+            Guard.NotNull(reader, nameof(reader));
+
             if (reader.TokenType == JsonToken.PropertyName) reader.Read();
 
             if (reader.TokenType == JsonToken.StartArray)
@@ -322,6 +370,8 @@ namespace SharpGLTF.IO
 
         protected static T DeserializePropertyValue<T>(JsonReader reader)
         {
+            Guard.NotNull(reader, nameof(reader));
+
             _TryCastValue(reader, typeof(T), out Object v);
 
             System.Diagnostics.Debug.Assert(reader.TokenType != JsonToken.StartArray);
@@ -334,6 +384,9 @@ namespace SharpGLTF.IO
 
         protected static void DeserializePropertyList<T>(JsonReader reader, IList<T> list)
         {
+            Guard.NotNull(reader, nameof(reader));
+            Guard.NotNull(list, nameof(list));
+
             if (reader.TokenType == JsonToken.PropertyName) reader.Read();
 
             if (reader.TokenType != JsonToken.StartArray) throw new JsonReaderException();
@@ -359,6 +412,9 @@ namespace SharpGLTF.IO
 
         protected static void DeserializePropertyDictionary<T>(JsonReader reader, IDictionary<string, T> dict)
         {
+            Guard.NotNull(reader, nameof(reader));
+            Guard.NotNull(dict, nameof(dict));
+
             if (reader.TokenType == JsonToken.PropertyName) reader.Read();
 
             if (reader.TokenType == JsonToken.StartArray) throw new JsonReaderException();
@@ -385,6 +441,8 @@ namespace SharpGLTF.IO
 
         private static bool _TryCastValue(JsonReader reader, Type vtype, out Object value)
         {
+            Guard.NotNull(reader, nameof(reader));
+
             value = null;
 
             if (reader.TokenType == JsonToken.EndArray) return false;

+ 6 - 2
src/SharpGLTF.Core/Memory/FloatingArrays.cs

@@ -15,6 +15,8 @@ namespace SharpGLTF.Memory
     /// </summary>
     struct FloatingAccessor
     {
+        private const string ERR_UNSUPPORTEDENCODING = "Unsupported encoding.";
+
         #region constructors
 
         public FloatingAccessor(BYTES source, int byteOffset, int itemsCount, int byteStride, int dimensions, ENCODING encoding, Boolean normalized)
@@ -72,7 +74,7 @@ namespace SharpGLTF.Memory
                             break;
                         }
 
-                    default: throw new ArgumentException(nameof(encoding));
+                    default: throw new ArgumentException(ERR_UNSUPPORTEDENCODING, nameof(encoding));
                 }
             }
             else
@@ -117,7 +119,7 @@ namespace SharpGLTF.Memory
                     case ENCODING.FLOAT:
                         break;
 
-                    default: throw new ArgumentException(nameof(encoding));
+                    default: throw new ArgumentException("Unsupported encoding.", nameof(encoding));
                 }
             }
         }
@@ -919,7 +921,9 @@ namespace SharpGLTF.Memory
 
         bool ICollection<Single[]>.IsReadOnly => false;
 
+        #pragma warning disable CA1819 // Properties should not return arrays
         public Single[] this[int index]
+        #pragma warning restore CA1819 // Properties should not return arrays
         {
             get
             {

+ 4 - 4
src/SharpGLTF.Core/Memory/IntegerArrays.cs

@@ -74,7 +74,7 @@ namespace SharpGLTF.Memory
                         break;
                     }
 
-                default: throw new ArgumentException(nameof(encoding));
+                default: throw new ArgumentException("Unsupported encoding.", nameof(encoding));
             }
         }
 
@@ -145,11 +145,11 @@ namespace SharpGLTF.Memory
 
         public int IndexOf(UInt32 item) { return this._FirstIndexOf(item); }
 
-        public void CopyTo(UInt32[] array, int arrayIndex) { this._CopyTo(array, arrayIndex); }
+        public void CopyTo(UInt32[] array, int arrayIndex) { Guard.NotNull(array, nameof(array)); this._CopyTo(array, arrayIndex); }
 
-        public void Fill(IEnumerable<Int32> values, int dstStart = 0) { values._CopyTo(this, dstStart); }
+        public void Fill(IEnumerable<Int32> values, int dstStart = 0) { Guard.NotNull(values, nameof(values)); values._CopyTo(this, dstStart); }
 
-        public void Fill(IEnumerable<UInt32> values, int dstStart = 0) { values._CopyTo(this, dstStart); }
+        public void Fill(IEnumerable<UInt32> values, int dstStart = 0) { Guard.NotNull(values, nameof(values)); values._CopyTo(this, dstStart); }
 
         void IList<UInt32>.Insert(int index, UInt32 item) { throw new NotSupportedException(); }
 

+ 14 - 0
src/SharpGLTF.Core/Memory/MemoryAccessor.cs

@@ -131,6 +131,8 @@ namespace SharpGLTF.Memory
 
         public static int SetInterleavedInfo(MemoryAccessInfo[] attributes, int byteOffset, int itemsCount)
         {
+            Guard.NotNull(attributes, nameof(attributes));
+
             var byteStride = 0;
 
             for (int i = 0; i < attributes.Length; ++i)
@@ -160,6 +162,8 @@ namespace SharpGLTF.Memory
 
         public static MemoryAccessInfo[] Slice(MemoryAccessInfo[] attributes, int start, int count)
         {
+            Guard.NotNull(attributes, nameof(attributes));
+
             var dst = new MemoryAccessInfo[attributes.Length];
 
             for (int i = 0; i < dst.Length; ++i)
@@ -194,6 +198,8 @@ namespace SharpGLTF.Memory
 
         public static IList<Single> CreateScalarSparseArray(MemoryAccessor bottom, IntegerArray topKeys, MemoryAccessor topValues)
         {
+            Guard.NotNull(bottom, nameof(bottom));
+            Guard.NotNull(topValues, nameof(topValues));
             Guard.IsTrue(bottom._Attribute.Dimensions == topValues._Attribute.Dimensions, nameof(topValues));
             Guard.IsTrue(topKeys.Count <= bottom._Attribute.ItemsCount, nameof(topKeys));
             Guard.IsTrue(topKeys.Count == topValues._Attribute.ItemsCount, nameof(topValues));
@@ -204,6 +210,8 @@ namespace SharpGLTF.Memory
 
         public static IList<Vector2> CreateVector2SparseArray(MemoryAccessor bottom, IntegerArray topKeys, MemoryAccessor topValues)
         {
+            Guard.NotNull(bottom, nameof(bottom));
+            Guard.NotNull(topValues, nameof(topValues));
             Guard.IsTrue(bottom._Attribute.Dimensions == topValues._Attribute.Dimensions, nameof(topValues));
             Guard.IsTrue(topKeys.Count <= bottom._Attribute.ItemsCount, nameof(topKeys));
             Guard.IsTrue(topKeys.Count == topValues._Attribute.ItemsCount, nameof(topValues));
@@ -214,6 +222,8 @@ namespace SharpGLTF.Memory
 
         public static IList<Vector3> CreateVector3SparseArray(MemoryAccessor bottom, IntegerArray topKeys, MemoryAccessor topValues)
         {
+            Guard.NotNull(bottom, nameof(bottom));
+            Guard.NotNull(topValues, nameof(topValues));
             Guard.IsTrue(bottom._Attribute.Dimensions == topValues._Attribute.Dimensions, nameof(topValues));
             Guard.IsTrue(topKeys.Count <= bottom._Attribute.ItemsCount, nameof(topKeys));
             Guard.IsTrue(topKeys.Count == topValues._Attribute.ItemsCount, nameof(topValues));
@@ -224,6 +234,8 @@ namespace SharpGLTF.Memory
 
         public static IList<Vector4> CreateVector4SparseArray(MemoryAccessor bottom, IntegerArray topKeys, MemoryAccessor topValues)
         {
+            Guard.NotNull(bottom, nameof(bottom));
+            Guard.NotNull(topValues, nameof(topValues));
             Guard.IsTrue(bottom._Attribute.Dimensions == topValues._Attribute.Dimensions, nameof(topValues));
             Guard.IsTrue(topKeys.Count <= bottom._Attribute.ItemsCount, nameof(topKeys));
             Guard.IsTrue(topKeys.Count == topValues._Attribute.ItemsCount, nameof(topValues));
@@ -234,6 +246,8 @@ namespace SharpGLTF.Memory
 
         public static IList<Vector4> CreateColorSparseArray(MemoryAccessor bottom, IntegerArray topKeys, MemoryAccessor topValues)
         {
+            Guard.NotNull(bottom, nameof(bottom));
+            Guard.NotNull(topValues, nameof(topValues));
             Guard.IsTrue(bottom._Attribute.Dimensions == topValues._Attribute.Dimensions, nameof(topValues));
             Guard.IsTrue(topKeys.Count <= bottom._Attribute.ItemsCount, nameof(topKeys));
             Guard.IsTrue(topKeys.Count == topValues._Attribute.ItemsCount, nameof(topValues));

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

@@ -67,7 +67,7 @@ namespace SharpGLTF.Memory
 
         public int IndexOf(T item) { return this._FirstIndexOf(item); }
 
-        public void CopyTo(T[] array, int arrayIndex) { this._CopyTo(array, arrayIndex); }
+        public void CopyTo(T[] array, int arrayIndex) { Guard.NotNull(array, nameof(array)); this._CopyTo(array, arrayIndex); }
 
         void IList<T>.Insert(int index, T item) { throw new NotSupportedException(); }
 

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

@@ -116,8 +116,8 @@ namespace SharpGLTF.Schema2
         /// <param name="normalized">The item normalization mode.</param>
         public void SetData(BufferView buffer, int bufferByteOffset, int itemCount, DimensionType dimensions, EncodingType encoding, Boolean normalized)
         {
+            Guard.NotNull(buffer, nameof(buffer));
             Guard.MustShareLogicalParent(this, buffer, nameof(buffer));
-
             Guard.MustBeGreaterThanOrEqualTo(bufferByteOffset, _byteOffsetMinimum, nameof(bufferByteOffset));
             Guard.MustBeGreaterThanOrEqualTo(itemCount, _countMinimum, nameof(itemCount));
 

+ 1 - 0
src/SharpGLTF.Core/Schema2/gltf.Animations.cs

@@ -159,6 +159,7 @@ namespace SharpGLTF.Schema2
 
         public AffineTransform GetLocalTransform(Node node, float time)
         {
+            Guard.NotNull(node, nameof(node));
             Guard.MustShareLogicalParent(this, node, nameof(node));
 
             var xform = node.LocalTransform;

+ 2 - 0
src/SharpGLTF.Core/Schema2/gltf.Buffer.cs

@@ -33,7 +33,9 @@ namespace SharpGLTF.Schema2
         /// </summary>
         public int LogicalIndex => this.LogicalParent.LogicalBuffers.IndexOfReference(this);
 
+        #pragma warning disable CA1819 // Properties should not return arrays
         public Byte[] Content => _Content;
+        #pragma warning restore CA1819 // Properties should not return arrays
 
         #endregion
 

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

@@ -172,7 +172,7 @@ namespace SharpGLTF.Schema2
         public BufferView UseBufferView(Buffer buffer, int byteOffset = 0, int? byteLength = null, int byteStride = 0, BufferMode? target = null)
         {
             Guard.NotNull(buffer, nameof(buffer));
-            Guard.MustShareLogicalParent(this, buffer, nameof(buffer));
+            Guard.MustShareLogicalParent(this, "this", buffer, nameof(buffer));
 
             byteLength = byteLength.AsValue(buffer.Content.Length - byteOffset);
 

+ 3 - 0
src/SharpGLTF.Core/Schema2/gltf.Camera.cs

@@ -154,6 +154,9 @@ namespace SharpGLTF.Schema2
 
         public static void CheckParameters(float xmag, float ymag, float znear, float zfar)
         {
+            Guard.MustBeGreaterThan(xmag, 0, nameof(xmag));
+            Guard.MustBeGreaterThan(ymag, 0, nameof(ymag));
+
             Guard.MustBeGreaterThanOrEqualTo(znear, 0, nameof(znear));
             Guard.MustBeGreaterThanOrEqualTo(zfar, 0, nameof(zfar));
             Guard.MustBeGreaterThan(zfar, znear, nameof(zfar));

+ 3 - 0
src/SharpGLTF.Core/Schema2/gltf.ExtraProperties.cs

@@ -184,6 +184,9 @@ namespace SharpGLTF.Schema2
         /// <param name="reader">The source reader.</param>
         protected override void DeserializeProperty(string property, JsonReader reader)
         {
+            Guard.NotNullOrEmpty(property, nameof(property));
+            Guard.NotNull(reader, nameof(reader));
+
             switch (property)
             {
                 case "extensions": _DeserializeExtensions(this, reader, _extensions); break;

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

@@ -165,7 +165,7 @@ namespace SharpGLTF.Schema2
             set => _baseColorFactor = value.AsNullable(_baseColorFactorDefault);
         }
 
-        public static Vector4 ParameterDefault => new Vector4((float)_metallicFactorDefault, (float)_roughnessFactorDefault,0,0);
+        public static Vector4 ParameterDefault => new Vector4((float)_metallicFactorDefault, (float)_roughnessFactorDefault, 0, 0);
 
         public Vector4 Parameter
         {

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

@@ -50,7 +50,7 @@ namespace SharpGLTF.Schema2
             get => this._material.HasValue ? LogicalParent.LogicalParent.LogicalMaterials[this._material.Value] : null;
             set
             {
-                if (value != null) Guard.MustShareLogicalParent(LogicalParent.LogicalParent, value, nameof(value));
+                if (value != null) Guard.MustShareLogicalParent(LogicalParent.LogicalParent, nameof(LogicalParent.LogicalParent), value, nameof(value));
 
                 this._material = value == null ? (int?)null : value.LogicalIndex;
             }
@@ -123,7 +123,7 @@ namespace SharpGLTF.Schema2
 
             if (accessor != null)
             {
-                Guard.MustShareLogicalParent(this.LogicalParent.LogicalParent, accessor, nameof(accessor));
+                Guard.MustShareLogicalParent(this.LogicalParent.LogicalParent, nameof(this.LogicalParent.LogicalParent), accessor, nameof(accessor));
                 _attributes[attributeKey] = accessor.LogicalIndex;
             }
             else
@@ -143,7 +143,7 @@ namespace SharpGLTF.Schema2
         {
             if (accessor == null) { this._indices = null; return; }
 
-            Guard.MustShareLogicalParent(this.LogicalParent.LogicalParent, accessor, nameof(accessor));
+            Guard.MustShareLogicalParent(this.LogicalParent.LogicalParent, nameof(this.LogicalParent.LogicalParent), accessor, nameof(accessor));
 
             this._indices = accessor.LogicalIndex;
         }

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

@@ -209,7 +209,7 @@ namespace SharpGLTF.Schema2
             {
                 if (value == null) { this._camera = null; return; }
 
-                Guard.MustShareLogicalParent(this.LogicalParent, value, nameof(value));
+                Guard.MustShareLogicalParent(this.LogicalParent, nameof(this.LogicalParent), value, nameof(value));
 
                 this._camera = value.LogicalIndex;
             }
@@ -225,7 +225,7 @@ namespace SharpGLTF.Schema2
             {
                 if (value == null) { this._mesh = null; return; }
 
-                Guard.MustShareLogicalParent(this.LogicalParent, value, nameof(value));
+                Guard.MustShareLogicalParent(this.LogicalParent, nameof(this.LogicalParent), value, nameof(value));
 
                 this._mesh = value.LogicalIndex;
             }
@@ -241,7 +241,7 @@ namespace SharpGLTF.Schema2
             {
                 if (value == null) { this._skin = null; return; }
 
-                Guard.MustShareLogicalParent(this.LogicalParent, value, nameof(value));
+                Guard.MustShareLogicalParent(this.LogicalParent, nameof(this.LogicalParent), value, nameof(value));
 
                 Guard.IsFalse(_matrix.HasValue, _NOTRANSFORMMESSAGE);
                 Guard.IsFalse(_scale.HasValue, _NOTRANSFORMMESSAGE);

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

@@ -134,7 +134,7 @@ namespace SharpGLTF.Schema2
                     return;
                 }
 
-                Guard.MustShareLogicalParent(this, value, nameof(value));
+                Guard.MustShareLogicalParent(this, "this", value, nameof(value));
 
                 _scene = value.LogicalIndex;
             }

+ 4 - 0
src/SharpGLTF.Core/Schema2/gltf.Serialization.Read.cs

@@ -89,6 +89,8 @@ namespace SharpGLTF.Schema2
         /// <returns>A <see cref="MODEL"/> instance.</returns>
         public static MODEL Read(Stream stream, ReadSettings settings)
         {
+            Guard.NotNull(stream, nameof(stream));
+
             bool binaryFile = glb._Identify(stream);
 
             if (binaryFile) return ReadGLB(stream, settings);
@@ -158,6 +160,8 @@ namespace SharpGLTF.Schema2
 
         public static MODEL ReadFromDictionary(Dictionary<string, BYTES> files, string fileName)
         {
+            Guard.NotNull(files, nameof(files));
+
             var jsonBytes = files[fileName];
 
             var settings = new ReadSettings(fn => files[fn]);

+ 1 - 0
src/SharpGLTF.Core/Schema2/gltf.Serialization.Write.cs

@@ -324,6 +324,7 @@ namespace SharpGLTF.Schema2
         /// </remarks>
         public void Write(WriteSettings settings, string baseName)
         {
+            Guard.NotNull(settings, nameof(settings));
             _Write(settings, baseName, this);
         }
 

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

@@ -50,7 +50,7 @@ namespace SharpGLTF.Schema2
             get => this._skeleton.HasValue ? this.LogicalParent.LogicalNodes[this._skeleton.Value] : null;
             set
             {
-                if (value != null) Guard.MustShareLogicalParent(this.LogicalParent, value, nameof(value));
+                if (value != null) Guard.MustShareLogicalParent(this.LogicalParent, nameof(this.LogicalParent), value, nameof(value));
                 this._skeleton = value == null ? (int?)null : value.LogicalIndex;
             }
         }

+ 6 - 3
src/SharpGLTF.Core/Schema2/gltf.Textures.cs

@@ -83,6 +83,7 @@ namespace SharpGLTF.Schema2
 
         public void SetImage(Image primaryImage)
         {
+            Guard.NotNull(primaryImage, nameof(primaryImage));
             Guard.MustShareLogicalParent(this, primaryImage, nameof(primaryImage));
 
             if (primaryImage.IsDds || primaryImage.IsWebp)
@@ -99,6 +100,8 @@ namespace SharpGLTF.Schema2
 
         public void SetImages(Image primaryImage, Image fallbackImage)
         {
+            Guard.NotNull(primaryImage, nameof(primaryImage));
+            Guard.NotNull(fallbackImage, nameof(fallbackImage));
             Guard.MustShareLogicalParent(this, primaryImage, nameof(primaryImage));
             Guard.MustShareLogicalParent(this, fallbackImage, nameof(fallbackImage));
             Guard.IsTrue(primaryImage.IsDds || primaryImage.IsWebp, "Primary image must be DDS or WEBP");
@@ -304,9 +307,9 @@ namespace SharpGLTF.Schema2
                 return null;
             }
 
-            if (primary  != null) Guard.MustShareLogicalParent(this, primary, nameof(primary));
-            if (fallback != null) Guard.MustShareLogicalParent(this, fallback, nameof(primary));
-            if (sampler  != null) Guard.MustShareLogicalParent(this, sampler, nameof(sampler));
+            if (primary  != null) Guard.MustShareLogicalParent(this, "this", primary, nameof(primary));
+            if (fallback != null) Guard.MustShareLogicalParent(this, "this", fallback, nameof(primary));
+            if (sampler  != null) Guard.MustShareLogicalParent(this, "this", sampler, nameof(sampler));
 
             // find if we have an equivalent texture
             var tex = _textures.FirstOrDefault(item => item._IsEqualentTo(primary, fallback, sampler));

+ 1 - 1
src/SharpGLTF.Core/Schema2/khr.lights.cs

@@ -55,7 +55,7 @@ namespace SharpGLTF.Schema2
 
         internal PunctualLight(PunctualLightType ltype)
         {
-            _type = ltype.ToString().ToLower();
+            _type = ltype.ToString().ToLowerInvariant();
 
             if (ltype == PunctualLightType.Spot) _spot = new PunctualLightSpot();
         }

+ 1 - 2
src/SharpGLTF.Core/Transforms/IndexWeight.cs

@@ -5,7 +5,6 @@ using System.Text;
 
 namespace SharpGLTF.Transforms
 {
-
     [System.Diagnostics.DebuggerDisplay("{Index} = {Weight}")]
     readonly struct IndexWeight
     {
@@ -17,7 +16,7 @@ namespace SharpGLTF.Transforms
             Weight = pair.Item2;
         }
 
-        public static implicit operator IndexWeight((int, float) pair) {return new IndexWeight(pair.Item1, pair.Item2); }
+        public static implicit operator IndexWeight((int, float) pair) { return new IndexWeight(pair.Item1, pair.Item2); }
 
         public IndexWeight(int i, float w)
         {

+ 12 - 6
src/SharpGLTF.Core/Transforms/MeshTransforms.cs

@@ -225,9 +225,9 @@ namespace SharpGLTF.Transforms
     {
         #region constructor
 
-        public SkinTransform(TRANSFORM[] invBindings, TRANSFORM[] xforms, SparseWeight8 morphWeights, bool useAbsoluteMorphTargets)
+        public SkinTransform(TRANSFORM[] invBindings, TRANSFORM[] worldXforms, SparseWeight8 morphWeights, bool useAbsoluteMorphTargets)
         {
-            Update(invBindings, xforms, morphWeights, useAbsoluteMorphTargets);
+            Update(invBindings, worldXforms, morphWeights, useAbsoluteMorphTargets);
         }
 
         #endregion
@@ -240,11 +240,11 @@ namespace SharpGLTF.Transforms
 
         #region API
 
-        public void Update(TRANSFORM[] invBindings, TRANSFORM[] xforms, SparseWeight8 morphWeights, bool useAbsoluteMorphTargets)
+        public void Update(TRANSFORM[] invBindings, TRANSFORM[] worldXforms, SparseWeight8 morphWeights, bool useAbsoluteMorphTargets)
         {
             Guard.NotNull(invBindings, nameof(invBindings));
-            Guard.NotNull(xforms, nameof(xforms));
-            Guard.IsTrue(invBindings.Length == xforms.Length, nameof(xforms), $"{invBindings} and {xforms} length mismatch.");
+            Guard.NotNull(worldXforms, nameof(worldXforms));
+            Guard.IsTrue(invBindings.Length == worldXforms.Length, nameof(worldXforms), $"{invBindings} and {worldXforms} length mismatch.");
 
             Update(morphWeights, useAbsoluteMorphTargets);
 
@@ -252,7 +252,7 @@ namespace SharpGLTF.Transforms
 
             for (int i = 0; i < _JointTransforms.Length; ++i)
             {
-                _JointTransforms[i] = invBindings[i] * xforms[i];
+                _JointTransforms[i] = invBindings[i] * worldXforms[i];
             }
         }
 
@@ -262,6 +262,8 @@ namespace SharpGLTF.Transforms
 
         public V3 TransformPosition(V3 localPosition, V3[] morphTargets, (int, float)[] skinWeights)
         {
+            Guard.NotNull(skinWeights, nameof(skinWeights));
+
             localPosition = MorphVectors(localPosition, morphTargets);
 
             var worldPosition = V3.Zero;
@@ -278,6 +280,8 @@ namespace SharpGLTF.Transforms
 
         public V3 TransformNormal(V3 localNormal, V3[] morphTargets, (int, float)[] skinWeights)
         {
+            Guard.NotNull(skinWeights, nameof(skinWeights));
+
             localNormal = MorphVectors(localNormal, morphTargets);
 
             var worldNormal = V3.Zero;
@@ -292,6 +296,8 @@ namespace SharpGLTF.Transforms
 
         public V4 TransformTangent(V4 localTangent, V3[] morphTargets, (int, float)[] skinWeights)
         {
+            Guard.NotNull(skinWeights, nameof(skinWeights));
+
             var localTangentV = MorphVectors(new V3(localTangent.X, localTangent.Y, localTangent.Z), morphTargets);
 
             var worldTangent = V3.Zero;

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

@@ -279,7 +279,7 @@ namespace SharpGLTF.Transforms
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         public bool IsWeightless => Weight0 == 0 & Weight1 == 0 & Weight2 == 0 & Weight3 == 0 & Weight4 == 0 & Weight5 == 0 & Weight6 == 0 & Weight7 == 0;
 
-        public float WeightSum => Weight0 + Weight1+ Weight2 + Weight3 + Weight4 + Weight5 + Weight6 + Weight7;
+        public float WeightSum => Weight0 + Weight1 + Weight2 + Weight3 + Weight4 + Weight5 + Weight6 + Weight7;
 
         #endregion
 

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

@@ -14,6 +14,29 @@ namespace SharpGLTF.Animations
     public class AnimatableProperty<T>
         where T : struct
     {
+        #region lifecycle
+
+        internal AnimatableProperty() { }
+
+        internal AnimatableProperty(AnimatableProperty<T> other)
+        {
+            if (other == null) return;
+
+            if (other._Tracks != null)
+            {
+                this._Tracks = new Dictionary<string, ICurveSampler<T>>();
+
+                foreach (var kvp in other._Tracks)
+                {
+                    this._Tracks[kvp.Key] = CurveFactory.CreateCurveBuilder(kvp.Value);
+                }
+            }
+
+            this.Value = other.Value;
+        }
+
+        #endregion
+
         #region data
 
         private Dictionary<string, ICurveSampler<T>> _Tracks;
@@ -28,12 +51,25 @@ namespace SharpGLTF.Animations
 
         #region properties
 
+        public bool IsAnimated => Tracks.Count > 0;
+
         public IReadOnlyDictionary<string, ICurveSampler<T>> Tracks => _Tracks == null ? Collections.EmptyDictionary<string, ICurveSampler<T>>.Instance : _Tracks;
 
         #endregion
 
         #region API
 
+        /// <summary>
+        /// Removes the animation <paramref name="track"/>.
+        /// </summary>
+        /// <param name="track">The name of the track.</param>
+        public void RemoveTrack(string track)
+        {
+            if (_Tracks == null) return;
+            _Tracks.Remove(track);
+            if (_Tracks.Count == 0) _Tracks = null;
+        }
+
         /// <summary>
         /// Evaluates the value of this <see cref="AnimatableProperty{T}"/> at a given <paramref name="offset"/> for a given <paramref name="track"/>.
         /// </summary>

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

@@ -3,10 +3,10 @@ using System.Collections.Generic;
 using System.Numerics;
 using System.Text;
 
+using SPARSE = SharpGLTF.Transforms.SparseWeight8;
+
 namespace SharpGLTF.Animations
 {
-    using SPARSE = Transforms.SparseWeight8;
-
     static class CurveFactory
     {
         public static CurveBuilder<T> CreateCurveBuilder<T>()
@@ -16,7 +16,17 @@ namespace SharpGLTF.Animations
             if (typeof(T) == typeof(Quaternion)) return new QuaternionCurveBuilder() as CurveBuilder<T>;
             if (typeof(T) == typeof(SPARSE)) return new SparseCurveBuilder() as CurveBuilder<T>;
 
-            throw new ArgumentException(nameof(T), "Generic argument not supported");
+            throw new ArgumentException($"{nameof(T)} not supported.", nameof(T));
+        }
+
+        public static CurveBuilder<T> CreateCurveBuilder<T>(ICurveSampler<T> curve)
+            where T : struct
+        {
+            if (curve is Vector3CurveBuilder v3cb) return v3cb.Clone() as CurveBuilder<T>;
+            if (curve is QuaternionCurveBuilder q4cb) return q4cb.Clone() as CurveBuilder<T>;
+            if (curve is SparseCurveBuilder sscb) return sscb.Clone() as CurveBuilder<T>;
+
+            throw new ArgumentException($"{nameof(T)} not supported.", nameof(T));
         }
     }
 

+ 3 - 0
src/SharpGLTF.Toolkit/Geometry/MeshBuilder.cs

@@ -114,6 +114,9 @@ namespace SharpGLTF.Geometry
 
         public void AddMesh(MeshBuilder<TMaterial, TvG, TvM, TvS> mesh, Func<TMaterial, TMaterial> materialTransform, Func<VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>> vertexTransform)
         {
+            if (mesh == null) return;
+            Guard.NotNull(materialTransform, nameof(materialTransform));
+
             foreach (var p in mesh.Primitives)
             {
                 var materialKey = materialTransform(p.Material);

+ 2 - 2
src/SharpGLTF.Toolkit/Geometry/PrimitiveBuilder.cs

@@ -252,7 +252,7 @@ namespace SharpGLTF.Geometry
 
             if (_Mesh.VertexPreprocessor != null)
             {
-                if (!_Mesh.VertexPreprocessor.PreprocessVertex(ref a)) return (-1,-1);
+                if (!_Mesh.VertexPreprocessor.PreprocessVertex(ref a)) return (-1, -1);
                 if (!_Mesh.VertexPreprocessor.PreprocessVertex(ref b)) return (-1, -1);
             }
 
@@ -332,7 +332,7 @@ namespace SharpGLTF.Geometry
 
         internal void AddPrimitive(PrimitiveBuilder<TMaterial, TvG, TvM, TvS> primitive, Func<VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>> vertexTransform)
         {
-            if (primitive == null) throw new ArgumentNullException(nameof(primitive));
+            if (primitive == null) return;
 
             if (_PrimitiveVertexCount == 1)
             {

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

@@ -170,7 +170,7 @@ namespace SharpGLTF.Geometry
             return v;
         }
 
-        public static VertexBuilder<TvG, TvM, TvS> Create(Vector3 position,Vector3 normal)
+        public static VertexBuilder<TvG, TvM, TvS> Create(Vector3 position, Vector3 normal)
         {
             var v = default(VertexBuilder<TvG, TvM, TvS>);
             v.Geometry.SetPosition(position);

+ 5 - 0
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexGeometry.cs

@@ -40,6 +40,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexPosition(IVertexGeometry src)
         {
+            Guard.NotNull(src, nameof(src));
             this.Position = src.GetPosition();
         }
 
@@ -103,6 +104,8 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexPositionNormal(IVertexGeometry src)
         {
+            Guard.NotNull(src, nameof(src));
+
             this.Position = src.GetPosition();
             src.TryGetNormal(out this.Normal);
         }
@@ -166,6 +169,8 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexPositionNormalTangent(IVertexGeometry src)
         {
+            Guard.NotNull(src, nameof(src));
+
             this.Position = src.GetPosition();
             src.TryGetNormal(out this.Normal);
             src.TryGetTangent(out this.Tangent);

+ 15 - 1
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexMaterial.cs

@@ -35,6 +35,8 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexColor1(IVertexMaterial src)
         {
+            Guard.NotNull(src, nameof(src));
+
             this.Color = src.MaxColors > 0 ? src.GetColor(0) : Vector4.One;
         }
 
@@ -94,6 +96,8 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexColor2(IVertexMaterial src)
         {
+            Guard.NotNull(src, nameof(src));
+
             this.Color0 = src.MaxColors > 0 ? src.GetColor(0) : Vector4.One;
             this.Color1 = src.MaxColors > 1 ? src.GetColor(1) : Vector4.One;
         }
@@ -153,6 +157,8 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexTexture1(IVertexMaterial src)
         {
+            Guard.NotNull(src, nameof(src));
+
             this.TexCoord = src.MaxTextCoords > 0 ? src.GetTexCoord(0) : Vector2.Zero;
         }
 
@@ -212,6 +218,8 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexTexture2(IVertexMaterial src)
         {
+            Guard.NotNull(src, nameof(src));
+
             this.TexCoord0 = src.MaxTextCoords > 0 ? src.GetTexCoord(0) : Vector2.Zero;
             this.TexCoord1 = src.MaxTextCoords > 1 ? src.GetTexCoord(1) : Vector2.Zero;
         }
@@ -275,11 +283,13 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexColor1Texture1(IVertexMaterial src)
         {
+            Guard.NotNull(src, nameof(src));
+
             this.Color = src.MaxColors > 0 ? src.GetColor(0) : Vector4.One;
             this.TexCoord = src.MaxTextCoords > 0 ? src.GetTexCoord(0) : Vector2.Zero;
         }
 
-        public static implicit operator VertexColor1Texture1((Vector4,Vector2) coloruv)
+        public static implicit operator VertexColor1Texture1((Vector4, Vector2) coloruv)
         {
             return new VertexColor1Texture1(coloruv.Item1, coloruv.Item2);
         }
@@ -340,6 +350,8 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexColor1Texture2(IVertexMaterial src)
         {
+            Guard.NotNull(src, nameof(src));
+
             this.Color = src.MaxColors > 0 ? src.GetColor(0) : Vector4.One;
             this.TexCoord0 = src.MaxTextCoords > 0 ? src.GetTexCoord(0) : Vector2.Zero;
             this.TexCoord1 = src.MaxTextCoords > 1 ? src.GetTexCoord(1) : Vector2.Zero;
@@ -413,6 +425,8 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public VertexColor2Texture2(IVertexMaterial src)
         {
+            Guard.NotNull(src, nameof(src));
+
             this.Color0 = src.MaxColors > 0 ? src.GetColor(0) : Vector4.One;
             this.Color1 = src.MaxColors > 1 ? src.GetColor(1) : Vector4.One;
             this.TexCoord0 = src.MaxTextCoords > 0 ? src.GetTexCoord(0) : Vector2.Zero;

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

@@ -11,11 +11,13 @@ using static System.FormattableString;
 
 namespace SharpGLTF.IO
 {
+    #pragma warning disable SA1135 // Using directives should be qualified
     using BYTES = ArraySegment<Byte>;
     using VEMPTY = Geometry.VertexTypes.VertexEmpty;
     using VERTEX = Geometry.VertexBuilder<Geometry.VertexTypes.VertexPositionNormal, Geometry.VertexTypes.VertexTexture1, Geometry.VertexTypes.VertexEmpty>;
     using VGEOMETRY = Geometry.VertexTypes.VertexPositionNormal;
     using VMATERIAL = Geometry.VertexTypes.VertexTexture1;
+    #pragma warning restore SA1135 // Using directives should be qualified
 
     /// <summary>
     /// Tiny wavefront object writer
@@ -91,7 +93,7 @@ namespace SharpGLTF.IO
             }
         }
 
-        private IReadOnlyDictionary<Material, string> _WriteMaterials(IDictionary<String, BYTES> files, string baseName, IEnumerable<Material> materials)
+        private static IReadOnlyDictionary<Material, string> _WriteMaterials(IDictionary<String, BYTES> files, string baseName, IEnumerable<Material> materials)
         {
             // write all image files
             var images = materials

+ 4 - 4
src/SharpGLTF.Toolkit/Materials/MaterialBuilder.cs

@@ -120,7 +120,7 @@ namespace SharpGLTF.Materials
             get => _CompatibilityFallbackMaterial;
             set
             {
-                if (_CompatibilityFallbackMaterial == this) throw new ArgumentException(nameof(value));
+                Guard.IsFalse(_CompatibilityFallbackMaterial == this, nameof(value), "Cannot use self as fallback material");
                 _CompatibilityFallbackMaterial = value;
             }
         }
@@ -176,7 +176,7 @@ namespace SharpGLTF.Materials
         {
             Guard.NotNullOrEmpty(channelKey, nameof(channelKey));
 
-            channelKey = channelKey.ToLower();
+            channelKey = channelKey.ToLowerInvariant();
 
             return _Channels.FirstOrDefault(item => string.Equals(channelKey, item.Key, StringComparison.OrdinalIgnoreCase));
         }
@@ -230,7 +230,7 @@ namespace SharpGLTF.Materials
         {
             this.UseChannel(channelKey)
                 .UseTexture()
-                .WithImage(primaryImagePath);
+                .WithPrimaryImage(primaryImagePath);
 
             return this;
         }
@@ -239,7 +239,7 @@ namespace SharpGLTF.Materials
         {
             this.UseChannel(channelKey)
                 .UseTexture()
-                .WithImage(primaryImagePath);
+                .WithPrimaryImage(primaryImagePath);
 
             return this;
         }

+ 31 - 4
src/SharpGLTF.Toolkit/Materials/TextureBuilder.cs

@@ -104,7 +104,7 @@ namespace SharpGLTF.Materials
         public BYTES PrimaryImageContent
         {
             get => _PrimaryImageContent;
-            set => WithImage(value);
+            set => WithPrimaryImage(value);
         }
 
         /// <summary>
@@ -127,16 +127,22 @@ namespace SharpGLTF.Materials
 
         public TextureBuilder WithCoordinateSet(int cset) { CoordinateSet = cset; return this; }
 
-        public TextureBuilder WithImage(string imagePath)
+        [Obsolete("Use WithPrimaryImage instead.")]
+        public TextureBuilder WithImage(string imagePath) { return WithPrimaryImage(imagePath); }
+
+        [Obsolete("Use WithPrimaryImage instead,")]
+        public TextureBuilder WithImage(BYTES image) { return WithPrimaryImage(image); }
+
+        public TextureBuilder WithPrimaryImage(string imagePath)
         {
             var primary = System.IO.File
                 .ReadAllBytes(imagePath)
                 .Slice(0);
 
-            return WithImage(primary);
+            return WithPrimaryImage(primary);
         }
 
-        public TextureBuilder WithImage(BYTES image)
+        public TextureBuilder WithPrimaryImage(BYTES image)
         {
             if (image.Count > 0)
             {
@@ -220,6 +226,27 @@ namespace SharpGLTF.Materials
         }
 
         #endregion
+
+        #region image utilities
+
+        /// <summary>
+        /// Checks if <paramref name="data"/> represents a stream of an encoded image.
+        /// </summary>
+        /// <param name="data">A stream of bytes.</param>
+        /// <param name="extension">
+        /// An image format, valid values are:
+        /// - PNG
+        /// - JPG
+        /// - DDS
+        /// - WEBP
+        /// </param>
+        /// <returns>True if <paramref name="data"/> is an image.</returns>
+        public static bool IsImage(ArraySegment<Byte> data, string extension)
+        {
+            return data._IsImage(extension);
+        }
+
+        #endregion
     }
 
     public class TextureTransformBuilder

+ 47 - 0
src/SharpGLTF.Toolkit/Scenes/Content.Schema2.cs

@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using SharpGLTF.Schema2;
+
+using SCHEMA2NODE = SharpGLTF.Scenes.Schema2SceneBuilder.IOperator<SharpGLTF.Schema2.Node>;
+
+namespace SharpGLTF.Scenes
+{
+    partial class MorphableMeshContent : SCHEMA2NODE
+    {
+        void SCHEMA2NODE.Setup(Node dstNode, Schema2SceneBuilder context)
+        {
+            if (!(_Target is SCHEMA2NODE schema2Target)) return;
+
+            schema2Target.Setup(dstNode, context);
+
+            // setup morphs here!
+        }
+    }
+
+    partial class MeshContent : SCHEMA2NODE
+    {
+        void SCHEMA2NODE.Setup(Node dstNode, Schema2SceneBuilder context)
+        {
+            dstNode.Mesh = context.GetMesh(_Mesh);
+        }
+    }
+
+    partial class OrthographicCameraContent : SCHEMA2NODE
+    {
+        void SCHEMA2NODE.Setup(Node dstNode, Schema2SceneBuilder context)
+        {
+            dstNode.WithOrthographicCamera(_XMag, _YMag, _ZNear, _ZFar);
+        }
+    }
+
+    partial class PerspectiveCameraContent : SCHEMA2NODE
+    {
+        void SCHEMA2NODE.Setup(Node dstNode, Schema2SceneBuilder context)
+        {
+            dstNode.WithPerspectiveCamera(_AspectRatio, _FovY, _ZNear, _ZFar);
+        }
+    }
+}

+ 11 - 238
src/SharpGLTF.Toolkit/Scenes/Content.cs

@@ -2,214 +2,41 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Numerics;
-using System.Text;
-using SharpGLTF.Geometry;
-using SharpGLTF.Materials;
-using SharpGLTF.Schema2;
+
+using MESHBUILDER = SharpGLTF.Geometry.IMeshBuilder<SharpGLTF.Materials.MaterialBuilder>;
 
 namespace SharpGLTF.Scenes
 {
-    using MESHBUILDER = IMeshBuilder<MaterialBuilder>;
-
-    interface IContentRoot
+    interface IRenderableContent
     {
         MESHBUILDER GetGeometryAsset();
-
-        NodeBuilder GetArmatureAsset();
-
-        void Setup(Scene dstScene, Schema2SceneBuilder context);
     }
 
-    interface IContent
-    {
-        void Setup(Node dstNode, Schema2SceneBuilder context);
-    }
-
-    interface IRenderableContent : IContent
-    {
-        MESHBUILDER GetGeometryAsset();
-    }
-
-    class StaticTransformer : IContentRoot
+    partial class MeshContent : IRenderableContent
     {
         #region lifecycle
 
-        public StaticTransformer(IContent content, Matrix4x4 xform)
-        {
-            _Transform = xform;
-            _Target = content;
-        }
-
-        public StaticTransformer(MESHBUILDER mesh, Matrix4x4 xform)
-        {
-            _Transform = xform;
-            _Target = new MeshContent(mesh);
-        }
-
-        #endregion
-
-        #region data
-
-        private IContent _Target; // Can be either a morphController or a mesh, or light or camera
-
-        private Matrix4x4 _Transform;
-
-        #endregion
-
-        #region API
-
-        public NodeBuilder GetArmatureAsset() { return null; }
-
-        public MESHBUILDER GetGeometryAsset() { return (_Target as IRenderableContent)?.GetGeometryAsset(); }
-
-        public void Setup(Scene dstScene, Schema2SceneBuilder context)
-        {
-            var node = dstScene.CreateNode();
-            node.LocalMatrix = _Transform;
-
-            _Target.Setup(node, context);
-        }
-
-        #endregion
-    }
-
-    class NodeTransformer : IContentRoot
-    {
-        #region lifecycle
-
-        public NodeTransformer(IContent content, NodeBuilder node)
-        {
-            _Node = node;
-            _Target = content;
-        }
-
-        public NodeTransformer(MESHBUILDER mesh, NodeBuilder node)
-        {
-            _Node = node;
-            _Target = new MeshContent(mesh);
-        }
-
-        #endregion
-
-        #region data
-
-        private IContent _Target; // Can be either a morphController or a mesh, or light or camera
-
-        private NodeBuilder _Node;
-
-        #endregion
-
-        #region API
-
-        public NodeBuilder GetArmatureAsset() { return _Node.Root; }
-
-        public MESHBUILDER GetGeometryAsset() { return (_Target as IRenderableContent)?.GetGeometryAsset(); }
-
-        public void Setup(Schema2.Scene dstScene, Schema2SceneBuilder context)
-        {
-            var node = context.GetNode(_Node);
-
-            if (node == null) dstScene.CreateNode();
-
-            _Target.Setup(node, context);
-        }
-
-        #endregion
-    }
-
-    class SkinTransformer : IContentRoot
-    {
-        #region lifecycle
-
-        public SkinTransformer(MESHBUILDER mesh, Matrix4x4 meshBindMatrix, NodeBuilder[] joints)
-        {
-            Guard.NotNull(mesh, nameof(mesh));
-            Guard.NotNull(joints, nameof(joints));
-            Guard.IsTrue(NodeBuilder.IsValidArmature(joints), nameof(joints));
-
-            _Target = new MeshContent(mesh);
-            _TargetBindMatrix = meshBindMatrix;
-            _Joints.AddRange(joints.Select(item => (item, (Matrix4x4?)null)));
-        }
-
-        public SkinTransformer(MESHBUILDER mesh, (NodeBuilder, Matrix4x4)[] joints)
+        public MeshContent(MESHBUILDER mesh)
         {
-            Guard.NotNull(mesh, nameof(mesh));
-            Guard.NotNull(joints, nameof(joints));
-            Guard.IsTrue(NodeBuilder.IsValidArmature(joints.Select(item => item.Item1)), nameof(joints));
-
-            _Target = new MeshContent(mesh);
-            _TargetBindMatrix = null;
-            _Joints.AddRange(joints.Select(item => (item.Item1, (Matrix4x4?)item.Item2)));
+            _Mesh = mesh;
         }
 
         #endregion
 
         #region data
 
-        private IRenderableContent _Target; // Can be either a morphController or a mesh
-        private Matrix4x4? _TargetBindMatrix;
-
-        // condition: all NodeBuilder objects must have the same root.
-        private readonly List<(NodeBuilder, Matrix4x4?)> _Joints = new List<(NodeBuilder, Matrix4x4?)>();
+        private MESHBUILDER _Mesh;
 
         #endregion
 
         #region API
 
-        public MESHBUILDER GetGeometryAsset() { return (_Target as IRenderableContent)?.GetGeometryAsset(); }
-
-        public NodeBuilder GetArmatureAsset() { return _Joints.Select(item => item.Item1.Root).Distinct().FirstOrDefault(); }
-
-        public void Setup(Scene dstScene, Schema2SceneBuilder context)
-        {
-            var skinnedMeshNode = dstScene.CreateNode();
-
-            if (_TargetBindMatrix.HasValue)
-            {
-                var dstNodes = new Node[_Joints.Count];
-
-                for (int i = 0; i < dstNodes.Length; ++i)
-                {
-                    var srcNode = _Joints[i];
-
-                    System.Diagnostics.Debug.Assert(!srcNode.Item2.HasValue);
-
-                    dstNodes[i] = context.GetNode(srcNode.Item1);
-                }
-
-                #if DEBUG
-                for (int i = 0; i < dstNodes.Length; ++i)
-                {
-                    var srcNode = _Joints[i];
-                    System.Diagnostics.Debug.Assert(dstNodes[i].WorldMatrix == srcNode.Item1.WorldMatrix);
-                }
-                #endif
-
-                skinnedMeshNode.WithSkinBinding(_TargetBindMatrix.Value, dstNodes);
-            }
-            else
-            {
-                var skinnedJoints = _Joints
-                .Select(j => (context.GetNode(j.Item1), j.Item2.Value) )
-                .ToArray();
-
-                skinnedMeshNode.WithSkinBinding(skinnedJoints);
-            }
-
-            // set skeleton
-            // var root = _Joints[0].Item1.Root;
-            // skinnedMeshNode.Skin.Skeleton = context.GetNode(root);
-
-            _Target.Setup(skinnedMeshNode, context);
-        }
+        public MESHBUILDER GetGeometryAsset() => _Mesh;
 
         #endregion
     }
 
-    // We really have two options here: Either implement this here, or as a derived of IMeshBuilder<MaterialBuilder>
-
-    class MorphMeshModifier : IRenderableContent // must be a child of a controller, and the parent of a mesh
+    partial class MorphableMeshContent : IRenderableContent
     {
         #region data
 
@@ -223,54 +50,10 @@ namespace SharpGLTF.Scenes
 
         public MESHBUILDER GetGeometryAsset() => _Target?.GetGeometryAsset();
 
-        public void Setup(Node dstNode, Schema2SceneBuilder context)
-        {
-            _Target.Setup(dstNode, context);
-
-            // setup morphs here!
-        }
-
-        #endregion
-    }
-
-    class MeshContent : IRenderableContent
-    {
-        #region lifecycle
-
-        public MeshContent(MESHBUILDER mesh)
-        {
-            _Mesh = mesh;
-        }
-
         #endregion
-
-        #region data
-
-        private MESHBUILDER _Mesh;
-
-        #endregion
-
-        #region API
-
-        public MESHBUILDER GetGeometryAsset() => _Mesh;
-
-        public void Setup(Node dstNode, Schema2SceneBuilder context)
-        {
-            dstNode.Mesh = context.GetMesh(_Mesh);
-        }
-
-        #endregion
-    }
-
-    class LightContent : IContent
-    {
-        public void Setup(Node dstNode, Schema2SceneBuilder context)
-        {
-            throw new NotImplementedException();
-        }
     }
 
-    class OrthographicCameraContent : IContent
+    partial class OrthographicCameraContent
     {
         public OrthographicCameraContent(float xmag, float ymag, float znear, float zfar)
         {
@@ -284,14 +67,9 @@ namespace SharpGLTF.Scenes
         private float _YMag;
         private float _ZNear;
         private float _ZFar;
-
-        public void Setup(Node dstNode, Schema2SceneBuilder context)
-        {
-            dstNode.WithOrthographicCamera(_XMag, _YMag, _ZNear, _ZFar);
-        }
     }
 
-    class PerspectiveCameraContent : IContent
+    partial class PerspectiveCameraContent
     {
         public PerspectiveCameraContent(float? aspectRatio, float fovy, float znear, float zfar = float.PositiveInfinity)
         {
@@ -305,10 +83,5 @@ namespace SharpGLTF.Scenes
         float _FovY;
         float _ZNear;
         float _ZFar;
-
-        public void Setup(Node dstNode, Schema2SceneBuilder context)
-        {
-            dstNode.WithPerspectiveCamera(_AspectRatio, _FovY, _ZNear, _ZFar);
-        }
     }
 }

+ 17 - 17
src/SharpGLTF.Toolkit/Scenes/InstanceBuilder.cs

@@ -2,9 +2,11 @@
 using System.Collections.Generic;
 using System.Text;
 
+using SCHEMA2SCENE = SharpGLTF.Scenes.Schema2SceneBuilder.IOperator<SharpGLTF.Schema2.Scene>;
+
 namespace SharpGLTF.Scenes
 {
-    public class InstanceBuilder
+    public class InstanceBuilder : SCHEMA2SCENE
     {
         #region lifecycle
 
@@ -17,39 +19,37 @@ namespace SharpGLTF.Scenes
 
         #region data
 
+        private string _Name;
+
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly SceneBuilder _Parent;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private IContentRoot _Content;
+        private ContentTransformer _ContentTransformer;
 
         #endregion
 
         #region properties
 
-        internal IContentRoot Content
+        public string Name
         {
-            get => _Content;
-            set => _Content = value;
+            get => _Name;
+            set => _Name = value;
         }
 
-        #endregion
-
-        #region API
-
-        internal Geometry.IMeshBuilder<Materials.MaterialBuilder> GetGeometryAsset()
+        public ContentTransformer Content
         {
-            return _Content?.GetGeometryAsset();
+            get => _ContentTransformer;
+            set => _ContentTransformer = value;
         }
 
-        internal NodeBuilder GetArmatureAsset()
-        {
-            return _Content?.GetArmatureAsset();
-        }
+        #endregion
+
+        #region API
 
-        internal void Setup(Schema2.Scene dstScene, Schema2SceneBuilder context)
+        void SCHEMA2SCENE.Setup(Schema2.Scene dstScene, Schema2SceneBuilder context)
         {
-            _Content.Setup(dstScene, context);
+            if (_ContentTransformer is SCHEMA2SCENE schema2scb) schema2scb.Setup(dstScene, context);
         }
 
         #endregion

+ 87 - 57
src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs

@@ -31,6 +31,9 @@ namespace SharpGLTF.Scenes
         private readonly List<NodeBuilder> _Children = new List<NodeBuilder>();
 
         private Matrix4x4? _Matrix;
+        private Animations.AnimatableProperty<Vector3> _Scale;
+        private Animations.AnimatableProperty<Quaternion> _Rotation;
+        private Animations.AnimatableProperty<Vector3> _Translation;
 
         #endregion
 
@@ -48,13 +51,16 @@ namespace SharpGLTF.Scenes
 
         #region properties - transform
 
-        public bool HasAnimations => Scale?.Tracks.Count > 0 || Rotation?.Tracks.Count > 0 || Translation?.Tracks.Count > 0;
+        /// <summary>
+        /// Gets a value indicating whether this <see cref="NodeBuilder"/> has animations.
+        /// </summary>
+        public bool HasAnimations => (_Scale?.IsAnimated ?? false) || (_Rotation?.IsAnimated ?? false) || (_Translation?.IsAnimated ?? false);
 
-        public Animations.AnimatableProperty<Vector3> Scale { get; private set; }
+        public Animations.AnimatableProperty<Vector3> Scale => _Scale;
 
-        public Animations.AnimatableProperty<Quaternion> Rotation { get; private set; }
+        public Animations.AnimatableProperty<Quaternion> Rotation => _Rotation;
 
-        public Animations.AnimatableProperty<Vector3> Translation { get; private set; }
+        public Animations.AnimatableProperty<Vector3> Translation => _Translation;
 
         /// <summary>
         /// Gets or sets the local transform <see cref="Matrix4x4"/> of this <see cref="NodeBuilder"/>.
@@ -64,18 +70,12 @@ namespace SharpGLTF.Scenes
             get => Transforms.AffineTransform.Evaluate(_Matrix, Scale?.Value, Rotation?.Value, Translation?.Value);
             set
             {
-                if (value == Matrix4x4.Identity)
-                {
-                    _Matrix = null;
-                }
-                else
-                {
-                    _Matrix = value;
-                }
-
-                Scale = null;
-                Rotation = null;
-                Translation = null;
+                if (HasAnimations) { _DecomposeMatrix(value); return; }
+
+                _Matrix = value != Matrix4x4.Identity ? value : (Matrix4x4?)null;
+                _Scale = null;
+                _Rotation = null;
+                _Translation = null;
             }
         }
 
@@ -95,23 +95,9 @@ namespace SharpGLTF.Scenes
 
                 _Matrix = null;
 
-                if (value.Scale != Vector3.One)
-                {
-                    if (Scale == null) Scale = new Animations.AnimatableProperty<Vector3>();
-                    Scale.Value = value.Scale;
-                }
-
-                if (value.Rotation != Quaternion.Identity)
-                {
-                    if (Rotation == null) Rotation = new Animations.AnimatableProperty<Quaternion>();
-                    Rotation.Value = value.Rotation;
-                }
-
-                if (value.Translation != Vector3.Zero)
-                {
-                    if (Translation == null) Translation = new Animations.AnimatableProperty<Vector3>();
-                    Translation.Value = value.Translation;
-                }
+                if (value.Scale != Vector3.One) UseScale().Value = value.Scale;
+                if (value.Rotation != Quaternion.Identity) UseRotation().Value = value.Rotation;
+                if (value.Translation != Vector3.Zero) UseTranslation().Value = value.Translation;
             }
         }
 
@@ -134,7 +120,7 @@ namespace SharpGLTF.Scenes
 
         #endregion
 
-        #region API
+        #region API - hierarchy
 
         public NodeBuilder CreateNode(string name = null)
         {
@@ -144,15 +130,54 @@ namespace SharpGLTF.Scenes
             return c;
         }
 
+        /// <summary>
+        /// Checks if the collection of joints can be used for skinning a mesh.
+        /// </summary>
+        /// <param name="joints">A collection of joints.</param>
+        /// <returns>True if the joints can be used for skinning.</returns>
+        public static bool IsValidArmature(IEnumerable<NodeBuilder> joints)
+        {
+            if (joints == null) return false;
+            if (!joints.Any()) return false;
+            if (joints.Any(item => item == null)) return false;
+
+            var root = joints.First().Root;
+
+            return joints.All(item => Object.ReferenceEquals(item.Root, root));
+        }
+
+        #endregion
+
+        #region API - transform
+
+        private void _DecomposeMatrix()
+        {
+            if (!_Matrix.HasValue) return;
+            if (_Matrix.Value == Matrix4x4.Identity) return;
+            _DecomposeMatrix(_Matrix.Value);
+            _Matrix = null;
+        }
+
+        private void _DecomposeMatrix(Matrix4x4 matrix)
+        {
+            var affine = Transforms.AffineTransform.Create(matrix);
+
+            UseScale().Value = affine.Scale;
+            UseRotation().Value = affine.Rotation;
+            UseTranslation().Value = affine.Translation;
+        }
+
         public Animations.AnimatableProperty<Vector3> UseScale()
         {
-            if (Scale == null)
+            _DecomposeMatrix();
+
+            if (_Scale == null)
             {
-                Scale = new Animations.AnimatableProperty<Vector3>();
-                Scale.Value = Vector3.One;
+                _Scale = new Animations.AnimatableProperty<Vector3>();
+                _Scale.Value = Vector3.One;
             }
 
-            return Scale;
+            return _Scale;
         }
 
         public Animations.CurveBuilder<Vector3> UseScale(string animationTrack)
@@ -162,13 +187,15 @@ namespace SharpGLTF.Scenes
 
         public Animations.AnimatableProperty<Quaternion> UseRotation()
         {
-            if (Rotation == null)
+            _DecomposeMatrix();
+
+            if (_Rotation == null)
             {
-                Rotation = new Animations.AnimatableProperty<Quaternion>();
-                Rotation.Value = Quaternion.Identity;
+                _Rotation = new Animations.AnimatableProperty<Quaternion>();
+                _Rotation.Value = Quaternion.Identity;
             }
 
-            return Rotation;
+            return _Rotation;
         }
 
         public Animations.CurveBuilder<Quaternion> UseRotation(string animationTrack)
@@ -178,13 +205,15 @@ namespace SharpGLTF.Scenes
 
         public Animations.AnimatableProperty<Vector3> UseTranslation()
         {
-            if (Translation == null)
+            _DecomposeMatrix();
+
+            if (_Translation == null)
             {
-                Translation = new Animations.AnimatableProperty<Vector3>();
-                Translation.Value = Vector3.Zero;
+                _Translation = new Animations.AnimatableProperty<Vector3>();
+                _Translation.Value = Vector3.Zero;
             }
 
-            return Translation;
+            return _Translation;
         }
 
         public Animations.CurveBuilder<Vector3> UseTranslation(string animationTrack)
@@ -218,17 +247,6 @@ namespace SharpGLTF.Scenes
             return vs == null ? lm : Transforms.AffineTransform.LocalToWorld(vs.GetWorldMatrix(animationTrack, time), lm);
         }
 
-        public static bool IsValidArmature(IEnumerable<NodeBuilder> joints)
-        {
-            if (joints == null) return false;
-            if (!joints.Any()) return false;
-            if (joints.Any(item => item == null)) return false;
-
-            var root = joints.First().Root;
-
-            return joints.All(item => Object.ReferenceEquals(item.Root, root));
-        }
-
         #endregion
 
         #region With* API
@@ -239,6 +257,18 @@ namespace SharpGLTF.Scenes
             return this;
         }
 
+        public NodeBuilder WithLocalScale(Vector3 scale)
+        {
+            this.UseScale().Value = scale;
+            return this;
+        }
+
+        public NodeBuilder WithLocalRotation(Quaternion rotation)
+        {
+            this.UseRotation().Value = rotation;
+            return this;
+        }
+
         #endregion
     }
 }

+ 17 - 4
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs

@@ -36,7 +36,7 @@ namespace SharpGLTF.Scenes
             // gather all MaterialBuilder unique instances
 
             var materialGroups = srcScene.Instances
-                .Select(item => item.GetGeometryAsset())
+                .Select(item => item.Content?.GetGeometryAsset())
                 .Where(item => item != null)
                 .SelectMany(item => item.Primitives)
                 .Select(item => item.Material)
@@ -60,7 +60,7 @@ namespace SharpGLTF.Scenes
             // and group them by their vertex attribute layout.
 
             var meshGroups = srcScene.Instances
-            .Select(item => item.GetGeometryAsset())
+            .Select(item => item.Content?.GetGeometryAsset())
             .Where(item => item != null)
             .Distinct()
             .ToList()
@@ -83,7 +83,7 @@ namespace SharpGLTF.Scenes
             // gather all NodeBuilder unique armatures
 
             var armatures = srcScene.Instances
-                .Select(item => item.GetArmatureAsset())
+                .Select(item => item.Content?.GetArmatureAsset())
                 .Where(item => item != null)
                 .Select(item => item.Root)
                 .Distinct()
@@ -98,7 +98,11 @@ namespace SharpGLTF.Scenes
 
             // process instances
 
-            foreach (var inst in srcScene.Instances)
+            var schema2Instances = srcScene
+                .Instances
+                .OfType<IOperator<Scene>>();
+
+            foreach (var inst in schema2Instances)
             {
                 inst.Setup(dstScene, this);
             }
@@ -132,6 +136,15 @@ namespace SharpGLTF.Scenes
         }
 
         #endregion
+
+        #region types
+
+        public interface IOperator<T>
+        {
+            void Setup(T dst, Schema2SceneBuilder context);
+        }
+
+        #endregion
     }
 
     public partial class SceneBuilder

+ 2 - 2
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.cs

@@ -45,10 +45,10 @@ namespace SharpGLTF.Scenes
             return instance;
         }
 
-        public InstanceBuilder AddSkinnedMesh(MESHBUILDER mesh, Matrix4x4 meshBindMatrix, params NodeBuilder[] joints)
+        public InstanceBuilder AddSkinnedMesh(MESHBUILDER mesh, Matrix4x4 meshWorldMatrix, params NodeBuilder[] joints)
         {
             var instance = new InstanceBuilder(this);
-            instance.Content = new SkinTransformer(mesh, meshBindMatrix, joints);
+            instance.Content = new SkinTransformer(mesh, meshWorldMatrix, joints);
 
             _Instances.Add(instance);
 

+ 87 - 0
src/SharpGLTF.Toolkit/Scenes/Transformers.Schema2.cs

@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using SharpGLTF.Schema2;
+
+using SCHEMA2NODE = SharpGLTF.Scenes.Schema2SceneBuilder.IOperator<SharpGLTF.Schema2.Node>;
+using SCHEMA2SCENE = SharpGLTF.Scenes.Schema2SceneBuilder.IOperator<SharpGLTF.Schema2.Scene>;
+
+namespace SharpGLTF.Scenes
+{
+    partial class StaticTransformer : SCHEMA2SCENE
+    {
+        void SCHEMA2SCENE.Setup(Scene dstScene, Schema2SceneBuilder context)
+        {
+            if (!(Content is SCHEMA2NODE schema2Target)) return;
+
+            var node = dstScene.CreateNode();
+            node.LocalMatrix = _WorldTransform;
+
+            schema2Target.Setup(node, context);
+        }
+    }
+
+    partial class NodeTransformer : SCHEMA2SCENE
+    {
+        void SCHEMA2SCENE.Setup(Scene dstScene, Schema2SceneBuilder context)
+        {
+            if (!(Content is SCHEMA2NODE schema2Target)) return;
+
+            var node = context.GetNode(_Node);
+
+            if (node == null) dstScene.CreateNode();
+
+            schema2Target.Setup(node, context);
+        }
+    }
+
+    partial class SkinTransformer : SCHEMA2SCENE
+    {
+        void SCHEMA2SCENE.Setup(Scene dstScene, Schema2SceneBuilder context)
+        {
+            if (!(Content is SCHEMA2NODE schema2Target)) return;
+
+            var skinnedMeshNode = dstScene.CreateNode();
+
+            if (_TargetBindMatrix.HasValue)
+            {
+                var dstNodes = new Node[_Joints.Count];
+
+                for (int i = 0; i < dstNodes.Length; ++i)
+                {
+                    var srcNode = _Joints[i];
+
+                    System.Diagnostics.Debug.Assert(!srcNode.Item2.HasValue);
+
+                    dstNodes[i] = context.GetNode(srcNode.Item1);
+                }
+
+                #if DEBUG
+                for (int i = 0; i < dstNodes.Length; ++i)
+                {
+                    var srcNode = _Joints[i];
+                    System.Diagnostics.Debug.Assert(dstNodes[i].WorldMatrix == srcNode.Item1.WorldMatrix);
+                }
+                #endif
+
+                skinnedMeshNode.WithSkinBinding(_TargetBindMatrix.Value, dstNodes);
+            }
+            else
+            {
+                var skinnedJoints = _Joints
+                .Select(j => (context.GetNode(j.Item1), j.Item2.Value))
+                .ToArray();
+
+                skinnedMeshNode.WithSkinBinding(skinnedJoints);
+            }
+
+            // set skeleton
+            // var root = _Joints[0].Item1.Root;
+            // skinnedMeshNode.Skin.Skeleton = context.GetNode(root);
+
+            schema2Target.Setup(skinnedMeshNode, context);
+        }
+    }
+}

+ 220 - 0
src/SharpGLTF.Toolkit/Scenes/Transformers.cs

@@ -0,0 +1,220 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+using MESHBUILDER = SharpGLTF.Geometry.IMeshBuilder<SharpGLTF.Materials.MaterialBuilder>;
+
+namespace SharpGLTF.Scenes
+{
+    /// <summary>
+    /// Wraps a content object (usually a Mesh, a Camera or a light)
+    /// </summary>
+    public abstract class ContentTransformer
+    {
+        #region lifecycle
+
+        protected ContentTransformer(Object content)
+        {
+            Guard.NotNull(content, nameof(content));
+
+            _Content = content;
+        }
+
+        protected ContentTransformer(MESHBUILDER mesh)
+        {
+            Guard.NotNull(mesh, nameof(mesh));
+
+            _Content = new MeshContent(mesh);
+        }
+
+        #endregion
+
+        #region data
+
+        private Object _Content;
+
+        #endregion
+
+        #region properties
+
+        public Object Content => _Content;
+
+        #endregion
+
+        #region API
+
+        public virtual MESHBUILDER GetGeometryAsset() { return (_Content as IRenderableContent)?.GetGeometryAsset(); }
+
+        public abstract NodeBuilder GetArmatureAsset();
+
+        #endregion
+    }
+
+    public partial class StaticTransformer : ContentTransformer
+    {
+        #region lifecycle
+
+        public StaticTransformer(Object content, Matrix4x4 xform)
+            : base(content)
+        {
+            _WorldTransform = xform;
+        }
+
+        public StaticTransformer(MESHBUILDER mesh, Matrix4x4 xform)
+            : base(mesh)
+        {
+            _WorldTransform = xform;
+        }
+
+        #endregion
+
+        #region data
+
+        private Matrix4x4 _WorldTransform;
+
+        #endregion
+
+        #region properties
+
+        public Matrix4x4 WorldTransform
+        {
+            get => _WorldTransform;
+            set => _WorldTransform = value;
+        }
+
+        #endregion
+
+        #region API
+
+        public override NodeBuilder GetArmatureAsset() { return null; }
+
+        #endregion
+    }
+
+    public partial class NodeTransformer : ContentTransformer
+    {
+        #region lifecycle
+
+        public NodeTransformer(Object content, NodeBuilder node)
+            : base(content)
+        {
+            _Node = node;
+        }
+
+        public NodeTransformer(MESHBUILDER mesh, NodeBuilder node)
+            : base(mesh)
+        {
+            _Node = node;
+        }
+
+        #endregion
+
+        #region data
+
+        private NodeBuilder _Node;
+
+        #endregion
+
+        #region properties
+
+        public NodeBuilder Transform
+        {
+            get => _Node;
+            set => _Node = value;
+        }
+
+        #endregion
+
+        #region API
+
+        public override NodeBuilder GetArmatureAsset() { return _Node.Root; }
+
+        #endregion
+    }
+
+    public partial class SkinTransformer : ContentTransformer
+    {
+        #region lifecycle
+
+        public SkinTransformer(MESHBUILDER mesh, Matrix4x4 meshWorldMatrix, NodeBuilder[] joints)
+            : base(mesh)
+        {
+            SetJoints(meshWorldMatrix, joints);
+        }
+
+        public SkinTransformer(MESHBUILDER mesh, (NodeBuilder, Matrix4x4)[] joints)
+            : base(mesh)
+        {
+            SetJoints(joints);
+        }
+
+        #endregion
+
+        #region data
+
+        private Matrix4x4? _TargetBindMatrix;
+
+        // condition: all NodeBuilder objects must have the same root.
+        private readonly List<(NodeBuilder, Matrix4x4?)> _Joints = new List<(NodeBuilder, Matrix4x4?)>();
+
+        #endregion
+
+        #region API
+
+        private void SetJoints(Matrix4x4 meshWorldMatrix, NodeBuilder[] joints)
+        {
+            Guard.NotNull(joints, nameof(joints));
+            Guard.IsTrue(NodeBuilder.IsValidArmature(joints), nameof(joints));
+
+            _TargetBindMatrix = meshWorldMatrix;
+            _Joints.Clear();
+            _Joints.AddRange(joints.Select(item => (item, (Matrix4x4?)null)));
+        }
+
+        private void SetJoints((NodeBuilder, Matrix4x4)[] joints)
+        {
+            Guard.NotNull(joints, nameof(joints));
+            Guard.IsTrue(NodeBuilder.IsValidArmature(joints.Select(item => item.Item1)), nameof(joints));
+
+            _TargetBindMatrix = null;
+            _Joints.Clear();
+            _Joints.AddRange(joints.Select(item => (item.Item1, (Matrix4x4?)item.Item2)));
+        }
+
+        public (NodeBuilder, Matrix4x4)[] GetJointBindings()
+        {
+            var jb = new (NodeBuilder, Matrix4x4)[_Joints.Count];
+
+            for (int i = 0; i < jb.Length; ++i)
+            {
+                var j = _Joints[i].Item1;
+                var m = _Joints[i].Item2 ?? Transforms.SkinTransform.CalculateInverseBinding(_TargetBindMatrix ?? Matrix4x4.Identity, j.WorldMatrix);
+
+                jb[i] = (j, m);
+            }
+
+            return jb;
+        }
+
+        public override NodeBuilder GetArmatureAsset()
+        {
+            return _Joints
+                .Select(item => item.Item1.Root)
+                .Distinct()
+                .FirstOrDefault();
+        }
+
+        public Transforms.ITransform GetWorldTransformer(string animationTrack, float time)
+        {
+            var jb = GetJointBindings();
+
+            var ww = jb.Select(item => item.Item1.GetWorldMatrix(animationTrack, time)).ToArray();
+            var bb = jb.Select(item => item.Item2).ToArray();
+
+            return new Transforms.SkinTransform(bb, ww, default, false);
+        }
+
+        #endregion
+    }
+}

+ 6 - 0
src/SharpGLTF.Toolkit/Scenes/readme.md

@@ -35,6 +35,12 @@ scene.SaveGLB("scene.glb");
 In order to have a hierarchical tree of nodes, you use the NodeBuilder object, with
 which you can create standalone nodes, or whole skeleton armatures.
 
+```c#
+var root = new NodeBuilder("root");
+var child = root.CreateNode("child");
+
+```
+
 In this way, NodeBuilder armatures become just another asset, like a mesh or a material,
 and a scene is just a collection of instances to be rendered.
 

+ 4 - 0
src/SharpGLTF.Toolkit/Schema2/LightExtensions.cs

@@ -22,6 +22,8 @@ namespace SharpGLTF.Schema2
         /// <returns>This <see cref="PunctualLight"/> instance.</returns>
         public static PunctualLight WithSpotCone(this PunctualLight light, float innerConeAngle, float outerConeAngle)
         {
+            Guard.NotNull(light, nameof(light));
+
             light.SetSpotCone(innerConeAngle, outerConeAngle);
             return light;
         }
@@ -44,6 +46,8 @@ namespace SharpGLTF.Schema2
         /// <returns>This <see cref="PunctualLight"/> instance.</returns>
         public static PunctualLight WithColor(this PunctualLight light, Vector3 color, float intensity = 1, float range = 0)
         {
+            Guard.NotNull(light, nameof(light));
+
             light.Color = color;
             light.Intensity = intensity;
             light.Range = range;

+ 88 - 88
src/SharpGLTF.Toolkit/Schema2/MeshExtensions.cs

@@ -18,12 +18,12 @@ namespace SharpGLTF.Schema2
 
         public static Mesh CreateMesh(this ModelRoot root, IMeshBuilder<Materials.MaterialBuilder> mesh)
         {
-            return root.CreateMeshes(mesh).First();
+            return root.CreateMeshes(mesh)[0];
         }
 
         public static Mesh CreateMesh<TMaterial>(this ModelRoot root, Func<TMaterial, Material> materialEvaluator, IMeshBuilder<TMaterial> mesh)
         {
-            return root.CreateMeshes<TMaterial>(materialEvaluator, mesh).First();
+            return root.CreateMeshes<TMaterial>(materialEvaluator, mesh)[0];
         }
 
         public static IReadOnlyList<Mesh> CreateMeshes(this ModelRoot root, params IMeshBuilder<Materials.MaterialBuilder>[] meshBuilders)
@@ -69,6 +69,8 @@ namespace SharpGLTF.Schema2
 
         public static MeshPrimitive WithIndicesAutomatic(this MeshPrimitive primitive, PrimitiveType primitiveType)
         {
+            Guard.NotNull(primitive, nameof(primitive));
+
             var root = primitive.LogicalParent.LogicalParent;
 
             primitive.DrawPrimitiveType = primitiveType;
@@ -79,6 +81,9 @@ namespace SharpGLTF.Schema2
 
         public static MeshPrimitive WithIndicesAccessor(this MeshPrimitive primitive, PrimitiveType primitiveType, IReadOnlyList<Int32> values)
         {
+            Guard.NotNull(primitive, nameof(primitive));
+            Guard.NotNull(values, nameof(values));
+
             var root = primitive.LogicalParent.LogicalParent;
 
             // create an index buffer and fill it
@@ -98,6 +103,9 @@ namespace SharpGLTF.Schema2
 
         public static MeshPrimitive WithVertexAccessor(this MeshPrimitive primitive, string attribute, IReadOnlyList<Single> values)
         {
+            Guard.NotNull(primitive, nameof(primitive));
+            Guard.NotNull(values, nameof(values));
+
             var root = primitive.LogicalParent.LogicalParent;
 
             // create a vertex buffer and fill it
@@ -115,6 +123,9 @@ namespace SharpGLTF.Schema2
 
         public static MeshPrimitive WithVertexAccessor(this MeshPrimitive primitive, string attribute, IReadOnlyList<Vector2> values)
         {
+            Guard.NotNull(primitive, nameof(primitive));
+            Guard.NotNull(values, nameof(values));
+
             var root = primitive.LogicalParent.LogicalParent;
 
             // create a vertex buffer and fill it
@@ -132,6 +143,9 @@ namespace SharpGLTF.Schema2
 
         public static MeshPrimitive WithVertexAccessor(this MeshPrimitive primitive, string attribute, IReadOnlyList<Vector3> values)
         {
+            Guard.NotNull(primitive, nameof(primitive));
+            Guard.NotNull(values, nameof(values));
+
             var root = primitive.LogicalParent.LogicalParent;
 
             // create a vertex buffer and fill it
@@ -150,6 +164,9 @@ namespace SharpGLTF.Schema2
 
         public static MeshPrimitive WithVertexAccessor(this MeshPrimitive primitive, string attribute, IReadOnlyList<Vector4> values)
         {
+            Guard.NotNull(primitive, nameof(primitive));
+            Guard.NotNull(values, nameof(values));
+
             var root = primitive.LogicalParent.LogicalParent;
 
             // create a vertex buffer and fill it
@@ -217,6 +234,9 @@ namespace SharpGLTF.Schema2
 
         public static MeshPrimitive WithVertexAccessors(this MeshPrimitive primitive, IEnumerable<Memory.MemoryAccessor> memAccessors)
         {
+            Guard.NotNull(memAccessors, nameof(memAccessors));
+            Guard.IsTrue(memAccessors.All(item => item != null), nameof(memAccessors));
+
             foreach (var va in memAccessors) primitive.WithVertexAccessor(va);
 
             return primitive;
@@ -224,6 +244,9 @@ namespace SharpGLTF.Schema2
 
         public static MeshPrimitive WithVertexAccessor(this MeshPrimitive primitive, Memory.MemoryAccessor memAccessor)
         {
+            Guard.NotNull(primitive, nameof(primitive));
+            Guard.NotNull(memAccessor, nameof(memAccessor));
+
             var root = primitive.LogicalParent.LogicalParent;
 
             primitive.SetVertexAccessor(memAccessor.Attribute.Name, root.CreateVertexAccessor(memAccessor));
@@ -233,6 +256,8 @@ namespace SharpGLTF.Schema2
 
         public static MeshPrimitive WithIndicesAccessor(this MeshPrimitive primitive, PrimitiveType primitiveType, Memory.MemoryAccessor memAccessor)
         {
+            Guard.NotNull(primitive, nameof(primitive));
+
             var root = primitive.LogicalParent.LogicalParent;
 
             var accessor = root.CreateAccessor();
@@ -251,6 +276,8 @@ namespace SharpGLTF.Schema2
 
         public static MeshPrimitive WithMaterial(this MeshPrimitive primitive, Material material)
         {
+            Guard.NotNull(primitive, nameof(primitive));
+
             primitive.Material = material;
             return primitive;
         }
@@ -259,6 +286,48 @@ namespace SharpGLTF.Schema2
 
         #region evaluation
 
+        public static int GetPrimitiveVertexSize(this PrimitiveType ptype)
+        {
+            switch (ptype)
+            {
+                case PrimitiveType.POINTS: return 1;
+                case PrimitiveType.LINES: return 2;
+                case PrimitiveType.LINE_LOOP: return 2;
+                case PrimitiveType.LINE_STRIP: return 2;
+                case PrimitiveType.TRIANGLES: return 3;
+                case PrimitiveType.TRIANGLE_FAN: return 3;
+                case PrimitiveType.TRIANGLE_STRIP: return 3;
+                default: throw new NotImplementedException();
+            }
+        }
+
+        public static IEnumerable<int> GetPointIndices(this MeshPrimitive primitive)
+        {
+            if (primitive == null || primitive.DrawPrimitiveType.GetPrimitiveVertexSize() != 1) return Enumerable.Empty<int>();
+
+            if (primitive.IndexAccessor == null) return Enumerable.Range(0, primitive.GetVertexAccessor("POSITION").Count);
+
+            return primitive.IndexAccessor.AsIndicesArray().Select(item => (int)item);
+        }
+
+        public static IEnumerable<(int, int)> GetLineIndices(this MeshPrimitive primitive)
+        {
+            if (primitive == null || primitive.DrawPrimitiveType.GetPrimitiveVertexSize() != 2) return Enumerable.Empty<(int, int)>();
+
+            if (primitive.IndexAccessor == null) return primitive.DrawPrimitiveType.GetLinesIndices(primitive.GetVertexAccessor("POSITION").Count);
+
+            return primitive.DrawPrimitiveType.GetLinesIndices(primitive.IndexAccessor.AsIndicesArray());
+        }
+
+        public static IEnumerable<(int, int, int)> GetTriangleIndices(this MeshPrimitive primitive)
+        {
+            if (primitive == null || primitive.DrawPrimitiveType.GetPrimitiveVertexSize() != 3) return Enumerable.Empty<(int, int, int)>();
+
+            if (primitive.IndexAccessor == null) return primitive.DrawPrimitiveType.GetTrianglesIndices(primitive.GetVertexAccessor("POSITION").Count);
+
+            return primitive.DrawPrimitiveType.GetTrianglesIndices(primitive.IndexAccessor.AsIndicesArray());
+        }
+
         public static IEnumerable<(IVertexBuilder, Material)> EvaluatePoints(this Mesh mesh, MESHXFORM xform = null)
         {
             if (mesh == null) return Enumerable.Empty<(IVertexBuilder, Material)>();
@@ -275,7 +344,6 @@ namespace SharpGLTF.Schema2
             if (!points.Any()) yield break;
 
             var vertices = prim.GetVertexColumns(xform);
-
             var vtype = vertices.GetCompatibleVertexType();
 
             foreach (var t in points)
@@ -302,7 +370,6 @@ namespace SharpGLTF.Schema2
             if (!lines.Any()) yield break;
 
             var vertices = prim.GetVertexColumns(xform);
-
             var vtype = vertices.GetCompatibleVertexType();
 
             foreach (var t in lines)
@@ -330,7 +397,6 @@ namespace SharpGLTF.Schema2
             if (!triangles.Any()) yield break;
 
             var vertices = prim.GetVertexColumns(xform);
-
             var vtype = vertices.GetCompatibleVertexType();
 
             foreach (var t in triangles)
@@ -343,25 +409,28 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        public static IEnumerable<(VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, Material)> EvaluateTriangles<TvG, TvM, TvS>(this Mesh mesh)
+        public static IEnumerable<(VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, Material)> EvaluateTriangles<TvG, TvM, TvS>(this Mesh mesh, MESHXFORM xform = null)
             where TvG : struct, IVertexGeometry
             where TvM : struct, IVertexMaterial
             where TvS : struct, IVertexSkinning
         {
             if (mesh == null) return Enumerable.Empty<(VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, Material)>();
 
-            return mesh.Primitives.SelectMany(item => item.EvaluateTriangles<TvG, TvM, TvS>());
+            return mesh.Primitives.SelectMany(item => item.EvaluateTriangles<TvG, TvM, TvS>(xform));
         }
 
-        public static IEnumerable<(VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, Material)> EvaluateTriangles<TvG, TvM, TvS>(this MeshPrimitive prim)
+        public static IEnumerable<(VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, Material)> EvaluateTriangles<TvG, TvM, TvS>(this MeshPrimitive prim, MESHXFORM xform = null)
             where TvG : struct, IVertexGeometry
             where TvM : struct, IVertexMaterial
             where TvS : struct, IVertexSkinning
         {
             if (prim == null) yield break;
+            if (xform != null && !xform.Visible) yield break;
 
-            var vertices = prim.GetVertexColumns();
             var triangles = prim.GetTriangleIndices();
+            if (!triangles.Any()) yield break;
+
+            var vertices = prim.GetVertexColumns(xform);
 
             bool hasNormals = vertices.Normals != null;
 
@@ -384,77 +453,6 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        public static IEnumerable<(VertexBuilder<TvG, TvM, VertexEmpty>, VertexBuilder<TvG, TvM, VertexEmpty>, VertexBuilder<TvG, TvM, VertexEmpty>, Material)> EvaluateTriangles<TvG, TvM>(this Mesh mesh, MESHXFORM xform)
-            where TvG : struct, IVertexGeometry
-            where TvM : struct, IVertexMaterial
-        {
-            if (mesh == null) return Enumerable.Empty<(VertexBuilder<TvG, TvM, VertexEmpty>, VertexBuilder<TvG, TvM, VertexEmpty>, VertexBuilder<TvG, TvM, VertexEmpty>, Material)>();
-
-            return mesh.Primitives.SelectMany(item => item.EvaluateTriangles<TvG, TvM>(xform));
-        }
-
-        public static IEnumerable<(VertexBuilder<TvG, TvM, VertexEmpty>, VertexBuilder<TvG, TvM, VertexEmpty>, VertexBuilder<TvG, TvM, VertexEmpty>, Material)> EvaluateTriangles<TvG, TvM>(this MeshPrimitive prim, MESHXFORM xform)
-            where TvG : struct, IVertexGeometry
-            where TvM : struct, IVertexMaterial
-        {
-            if (prim == null) yield break;
-            if (xform == null || !xform.Visible) yield break;
-
-            var vertices = prim.GetVertexColumns(xform);
-            var triangles = prim.GetTriangleIndices();
-
-            foreach (var t in triangles)
-            {
-                var a = vertices.GetVertex<TvG, TvM>(t.Item1);
-                var b = vertices.GetVertex<TvG, TvM>(xform.FlipFaces ? t.Item3 : t.Item2);
-                var c = vertices.GetVertex<TvG, TvM>(xform.FlipFaces ? t.Item2 : t.Item3);
-
-                yield return ((a.Geometry, a.Material), (b.Geometry, b.Material), (c.Geometry, c.Material), prim.Material);
-            }
-        }
-
-        public static IEnumerable<int> GetPointIndices(this MeshPrimitive primitive)
-        {
-            if (primitive == null || primitive.DrawPrimitiveType.GetPrimitiveVertexSize() != 1) return Enumerable.Empty<int>();
-
-            if (primitive.IndexAccessor == null) return Enumerable.Range(0, primitive.GetVertexAccessor("POSITION").Count);
-
-            return primitive.IndexAccessor.AsIndicesArray().Select(item => (int)item);
-        }
-
-        public static IEnumerable<(int, int)> GetLineIndices(this MeshPrimitive primitive)
-        {
-            if (primitive == null || primitive.DrawPrimitiveType.GetPrimitiveVertexSize() != 2) return Enumerable.Empty<(int, int)>();
-
-            if (primitive.IndexAccessor == null) return primitive.DrawPrimitiveType.GetLinesIndices(primitive.GetVertexAccessor("POSITION").Count);
-
-            return primitive.DrawPrimitiveType.GetLinesIndices(primitive.IndexAccessor.AsIndicesArray());
-        }
-
-        public static IEnumerable<(int, int, int)> GetTriangleIndices(this MeshPrimitive primitive)
-        {
-            if (primitive == null || primitive.DrawPrimitiveType.GetPrimitiveVertexSize() != 3) return Enumerable.Empty<(int, int, int)>();
-
-            if (primitive.IndexAccessor == null) return primitive.DrawPrimitiveType.GetTrianglesIndices(primitive.GetVertexAccessor("POSITION").Count);
-
-            return primitive.DrawPrimitiveType.GetTrianglesIndices(primitive.IndexAccessor.AsIndicesArray());
-        }
-
-        public static int GetPrimitiveVertexSize(this PrimitiveType ptype)
-        {
-            switch (ptype)
-            {
-                case PrimitiveType.POINTS: return 1;
-                case PrimitiveType.LINES: return 2;
-                case PrimitiveType.LINE_LOOP: return 2;
-                case PrimitiveType.LINE_STRIP: return 2;
-                case PrimitiveType.TRIANGLES: return 3;
-                case PrimitiveType.TRIANGLE_FAN: return 3;
-                case PrimitiveType.TRIANGLE_STRIP: return 3;
-                default: throw new NotImplementedException();
-            }
-        }
-
         #endregion
 
         #region mesh conversion
@@ -554,16 +552,18 @@ namespace SharpGLTF.Schema2
             where TvS : struct, IVertexSkinning
         {
             Guard.NotNull(meshBuilder, nameof(meshBuilder));
+            Guard.NotNull(materialFunc, nameof(materialFunc));
 
             if (srcMesh == null) return;
 
-            Guard.NotNull(materialFunc, nameof(materialFunc));
-
             foreach (var srcPrim in srcMesh.Primitives)
             {
-                var dstPrim = meshBuilder.UsePrimitive(materialFunc(srcPrim.Material));
+                if (srcPrim != null) continue;
+
+                var dstMat = materialFunc(srcPrim.Material);
+                var dstPrim = meshBuilder.UsePrimitive(dstMat);
 
-                foreach (var tri in srcPrim.EvaluateTriangles<TvG, TvM, TvS>())
+                foreach (var tri in srcPrim.EvaluateTriangles<TvG, TvM, TvS>(null))
                 {
                     dstPrim.AddTriangle(tri.Item1, tri.Item2, tri.Item3);
                 }
@@ -578,11 +578,11 @@ namespace SharpGLTF.Schema2
         /// <typeparam name="TvG">A subtype of <see cref="IVertexGeometry"/></typeparam>
         /// <typeparam name="TvM">A subtype of <see cref="IVertexMaterial"/></typeparam>
         /// <param name="srcScene">The source <see cref="Scene"/> to evaluate.</param>
+        /// <param name="materialFunc">A function to convert <see cref="Material"/> into <typeparamref name="TMaterial"/>.</param>
         /// <param name="animation">The source <see cref="Animation"/> to evaluate.</param>
         /// <param name="time">A time point, in seconds, within <paramref name="animation"/>.</param>
-        /// <param name="materialFunc">A function to convert <see cref="Material"/> into <typeparamref name="TMaterial"/>.</param>
         /// <returns>A new <see cref="MeshBuilder{TMaterial, TvG, TvM, TvS}"/> containing the evaluated geometry.</returns>
-        public static MeshBuilder<TMaterial, TvG, TvM, VertexEmpty> ToStaticMeshBuilder<TMaterial, TvG, TvM>(this Scene srcScene, Animation animation, float time, Func<Material, TMaterial> materialFunc)
+        public static MeshBuilder<TMaterial, TvG, TvM, VertexEmpty> ToStaticMeshBuilder<TMaterial, TvG, TvM>(this Scene srcScene, Func<Material, TMaterial> materialFunc, Animation animation, float time)
             where TvG : struct, IVertexGeometry
             where TvM : struct, IVertexMaterial
         {
@@ -624,7 +624,7 @@ namespace SharpGLTF.Schema2
                 return materials[srcMaterial] = dstMaterial;
             }
 
-            return srcScene.ToStaticMeshBuilder<Materials.MaterialBuilder, TvG, TvM>(animation, time, convertMaterial);
+            return srcScene.ToStaticMeshBuilder<Materials.MaterialBuilder, TvG, TvM>(convertMaterial, animation, time);
         }
 
         public static IMeshBuilder<Materials.MaterialBuilder> ToMeshBuilder(this Mesh srcMesh)
@@ -675,7 +675,7 @@ namespace SharpGLTF.Schema2
             foreach (var srcTri in srcMesh.EvaluateLines())
             {
                 var dstPrim = GetPrimitive(srcTri.Item3, 2);
-                dstPrim.AddLine(srcTri.Item1,srcTri.Item2);
+                dstPrim.AddLine(srcTri.Item1, srcTri.Item2);
             }
 
             foreach (var srcTri in srcMesh.EvaluateTriangles())

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

@@ -271,7 +271,7 @@ namespace SharpGLTF.Schema2
 
             var xform = node.GetMeshWorldTransform(animation, time);
 
-            return mesh.EvaluateTriangles<TvG, TvM>(xform);
+            return mesh.EvaluateTriangles<TvG, TvM, VertexEmpty>(xform);
         }
 
         public static Scenes.SceneBuilder ToSceneBuilder(this Scene srcScene)