Browse Source

[WIP] Adding GpuMeshIntancing extension and support classes.

Vicente Penades 4 years ago
parent
commit
4aab2d63a8
46 changed files with 2002 additions and 582 deletions
  1. 1 2
      examples/SharpGLTF.Plotly/PlotlyToolkit.cs
  2. 2 2
      examples/SharpGLTF.Runtime.MonoGame/MonoGameModelInstance.cs
  3. 5 5
      examples/SharpGLTF.Runtime.MonoGame/MonoGameModelTemplate.cs
  4. 1 0
      src/Shared/_Extensions.cs
  5. 63 1
      src/SharpGLTF.Core/Runtime/DrawableInstance.cs
  6. 51 1
      src/SharpGLTF.Core/Runtime/DrawableTemplate.cs
  7. 9 9
      src/SharpGLTF.Core/Runtime/MeshDecoder.Schema2.cs
  8. 6 4
      src/SharpGLTF.Core/Runtime/MeshDecoder.cs
  9. 17 16
      src/SharpGLTF.Core/Runtime/NodeTemplate.cs
  10. 18 1
      src/SharpGLTF.Core/Runtime/RuntimeOptions.cs
  11. 38 18
      src/SharpGLTF.Core/Runtime/SceneInstance.cs
  12. 31 8
      src/SharpGLTF.Core/Runtime/SceneTemplate.cs
  13. 56 0
      src/SharpGLTF.Core/Schema2/Generated/ext.MeshGpuInstancing.g.cs
  14. 43 5
      src/SharpGLTF.Core/Schema2/gltf.Accessors.cs
  15. 1 0
      src/SharpGLTF.Core/Schema2/gltf.ExtensionsFactory.cs
  16. 147 0
      src/SharpGLTF.Core/Schema2/gltf.Node.MeshInstancing.cs
  17. 74 52
      src/SharpGLTF.Core/Schema2/gltf.Node.cs
  18. 314 98
      src/SharpGLTF.Core/Transforms/AffineTransform.cs
  19. 100 0
      src/SharpGLTF.Core/Transforms/MeshTransforms.cs
  20. 13 2
      src/SharpGLTF.Core/Validation/ValidationMode.cs
  21. 3 0
      src/SharpGLTF.Toolkit/Geometry/MeshBuilderToolkit.cs
  22. 53 9
      src/SharpGLTF.Toolkit/Geometry/VertexBufferColumns.cs
  23. 6 6
      src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexSkinning.cs
  24. 5 1
      src/SharpGLTF.Toolkit/IO/WavefrontWriter.cs
  25. 3 3
      src/SharpGLTF.Toolkit/Scenes/Content.Schema2.cs
  26. 14 3
      src/SharpGLTF.Toolkit/Scenes/Content.cs
  27. 1 8
      src/SharpGLTF.Toolkit/Scenes/InstanceBuilder.cs
  28. 90 85
      src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs
  29. 161 36
      src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs
  30. 15 22
      src/SharpGLTF.Toolkit/Scenes/SceneBuilder.cs
  31. 51 0
      src/SharpGLTF.Toolkit/Scenes/TransformChainBuilder.cs
  32. 170 20
      src/SharpGLTF.Toolkit/Scenes/Transformers.Schema2.cs
  33. 42 22
      src/SharpGLTF.Toolkit/Scenes/Transformers.cs
  34. 50 0
      src/SharpGLTF.Toolkit/Schema2/AccessorExtensions.cs
  35. 166 28
      src/SharpGLTF.Toolkit/Schema2/MeshExtensions.cs
  36. 6 6
      src/SharpGLTF.Toolkit/Schema2/SceneExtensions.cs
  37. BIN
      tests/Assets/gltf-GpuMeshInstancing/GrassFieldInstanced.glb
  38. BIN
      tests/Assets/gltf-GpuMeshInstancing/InstanceTest.glb
  39. 3 0
      tests/SharpGLTF.NUnit/NUnitGltfUtils.cs
  40. 6 0
      tests/SharpGLTF.NUnit/NumericsUtils.cs
  41. 67 59
      tests/SharpGLTF.NUnit/TestFiles.cs
  42. 1 1
      tests/SharpGLTF.Tests/Runtime/SceneTemplateTests.cs
  43. 4 4
      tests/SharpGLTF.Tests/Schema2/Authoring/ExtensionsCreationTests.cs
  44. 37 21
      tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSampleTests.cs
  45. 1 1
      tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSpecialModelsTest.cs
  46. 57 23
      tests/SharpGLTF.Toolkit.Tests/Scenes/SceneBuilderTests.cs

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

@@ -29,8 +29,7 @@ namespace SharpGLTF
             var meshes = srcScene.LogicalParent.LogicalMeshes;
 
             // get the drawable instances.
-            var instances = sceneInstance
-                .DrawableInstances
+            var instances = sceneInstance                
                 .Where(item => item.Transform.Visible);
 
             // prepare the PlotlyScene.

+ 2 - 2
examples/SharpGLTF.Runtime.MonoGame/MonoGameModelInstance.cs

@@ -58,9 +58,9 @@ namespace SharpGLTF.Runtime
         /// <param name="world">The world matrix.</param>
         public void Draw(Matrix projection, Matrix view, Matrix world)
         {
-            foreach (var d in _Controller.DrawableInstances)
+            foreach (var inst in _Controller)
             {
-                Draw(_Template._Meshes[d.Template.LogicalMeshIndex], projection, view, world, d.Transform);
+                Draw(_Template._Meshes[inst.Template.LogicalMeshIndex], projection, view, world, inst.Transform);
             }
         }
 

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

@@ -117,17 +117,17 @@ namespace SharpGLTF.Runtime
 
         private BoundingSphere CalculateBounds(SceneTemplate scene)
         {
-            var instance = scene.CreateInstance();            
+            var instances = scene.CreateInstance();            
 
             var bounds = default(BoundingSphere);
 
-            foreach (var d in instance.DrawableInstances)
+            foreach (var inst in instances)
             {
-                var b = _Meshes[d.Template.LogicalMeshIndex].BoundingSphere;
+                var b = _Meshes[inst.Template.LogicalMeshIndex].BoundingSphere;
 
-                if (d.Transform is Transforms.RigidTransform statXform) b = b.Transform(statXform.WorldMatrix.ToXna());
+                if (inst.Transform is Transforms.RigidTransform statXform) b = b.Transform(statXform.WorldMatrix.ToXna());
 
-                if (d.Transform is Transforms.SkinnedTransform skinXform)
+                if (inst.Transform is Transforms.SkinnedTransform skinXform)
                 {
                     // this is a bit agressive and probably over-reaching, but with skins you never know the actual bounds
                     // unless you calculate the bounds frame by frame.

+ 1 - 0
src/Shared/_Extensions.cs

@@ -181,6 +181,7 @@ namespace SharpGLTF
         [Flags]
         internal enum MatrixCheck
         {
+            Finite = 0,
             NonZero = 1,
             Identity = 2,
             IdentityColumn4 = 4,

+ 63 - 1
src/SharpGLTF.Core/Runtime/DrawableInstance.cs

@@ -4,17 +4,79 @@ using System.Text;
 
 namespace SharpGLTF.Runtime
 {
-    [System.Diagnostics.DebuggerDisplay("{Template.Name} {MeshIndex}")]
+    [System.Diagnostics.DebuggerDisplay("{_ToDebuggerDisplayString(),nq}")]
     public readonly struct DrawableInstance
     {
+        #region diagnostics
+
+        private string _ToDebuggerDisplayString()
+        {
+            if (Template == null || Transform == null) return "⚠ Empty";
+
+            var text = string.Empty;
+
+            if (Transform.Visible) text += "👁 ";
+
+            text += "[";
+            if (Template.NodeName != null) text += Template.NodeName + " ";
+            text += Template.LogicalMeshIndex;
+            text += "] ";
+
+            if (Transform is Transforms.RigidTransform rigid)
+            {
+                text += "Rigid";
+            }
+
+            if (Transform is Transforms.SkinnedTransform skinned)
+            {
+                text += $"Skinned 🦴={skinned.SkinMatrices.Count}";
+            }
+
+            if (Transform is Transforms.InstancingTransform instanced)
+            {
+                text += $"Instanced 🏠={instanced.LocalMatrices.Count}";
+            }
+
+            return text;
+        }
+
+        #endregion
+
+        #region constructor
+
         internal DrawableInstance(IDrawableTemplate t, Transforms.IGeometryTransform xform)
         {
             Template = t;
             Transform = xform;
         }
 
+        #endregion
+
+        #region data
+
+        /// <summary>
+        /// Represents what to draw.
+        /// </summary>
         public readonly IDrawableTemplate Template;
 
+        /// <summary>
+        /// Represents where to draw the <see cref="Template"/>.
+        /// </summary>
+        /// <remarks>
+        /// This value can be casted to any of:
+        /// <list type="table">
+        /// <item><see cref="Transforms.RigidTransform"/></item>
+        /// <item><see cref="Transforms.SkinnedTransform"/></item>
+        /// <item><see cref="Transforms.InstancingTransform"/></item>
+        /// </list>
+        /// </remarks>
         public readonly Transforms.IGeometryTransform Transform;
+
+        #endregion
+
+        #region properties
+        public int InstanceCount => (this.Transform as Transforms.InstancingTransform)?.LocalMatrices.Count ?? 1;
+
+        #endregion
     }
 }

+ 51 - 1
src/SharpGLTF.Core/Runtime/DrawableTemplate.cs

@@ -3,6 +3,8 @@ using System.Collections.Generic;
 using System.Numerics;
 using System.Text;
 
+using TRANSFORM = SharpGLTF.Transforms.AffineTransform;
+
 namespace SharpGLTF.Runtime
 {
     public interface IDrawableTemplate
@@ -30,8 +32,10 @@ namespace SharpGLTF.Runtime
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly String _NodeName;
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly int _LogicalMeshIndex;
 
         #endregion
@@ -59,7 +63,7 @@ namespace SharpGLTF.Runtime
     /// <summary>
     /// Defines a reference to a drawable rigid mesh
     /// </summary>
-    sealed class RigidDrawableTemplate : DrawableTemplate
+    class RigidDrawableTemplate : DrawableTemplate
     {
         #region lifecycle
 
@@ -73,6 +77,7 @@ namespace SharpGLTF.Runtime
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly int _NodeIndex;
 
         #endregion
@@ -93,6 +98,51 @@ namespace SharpGLTF.Runtime
         #endregion
     }
 
+    class InstancedDrawableTemplate : RigidDrawableTemplate
+    {
+        #region lifecycle
+
+        internal InstancedDrawableTemplate(Schema2.Node node, Func<Schema2.Node, int> indexFunc)
+            : base(node, indexFunc)
+        {
+            var instancing = node.GetGpuInstancing();
+
+            _Instances = new TRANSFORM[instancing.Count];
+
+            for (int i = 0; i < _Instances.Length; ++i)
+            {
+                _Instances[i] = instancing.GetLocalTransform(i);
+            }
+        }
+
+        #endregion
+
+        #region data
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly TRANSFORM[] _Instances;
+
+        #endregion
+
+        #region properties
+
+        public IReadOnlyList<TRANSFORM> Instances => _Instances;
+
+        #endregion
+
+        #region API
+
+        public override Transforms.IGeometryTransform CreateGeometryTransform() { return new Transforms.InstancingTransform(_Instances); }
+
+        public override void UpdateGeometryTransform(Transforms.IGeometryTransform rigidTransform, ArmatureInstance armature)
+        {
+            base.UpdateGeometryTransform(rigidTransform, armature);
+            (rigidTransform as Transforms.InstancingTransform).UpdateInstances();
+        }
+
+        #endregion
+    }
+
     /// <summary>
     /// Defines a reference to a drawable skinned mesh
     /// </summary>

+ 9 - 9
src/SharpGLTF.Core/Runtime/MeshDecoder.Schema2.cs

@@ -93,9 +93,9 @@ namespace SharpGLTF.Runtime
     }
 
     [System.Diagnostics.DebuggerDisplay("{_GetDebugString(),nq}")]
-    sealed class _MeshPrimitiveDecoder<TMaterial>
-        : _MeshPrimitiveDecoder
-        , IMeshPrimitiveDecoder<TMaterial>
+    sealed class _MeshPrimitiveDecoder<TMaterial> :
+        _MeshPrimitiveDecoder,
+        IMeshPrimitiveDecoder<TMaterial>
         where TMaterial : class
     {
         #region lifecycle
@@ -345,9 +345,9 @@ namespace SharpGLTF.Runtime
     }
 
     [System.Diagnostics.DebuggerDisplay("Vertices: {VertexCount}")]
-    sealed class _MeshGeometryDecoder
-        : VertexNormalsFactory.IMeshPrimitive
-        , VertexTangentsFactory.IMeshPrimitive
+    sealed class _MeshGeometryDecoder :
+        VertexNormalsFactory.IMeshPrimitive,
+        VertexTangentsFactory.IMeshPrimitive
     {
         #region  lifecycle
 
@@ -423,9 +423,9 @@ namespace SharpGLTF.Runtime
     }
 
     [System.Diagnostics.DebuggerDisplay("Vertices: {VertexCount}")]
-    sealed class _MorphTargetDecoder
-        : VertexNormalsFactory.IMeshPrimitive
-        , VertexTangentsFactory.IMeshPrimitive
+    sealed class _MorphTargetDecoder :
+        VertexNormalsFactory.IMeshPrimitive,
+        VertexTangentsFactory.IMeshPrimitive
     {
         #region  lifecycle
 

+ 6 - 4
src/SharpGLTF.Core/Runtime/MeshDecoder.cs

@@ -313,7 +313,6 @@ namespace SharpGLTF.Runtime
             }
 
             return instance
-                .DrawableInstances
                 .Where(item => item.Transform.Visible)
                 .SelectMany(item => meshes[item.Template.LogicalMeshIndex].GetWorldVertices(item.Transform));
         }
@@ -324,11 +323,14 @@ namespace SharpGLTF.Runtime
             Guard.NotNull(mesh, nameof(mesh));
             Guard.NotNull(xform, nameof(xform));
 
-            foreach (var primitive in mesh.Primitives)
+            foreach (var childXform in Transforms.InstancingTransform.Evaluate(xform))
             {
-                for (int i = 0; i < primitive.VertexCount; ++i)
+                foreach (var primitive in mesh.Primitives)
                 {
-                    yield return primitive.GetPosition(i, xform);
+                    for (int i = 0; i < primitive.VertexCount; ++i)
+                    {
+                        yield return primitive.GetPosition(i, childXform);
+                    }
                 }
             }
         }

+ 17 - 16
src/SharpGLTF.Core/Runtime/NodeTemplate.cs

@@ -1,8 +1,8 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Numerics;
-using System.Text;
+
+using TRANSFORM = SharpGLTF.Transforms.AffineTransform;
 
 namespace SharpGLTF.Runtime
 {
@@ -24,12 +24,14 @@ namespace SharpGLTF.Runtime
             Name = srcNode.Name;
             Extras = RuntimeOptions.ConvertExtras(srcNode, options);
 
-            _LocalMatrix = srcNode.LocalMatrix;
             _LocalTransform = srcNode.LocalTransform;
 
-            _Scale = new AnimatableProperty<Vector3>(_LocalTransform.Scale);
-            _Rotation = new AnimatableProperty<Quaternion>(_LocalTransform.Rotation);
-            _Translation = new AnimatableProperty<Vector3>(_LocalTransform.Translation);
+            if (_LocalTransform.TryDecompose(out TRANSFORM lxform))
+            {
+                _Scale = new AnimatableProperty<Vector3>(lxform.Scale);
+                _Rotation = new AnimatableProperty<Quaternion>(lxform.Rotation);
+                _Translation = new AnimatableProperty<Vector3>(lxform.Translation);
+            }
 
             var mw = Transforms.SparseWeight8.Create(srcNode.MorphWeights);
             _Morphing = new AnimatableProperty<Transforms.SparseWeight8>(mw);
@@ -73,8 +75,7 @@ namespace SharpGLTF.Runtime
         private readonly int _ParentIndex;
         private readonly int[] _ChildIndices;
 
-        private readonly Matrix4x4 _LocalMatrix;
-        private readonly Transforms.AffineTransform _LocalTransform;
+        private readonly TRANSFORM _LocalTransform;
 
         private readonly bool _UseAnimatedTransforms;
         private readonly AnimatableProperty<Vector3> _Scale;
@@ -105,7 +106,7 @@ namespace SharpGLTF.Runtime
         /// </summary>
         public IReadOnlyList<int> ChildIndices => _ChildIndices;
 
-        public Matrix4x4 LocalMatrix => _LocalMatrix;
+        public Matrix4x4 LocalMatrix => _LocalTransform.Matrix;
 
         #endregion
 
@@ -132,7 +133,7 @@ namespace SharpGLTF.Runtime
             return Transforms.SparseWeight8.Blend(xforms, weight);
         }
 
-        public Transforms.AffineTransform GetLocalTransform(int trackLogicalIndex, float time)
+        public TRANSFORM GetLocalTransform(int trackLogicalIndex, float time)
         {
             if (!_UseAnimatedTransforms || trackLogicalIndex < 0) return _LocalTransform;
 
@@ -140,33 +141,33 @@ namespace SharpGLTF.Runtime
             var r = _Rotation?.GetValueAt(trackLogicalIndex, time);
             var t = _Translation?.GetValueAt(trackLogicalIndex, time);
 
-            return new Transforms.AffineTransform(s, r, t);
+            return new TRANSFORM(s, r, t);
         }
 
-        public Transforms.AffineTransform GetLocalTransform(ReadOnlySpan<int> track, ReadOnlySpan<float> time, ReadOnlySpan<float> weight)
+        public TRANSFORM GetLocalTransform(ReadOnlySpan<int> track, ReadOnlySpan<float> time, ReadOnlySpan<float> weight)
         {
             if (!_UseAnimatedTransforms) return _LocalTransform;
 
-            Span<Transforms.AffineTransform> xforms = stackalloc Transforms.AffineTransform[track.Length];
+            Span<TRANSFORM> xforms = stackalloc TRANSFORM[track.Length];
 
             for (int i = 0; i < xforms.Length; ++i)
             {
                 xforms[i] = GetLocalTransform(track[i], time[i]);
             }
 
-            return Transforms.AffineTransform.Blend(xforms, weight);
+            return TRANSFORM.Blend(xforms, weight);
         }
 
         public Matrix4x4 GetLocalMatrix(int trackLogicalIndex, float time)
         {
-            if (!_UseAnimatedTransforms || trackLogicalIndex < 0) return _LocalMatrix;
+            if (!_UseAnimatedTransforms || trackLogicalIndex < 0) return _LocalTransform.Matrix;
 
             return GetLocalTransform(trackLogicalIndex, time).Matrix;
         }
 
         public Matrix4x4 GetLocalMatrix(ReadOnlySpan<int> track, ReadOnlySpan<float> time, ReadOnlySpan<float> weight)
         {
-            if (!_UseAnimatedTransforms) return _LocalMatrix;
+            if (!_UseAnimatedTransforms) return _LocalTransform.Matrix;
 
             return GetLocalTransform(track, time, weight).Matrix;
         }

+ 18 - 1
src/SharpGLTF.Core/Runtime/RuntimeOptions.cs

@@ -4,6 +4,14 @@ using System.Text;
 
 namespace SharpGLTF.Runtime
 {
+    public enum MeshInstancing
+    {
+        Discard,
+        Enabled,
+        SingleMesh,
+        // TODO: add options to trim the number of instances
+    }
+
     public class RuntimeOptions
     {
         /// <summary>
@@ -11,8 +19,17 @@ namespace SharpGLTF.Runtime
         /// </summary>
         public bool IsolateMemory { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether GPU instancing is enabled or disabled.
+        /// </summary>
+        /// <remarks>
+        /// When true, if a gltf mesh has gpu instancing elements, they will be converted<br/>
+        /// internally to the runtime as <see cref="InstancedDrawableTemplate"/> elements.
+        /// </remarks>
+        public MeshInstancing GpuMeshInstancing { get; set; } = MeshInstancing.Enabled;
+
         /// <summary>
-        /// Custom extras converter.
+        /// Gets or sets the custom extras converter.
         /// </summary>
         public Converter<Schema2.ExtraProperties, Object> ExtrasConverterCallback { get; set; }
 

+ 38 - 18
src/SharpGLTF.Core/Runtime/SceneInstance.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
@@ -13,7 +14,7 @@ namespace SharpGLTF.Runtime
     /// <summary>
     /// Represents a specific and independent state of a <see cref="SceneTemplate"/>.
     /// </summary>
-    public sealed class SceneInstance
+    public sealed class SceneInstance : IReadOnlyList<DrawableInstance>
     {
         #region lifecycle
 
@@ -40,9 +41,13 @@ namespace SharpGLTF.Runtime
         /// <summary>
         /// Represents the skeleton that's going to be used by each drawing command to draw the model matrices.
         /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly ArmatureInstance _Armature;
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly DrawableTemplate[] _DrawableReferences;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly IGeometryTransform[] _DrawableTransforms;
 
         #endregion
@@ -51,24 +56,11 @@ namespace SharpGLTF.Runtime
 
         public ArmatureInstance Armature => _Armature;
 
-        /// <summary>
-        /// Gets the number of drawable instances.
-        /// </summary>
-        public int DrawableInstancesCount => _DrawableTransforms.Length;
+        /// <inheritdoc/>
+        public int Count => _DrawableTransforms.Length;
 
-        /// <summary>
-        /// Gets the current sequence of drawing commands.
-        /// </summary>
-        public IEnumerable<DrawableInstance> DrawableInstances
-        {
-            get
-            {
-                for (int i = 0; i < _DrawableReferences.Length; ++i)
-                {
-                    yield return GetDrawableInstance(i);
-                }
-            }
-        }
+        /// <inheritdoc/>
+        public DrawableInstance this[int index] => GetDrawableInstance(index);
 
         #endregion
 
@@ -91,6 +83,34 @@ namespace SharpGLTF.Runtime
             return new DrawableInstance(dref, _DrawableTransforms[index]);
         }
 
+        public IEnumerator<DrawableInstance> GetEnumerator()
+        {
+            for (int i = 0; i < Count; ++i) { yield return this[i]; }
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            for (int i = 0; i < Count; ++i) { yield return this[i]; }
+        }
+
+        #endregion
+
+        #region obsolete
+
+        [Obsolete("Use .Count", true)]
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        public int DrawableInstancesCount => Count;
+
+        [Obsolete("use <this>", true)]
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        public IEnumerable<DrawableInstance> DrawableInstances
+        {
+            get
+            {
+                for (int i = 0; i < Count; ++i) { yield return this[i]; }
+            }
+        }
+
         #endregion
     }
 }

+ 31 - 8
src/SharpGLTF.Core/Runtime/SceneTemplate.cs

@@ -25,6 +25,8 @@ namespace SharpGLTF.Runtime
         {
             Guard.NotNull(srcScene, nameof(srcScene));
 
+            if (options == null) options = new RuntimeOptions();
+
             var armature = ArmatureTemplate.Create(srcScene, options);
 
             // gather scene nodes.
@@ -45,22 +47,43 @@ namespace SharpGLTF.Runtime
                 .Where(item => item.Mesh != null)
                 .ToList();
 
-            var drawables = new DrawableTemplate[instances.Count];
+            var drawables = new List<DrawableTemplate>();
 
-            for (int i = 0; i < drawables.Length; ++i)
+            for (int i = 0; i < instances.Count; ++i)
             {
                 var srcInstance = instances[i];
 
-                drawables[i] = srcInstance.Skin != null
-                    ?
-                    new SkinnedDrawableTemplate(srcInstance, indexSolver)
-                    :
-                    (DrawableTemplate)new RigidDrawableTemplate(srcInstance, indexSolver);
+                if (srcInstance.Skin != null)
+                {
+                    drawables.Add(new SkinnedDrawableTemplate(srcInstance, indexSolver));
+                    continue;
+                }
+
+                if (srcInstance.GetGpuInstancing() == null)
+                {
+                    drawables.Add(new RigidDrawableTemplate(srcInstance, indexSolver));
+                    continue;
+                }
+
+                switch (options.GpuMeshInstancing)
+                {
+                    case MeshInstancing.Discard: break;
+
+                    case MeshInstancing.Enabled:
+                        drawables.Add(new InstancedDrawableTemplate(srcInstance, indexSolver));
+                        break;
+
+                    case MeshInstancing.SingleMesh:
+                        drawables.Add(new RigidDrawableTemplate(srcInstance, indexSolver));
+                        break;
+
+                    default: throw new NotImplementedException();
+                }
             }
 
             var extras = RuntimeOptions.ConvertExtras(srcScene, options);
 
-            return new SceneTemplate(srcScene.Name, extras, armature, drawables);
+            return new SceneTemplate(srcScene.Name, extras, armature, drawables.ToArray());
         }
 
         private SceneTemplate(string name, Object extras, ArmatureTemplate armature, DrawableTemplate[] drawables)

+ 56 - 0
src/SharpGLTF.Core/Schema2/Generated/ext.MeshGpuInstancing.g.cs

@@ -0,0 +1,56 @@
+// <auto-generated/>
+
+//------------------------------------------------------------------------------------------------
+//      This file has been programatically generated; DON´T EDIT!
+//------------------------------------------------------------------------------------------------
+
+#pragma warning disable SA1001
+#pragma warning disable SA1027
+#pragma warning disable SA1028
+#pragma warning disable SA1121
+#pragma warning disable SA1205
+#pragma warning disable SA1309
+#pragma warning disable SA1402
+#pragma warning disable SA1505
+#pragma warning disable SA1507
+#pragma warning disable SA1508
+#pragma warning disable SA1652
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Numerics;
+using System.Text.Json;
+
+namespace SharpGLTF.Schema2
+{
+	using Collections;
+
+	/// <summary>
+	/// glTF extension defines instance attributes for a node with a mesh.
+	/// </summary>
+	partial class MeshGpuInstancing : ExtraProperties
+	{
+	
+		private Dictionary<String,Int32> _attributes;
+		
+	
+		protected override void SerializeProperties(Utf8JsonWriter writer)
+		{
+			base.SerializeProperties(writer);
+			SerializeProperty(writer, "attributes", _attributes);
+		}
+	
+		protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader)
+		{
+			switch (jsonPropertyName)
+			{
+				case "attributes": DeserializePropertyDictionary<Int32>(ref reader, _attributes); break;
+				default: base.DeserializeProperty(jsonPropertyName,ref reader); break;
+			}
+		}
+	
+	}
+
+}

+ 43 - 5
src/SharpGLTF.Core/Schema2/gltf.Accessors.cs

@@ -2,8 +2,8 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Numerics;
+
 using SharpGLTF.Memory;
-using SharpGLTF.Validation;
 
 using VALIDATIONCTX = SharpGLTF.Validation.ValidationContext;
 
@@ -34,6 +34,15 @@ namespace SharpGLTF.Schema2
 
         #endregion
 
+        #region data
+
+        /// <summary>
+        /// This must be null, or always in sync with <see cref="_type"/>
+        /// </summary>
+        private DimensionType? _CachedType;
+
+        #endregion
+
         #region properties
 
         internal int _SourceBufferViewIndex => this._bufferView.AsValue(-1);
@@ -86,7 +95,19 @@ namespace SharpGLTF.Schema2
 
         private DimensionType _GetDimensions()
         {
-            return Enum.TryParse<DimensionType>(this._type, out var r) ? r : DimensionType.CUSTOM;
+            if (_CachedType.HasValue)
+            {
+                #if DEBUG
+                var parsedType = Enum.TryParse<DimensionType>(this._type, out var rr) ? rr : DimensionType.CUSTOM;
+                System.Diagnostics.Debug.Assert(_CachedType.Value == parsedType);
+                #endif
+
+                return _CachedType.Value;
+            }
+
+            _CachedType = Enum.TryParse<DimensionType>(this._type, out var r) ? r : DimensionType.CUSTOM;
+
+            return _CachedType.Value;
         }
 
         internal MemoryAccessor _GetMemoryAccessor(string name = null)
@@ -170,24 +191,37 @@ namespace SharpGLTF.Schema2
             this._byteOffset = bufferByteOffset.AsNullable(_byteOffsetDefault, _byteOffsetMinimum, int.MaxValue);
             this._count = itemCount;
 
+            this._CachedType = dimensions;
             this._type = Enum.GetName(typeof(DimensionType), dimensions);
+
             this._componentType = encoding;
             this._normalized = normalized.AsNullable(_normalizedDefault);
 
             UpdateBounds();
         }
 
-        public Matrix2x2Array AsMatrix2x2Array()
+        public IList<Matrix3x2> AsMatrix2x2Array()
         {
             return _GetMemoryAccessor().AsMatrix2x2Array();
         }
 
-        public Matrix3x3Array AsMatrix3x3Array()
+        public IList<Matrix4x4> AsMatrix3x3Array()
         {
             return _GetMemoryAccessor().AsMatrix3x3Array();
         }
 
-        public Matrix4x4Array AsMatrix4x4Array()
+        public IList<Matrix4x4> AsMatrix4x3Array()
+        {
+            const int dimsize = 4 * 3;
+
+            var view = SourceBufferView;
+            var stride = Math.Max(dimsize * this.Encoding.ByteLength(), view.ByteStride);
+            var content = view.Content.Slice(this.ByteOffset, Count * stride);
+
+            return new Matrix4x3Array(content, stride, this.Encoding, this.Normalized);
+        }
+
+        public IList<Matrix4x4> AsMatrix4x4Array()
         {
             return _GetMemoryAccessor().AsMatrix4x4Array();
         }
@@ -358,6 +392,10 @@ namespace SharpGLTF.Schema2
         {
             base.OnValidateContent(validate);
 
+            // if Accessor.Type uses a custom dimension,
+            // we cannot check the rest of the accessor.
+            if (this.Dimensions == DimensionType.CUSTOM) return;
+
             BufferView.VerifyAccess(validate, this.SourceBufferView, this.ByteOffset, this.Format, this.Count);
 
             validate.That(() => MemoryAccessor.VerifyAccessorBounds(_GetMemoryAccessor(), _min, _max));

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

@@ -26,6 +26,7 @@ namespace SharpGLTF.Schema2
             RegisterExtension<ModelRoot, _ModelPunctualLights>("KHR_lights_punctual");
 
             RegisterExtension<Node, _NodePunctualLight>("KHR_lights_punctual");
+            RegisterExtension<Node, MeshGpuInstancing>("EXT_mesh_gpu_instancing");
 
             RegisterExtension<Material, MaterialUnlit>("KHR_materials_unlit");
             RegisterExtension<Material, MaterialSheen>("KHR_materials_sheen");

+ 147 - 0
src/SharpGLTF.Core/Schema2/gltf.Node.MeshInstancing.cs

@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+using SharpGLTF.Collections;
+
+using TRANSFORM = SharpGLTF.Transforms.AffineTransform;
+
+namespace SharpGLTF.Schema2
+{
+    [System.Diagnostics.DebuggerDisplay("{Count}")]
+    public partial class MeshGpuInstancing
+    {
+        #region lifecycle
+        internal MeshGpuInstancing(Node node)
+        {
+            _Owner = node;
+            _attributes = new Dictionary<string, int>();
+        }
+
+        #endregion
+
+        #region data (not serializable)
+
+        private readonly Node _Owner;
+
+        #endregion
+
+        #region properties
+
+        public Node LogicalParent => _Owner;
+        public Node VisualParent => _Owner;
+
+        /// <summary>
+        /// Gets a value indicating the number of instances to draw.
+        /// </summary>
+        public int Count => _GetCount();
+
+        public IReadOnlyDictionary<string, Accessor> Accessors => _GetAccessors();
+
+        public IEnumerable<TRANSFORM> LocalTransforms => _GetLocalTransforms();
+
+        #endregion
+
+        #region API
+
+        private int _GetCount()
+        {
+            return _attributes.Count == 0
+                ? 0
+                : _attributes.Values
+                .Select(item => _Owner.LogicalParent.LogicalAccessors[item].Count)
+                .Min();
+        }
+
+        private IReadOnlyDictionary<string, Accessor> _GetAccessors()
+        {
+            return new ReadOnlyLinqDictionary<String, int, Accessor>(_attributes, alidx => this.LogicalParent.LogicalParent.LogicalAccessors[alidx]);
+        }
+
+        private IEnumerable<TRANSFORM> _GetLocalTransforms()
+        {
+            int c = _GetCount();
+            for (int i = 0; i < c; ++i) yield return GetLocalTransform(i);
+        }
+
+        public void ClearAccessors()
+        {
+            _attributes.Clear();
+        }
+
+        public Accessor GetAccessor(string attributeKey)
+        {
+            Guard.NotNullOrEmpty(attributeKey, nameof(attributeKey));
+
+            if (!_attributes.TryGetValue(attributeKey, out int idx)) return null;
+
+            return _Owner.LogicalParent.LogicalAccessors[idx];
+        }
+
+        public void SetAccessor(string attributeKey, Accessor accessor)
+        {
+            Guard.NotNullOrEmpty(attributeKey, nameof(attributeKey));
+
+            if (accessor != null)
+            {
+                Guard.MustShareLogicalParent(_Owner.LogicalParent, nameof(_Owner.LogicalParent), accessor, nameof(accessor));
+                if (_attributes.Count > 0) Guard.MustBeEqualTo(Count, accessor.Count, nameof(accessor));
+
+                _attributes[attributeKey] = accessor.LogicalIndex;
+            }
+            else
+            {
+                _attributes.Remove(attributeKey);
+            }
+        }
+
+        public TRANSFORM GetLocalTransform(int index)
+        {
+            // var m = GetAccessor("TRANSFORM")?.AsMatrix4x4Array()?[index];
+            var s = GetAccessor("SCALE")?.AsVector3Array()?[index];
+            var r = GetAccessor("ROTATION")?.AsQuaternionArray()?[index];
+            var t = GetAccessor("TRANSLATION")?.AsVector3Array()?[index];
+
+            return TRANSFORM.CreateFromAny(null, s, r, t);
+        }
+
+        public Matrix4x4 GetLocalMatrix(int index)
+        {
+            return GetLocalTransform(index).Matrix;
+        }
+
+        public Matrix4x4 GetWorldMatrix(int index)
+        {
+            return GetLocalMatrix(index) * _Owner.WorldMatrix;
+        }
+
+        #endregion
+    }
+
+    partial class Node
+    {
+        public MeshGpuInstancing GetGpuInstancing()
+        {
+            return this.GetExtension<MeshGpuInstancing>();
+        }
+
+        public MeshGpuInstancing UseGpuInstancing()
+        {
+            var ext = GetGpuInstancing();
+            if (ext == null)
+            {
+                ext = new MeshGpuInstancing(this);
+                this.SetExtension(ext);
+            }
+
+            return ext;
+        }
+
+        public void RemoveGpuInstancing()
+        {
+            this.RemoveExtensions<MeshGpuInstancing>();
+        }
+    }
+}

+ 74 - 52
src/SharpGLTF.Core/Schema2/gltf.Node.cs

@@ -3,6 +3,8 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Numerics;
 
+using TRANSFORM = SharpGLTF.Transforms.AffineTransform;
+
 namespace SharpGLTF.Schema2
 {
     /// <summary>
@@ -25,34 +27,33 @@ namespace SharpGLTF.Schema2
         {
             var txt = $"Node[{LogicalIndex}ᴵᵈˣ]";
 
-            if (!string.IsNullOrWhiteSpace(this.Name)) txt += $" {this.Name}";
+            if (!string.IsNullOrWhiteSpace(this.Name)) txt += $" \"{this.Name}\" ";
 
-            if (_matrix.HasValue)
-            {
-                if (_matrix.Value != Matrix4x4.Identity)
-                {
-                    var xform = this.LocalTransform;
-                    if (xform.Scale != Vector3.One) txt += $" 𝐒:{xform.Scale}";
-                    if (xform.Rotation != Quaternion.Identity) txt += $" 𝐑:{xform.Rotation}";
-                    if (xform.Translation != Vector3.Zero) txt += $" 𝚻:{xform.Translation}";
-                }
-            }
-            else
+            if (this.Mesh != null)
             {
-                if (_scale.HasValue) txt += $" 𝐒:{_scale.Value}";
-                if (_rotation.HasValue) txt += $" 𝐑:{_rotation.Value}";
-                if (_translation.HasValue) txt += $" 𝚻:{_translation.Value}";
+                if (this.Skin != null) txt += $" / Skin[{this.Skin.LogicalIndex}ᴵᵈˣ]";
+                txt += $" / Mesh[{this.Mesh.LogicalIndex}ᴵᵈˣ]";
+
+                var instances = this.GetGpuInstancing();
+                if (instances != null) txt += $" x {instances.Count} instances.";
             }
 
-            if (this.Mesh != null)
+            if (this.VisualChildren.Any())
             {
-                if (this.Skin != null) txt += $" ⇨ Skin[{this.Skin.LogicalIndex}ᴵᵈˣ]";
-                txt += $" ⇨ Mesh[{this.Mesh.LogicalIndex}ᴵᵈˣ]";
+                if (this.VisualChildren.Count() < 16)
+                {
+                    var indices = string.Join(", ", this.VisualChildren.Select(item => item.LogicalIndex));
+                    txt += $" / Children[{indices}]";
+                }
+                else
+                {
+                    txt += $" / Children x {this.VisualChildren.Count()}";
+                }
             }
 
-            if (this.VisualChildren.Any())
+            if (!this.LocalTransform.IsIdentity)
             {
-                txt += $" | Children[{this.VisualChildren.Count()}]";
+                txt += " At " + this.LocalTransform.ToDebuggerDisplayString();
             }
 
             return txt;
@@ -189,53 +190,51 @@ namespace SharpGLTF.Schema2
         /// <summary>
         /// Gets or sets the local Scale, Rotation and Translation of this <see cref="Node"/>.
         /// </summary>
-        public Transforms.AffineTransform LocalTransform
+        public TRANSFORM LocalTransform
         {
-            get => Transforms.AffineTransform.CreateFromAny(_matrix, _scale, _rotation, _translation);
+            get => TRANSFORM.CreateFromAny(_matrix, _scale, _rotation, _translation);
             set
             {
                 Guard.IsFalse(this._skin.HasValue, _NOTRANSFORMMESSAGE);
                 Guard.IsTrue(value.IsValid, nameof(value));
 
-                var decomposed = value.GetDecomposed();
-
-                _matrix = null;
-                _scale = decomposed.Scale.AsNullable(Vector3.One);
-                _rotation = decomposed.Rotation.Sanitized().AsNullable(Quaternion.Identity);
-                _translation = decomposed.Translation.AsNullable(Vector3.Zero);
-            }
-        }
-
-        #pragma warning restore CA1721 // Property names should not match get methods
+                if (value.IsMatrix && this.IsTransformAnimated)
+                {
+                    value = value.GetDecomposed(); // animated nodes require a decomposed transform.
+                }
 
-        /// <summary>
-        /// Gets or sets the local transform <see cref="Matrix4x4"/> of this <see cref="Node"/>.
-        /// </summary>
-        public Matrix4x4 LocalMatrix
-        {
-            get => Transforms.Matrix4x4Factory.CreateFrom(_matrix, _scale, _rotation, _translation);
-            set
-            {
-                if (value == Matrix4x4.Identity)
+                if (value.IsMatrix)
+                {
+                    _matrix = value.Matrix.AsNullable(Matrix4x4.Identity);
+                    _scale = null;
+                    _rotation = null;
+                    _translation = null;
+                }
+                else if (value.IsSRT)
                 {
                     _matrix = null;
+                    _scale = value.Scale.AsNullable(Vector3.One);
+                    _rotation = value.Rotation.Sanitized().AsNullable(Quaternion.Identity);
+                    _translation = value.Translation.AsNullable(Vector3.Zero);
                 }
                 else
                 {
-                    Guard.IsFalse(this._skin.HasValue, _NOTRANSFORMMESSAGE);
-                    _matrix = value;
+                    throw new ArgumentException("Undefined", nameof(value));
                 }
-
-                _scale = null;
-                _rotation = null;
-                _translation = null;
             }
         }
 
-        #pragma warning disable CA1721 // Property names should not match get methods
+        /// <summary>
+        /// Gets or sets the local transform <see cref="Matrix4x4"/> of this <see cref="Node"/>.
+        /// </summary>
+        public Matrix4x4 LocalMatrix
+        {
+            get => LocalTransform.Matrix;
+            set => LocalTransform = value;
+        }
 
         /// <summary>
-        /// Gets or sets the world transform <see cref="Matrix4x4"/> of this <see cref="Node"/>.
+        /// Gets or sets the world <see cref="Matrix4x4"/> of this <see cref="Node"/>.
         /// </summary>
         public Matrix4x4 WorldMatrix
         {
@@ -251,6 +250,16 @@ namespace SharpGLTF.Schema2
             }
         }
 
+        /// <summary>
+        /// Gets the local <see cref="Transforms.Matrix4x4Double"/> of this <see cref="Node"/>.
+        /// </summary>
+        /// <remarks>
+        /// <para>
+        /// This method is equivalent to <see cref="WorldMatrix"/>, but since the world matrix<br/>
+        /// is calculated by concatenating all the local matrices in the hierarchy, there's chances<br/>
+        /// to have some precission loss on large transform chains.
+        /// </para>
+        /// </remarks>
         internal Transforms.Matrix4x4Double LocalMatrixPrecise
         {
             get
@@ -270,6 +279,19 @@ namespace SharpGLTF.Schema2
             }
         }
 
+        /// <summary>
+        /// Gets the world <see cref="Transforms.Matrix4x4Double"/> of this <see cref="Node"/>.
+        /// </summary>
+        /// <remarks>
+        /// <para>
+        /// This method is equivalent to <see cref="WorldMatrix"/>, but since the world matrix<br/>
+        /// is calculated by concatenating all the local matrices in the hierarchy, there's chances<br/>
+        /// to have some precission loss on large transform chains.
+        /// </para>
+        /// <para>
+        /// Precission is specially relevant when calculating the Inverse Bind Matrix.
+        /// </para>
+        /// </remarks>
         internal Transforms.Matrix4x4Double WorldMatrixPrecise
         {
             get
@@ -326,7 +348,7 @@ namespace SharpGLTF.Schema2
 
         #region API - Transforms
 
-        public Transforms.AffineTransform GetLocalTransform(Animation animation, float time)
+        public TRANSFORM GetLocalTransform(Animation animation, float time)
         {
             if (animation == null) return this.LocalTransform;
 
@@ -812,7 +834,7 @@ namespace SharpGLTF.Schema2
 
         #region API
 
-        public Transforms.AffineTransform GetLocalTransform(Single time)
+        public TRANSFORM GetLocalTransform(Single time)
         {
             var xform = TargetNode.LocalTransform.GetDecomposed();
 
@@ -820,7 +842,7 @@ namespace SharpGLTF.Schema2
             var r = Rotation?.CreateCurveSampler()?.GetPoint(time) ?? xform.Rotation;
             var t = Translation?.CreateCurveSampler()?.GetPoint(time) ?? xform.Translation;
 
-            return new Transforms.AffineTransform(s, r, t);
+            return new TRANSFORM(s, r, t);
         }
 
         public IReadOnlyList<float> GetMorphingWeights(Single time)

+ 314 - 98
src/SharpGLTF.Core/Transforms/AffineTransform.cs

@@ -6,49 +6,92 @@ using System.Text;
 namespace SharpGLTF.Transforms
 {
     /// <summary>
-    /// Represents an affine transform in 3D space, with two exclusive representantions:<br/>
+    /// Represents an affine transform in 3D space, with two mutually exclusive representantions:<br/>
     /// <list type="bullet">
     /// <item>
-    /// When <see cref="IsMatrix"/> is true, A 4x3 Matrix. which is publicly<br/>
-    /// exposed as a <see cref="Matrix4x4"/> matrix.
+    /// As a 4x3 Matrix. When <see cref="IsMatrix"/> is true.<br/>
+    /// Publicly exposed as <see cref="Matrix"/>.
     /// </item>
     /// <item>
-    /// When <see cref="IsDecomposed"/> is true, A decomposed transform defined by:<br/>
-    /// <see cref="Vector3"/> Scale.<br/>
-    /// <see cref="Quaternion"/> Rotation.<br/>
-    /// <see cref="Vector3"/> Translation.
+    /// As a Scale/Rotation/Translation chain. When <see cref="IsSRT"/> is true.<br/>
+    /// Publicly exposed as: <see cref="Scale"/>, <see cref="Rotation"/>, <see cref="Translation"/>.
     /// </item>
     /// </list>
     /// </summary>
     /// <remarks>
     /// <para>
     /// Depending on how <see cref="AffineTransform"/> structures are created, the underlaying<br/>
-    /// fields must be interprested as a Matrix4x3 or a decomposed sequence of Scale, Rotation and<br/>
-    /// Translation.
+    /// fields must be interprested as a Matrix4x3 or a Scale/Rotation/Translation chain.
     /// </para>
     /// <para>
-    /// This approach allows <see cref="AffineTransform"/> to preserve the source data, avoiding loosing<br/>
-    /// precission when decomposing a matrix, or creating a matrix from a SRT transform.
+    /// This approach allows <see cref="AffineTransform"/> preserving the source transform, avoiding loosing<br/>
+    /// precission when decomposing a matrix, or creating a matrix from a SRT chain.
     /// </para>
     /// <para>
     /// Decomposing matrices is tricky because not all valid matrices can be decomposed; in particular<br/>
     /// squewed matrices will fail to decompose. See <see href="https://github.com/vpenades/SharpGLTF/issues/41"/>.
     /// </para>
     /// </remarks>
-    [System.Diagnostics.DebuggerDisplay("AffineTransform 𝐒:{Scale} 𝐑:{Rotation} 𝚻:{Translation}")]
-    public readonly struct AffineTransform
+    [System.Diagnostics.DebuggerDisplay("AffineTransform {ToDebuggerDisplayString(),nq}")]
+    public readonly struct AffineTransform : IEquatable<AffineTransform>
     {
+        #region diagnostics
+
+        internal string ToDebuggerDisplayString()
+        {
+            if (!IsValid) return "INVALID";
+
+            if (IsIdentity) return "IDENTITY";
+
+            if (TryDecompose(out var decomposed))
+            {
+                var s = decomposed._GetScale();
+                var r = decomposed._GetRotation();
+
+                var txt = string.Empty;
+
+                var ss = Vector3.Max(Vector3.Zero, s - Vector3.One);
+                var hasScale = ss.X > 0.000001f || ss.Y > 0.000001f || ss.Z > 0.000001f;
+
+                if (hasScale) txt += $"𝐒:{s} ";
+                if (r != Quaternion.Identity) txt += $"𝐑:{r} ";
+                if (decomposed.Translation != Vector3.Zero) txt += $"𝚻:{decomposed.Translation} ";
+
+                return txt;
+            }
+            else
+            {
+                return "Skewed Matrix ";
+            }
+        }
+
+        #endregion
+
         #region constants
 
-        private const string _CannotDecomposeMessage = "Matrix is invalid or skewed.";
+        private const string _CannotDecomposeError = "Matrix is invalid or skewed.";
+
+        private const string _RequiresSRTError = "Needs to be in SRT representation. Call GetDecomposed() first.";
 
         public static readonly AffineTransform Identity = new AffineTransform(null, null, null);
 
+        private const int DATA_UNDEFINED = 0;
+        private const int DATA_SRT = 1;
+        private const int DATA_MAT = 2;
+
         #endregion
 
         #region factories
 
-        public static implicit operator AffineTransform(Matrix4x4 matrix) { return new AffineTransform(matrix); }
+        public static implicit operator AffineTransform((Quaternion r, Vector3 t) xform)
+        {
+            return new AffineTransform(null, xform.r, xform.t);
+        }
+
+        public static implicit operator AffineTransform(Matrix4x4 matrix)
+        {
+            return new AffineTransform(matrix);
+        }
 
         public static AffineTransform CreateDecomposed(Matrix4x4 matrix)
         {
@@ -61,6 +104,9 @@ namespace SharpGLTF.Transforms
         {
             if (matrix.HasValue)
             {
+                Guard.MustBeNull(scale, nameof(scale));
+                Guard.MustBeNull(scale, nameof(rotation));
+                Guard.MustBeNull(scale, nameof(translation));
                 return new AffineTransform(matrix.Value);
             }
             else
@@ -71,31 +117,74 @@ namespace SharpGLTF.Transforms
 
         public AffineTransform WithScale(Vector3 scale)
         {
-            return new AffineTransform(scale, this.Rotation, this.Translation);
+            if (_Representation == DATA_UNDEFINED) return new AffineTransform(scale, null, null);
+
+            var tmp = this;
+            if (tmp.IsMatrix) tmp = tmp.GetDecomposed();
+            return new AffineTransform(scale, tmp.Rotation, tmp.Translation);
         }
 
         public AffineTransform WithRotation(Quaternion rotation)
         {
-            return new AffineTransform(this.Scale, rotation, this.Translation);
+            if (_Representation == DATA_UNDEFINED) return new AffineTransform(null, rotation, null);
+
+            var tmp = this;
+            if (tmp.IsMatrix) tmp = tmp.GetDecomposed();
+            return new AffineTransform(tmp.Scale, rotation, tmp.Translation);
         }
 
         public AffineTransform WithTranslation(Vector3 translation)
         {
-            return new AffineTransform(this.Scale, this.Rotation, translation);
+            if (_Representation == DATA_UNDEFINED) return new AffineTransform(null, null, translation);
+
+            if (this.IsSRT) return new AffineTransform(this.Scale, this.Rotation, translation);
+            var tmp = this.Matrix;
+            tmp.Translation = translation;
+            return tmp;
         }
 
         #endregion
 
         #region constructors
 
+        public AffineTransform(Vector3? scale, Quaternion? rotation, Vector3? translation)
+            : this(scale ?? Vector3.One, rotation ?? Quaternion.Identity, translation ?? Vector3.Zero)
+        { }
+
+        public AffineTransform(Vector3 scale, Quaternion rotation, Vector3 translation)
+        {
+            rotation = rotation.Sanitized();
+
+            Guard.IsTrue(scale._IsFinite(), nameof(scale));
+            Guard.IsTrue(rotation._IsFinite(), nameof(rotation));
+            Guard.IsTrue(translation._IsFinite(), nameof(translation));
+
+            _Representation = DATA_SRT;
+
+            _M11 = scale.X;
+            _M12 = scale.Y;
+            _M13 = scale.Z;
+
+            _M21 = rotation.X;
+            _M22 = rotation.Y;
+            _M23 = rotation.Z;
+            _M31 = rotation.W;
+
+            _M32 = 0; // unused
+            _M33 = 0; // unused
+
+            _Translation = translation;
+        }
+
         public AffineTransform(Matrix4x4 matrix)
         {
-            if (matrix.M14 != 0) throw new ArgumentException(nameof(matrix));
-            if (matrix.M24 != 0) throw new ArgumentException(nameof(matrix));
-            if (matrix.M34 != 0) throw new ArgumentException(nameof(matrix));
-            if (matrix.M44 != 1) throw new ArgumentException(nameof(matrix));
+            Guard.IsTrue(matrix._IsFinite(), nameof(matrix));
+            Guard.MustBeEqualTo(matrix.M14, 0, nameof(matrix.M14));
+            Guard.MustBeEqualTo(matrix.M24, 0, nameof(matrix.M24));
+            Guard.MustBeEqualTo(matrix.M34, 0, nameof(matrix.M34));
+            Guard.MustBeEqualTo(matrix.M44, 1, nameof(matrix.M44));
 
-            _Representation = 0;
+            _Representation = DATA_MAT;
 
             _M11 = matrix.M11;
             _M12 = matrix.M12;
@@ -112,111 +201,161 @@ namespace SharpGLTF.Transforms
             _Translation = matrix.Translation;
         }
 
-        public AffineTransform(Vector3? scale, Quaternion? rotation, Vector3? translation)
-            : this(scale ?? Vector3.One, rotation ?? Quaternion.Identity, translation ?? Vector3.Zero)
-        { }
-
-        public AffineTransform(Vector3 scale, Quaternion rotation, Vector3 translation)
-        {
-            _Representation = 1;
-
-            _M11 = scale.X;
-            _M12 = scale.Y;
-            _M13 = scale.Z;
-
-            _M21 = rotation.X;
-            _M22 = rotation.Y;
-            _M23 = rotation.Z;
-            _M31 = rotation.W;
-            _M32 = 0;
-            _M33 = 0;
-
-            this._Translation = translation;
-        }
-
         #endregion
 
         #region data
 
         /// <summary>
-        /// Determines the underlaying representation:<br/>
-        /// 0 - Fields must be interpreted as a Matrix4x3.<br/>
-        /// 1 - Fields must be interpreted as a Scale, Rotation and Translation sequence.
+        /// Determines what's represented by the data fields:<br/>
+        /// <list type="bullet">
+        /// <item><see cref="DATA_UNDEFINED"/> - Not defined.</item>
+        /// <item><see cref="DATA_MAT"/> - Fields must be interpreted as a 4x3 Matrix.</item>
+        /// <item><see cref="DATA_SRT"/> - Fields must be interpreted as a Scale, Rotation and Translation chain.</item>
+        /// </list>
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly Int32 _Representation;
 
         /// <summary>
         /// Matrix:  M11<br/>
-        /// Decomposed: Scale.X
+        /// SRT: Scale.X
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly float _M11;
 
         /// <summary>
         /// Matrix:  M12<br/>
-        /// Decomposed: Scale.Y
+        /// SRT: Scale.Y
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly float _M12;
 
         /// <summary>
         /// Matrix:  M13<br/>
-        /// Decomposed: Scale.Z
+        /// SRT: Scale.Z
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly float _M13;
 
         /// <summary>
         /// Matrix:  M21<br/>
-        /// Decomposed: Rotation.X
+        /// SRT: Rotation.X
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly float _M21;
 
         /// <summary>
         /// Matrix:  M22<br/>
-        /// Decomposed: Rotation.Y
+        /// SRT: Rotation.Y
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly float _M22;
 
         /// <summary>
         /// Matrix:  M23<br/>
-        /// Decomposed: Rotation.Z
+        /// SRT: Rotation.Z
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly float _M23;
 
         /// <summary>
         /// Matrix:  M31<br/>
-        /// Decomposed: Rotation.W
+        /// SRT: Rotation.W
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly float _M31;
 
         /// <summary>
         /// Matrix:  M32<br/>
-        /// Decomposed: unused
+        /// SRT: unused
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly float _M32;
 
         /// <summary>
         /// Matrix:  M32<br/>
-        /// Decomposed: unused
+        /// SRT: unused
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly float _M33;
 
+        /// <summary>
+        /// Matrix and SRT: Translation
+        /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly Vector3 _Translation;
 
+        public override int GetHashCode()
+        {
+            // we can only use the translation as hash code because it's the only value that
+            // is the same on SRT and Matrix representations.... otherwhise we would have
+            // to take the hash code of the matrix representation.
+            return _Translation.GetHashCode();
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is AffineTransform other && this.Equals(other);
+        }
+
+        public bool Equals(AffineTransform other)
+        {
+            if (this.IsSRT && other.IsSRT)
+            {
+                if (this._Translation != other._Translation) return false;
+                if (this._M11 != other._M11) return false;
+                if (this._M12 != other._M12) return false;
+                if (this._M13 != other._M13) return false;
+
+                if (this._M21 != other._M21) return false;
+                if (this._M22 != other._M22) return false;
+                if (this._M23 != other._M23) return false;
+                if (this._M31 != other._M31) return false;
+
+                System.Diagnostics.Debug.Assert(this._M32 == other._M32);
+                System.Diagnostics.Debug.Assert(this._M33 == other._M33);
+            }
+
+            return this.Matrix.Equals(other.Matrix);
+        }
+
         #endregion
 
         #region properties
-        public bool IsMatrix => _Representation == 0;
-        public bool IsDecomposed => _Representation == 1;
+
+        public bool IsValid
+        {
+            get
+            {
+                if (_Representation == DATA_UNDEFINED) return false;
+
+                if (!Translation._IsFinite()) return false;
+
+                if (!_M11._IsFinite()) return false;
+                if (!_M12._IsFinite()) return false;
+                if (!_M13._IsFinite()) return false;
+
+                if (!_M21._IsFinite()) return false;
+                if (!_M22._IsFinite()) return false;
+                if (!_M23._IsFinite()) return false;
+
+                if (!_M31._IsFinite()) return false;
+                if (!_M32._IsFinite()) return false;
+                if (!_M33._IsFinite()) return false;
+
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether this <see cref="AffineTransform"/> represents a <see cref="Matrix4x4"/>.
+        /// </summary>
+        public bool IsMatrix => _Representation == DATA_MAT;
+
+        /// <summary>
+        /// Gets a value indicating whether this <see cref="AffineTransform"/> represents a SRT chain.
+        /// </summary>
+        public bool IsSRT => _Representation == DATA_SRT;
 
         /// <summary>
         /// Gets the scale.
@@ -244,23 +383,31 @@ namespace SharpGLTF.Transforms
         /// </summary>
         public Matrix4x4 Matrix => _GetMatrix();
 
-        public bool IsValid
+        /// <summary>
+        /// Gets a value indicating whether this transform can be decomposed to SRT without precission loss.
+        /// </summary>
+        public bool IsLosslessDecomposable
         {
             get
             {
-                if (!Translation._IsFinite()) return false;
+                _VerifyDefined();
 
-                if (!_M11._IsFinite()) return false;
-                if (!_M12._IsFinite()) return false;
-                if (!_M13._IsFinite()) return false;
+                if (IsSRT) return true;
 
-                if (!_M21._IsFinite()) return false;
-                if (!_M22._IsFinite()) return false;
-                if (!_M23._IsFinite()) return false;
+                // row 1
+                if (_M11 != 0) return false;
+                if (_M12 == 0) return false;
+                if (_M13 == 0) return false;
 
-                if (!_M31._IsFinite()) return false;
-                if (!_M32._IsFinite()) return false;
-                if (!_M33._IsFinite()) return false;
+                // row 2
+                if (_M21 == 0) return false;
+                if (_M22 != 0) return false;
+                if (_M23 == 0) return false;
+
+                // row 3
+                if (_M31 == 0) return false;
+                if (_M32 == 0) return false;
+                if (_M33 != 0) return false;
 
                 return true;
             }
@@ -270,10 +417,10 @@ namespace SharpGLTF.Transforms
         {
             get
             {
-                if (Translation != Vector3.Zero) return false;
-
-                if (IsDecomposed)
+                if (IsSRT)
                 {
+                    if (Translation != Vector3.Zero) return false;
+
                     // scale
                     if (_M11 != 1) return false;
                     if (_M12 != 1) return false;
@@ -285,8 +432,10 @@ namespace SharpGLTF.Transforms
                     if (_M23 != 0) return false;
                     if (_M31 != 1) return false;
                 }
-                else
+                else if (IsMatrix)
                 {
+                    if (Translation != Vector3.Zero) return false;
+
                     // row 1
                     if (_M11 != 1) return false;
                     if (_M12 != 0) return false;
@@ -302,6 +451,10 @@ namespace SharpGLTF.Transforms
                     if (_M32 != 0) return false;
                     if (_M33 != 1) return false;
                 }
+                else
+                {
+                    _VerifyDefined();
+                }
 
                 return true;
             }
@@ -311,6 +464,11 @@ namespace SharpGLTF.Transforms
 
         #region API
 
+        private void _VerifyDefined()
+        {
+            if (_Representation == DATA_UNDEFINED) throw new InvalidOperationException("Undefined");
+        }
+
         private Matrix4x4 _GetMatrix()
         {
             if (IsMatrix)
@@ -320,54 +478,109 @@ namespace SharpGLTF.Transforms
                     _M11, _M12, _M13, 0,
                     _M21, _M22, _M23, 0,
                     _M31, _M32, _M33, 0,
-                    _Translation.X, _Translation.Y, _Translation.Z, 1
+                    _Translation.X,
+                    _Translation.Y,
+                    _Translation.Z, 1
                 );
             }
-
-            var m = Matrix4x4.CreateScale(this.Scale) * Matrix4x4.CreateFromQuaternion(this.Rotation.Sanitized());
-            m.Translation = this.Translation;
-            return m;
+            else if (IsSRT)
+            {
+                var m = Matrix4x4.CreateScale(this.Scale) * Matrix4x4.CreateFromQuaternion(this.Rotation);
+                m.Translation = this.Translation;
+                return m;
+            }
+            else
+            {
+                _VerifyDefined();
+                return default;
+            }
         }
 
         private Vector3 _GetScale()
         {
-            if (IsDecomposed) return new Vector3(_M11, _M12, _M13);
-            if (Matrix4x4.Decompose(_GetMatrix(), out var scale, out _, out _)) return scale;
-            throw new InvalidOperationException(_CannotDecomposeMessage);
+            if (IsSRT) return new Vector3(_M11, _M12, _M13);
+            throw new InvalidOperationException(_RequiresSRTError);
         }
 
         private Quaternion _GetRotation()
         {
-            if (IsDecomposed) return new Quaternion(_M21, _M22, _M23, _M31);
-            if (Matrix4x4.Decompose(_GetMatrix(), out _, out var rotation, out _)) return rotation;
-            throw new InvalidOperationException(_CannotDecomposeMessage);
+            if (IsSRT) return new Quaternion(_M21, _M22, _M23, _M31);
+            throw new InvalidOperationException(_RequiresSRTError);
         }
 
         public AffineTransform GetDecomposed()
         {
-            if (IsDecomposed) return this;
-            if (!Matrix4x4.Decompose(Matrix, out var s, out var r, out var t)) throw new InvalidOperationException(_CannotDecomposeMessage);
-            return new AffineTransform(s, r, t);
+            return TryDecompose(out AffineTransform xform)
+                ? xform
+                : throw new InvalidOperationException(_CannotDecomposeError);
+        }
+
+        public bool TryDecompose(out AffineTransform transform)
+        {
+            if (IsSRT) { transform = this; return true; }
+
+            if (IsLosslessDecomposable)
+            {
+                transform = new AffineTransform
+                    (
+                    new Vector3(_M11, _M22, _M33),
+                    Quaternion.Identity,
+                    this.Translation
+                    );
+
+                return true;
+            }
+
+            var x = Matrix4x4.Decompose(Matrix, out var s, out var r, out var t);
+
+            transform = x ? new AffineTransform(s, r, t) : this;
+
+            return x;
+        }
+
+        public bool TryDecompose(out Vector3 scale, out Quaternion rotation, out Vector3 translation)
+        {
+            if (IsSRT)
+            {
+                scale = _GetScale();
+                rotation = _GetRotation();
+                translation = _Translation;
+                return true;
+            }
+
+            if (IsLosslessDecomposable)
+            {
+                scale = new Vector3(_M11, _M22, _M33);
+                rotation = Quaternion.Identity;
+                translation = _Translation;
+                return true;
+            }
+
+            return Matrix4x4.Decompose(Matrix, out scale, out rotation, out translation);
         }
 
         public static AffineTransform Blend(ReadOnlySpan<AffineTransform> transforms, ReadOnlySpan<float> weights)
         {
-            var s = Vector3.Zero;
-            var r = default(Quaternion);
-            var t = Vector3.Zero;
+            var sss = Vector3.Zero;
+            var rrr = default(Quaternion);
+            var ttt = Vector3.Zero;
 
             for (int i = 0; i < transforms.Length; ++i)
             {
+                Guard.IsFalse(transforms[i]._Representation == DATA_UNDEFINED, nameof(transforms));
+
                 var w = weights[i];
 
-                s += transforms[i].Scale * w;
-                r += transforms[i].Rotation * w;
-                t += transforms[i].Translation * w;
+                transforms[i].TryDecompose(out var s, out var r, out var t);
+
+                sss += s * w;
+                rrr += r * w;
+                ttt += t * w;
             }
 
-            r = Quaternion.Normalize(r);
+            rrr = Quaternion.Normalize(rrr);
 
-            return new  AffineTransform(s, r, t);
+            return new  AffineTransform(sss, rrr, ttt);
         }
 
         public static AffineTransform operator *(in AffineTransform a, in AffineTransform b)
@@ -394,13 +607,16 @@ namespace SharpGLTF.Transforms
         /// </returns>
         public static AffineTransform Multiply(in AffineTransform a, in AffineTransform b)
         {
-            // if any of the two operators is a matrix, perform a matrix multiplication.
+            // if any of the two operators is a matrix, do a matrix multiplication.
             if (a.IsMatrix || b.IsMatrix)
             {
                 return new AffineTransform(a.Matrix * b.Matrix);
             }
 
-            // if the B operator has an uneven scale AND a rotation, performa a matrix multiplication
+            Guard.IsFalse(a._Representation == DATA_UNDEFINED, nameof(a));
+            Guard.IsFalse(b._Representation == DATA_UNDEFINED, nameof(b));
+
+            // if the B operator has an uneven scale AND a rotation, do a matrix multiplication
             // which produces a squeezed matrix and cannot be decomposed.
 
             var sb = b.Scale;

+ 100 - 0
src/SharpGLTF.Core/Transforms/MeshTransforms.cs

@@ -62,6 +62,14 @@ namespace SharpGLTF.Transforms
         V4 MorphColors(V4 color, IReadOnlyList<V4> morphTargets);
     }
 
+    public interface IGeometryInstancing
+    {
+        /// <summary>
+        /// Gets the list of instances produced by this transform.
+        /// </summary>
+        IReadOnlyList<RigidTransform> WorldTransforms { get; }
+    }
+
     public abstract class MorphTransform
     {
         #region constructor
@@ -85,6 +93,7 @@ namespace SharpGLTF.Transforms
         /// - Index of value <see cref="COMPLEMENT_INDEX"/> points to the Mesh master positions.
         /// - All other indices point to Mesh MorphTarget[index] positions.
         /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private SparseWeight8 _Weights;
 
         public const int COMPLEMENT_INDEX = 65536;
@@ -93,6 +102,7 @@ namespace SharpGLTF.Transforms
         /// True if morph targets represent absolute values.
         /// False if morph targets represent values relative to master value.
         /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private bool _AbsoluteMorphTargets;
 
         #endregion
@@ -215,8 +225,13 @@ namespace SharpGLTF.Transforms
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private TRANSFORM _WorldMatrix;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private Boolean _Visible;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private Boolean _FlipFaces;
 
         #endregion
@@ -299,6 +314,7 @@ namespace SharpGLTF.Transforms
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private TRANSFORM[] _SkinTransforms;
 
         #endregion
@@ -421,4 +437,88 @@ namespace SharpGLTF.Transforms
 
         #endregion
     }
+
+    public class InstancingTransform : RigidTransform, IGeometryInstancing
+    {
+        #region lifecycle
+
+        public InstancingTransform(AffineTransform[] instances)
+        {
+            _LocalMatrices = new TRANSFORM[instances.Length];
+
+            for (int i = 0; i < _LocalMatrices.Length; ++i)
+            {
+                _LocalMatrices[i] = instances[i].Matrix;
+            }
+
+            _WorldTransforms = new Lazy<RigidTransform[]>(_CreateTransforms);
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly Matrix4x4[] _LocalMatrices;
+
+        private Lazy<RigidTransform[]> _WorldTransforms;
+
+        #endregion
+
+        #region properties
+
+        /// <summary>
+        /// Gets the local matrices for every instanced mesh
+        /// </summary>
+        public IReadOnlyList<TRANSFORM> LocalMatrices => _LocalMatrices;
+
+        /// <summary>
+        /// Gets the local transforms for every instanced mesh
+        /// </summary>
+        public IReadOnlyList<RigidTransform> WorldTransforms => UpdateInstances();
+
+        #endregion
+
+        #region API
+
+        private RigidTransform[] _CreateTransforms()
+        {
+            var xforms = new RigidTransform[_LocalMatrices.Length];
+
+            for (int i = 0; i < xforms.Length; ++i)
+            {
+                xforms[i] = new RigidTransform();
+            }
+
+            return xforms;
+        }
+
+        public RigidTransform[] UpdateInstances()
+        {
+            var xforms = _WorldTransforms.Value;
+
+            for (int i = 0; i < xforms.Length; ++i)
+            {
+                var xform = AffineTransform
+                    .Multiply(_LocalMatrices[i], this.WorldMatrix)
+                    .Matrix;
+
+                xforms[i].Update(xform);
+            }
+
+            return xforms;
+        }
+
+        public static IEnumerable<IGeometryTransform> Evaluate(IGeometryTransform xform)
+        {
+            if (xform is IGeometryInstancing instanced)
+            {
+                foreach (var xinst in instanced.WorldTransforms) yield return xinst;
+                yield break;
+            }
+
+            yield return xform;
+        }
+
+        #endregion
+    }
 }

+ 13 - 2
src/SharpGLTF.Core/Validation/ValidationMode.cs

@@ -10,12 +10,23 @@ namespace SharpGLTF.Validation
     public enum ValidationMode
     {
         /// <summary>
-        /// Skip validation completely.
+        /// Skips validation completely.
         /// </summary>
+        /// <remarks>
+        /// <para>
+        /// This mode is intended to be used in scenarios where you know the models you're loading are perfectly
+        /// valid, and you want to skip validation because you want to speed up model loading.
+        /// </para>
+        /// <para>
+        /// Using this mode for loading malformed glTF models is not supported nor recomended, because although the
+        /// loading will not give any errors, it's impossible to guarantee the API will work correcly afterwards.
+        /// </para>
+        /// </remarks>
         Skip,
 
         /// <summary>
-        /// In some specific cases, the file can be fixed, at which point the errors successfully fixed will be reported as warnings.
+        /// In some specific cases, the file can be fixed, at which point the errors successfully
+        /// fixed will be reported as warnings.
         /// </summary>
         TryFix,
 

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

@@ -167,6 +167,7 @@ namespace SharpGLTF.Geometry
 
         public static bool IsEmpty<TMaterial>(this IPrimitiveReader<TMaterial> primitive)
         {
+            if (primitive == null) return true;
             if (primitive.Points.Count > 0) return false;
             if (primitive.Lines.Count > 0) return false;
             if (primitive.Triangles.Count > 0) return false;
@@ -175,6 +176,8 @@ namespace SharpGLTF.Geometry
 
         public static bool IsEmpty<TMaterial>(this IMeshBuilder<TMaterial> mesh)
         {
+            if (mesh == null) return true;
+            if (mesh.Primitives.Count == 0) return true;
             return mesh.Primitives.All(prim => prim.IsEmpty());
         }
 

+ 53 - 9
src/SharpGLTF.Toolkit/Geometry/VertexBufferColumns.cs

@@ -25,6 +25,31 @@ namespace SharpGLTF.Geometry
 
         #endregion
 
+        #region lifecycle
+
+        public VertexBufferColumns() { }
+
+        public VertexBufferColumns(VertexBufferColumns other)
+        {
+            this.Positions = other.Positions;
+            this.Normals = other.Normals;
+            this.Tangents = other.Tangents;
+            this.Colors0 = other.Colors0;
+            this.Colors1 = other.Colors1;
+            this.TexCoords0 = other.TexCoords0;
+            this.TexCoords1 = other.TexCoords1;
+            this.TexCoords2 = other.TexCoords2;
+            this.TexCoords3 = other.TexCoords3;
+            this.Joints0 = other.Joints0;
+            this.Joints1 = other.Joints1;
+            this.Weights0 = other.Weights0;
+            this.Weights1 = other.Weights1;
+
+            this._MorphTargets = other._MorphTargets;
+        }
+
+        #endregion
+
         #region Data Columns
 
         #pragma warning disable CA2227 // Collection properties should be read only
@@ -38,6 +63,8 @@ namespace SharpGLTF.Geometry
 
         public IList<Vector2> TexCoords0 { get; set; }
         public IList<Vector2> TexCoords1 { get; set; }
+        public IList<Vector2> TexCoords2 { get; set; }
+        public IList<Vector2> TexCoords3 { get; set; }
 
         public IList<Vector4> Joints0 { get; set; }
         public IList<Vector4> Joints1 { get; set; }
@@ -49,7 +76,11 @@ namespace SharpGLTF.Geometry
 
         private List<VertexBufferColumns> _MorphTargets;
 
-        public IReadOnlyList<VertexBufferColumns> MorphTargets => _MorphTargets == null ? (IReadOnlyList<VertexBufferColumns>)Array.Empty<VertexBufferColumns>() : _MorphTargets;
+        #endregion
+
+        #region properties
+
+        public IReadOnlyList<VertexBufferColumns> MorphTargets => _MorphTargets != null ? _MorphTargets : (IReadOnlyList<VertexBufferColumns>)Array.Empty<VertexBufferColumns>();
 
         #endregion
 
@@ -82,6 +113,8 @@ namespace SharpGLTF.Geometry
 
             this.TexCoords0 = _IsolateColumn(this.TexCoords0);
             this.TexCoords1 = _IsolateColumn(this.TexCoords1);
+            this.TexCoords2 = _IsolateColumn(this.TexCoords2);
+            this.TexCoords3 = _IsolateColumn(this.TexCoords3);
 
             this.Joints0 = _IsolateColumn(this.Joints0);
             this.Joints1 = _IsolateColumn(this.Joints1);
@@ -94,6 +127,13 @@ namespace SharpGLTF.Geometry
             foreach (var mt in _MorphTargets) mt.IsolateColumns();
         }
 
+        public VertexBufferColumns WithTransform(Transforms.IGeometryTransform transform)
+        {
+            var clone = new VertexBufferColumns(this);
+            clone._ApplyTransform(transform);
+            return clone;
+        }
+
         /// <summary>
         /// Applies a transform to the columns of this <see cref="VertexBufferColumns"/>
         /// </summary>
@@ -103,7 +143,7 @@ namespace SharpGLTF.Geometry
         /// Once it's applied, skinning and morphing columns are removed, since they're baked
         /// into the position, normal and tangent columns.
         /// </remarks>
-        public void ApplyTransform(Transforms.IGeometryTransform transform)
+        private void _ApplyTransform(Transforms.IGeometryTransform transform)
         {
             Guard.NotNull(this.Positions, nameof(this.Positions), "Missing Positions column");
             if (this.Normals != null) Guard.IsTrue(this.Positions.Count == this.Normals.Count, nameof(this.Normals), ERR_COLUMNLEN);
@@ -112,12 +152,14 @@ namespace SharpGLTF.Geometry
             if (this.Colors1 != null) Guard.IsTrue(this.Positions.Count == this.Colors1.Count, nameof(this.Colors1), ERR_COLUMNLEN);
             if (this.TexCoords0 != null) Guard.IsTrue(this.Positions.Count == this.TexCoords0.Count, nameof(this.TexCoords0), ERR_COLUMNLEN);
             if (this.TexCoords1 != null) Guard.IsTrue(this.Positions.Count == this.TexCoords1.Count, nameof(this.TexCoords1), ERR_COLUMNLEN);
+            if (this.TexCoords2 != null) Guard.IsTrue(this.Positions.Count == this.TexCoords2.Count, nameof(this.TexCoords2), ERR_COLUMNLEN);
+            if (this.TexCoords3 != null) Guard.IsTrue(this.Positions.Count == this.TexCoords3.Count, nameof(this.TexCoords3), ERR_COLUMNLEN);
             if (this.Joints0 != null) Guard.IsTrue(this.Positions.Count == this.Joints0.Count, nameof(this.Joints0), ERR_COLUMNLEN);
             if (this.Joints1 != null) Guard.IsTrue(this.Positions.Count == this.Joints1.Count, nameof(this.Joints1), ERR_COLUMNLEN);
             if (this.Weights0 != null) Guard.IsTrue(this.Positions.Count == this.Weights0.Count, nameof(this.Weights0), ERR_COLUMNLEN);
             if (this.Weights1 != null) Guard.IsTrue(this.Positions.Count == this.Weights1.Count, nameof(this.Weights1), ERR_COLUMNLEN);
 
-            // since the attributes we want to overwrite might be binded directly to the model's buffer
+            // since the attributes we want to overwrite might be bound directly to the model's buffer
             // data, and we don't want to modify the source data, we isolate the columns to be overwritten.
 
             this.Positions = _IsolateColumn(this.Positions);
@@ -132,7 +174,7 @@ namespace SharpGLTF.Geometry
             Vector3[] morphPositions = null;
             Vector3[] morphNormals = null;
             Vector3[] morphTangents = null;
-            Vector4[] morphColors0 = null;
+            Vector4[] morphColors0 = null; // we clone it because it can be affected by morph targets
 
             if (_MorphTargets != null)
             {
@@ -179,9 +221,7 @@ namespace SharpGLTF.Geometry
                 }
             }
 
-            // we've just applied the transform,
-            // so we clear animation columns since
-            // they're irrelevant now.
+            // we've just applied the transform, so we make this a rigid geometry.
 
             _MorphTargets = null;
 
@@ -242,11 +282,13 @@ namespace SharpGLTF.Geometry
 
             int numCols = 0;
             if (Colors0 != null) numCols = 1;
-            if (Colors0 != null && Colors1 != null) numCols = 2;
+            if (numCols == 1 && Colors1 != null) numCols = 2;
 
             int numTexs = 0;
             if (TexCoords0 != null) numTexs = 1;
-            if (TexCoords0 != null && TexCoords1 != null) numTexs = 2;
+            if (numTexs == 1 && TexCoords1 != null) numTexs = 2;
+            if (numTexs == 2 && TexCoords2 != null) numTexs = 3;
+            if (numTexs == 3 && TexCoords3 != null) numTexs = 4;
 
             int numJoints = 0;
             if (Joints0 != null) numJoints = 4;
@@ -277,6 +319,8 @@ namespace SharpGLTF.Geometry
 
             if (m.MaxTextCoords > 0) m.SetTexCoord(0, TexCoords0 == null ? Vector2.Zero : TexCoords0[index]);
             if (m.MaxTextCoords > 1) m.SetTexCoord(1, TexCoords1 == null ? Vector2.Zero : TexCoords1[index]);
+            if (m.MaxTextCoords > 2) m.SetTexCoord(2, TexCoords2 == null ? Vector2.Zero : TexCoords2[index]);
+            if (m.MaxTextCoords > 3) m.SetTexCoord(3, TexCoords3 == null ? Vector2.Zero : TexCoords3[index]);
 
             return m;
         }

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

@@ -123,7 +123,7 @@ namespace SharpGLTF.Geometry.VertexTypes
         /// </summary>
         /// <remarks>
         /// <para><b>⚠️ AVOID SETTING THIS VALUE DIRECTLY ⚠️</b></para>
-        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// Consider using the constructor, or setter methods like<see cref="SetBindings(in SparseWeight8)"/> instead of setting this value directly.
         /// </remarks>
         [VertexAttribute("JOINTS_0", ENCODING.UNSIGNED_SHORT, false)]
         public Vector4 Joints;
@@ -133,7 +133,7 @@ namespace SharpGLTF.Geometry.VertexTypes
         /// </summary>
         /// <remarks>
         /// <para><b>⚠️ AVOID SETTING THIS VALUE DIRECTLY ⚠️</b></para>
-        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// Consider using the constructor, or setter methods like <see cref="SetBindings(in SparseWeight8)"/> instead of setting this value directly.
         /// </remarks>
         [VertexAttribute("WEIGHTS_0")]
         public Vector4 Weights;
@@ -254,7 +254,7 @@ namespace SharpGLTF.Geometry.VertexTypes
         /// </summary>
         /// <remarks>
         /// <para><b>⚠️ AVOID SETTING THIS VALUE DIRECTLY ⚠️</b></para>
-        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// Consider using the constructor, or setter methods like <see cref="SetBindings(in SparseWeight8)"/> instead of setting this value directly.
         /// </remarks>
         [VertexAttribute("JOINTS_0", ENCODING.UNSIGNED_SHORT, false)]
         public Vector4 Joints0;
@@ -264,7 +264,7 @@ namespace SharpGLTF.Geometry.VertexTypes
         /// </summary>
         /// <remarks>
         /// <para><b>⚠️ AVOID SETTING THIS VALUE DIRECTLY ⚠️</b></para>
-        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// Consider using the constructor, or setter methods like <see cref="SetBindings(in SparseWeight8)"/> instead of setting this value directly.
         /// </remarks>
         [VertexAttribute("JOINTS_1", ENCODING.UNSIGNED_SHORT, false)]
         public Vector4 Joints1;
@@ -274,7 +274,7 @@ namespace SharpGLTF.Geometry.VertexTypes
         /// </summary>
         /// <remarks>
         /// <para><b>⚠️ AVOID SETTING THESE VALUES DIRECTLY ⚠️</b></para>
-        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// Consider using the constructor, or setter methods like <see cref="SetBindings(in SparseWeight8)"/> instead of setting this value directly.
         /// </remarks>
         [VertexAttribute("WEIGHTS_0")]
         public Vector4 Weights0;
@@ -284,7 +284,7 @@ namespace SharpGLTF.Geometry.VertexTypes
         /// </summary>
         /// <remarks>
         /// <para><b>⚠️ AVOID SETTING THESE VALUES DIRECTLY ⚠️</b></para>
-        /// Consider using the constructor, <see cref="SetBindings(in SparseWeight8)"/> or <see cref="SetBindings((int Index, float Weight)[])"/> instead of setting this value directly.
+        /// Consider using the constructor, or setter methods like <see cref="SetBindings(in SparseWeight8)"/> instead of setting this value directly.
         /// </remarks>
         [VertexAttribute("WEIGHTS_1")]
         public Vector4 Weights1;

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

@@ -221,7 +221,11 @@ namespace SharpGLTF.IO
 
         public void AddModel(ModelRoot model, Animation animation, float time)
         {
-            foreach (var triangle in Toolkit.EvaluateTriangles<VGEOMETRY, VMATERIAL>(model.DefaultScene, animation, time))
+            var options = new Runtime.RuntimeOptions();
+            options.IsolateMemory = false;
+            options.GpuMeshInstancing = Runtime.MeshInstancing.SingleMesh;
+
+            foreach (var triangle in Toolkit.EvaluateTriangles<VGEOMETRY, VMATERIAL>(model.DefaultScene, options, animation, time))
             {
                 var dstMaterial = GetMaterialFromTriangle(triangle.Material);
                 this.AddTriangle(dstMaterial, triangle.A, triangle.B, triangle.C);

+ 3 - 3
src/SharpGLTF.Toolkit/Scenes/Content.Schema2.cs

@@ -11,7 +11,7 @@ namespace SharpGLTF.Scenes
 {
     partial class MeshContent : SCHEMA2NODE
     {
-        void SCHEMA2NODE.Setup(Node dstNode, Schema2SceneBuilder context)
+        void SCHEMA2NODE.ApplyTo(Node dstNode, Schema2SceneBuilder context)
         {
             // we try to assign our mesh to the target node.
             // but if the target node already has a mesh, we need to create
@@ -24,7 +24,7 @@ namespace SharpGLTF.Scenes
 
     partial class CameraContent : SCHEMA2NODE
     {
-        public void Setup(Node dstNode, Schema2SceneBuilder context)
+        public void ApplyTo(Node dstNode, Schema2SceneBuilder context)
         {
             if (_Camera is CameraBuilder.Orthographic ortho)
             {
@@ -42,7 +42,7 @@ namespace SharpGLTF.Scenes
 
     partial class LightContent : SCHEMA2NODE
     {
-        public void Setup(Node dstNode, Schema2SceneBuilder context)
+        public void ApplyTo(Node dstNode, Schema2SceneBuilder context)
         {
             if (_Light is LightBuilder.Directional directional)
             {

+ 14 - 3
src/SharpGLTF.Toolkit/Scenes/Content.cs

@@ -31,9 +31,10 @@ namespace SharpGLTF.Scenes
     /// Represents a <see cref="MESHBUILDER"/> content of <see cref="ContentTransformer.Content"/>.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("Mesh")]
-    partial class MeshContent
-        : IRenderableContent
-        , ICloneable
+    partial class MeshContent :
+        IRenderableContent,
+        ICloneable,
+        IEquatable<IRenderableContent>
     {
         #region lifecycle
 
@@ -59,6 +60,16 @@ namespace SharpGLTF.Scenes
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private MESHBUILDER _Mesh;
 
+        public override int GetHashCode() { return _Mesh.GetHashCode(); }
+
+        public override bool Equals(object obj) { return obj is IRenderableContent other && this.Equals(other); }
+
+        public bool Equals(IRenderableContent other)
+        {
+            if (other is MeshContent otherMesh) return this._Mesh == otherMesh._Mesh;
+            throw new ArgumentException("Type mismatch", nameof(other));
+        }
+
         #endregion
 
         #region properties

+ 1 - 8
src/SharpGLTF.Toolkit/Scenes/InstanceBuilder.cs

@@ -3,15 +3,13 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 
-using SCHEMA2SCENE = SharpGLTF.Scenes.Schema2SceneBuilder.IOperator<SharpGLTF.Schema2.Scene>;
-
 namespace SharpGLTF.Scenes
 {
     /// <summary>
     /// Represents an element within <see cref="SceneBuilder.Instances"/>
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("{Content}")]
-    public sealed class InstanceBuilder : SCHEMA2SCENE
+    public sealed class InstanceBuilder
     {
         #region lifecycle
 
@@ -116,11 +114,6 @@ namespace SharpGLTF.Scenes
             return clone;
         }
 
-        void SCHEMA2SCENE.Setup(Schema2.Scene dstScene, Schema2SceneBuilder context)
-        {
-            if (_ContentTransformer is SCHEMA2SCENE schema2scb) schema2scb.Setup(dstScene, context);
-        }
-
         #endregion
     }
 }

+ 90 - 85
src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs

@@ -3,6 +3,9 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Numerics;
 
+using MATRIX = System.Numerics.Matrix4x4;
+using TRANSFORM = SharpGLTF.Transforms.AffineTransform;
+
 namespace SharpGLTF.Scenes
 {
     /// <summary>
@@ -19,22 +22,10 @@ namespace SharpGLTF.Scenes
 
             if (!string.IsNullOrWhiteSpace(this.Name)) txt += $" {this.Name}";
 
-            if (_Matrix.HasValue)
-            {
-                if (_Matrix.Value != Matrix4x4.Identity)
-                {
-                    var xform = this.LocalTransform;
-                    if (xform.Scale != Vector3.One) txt += $" 𝐒:{xform.Scale}";
-                    if (xform.Rotation != Quaternion.Identity) txt += $" 𝐑:{xform.Rotation}";
-                    if (xform.Translation != Vector3.Zero) txt += $" 𝚻:{xform.Translation}";
-                }
-            }
-            else
-            {
-                if (_Scale != null) txt += $" 𝐒:{_Scale.Value}";
-                if (_Rotation != null) txt += $" 𝐑:{_Rotation.Value}";
-                if (_Translation != null) txt += $" 𝚻:{_Translation.Value}";
-            }
+            var xform = this.LocalTransform.GetDecomposed();
+            if (xform.Scale != Vector3.One) txt += $" 𝐒:{xform.Scale}";
+            if (xform.Rotation != Quaternion.Identity) txt += $" 𝐑:{xform.Rotation}";
+            if (xform.Translation != Vector3.Zero) txt += $" 𝚻:{xform.Translation}";
 
             if (this.VisualChildren.Any())
             {
@@ -94,7 +85,7 @@ namespace SharpGLTF.Scenes
 
         private readonly List<NodeBuilder> _Children = new List<NodeBuilder>();
 
-        private Matrix4x4? _Matrix;
+        private MATRIX? _Matrix;
         private Animations.AnimatableProperty<Vector3> _Scale;
         private Animations.AnimatableProperty<Quaternion> _Rotation;
         private Animations.AnimatableProperty<Vector3> _Translation;
@@ -129,29 +120,84 @@ namespace SharpGLTF.Scenes
         /// </summary>
         public bool HasAnimations => (_Scale?.IsAnimated ?? false) || (_Rotation?.IsAnimated ?? false) || (_Translation?.IsAnimated ?? false);
 
+        /// <summary>
+        /// Gets the current Scale transform, or null.
+        /// </summary>
         public Animations.AnimatableProperty<Vector3> Scale => _Scale;
 
+        /// <summary>
+        /// Gets the current rotation transform, or null.
+        /// </summary>
         public Animations.AnimatableProperty<Quaternion> Rotation => _Rotation;
 
+        /// <summary>
+        /// Gets the current translation transform, or null.
+        /// </summary>
         public Animations.AnimatableProperty<Vector3> Translation => _Translation;
 
         /// <summary>
-        /// Gets or sets the local transform <see cref="Matrix4x4"/> of this <see cref="NodeBuilder"/>.
+        /// Gets or sets the local transform <see cref="MATRIX"/> of this <see cref="NodeBuilder"/>.
+        /// </summary>
+        /// <remarks>
+        /// When setting the value, If there's no animations currently attached to this node,<br/>
+        /// the transform is stored as a matrix. Otherwise, it's decomposed to a SRT chain.
+        /// </remarks>
+        public MATRIX LocalMatrix
+        {
+            get => LocalTransform.Matrix;
+            set => LocalTransform = value;
+        }
+
+        /// <summary>
+        /// Gets or sets the local Scale, Rotation and Translation of this <see cref="NodeBuilder"/>.
         /// </summary>
-        public Matrix4x4 LocalMatrix
+        public TRANSFORM LocalTransform
         {
-            get => Transforms.Matrix4x4Factory.CreateFrom(_Matrix, Scale?.Value, Rotation?.Value, Translation?.Value);
+            get => TRANSFORM.CreateFromAny(_Matrix, _Scale?.Value, _Rotation?.Value, _Translation?.Value);
             set
             {
-                if (HasAnimations) { _DecomposeMatrix(value); return; }
+                Guard.IsTrue(value.IsValid, nameof(value));
 
-                _Matrix = value != Matrix4x4.Identity ? value : (Matrix4x4?)null;
-                _Scale = null;
-                _Rotation = null;
-                _Translation = null;
+                // we cannot set a matrix while holding animation tracks because it would destroy them.
+                if (HasAnimations) value = value.GetDecomposed();
+
+                if (value.IsSRT)
+                {
+                    _Matrix = null;
+                    if (_Scale != null || value.Scale != Vector3.One) UseScale().Value = value.Scale;
+                    if (_Rotation != null || value.Rotation != Quaternion.Identity) UseRotation().Value = value.Rotation;
+                    if (_Translation != null || value.Translation != Vector3.Zero) UseTranslation().Value = value.Translation;
+                }
+                else
+                {
+                    _Matrix = value.Matrix;
+                    _Scale = null;
+                    _Rotation = null;
+                    _Translation = null;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the world transform <see cref="MATRIX"/> of this <see cref="NodeBuilder"/>.
+        /// </summary>
+        public MATRIX WorldMatrix
+        {
+            get
+            {
+                var p = this.Parent;
+                return p == null ? LocalMatrix : Transforms.Matrix4x4Factory.LocalToWorld(p.WorldMatrix, LocalMatrix);
+            }
+            set
+            {
+                var p = this.Parent;
+                LocalMatrix = p == null ? value : Transforms.Matrix4x4Factory.WorldToLocal(p.WorldMatrix, value);
             }
         }
 
+        /// <summary>
+        /// Equivalent to <see cref="LocalMatrix"/> but calculated at double precission.
+        /// </summary>
         internal Transforms.Matrix4x4Double LocalMatrixPrecise
         {
             get
@@ -171,43 +217,9 @@ namespace SharpGLTF.Scenes
             }
         }
 
-        #pragma warning disable CA1721 // Property names should not match get methods
-
-        /// <summary>
-        /// Gets or sets the local Scale, Rotation and Translation of this <see cref="NodeBuilder"/>.
-        /// </summary>
-        public Transforms.AffineTransform LocalTransform
-        {
-            get => Transforms.AffineTransform.CreateFromAny(_Matrix, _Scale?.Value, _Rotation?.Value, Translation?.Value);
-            set
-            {
-                Guard.IsTrue(value.IsValid, nameof(value));
-
-                _Matrix = null;
-
-                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;
-            }
-        }
-
         /// <summary>
-        /// Gets or sets the world transform <see cref="Matrix4x4"/> of this <see cref="NodeBuilder"/>.
+        /// Equivalent to <see cref="WorldMatrix"/> but calculated at double precission.
         /// </summary>
-        public Matrix4x4 WorldMatrix
-        {
-            get
-            {
-                var vs = this.Parent;
-                return vs == null ? LocalMatrix : Transforms.Matrix4x4Factory.LocalToWorld(vs.WorldMatrix, LocalMatrix);
-            }
-            set
-            {
-                var vs = this.Parent;
-                LocalMatrix = vs == null ? value : Transforms.Matrix4x4Factory.WorldToLocal(vs.WorldMatrix, value);
-            }
-        }
-
         internal Transforms.Matrix4x4Double WorldMatrixPrecise
         {
             get
@@ -217,8 +229,6 @@ namespace SharpGLTF.Scenes
             }
         }
 
-        #pragma warning restore CA1721 // Property names should not match get methods
-
         #endregion
 
         #region API - hierarchy
@@ -287,29 +297,24 @@ namespace SharpGLTF.Scenes
 
         #region API - transform
 
-        private void _DecomposeMatrix()
+        private void _UseDecomposedTransform()
         {
-            if (!_Matrix.HasValue) return;
-
-            var m = _Matrix.Value;
-            _Matrix = null;
+            var xform = this.LocalTransform;
+            if (xform.IsSRT) return;
 
-            // we do the decomposition AFTER setting _Matrix to null to prevent an infinite recursive loop. Fixes #37
-            if (m != Matrix4x4.Identity) _DecomposeMatrix(m);
-        }
+            // try to convert from matrix representation to decomposed representation.
 
-        private void _DecomposeMatrix(Matrix4x4 matrix)
-        {
-            var affine = Transforms.AffineTransform.CreateDecomposed(matrix);
+            xform = xform.GetDecomposed();
 
-            UseScale().Value = affine.Scale;
-            UseRotation().Value = affine.Rotation;
-            UseTranslation().Value = affine.Translation;
+            _Matrix = null;
+            UseScale().Value = xform.Scale;
+            UseRotation().Value = xform.Rotation;
+            UseTranslation().Value = xform.Translation;
         }
 
         public Animations.AnimatableProperty<Vector3> UseScale()
         {
-            _DecomposeMatrix();
+            _UseDecomposedTransform();
 
             if (_Scale == null)
             {
@@ -327,7 +332,7 @@ namespace SharpGLTF.Scenes
 
         public Animations.AnimatableProperty<Quaternion> UseRotation()
         {
-            _DecomposeMatrix();
+            _UseDecomposedTransform();
 
             if (_Rotation == null)
             {
@@ -345,7 +350,7 @@ namespace SharpGLTF.Scenes
 
         public Animations.AnimatableProperty<Vector3> UseTranslation()
         {
-            _DecomposeMatrix();
+            _UseDecomposedTransform();
 
             if (_Translation == null)
             {
@@ -367,7 +372,7 @@ namespace SharpGLTF.Scenes
 
         public void SetRotationTrack(string track, Animations.ICurveSampler<Quaternion> curve) { UseRotation().SetTrack(track, curve); }
 
-        public Transforms.AffineTransform GetLocalTransform(string animationTrack, float time)
+        public TRANSFORM GetLocalTransform(string animationTrack, float time)
         {
             if (animationTrack == null) return this.LocalTransform;
 
@@ -375,10 +380,10 @@ namespace SharpGLTF.Scenes
             var rotation = Rotation?.GetValueAt(animationTrack, time);
             var translation = Translation?.GetValueAt(animationTrack, time);
 
-            return new Transforms.AffineTransform(scale, rotation, translation);
+            return new TRANSFORM(scale, rotation, translation);
         }
 
-        public Matrix4x4 GetWorldMatrix(string animationTrack, float time)
+        public MATRIX GetWorldMatrix(string animationTrack, float time)
         {
             if (animationTrack == null) return this.WorldMatrix;
 
@@ -387,11 +392,11 @@ namespace SharpGLTF.Scenes
             return vs == null ? lm : Transforms.Matrix4x4Factory.LocalToWorld(vs.GetWorldMatrix(animationTrack, time), lm);
         }
 
-        public Matrix4x4 GetInverseBindMatrix(Matrix4x4? meshWorldMatrix = null)
+        public MATRIX GetInverseBindMatrix(MATRIX? meshWorldMatrix = null)
         {
-            Transforms.Matrix4x4Double mwx = meshWorldMatrix ?? Matrix4x4.Identity;
+            Transforms.Matrix4x4Double mwx = meshWorldMatrix ?? MATRIX.Identity;
 
-            return (Matrix4x4)Transforms.SkinnedTransform.CalculateInverseBinding(mwx, this.WorldMatrixPrecise);
+            return (MATRIX)Transforms.SkinnedTransform.CalculateInverseBinding(mwx, this.WorldMatrixPrecise);
         }
 
         #endregion

+ 161 - 36
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs

@@ -25,12 +25,32 @@ namespace SharpGLTF.Scenes
 
         #endregion
 
+        #region settings
+        public int GpuMeshInstancingMinCount { get; set; }
+
+        #endregion
+
         #region API
 
         public Mesh GetMesh(MESHBUILDER key) { return key == null ? null : _Meshes.TryGetValue(key, out Mesh val) ? val : null; }
 
         public Node GetNode(NodeBuilder key) { return key == null ? null : _Nodes.TryGetValue(key, out Node val) ? val : null; }
 
+        public static bool HasContent(Node node, bool checkTransform = true)
+        {
+            if (checkTransform && node.LocalMatrix != Matrix4x4.Identity) return true;
+
+            if (node.VisualChildren.Any()) return true;
+
+            if (node.Mesh != null) return true;
+            if (node.Skin != null) return true;
+            if (node.Camera != null) return true;
+            if (node.PunctualLight != null) return true;
+            if (node.GetGpuInstancing() != null) return true;
+
+            return false;
+        }
+
         public void AddGeometryResources(ModelRoot root, IEnumerable<SceneBuilder> srcScenes, SceneBuilderSchema2Settings settings)
         {
             // gather all unique MeshBuilders
@@ -38,7 +58,7 @@ namespace SharpGLTF.Scenes
             var srcMeshes = srcScenes
                 .SelectMany(item => item.Instances)
                 .Select(item => item.Content?.GetGeometryAsset())
-                .Where(item => item != null)
+                .Where(item => !Geometry.MeshBuilderToolkit.IsEmpty(item))
                 .Distinct()
                 .ToArray();
 
@@ -73,7 +93,7 @@ namespace SharpGLTF.Scenes
             // TODO: here we could check that every dstMesh has been correctly created.
         }
 
-        private void AddArmatureResources(Func<Node> nodeFactory, IEnumerable<SceneBuilder> srcScenes)
+        private void AddArmatureResources(IEnumerable<SceneBuilder> srcScenes, Func<Node> nodeFactory)
         {
             // ALIGNMENT ISSUE:
             // the toolkit builder is designed in a way that every instance can reuse the same node many times, even from different scenes.
@@ -93,11 +113,11 @@ namespace SharpGLTF.Scenes
 
             foreach (var armature in armatures)
             {
-                CreateArmature(nodeFactory, armature);
+                CreateArmature(armature, nodeFactory);
             }
         }
 
-        private void CreateArmature(Func<Node> nodeFactory, NodeBuilder srcNode)
+        private void CreateArmature(NodeBuilder srcNode, Func<Node> nodeFactory)
         {
             var dstNode = nodeFactory();
 
@@ -107,7 +127,7 @@ namespace SharpGLTF.Scenes
 
             if (srcNode.HasAnimations)
             {
-                dstNode.LocalTransform = srcNode.LocalTransform;
+                dstNode.LocalTransform = srcNode.LocalTransform.GetDecomposed();
 
                 // Copies all the animations to the target node.
                 if (srcNode.Scale != null) foreach (var t in srcNode.Scale.Tracks) dstNode.WithScaleAnimation(t.Key, t.Value);
@@ -116,14 +136,17 @@ namespace SharpGLTF.Scenes
             }
             else
             {
-                dstNode.LocalMatrix = srcNode.LocalMatrix;
+                dstNode.LocalTransform = srcNode.LocalTransform;
             }
 
-            foreach (var c in srcNode.VisualChildren) CreateArmature(() => dstNode.CreateNode(), c);
+            foreach (var c in srcNode.VisualChildren) CreateArmature(c, () => dstNode.CreateNode());
         }
 
         public static void SetMorphAnimation(Node dstNode, Animations.AnimatableProperty<Transforms.SparseWeight8> animation)
         {
+            Guard.NotNull(dstNode, nameof(dstNode));
+            Guard.NotNull(dstNode.Mesh, nameof(dstNode.Mesh), "call after IOperator.ApplyTo");
+
             if (animation == null) return;
 
             var dstMesh = dstNode.Mesh;
@@ -136,25 +159,54 @@ namespace SharpGLTF.Scenes
         public void AddScene(Scene dstScene, SceneBuilder srcScene)
         {
             _Nodes.Clear();
-            AddArmatureResources(() => dstScene.CreateNode(), new[] { srcScene });
+            AddArmatureResources(new[] { srcScene }, () => dstScene.CreateNode());
+
+            // gather single operators (RigidTransformer and SkinnedTransformer)
 
-            var schema2Instances = srcScene
+            var srcSingleOperators = srcScene
                 .Instances
+                .Select(item => item.Content)
+                .Where(item => !Geometry.MeshBuilderToolkit.IsEmpty(item.GetGeometryAsset()))
                 .OfType<IOperator<Scene>>();
 
-            foreach (var inst in schema2Instances)
+            // gather multi operators (Fixed Transformer)
+
+            var srcChildren = srcScene
+                .Instances
+                .Select(item => item.Content)
+                .Where(item => !Geometry.MeshBuilderToolkit.IsEmpty(item.GetGeometryAsset()))
+                .OfType<FixedTransformer>();
+
+            var srcMultiOperators = _MeshInstancing.CreateFrom(srcChildren, this.GpuMeshInstancingMinCount);
+
+            // apply operators
+
+            var srcOperators = srcSingleOperators.Concat(srcMultiOperators);
+
+            foreach (var op in srcOperators)
             {
-                inst.Setup(dstScene, this);
+                op.ApplyTo(dstScene, this);
             }
+
+            #if DEBUG
+            srcScene._VerifyConversion(dstScene);
+            #endif
         }
 
         #endregion
 
-        #region types
+        #region nested types
 
+        /// <summary>
+        /// Represents an object that can operate on a target object.
+        /// </summary>
+        /// <typeparam name="T">
+        /// The target type.
+        /// This is usually <see cref="Scene"/> or <see cref="Node"/>.
+        /// </typeparam>
         public interface IOperator<T>
         {
-            void Setup(T dst, Schema2SceneBuilder context);
+            void ApplyTo(T target, Schema2SceneBuilder context);
         }
 
         #endregion
@@ -165,18 +217,47 @@ namespace SharpGLTF.Scenes
         public static SceneBuilderSchema2Settings Default => new SceneBuilderSchema2Settings
         {
             UseStridedBuffers = true,
-            CompactVertexWeights = false
+            CompactVertexWeights = false,
+            GpuMeshInstancingMinCount = int.MaxValue
+        };
+
+        public static SceneBuilderSchema2Settings WithGpuInstancing => new SceneBuilderSchema2Settings
+        {
+            UseStridedBuffers = true,
+            CompactVertexWeights = false,
+            GpuMeshInstancingMinCount = 3
         };
 
         public bool UseStridedBuffers;
 
         public bool CompactVertexWeights;
+
+        public int GpuMeshInstancingMinCount;
     }
 
     public partial class SceneBuilder : IConvertibleToGltf2
     {
         #region from SceneBuilder to Schema2
 
+        /// <summary>
+        /// Converts this <see cref="SceneBuilder"/> instance into a <see cref="ModelRoot"/> instance.
+        /// </summary>
+        /// <returns>A new <see cref="ModelRoot"/> instance.</returns>
+        public ModelRoot ToGltf2()
+        {
+            return ToGltf2(new[] { this }, SceneBuilderSchema2Settings.Default);
+        }
+
+        /// <summary>
+        /// Converts this <see cref="SceneBuilder"/> instance into a <see cref="ModelRoot"/> instance.
+        /// </summary>
+        /// <param name="settings">Conversion settings.</param>
+        /// <returns>A new <see cref="ModelRoot"/> instance.</returns>
+        public ModelRoot ToGltf2(SceneBuilderSchema2Settings settings)
+        {
+            return ToGltf2(new[] { this }, settings);
+        }
+
         /// <summary>
         /// Converts a collection of <see cref="SceneBuilder"/> instances to a single <see cref="ModelRoot"/> instance.
         /// </summary>
@@ -188,6 +269,7 @@ namespace SharpGLTF.Scenes
             Guard.NotNull(srcScenes, nameof(srcScenes));
 
             var context = new Schema2SceneBuilder();
+            context.GpuMeshInstancingMinCount = settings.GpuMeshInstancingMinCount;
 
             var dstModel = ModelRoot.CreateModel();
             context.AddGeometryResources(dstModel, srcScenes, settings);
@@ -205,30 +287,11 @@ namespace SharpGLTF.Scenes
             return dstModel;
         }
 
-        /// <summary>
-        /// Converts this <see cref="SceneBuilder"/> instance into a <see cref="ModelRoot"/> instance.
-        /// </summary>
-        /// <param name="settings">Conversion settings.</param>
-        /// <returns>A new <see cref="ModelRoot"/> instance.</returns>
-        public ModelRoot ToGltf2(SceneBuilderSchema2Settings settings)
-        {
-            return ToGltf2(new[] { this }, settings);
-        }
-
-        /// <summary>
-        /// Converts this <see cref="SceneBuilder"/> instance into a <see cref="ModelRoot"/> instance.
-        /// </summary>
-        /// <returns>A new <see cref="ModelRoot"/> instance.</returns>
-        public ModelRoot ToGltf2()
-        {
-            return ToGltf2(new[] { this }, SceneBuilderSchema2Settings.Default);
-        }
-
         #endregion
 
         #region from Schema2 to SceneBuilder
 
-        internal static SceneBuilder CreateFrom(Scene srcScene)
+        public static SceneBuilder CreateFrom(Scene srcScene)
         {
             if (srcScene == null) return null;
 
@@ -268,6 +331,10 @@ namespace SharpGLTF.Scenes
 
             _AddLightInstances(dstScene, dstNodes, srcCameraInstances);
 
+            #if DEBUG
+            dstScene._VerifyConversion(srcScene);
+            #endif
+
             return dstScene;
         }
 
@@ -285,9 +352,25 @@ namespace SharpGLTF.Scenes
                 if (srcInstance.Skin == null)
                 {
                     var dstNode = dstNodes[srcInstance];
-                    var dstInst = dstScene.AddRigidMesh(dstMesh, dstNode);
 
-                    _CopyMorphingAnimation(dstInst, srcInstance);
+                    var gpuInstancing = srcInstance.GetGpuInstancing();
+
+                    if (gpuInstancing != null)
+                    {
+                        foreach (var xinst in gpuInstancing.LocalTransforms)
+                        {
+                            var dstInst = dstScene.AddRigidMesh(dstMesh, dstNode, xinst);
+
+                            // if we add morphing the the mesh, all meshes would morph simultaneously??
+                            _CopyMorphingAnimation(dstInst, srcInstance);
+                        }
+                    }
+                    else
+                    {
+                        var dstInst = dstScene.AddRigidMesh(dstMesh, dstNode);
+
+                        _CopyMorphingAnimation(dstInst, srcInstance);
+                    }
                 }
                 else
                 {
@@ -403,5 +486,47 @@ namespace SharpGLTF.Scenes
         }
 
         #endregion
+
+        #region utilities
+
+        internal void _VerifyConversion(Scene gltfScene)
+        {
+            // renderable instances
+
+            var renderableInstCount = this.Instances
+                .Select(item => item.Content.GetGeometryAsset())
+                .Where(item => !Geometry.MeshBuilderToolkit.IsEmpty(item))
+                .Count();
+
+            // check if we have created the same amount of instances defined in the SceneBuilder.
+
+            var renderableGltfCount = Node.Flatten(gltfScene)
+                .Where(item => item.Mesh != null)
+                .Sum(item => item.GetGpuInstancing()?.Count ?? 1);
+
+            if (renderableInstCount != renderableGltfCount)
+            {
+                throw new InvalidOperationException($"Expected {this.Instances.Count}, but found {renderableGltfCount}");
+            }
+
+            // create a viewer to compare against
+
+            var gltfViewOptions = new Runtime.RuntimeOptions();
+            gltfViewOptions.IsolateMemory = false;
+            gltfViewOptions.GpuMeshInstancing = Runtime.MeshInstancing.Enabled;
+
+            var gltfView = Runtime.SceneTemplate
+                .Create(gltfScene, gltfViewOptions)
+                .CreateInstance();
+
+            var renderableViewCount = gltfView.Sum(item => item.InstanceCount);
+
+            if (renderableInstCount != renderableViewCount)
+            {
+                throw new InvalidOperationException($"Expected {this.Instances.Count}, but found {renderableViewCount}");
+            }
+        }
+
+        #endregion
     }
 }

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

@@ -86,49 +86,42 @@ namespace SharpGLTF.Scenes
 
         #endregion
 
-        #region Obsolete API
+        #region API
 
-        [Obsolete("Remove name parameter and use .WithName(name);", true)]
-        public InstanceBuilder AddRigidMesh(MESHBUILDER mesh, String nodeName, Matrix4x4 meshWorldMatrix)
+        public InstanceBuilder AddRigidMesh(MESHBUILDER mesh, NodeBuilder node)
         {
-            return AddRigidMesh(mesh, meshWorldMatrix).WithName(nodeName);
-        }
+            Guard.NotNull(mesh, nameof(mesh));
+            Guard.NotNull(node, nameof(node));
 
-        [Obsolete("Remove name parameter and use .WithName(name);", true)]
-        public InstanceBuilder AddSkinnedMesh(MESHBUILDER mesh, String nodeName, Matrix4x4 meshWorldMatrix, params NodeBuilder[] joints)
-        {
-            return AddSkinnedMesh(mesh, meshWorldMatrix, joints).WithName(nodeName);
-        }
+            var instance = new InstanceBuilder(this);
+            instance.Content = new RigidTransformer(mesh, node);
 
-        [Obsolete("Remove name parameter and use .WithName(name);", true)]
-        public InstanceBuilder AddSkinnedMesh(MESHBUILDER mesh, string nodeName, params (NodeBuilder Joint, Matrix4x4 InverseBindMatrix)[] joints)
-        {
-            return AddSkinnedMesh(mesh, joints).WithName(nodeName);
-        }
+            _Instances.Add(instance);
 
-        #endregion
+            return instance;
+        }
 
-        #region API
-        public InstanceBuilder AddRigidMesh(MESHBUILDER mesh, Matrix4x4 meshWorldMatrix)
+        public InstanceBuilder AddRigidMesh(MESHBUILDER mesh, Transforms.AffineTransform meshWorldTransform)
         {
             Guard.NotNull(mesh, nameof(mesh));
-            Guard.IsTrue(meshWorldMatrix.IsValid(_Extensions.MatrixCheck.Decomposable | _Extensions.MatrixCheck.IdentityColumn4), nameof(meshWorldMatrix));
 
             var instance = new InstanceBuilder(this);
-            instance.Content = new FixedTransformer(mesh, meshWorldMatrix);
+            instance.Content = new FixedTransformer(mesh, meshWorldTransform);
 
             _Instances.Add(instance);
 
             return instance;
         }
 
-        public InstanceBuilder AddRigidMesh(MESHBUILDER mesh, NodeBuilder node)
+        public InstanceBuilder AddRigidMesh(MESHBUILDER mesh, NodeBuilder node, Transforms.AffineTransform instanceTransform)
         {
             Guard.NotNull(mesh, nameof(mesh));
             Guard.NotNull(node, nameof(node));
 
+            if (instanceTransform.IsIdentity) return AddRigidMesh(mesh, node);
+
             var instance = new InstanceBuilder(this);
-            instance.Content = new RigidTransformer(mesh, node);
+            instance.Content = new FixedTransformer(mesh, node, instanceTransform);
 
             _Instances.Add(instance);
 

+ 51 - 0
src/SharpGLTF.Toolkit/Scenes/TransformChainBuilder.cs

@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+
+using TRANSFORM = SharpGLTF.Transforms.AffineTransform;
+
+namespace SharpGLTF.Scenes
+{
+    public struct TransformChainBuilder
+    {
+        public static implicit operator TransformChainBuilder(NodeBuilder node)
+        {
+            return new TransformChainBuilder(node);
+        }
+
+        public static implicit operator TransformChainBuilder(TRANSFORM transform)
+        {
+            return new TransformChainBuilder(transform);
+        }
+
+        public static implicit operator TransformChainBuilder(Matrix4x4 transform)
+        {
+            return new TransformChainBuilder(transform);
+        }
+
+        public TransformChainBuilder(TRANSFORM transform)
+        {
+            _ParentTransform = null;
+            _ChildTransform = transform;
+        }
+
+        public TransformChainBuilder(NodeBuilder node)
+        {
+            _ParentTransform = node;
+            _ChildTransform = null;
+        }
+
+        public TransformChainBuilder(NodeBuilder parent, TRANSFORM child)
+        {
+            _ParentTransform = parent;
+            _ChildTransform = child;
+        }
+
+        private readonly NodeBuilder _ParentTransform;
+        private readonly TRANSFORM? _ChildTransform;
+
+        public NodeBuilder Parent => _ParentTransform;
+        public TRANSFORM? Child => _ChildTransform;
+    }
+}

+ 170 - 20
src/SharpGLTF.Toolkit/Scenes/Transformers.Schema2.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Text;
 
 using SharpGLTF.Schema2;
 
@@ -10,49 +9,201 @@ using SCHEMA2SCENE = SharpGLTF.Scenes.Schema2SceneBuilder.IOperator<SharpGLTF.Sc
 
 namespace SharpGLTF.Scenes
 {
-    partial class FixedTransformer : SCHEMA2SCENE
+    /// <summary>
+    /// Groups instances of the same mesh being attached to the same node.
+    /// </summary>
+    [System.Diagnostics.DebuggerDisplay("Rigid Node[{_DebugName,nq}] = GpuMeshInstances[{_Children.Count}]")]
+    internal readonly struct _MeshInstancing : SCHEMA2SCENE
     {
-        void SCHEMA2SCENE.Setup(Scene dstScene, Schema2SceneBuilder context)
+        #region debug
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private string _DebugName => string.IsNullOrWhiteSpace(_ParentNode.Name) ? "*" : _ParentNode.Name;
+
+        #endregion
+
+        #region lifecycle
+
+        /// <summary>
+        /// Groups a collection of <see cref="FixedTransformer"/> items into a sequence of <see cref="_MeshInstancing"/>.
+        /// </summary>
+        /// <param name="instances">The input instances.</param>
+        /// <param name="gpuMinCount">The minimum number of instances required to enable gpu mesh instancing extension.</param>
+        /// <returns>A collection of grouped instances.</returns>
+        public static IEnumerable<SCHEMA2SCENE> CreateFrom(IEnumerable<FixedTransformer> instances, int gpuMinCount)
         {
-            if (!(Content is SCHEMA2NODE schema2Target)) return;
+            // gather all FixedTransformers with renderables
+
+            var renderables = instances.Where(item => item.HasRenderableContent);
+
+            // gather all renderables attached to the scene root.
+
+            var nullParentGroup = renderables
+                .Where(item => item.ParentNode == null)
+                .GroupBy(item => (IRenderableContent)item.Content);
 
-            var dstNode = dstScene.CreateNode();
+            foreach (var nullParentAndSameContent in nullParentGroup)
+            {
+                yield return new _MeshInstancing(null, nullParentAndSameContent, gpuMinCount);
+            }
 
-            dstNode.Name = _NodeName;
-            dstNode.Extras = _NodeExtras;
-            dstNode.LocalMatrix = _WorldTransform;
+            // gather all renderables attached to the same child NodeBuilder.
 
-            schema2Target.Setup(dstNode, context);
+            var sameParentGroup = renderables
+                .Where(item => item.ParentNode != null)
+                .GroupBy(item => item.ParentNode);
+
+            foreach (var sameParent in sameParentGroup)
+            {
+                var sameParentAndSameContent = sameParent.GroupBy(item => (IRenderableContent)item.Content);
 
-            Schema2SceneBuilder.SetMorphAnimation(dstNode, this.Morphings);
+                foreach (var group in sameParentAndSameContent)
+                {
+                    yield return new _MeshInstancing(sameParent.Key, group, gpuMinCount);
+                }
+            }
         }
+
+        private _MeshInstancing(NodeBuilder parentNode, IEnumerable<FixedTransformer> children, int gpuMinCount)
+        {
+            System.Diagnostics.Debug.Assert(children.All(item => item.ParentNode == parentNode), "all items must have the same parentNode");
+
+            #if DEBUG
+            var hasMoreThanOne = children
+                .Select(item => item.Content)
+                .Cast<IRenderableContent>()
+                .Distinct()
+                .Skip(1)
+                .Any();
+
+            System.Diagnostics.Debug.Assert(!hasMoreThanOne, "Content must be the same for all");
+            #endif
+
+            _ParentNode = parentNode;
+            _Children = children.ToList();
+            _GpuMinCount = gpuMinCount;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly NodeBuilder _ParentNode;
+
+        private readonly IReadOnlyList<FixedTransformer> _Children;
+
+        private readonly int _GpuMinCount;
+
+        #endregion
+
+        #region API
+
+        public void ApplyTo(Scene dstScene, Schema2SceneBuilder context)
+        {
+            System.Diagnostics.Debug.Assert(_Children.Count > 0);
+
+            if (_ParentNode == null)
+            {
+                _AddInstances(dstScene, context);
+                return;
+            }
+            else
+            {
+                var dstNode = context.GetNode(_ParentNode);
+
+                if (Schema2SceneBuilder.HasContent(dstNode))
+                {
+                    dstNode = dstNode.CreateNode();
+                }
+
+                _AddInstances(dstNode, context);
+                return;
+            }
+        }
+
+        private void _AddInstances(IVisualNodeContainer dst, Schema2SceneBuilder context)
+        {
+            if (_Children.Count < _GpuMinCount)
+            {
+                foreach (var srcChild in _Children)
+                {
+                    if (srcChild.Content is SCHEMA2NODE srcOperator)
+                    {
+                        var dstNode = dst.CreateNode();
+                        dstNode.LocalTransform = srcChild.ChildTransform;
+                        srcOperator.ApplyTo(dstNode, context);
+
+                        System.Diagnostics.Debug.Assert(dstNode.WorldMatrix == srcChild.GetPoseWorldMatrix(), "Transform mismatch!");
+                    }
+                }
+            }
+            else
+            {
+                if (_Children.First().Content is SCHEMA2NODE srcOperator)
+                {
+                    var xforms = _Children
+                        .Select(item => item.ChildTransform)
+                        .ToList();
+
+                    if (!(dst is Node dstNode)) dstNode = dst.CreateNode();
+
+                    System.Diagnostics.Debug.Assert(dstNode.Mesh == null);
+                    System.Diagnostics.Debug.Assert(dstNode.Skin == null);
+                    System.Diagnostics.Debug.Assert(dstNode.GetGpuInstancing() == null);
+
+                    srcOperator.ApplyTo(dstNode, context);
+
+                    dstNode
+                        .UseGpuInstancing()
+                        .WithInstanceAccessors(xforms);
+
+                    #if DEBUG
+                    var dstInstances = dstNode.GetGpuInstancing();
+                    for (int i = 0; i < _Children.Count; ++i)
+                    {
+                        var srcXform = _Children[i].GetPoseWorldMatrix();
+                        var dstXform = dstInstances.GetWorldMatrix(i);
+
+                        System.Diagnostics.Debug.Assert( srcXform == dstXform, "Transform mismatch!");
+                    }
+                    #endif
+                }
+            }
+        }
+
+        #endregion
     }
 
     partial class RigidTransformer : SCHEMA2SCENE
     {
-        void SCHEMA2SCENE.Setup(Scene dstScene, Schema2SceneBuilder context)
+        void SCHEMA2SCENE.ApplyTo(Scene dstScene, Schema2SceneBuilder context)
         {
-            if (!(Content is SCHEMA2NODE schema2Target)) return;
+            if (Content is SCHEMA2NODE schema2Target)
+            {
+                var dstNode = context.GetNode(_Node);
 
-            var dstNode = context.GetNode(_Node);
+                schema2Target.ApplyTo(dstNode, context);
 
-            schema2Target.Setup(dstNode, context);
+                Schema2SceneBuilder.SetMorphAnimation(dstNode, this.Morphings);
 
-            Schema2SceneBuilder.SetMorphAnimation(dstNode, this.Morphings);
+                System.Diagnostics.Debug.Assert(dstNode.WorldMatrix == this.GetPoseWorldMatrix(), "Transform mismatch!");
+            }
         }
     }
 
     partial class SkinnedTransformer : SCHEMA2SCENE
     {
-        void SCHEMA2SCENE.Setup(Scene dstScene, Schema2SceneBuilder context)
+        void SCHEMA2SCENE.ApplyTo(Scene dstScene, Schema2SceneBuilder context)
         {
             if (!(Content is SCHEMA2NODE schema2Target)) return;
 
+            // a skinned mesh is controlled indirectly by its bones,
+            // so we need to create a dummy container for it:
             var skinnedMeshNode = dstScene.CreateNode();
             skinnedMeshNode.Name = _NodeName;
             skinnedMeshNode.Extras = _NodeExtras;
 
-            if (_MeshPoseWorldMatrix.HasValue)
+            if (_MeshPoseWorldTransform.HasValue)
             {
                 var dstNodes = new Node[_Joints.Count];
 
@@ -73,7 +224,7 @@ namespace SharpGLTF.Scenes
                 }
                 #endif
 
-                skinnedMeshNode.WithSkinBinding(_MeshPoseWorldMatrix.Value, dstNodes);
+                skinnedMeshNode.WithSkinBinding(_MeshPoseWorldTransform.Value.Matrix, dstNodes);
             }
             else
             {
@@ -88,8 +239,7 @@ namespace SharpGLTF.Scenes
             // var root = _Joints[0].Joint.Root;
             // skinnedMeshNode.Skin.Skeleton = context.GetNode(root);
 
-            schema2Target.Setup(skinnedMeshNode, context);
-
+            schema2Target.ApplyTo(skinnedMeshNode, context);
             Schema2SceneBuilder.SetMorphAnimation(skinnedMeshNode, this.Morphings);
         }
     }

+ 42 - 22
src/SharpGLTF.Toolkit/Scenes/Transformers.cs

@@ -7,6 +7,7 @@ using System.Text;
 using SharpGLTF.IO;
 
 using MESHBUILDER = SharpGLTF.Geometry.IMeshBuilder<SharpGLTF.Materials.MaterialBuilder>;
+using TRANSFORM = SharpGLTF.Transforms.AffineTransform;
 
 namespace SharpGLTF.Scenes
 {
@@ -90,6 +91,11 @@ namespace SharpGLTF.Scenes
 
         public Animations.AnimatableProperty<Transforms.SparseWeight8> Morphings => _Morphings;
 
+        /// <summary>
+        /// Gets a value indicating whether <see cref="Content"/> implements <see cref="IRenderableContent"/>
+        /// </summary>
+        public bool HasRenderableContent => _Content is IRenderableContent;
+
         #endregion
 
         #region API
@@ -164,25 +170,33 @@ namespace SharpGLTF.Scenes
     {
         #region lifecycle
 
-        internal FixedTransformer(Object content, Matrix4x4 xform)
+        internal FixedTransformer(Object content, Transforms.AffineTransform transform)
+            : base(content)
+        {
+            _ChildTransform = transform;
+        }
+
+        internal FixedTransformer(Object content, NodeBuilder parentNode, Transforms.AffineTransform childTransform)
             : base(content)
         {
-            _WorldTransform = xform;
+            _ParentNode = parentNode;
+            _ChildTransform = childTransform;
         }
 
-        protected FixedTransformer(FixedTransformer other)
+        protected FixedTransformer(FixedTransformer other, DeepCloneContext args)
             : base(other)
         {
             Guard.NotNull(other, nameof(other));
 
+            this._ParentNode = args.GetNode(other._ParentNode);
             this._NodeName = other._NodeName;
             this._NodeExtras = other._NodeExtras.DeepClone();
-            this._WorldTransform = other._WorldTransform;
+            this._ChildTransform = other._ChildTransform;
         }
 
         public override ContentTransformer DeepClone(DeepCloneContext args)
         {
-            return new FixedTransformer(this);
+            return new FixedTransformer(this, args);
         }
 
         #endregion
@@ -196,7 +210,10 @@ namespace SharpGLTF.Scenes
         private IO.JsonContent _NodeExtras;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private Matrix4x4 _WorldTransform;
+        private NodeBuilder _ParentNode;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private Transforms.AffineTransform _ChildTransform;
 
         #endregion
 
@@ -216,19 +233,20 @@ namespace SharpGLTF.Scenes
             set => _NodeExtras = value;
         }
 
-        public Matrix4x4 WorldMatrix
-        {
-            get => _WorldTransform;
-            set => _WorldTransform = value;
-        }
+        public NodeBuilder ParentNode => _ParentNode;
+
+        public Transforms.AffineTransform ChildTransform => _ChildTransform;
 
         #endregion
 
         #region API
 
-        public override NodeBuilder GetArmatureRoot() { return null; }
+        public override NodeBuilder GetArmatureRoot() { return _ParentNode?.Root; }
 
-        public override Matrix4x4 GetPoseWorldMatrix() => WorldMatrix;
+        public override Matrix4x4 GetPoseWorldMatrix()
+        {
+            return _ParentNode == null ? _ChildTransform.Matrix : _ChildTransform.Matrix * _ParentNode.WorldMatrix;
+        }
 
         #endregion
 
@@ -313,10 +331,10 @@ namespace SharpGLTF.Scenes
     {
         #region lifecycle
 
-        internal SkinnedTransformer(MESHBUILDER mesh, Matrix4x4 meshWorldMatrix, NodeBuilder[] joints)
+        internal SkinnedTransformer(MESHBUILDER mesh, TRANSFORM meshWorldTransform, NodeBuilder[] joints)
             : base(mesh)
         {
-            SetJoints(meshWorldMatrix, joints);
+            SetJoints(meshWorldTransform, joints);
         }
 
         internal SkinnedTransformer(MESHBUILDER mesh, (NodeBuilder Joint, Matrix4x4 InverseBindMatrix)[] joints)
@@ -332,7 +350,7 @@ namespace SharpGLTF.Scenes
 
             this._NodeName = other._NodeName;
             this._NodeExtras = other._NodeExtras.DeepClone();
-            this._MeshPoseWorldMatrix = other._MeshPoseWorldMatrix;
+            this._MeshPoseWorldTransform = other._MeshPoseWorldTransform;
 
             foreach (var (joint, inverseBindMatrix) in other._Joints)
             {
@@ -361,7 +379,7 @@ namespace SharpGLTF.Scenes
         /// Defines the world matrix of the mesh at the time of binding.
         /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
-        private Matrix4x4? _MeshPoseWorldMatrix;
+        private TRANSFORM? _MeshPoseWorldTransform;
 
         // condition: all NodeBuilder objects must have the same root.
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
@@ -389,12 +407,12 @@ namespace SharpGLTF.Scenes
 
         #region API
 
-        private void SetJoints(Matrix4x4 meshWorldMatrix, NodeBuilder[] joints)
+        private void SetJoints(TRANSFORM meshWorldTransform, NodeBuilder[] joints)
         {
             Guard.NotNull(joints, nameof(joints));
             Guard.IsTrue(NodeBuilder.IsValidArmature(joints), nameof(joints));
 
-            _MeshPoseWorldMatrix = meshWorldMatrix;
+            _MeshPoseWorldTransform = meshWorldTransform;
             _Joints.Clear();
             _Joints.AddRange(joints.Select(item => (item, (Matrix4x4?)null)));
         }
@@ -404,7 +422,7 @@ namespace SharpGLTF.Scenes
             Guard.NotNull(joints, nameof(joints));
             Guard.IsTrue(NodeBuilder.IsValidArmature(joints.Select(item => item.Joint)), nameof(joints));
 
-            _MeshPoseWorldMatrix = null;
+            _MeshPoseWorldTransform = null;
             _Joints.Clear();
             _Joints.AddRange(joints.Select(item => (item.Joint, (Matrix4x4?)item.InverseBindMatrix)));
         }
@@ -413,10 +431,12 @@ namespace SharpGLTF.Scenes
         {
             var jb = new (NodeBuilder Joint, Matrix4x4 InverseBindMatrix)[_Joints.Count];
 
+            var meshPoseWorld = _MeshPoseWorldTransform?.Matrix ?? Matrix4x4.Identity;
+
             for (int i = 0; i < jb.Length; ++i)
             {
                 var j = _Joints[i].Joint;
-                var m = _Joints[i].InverseBindMatrix ?? Transforms.SkinnedTransform.CalculateInverseBinding(_MeshPoseWorldMatrix ?? Matrix4x4.Identity, j.WorldMatrix);
+                var m = _Joints[i].InverseBindMatrix ?? Transforms.SkinnedTransform.CalculateInverseBinding(meshPoseWorld, j.WorldMatrix);
 
                 jb[i] = (j, m);
             }
@@ -445,7 +465,7 @@ namespace SharpGLTF.Scenes
                 );
         }
 
-        public override Matrix4x4 GetPoseWorldMatrix() => _MeshPoseWorldMatrix ?? Matrix4x4.Identity;
+        public override Matrix4x4 GetPoseWorldMatrix() => _MeshPoseWorldTransform?.Matrix ?? Matrix4x4.Identity;
 
         #endregion
     }

+ 50 - 0
src/SharpGLTF.Toolkit/Schema2/AccessorExtensions.cs

@@ -18,5 +18,55 @@ namespace SharpGLTF.Schema2
 
             return accessor;
         }
+
+        public static unsafe BufferView CreateBufferView<T>(this ModelRoot root, IReadOnlyList<T> data)
+            where T : unmanaged
+        {
+            var view = root.CreateBufferView(sizeof(T) * data.Count);
+
+            if (typeof(T) == typeof(int))
+            {
+                new Memory.IntegerArray(view.Content, IndexEncodingType.UNSIGNED_INT).Fill(data as IReadOnlyList<int>);
+                return view;
+            }
+
+            if (typeof(T) == typeof(Single))
+            {
+                new Memory.ScalarArray(view.Content).Fill(data as IReadOnlyList<Single>);
+                return view;
+            }
+
+            if (typeof(T) == typeof(Vector2))
+            {
+                new Memory.Vector2Array(view.Content).Fill(data as IReadOnlyList<Vector2>);
+                return view;
+            }
+
+            if (typeof(T) == typeof(Vector3))
+            {
+                new Memory.Vector3Array(view.Content).Fill(data as IReadOnlyList<Vector3>);
+                return view;
+            }
+
+            if (typeof(T) == typeof(Vector4))
+            {
+                new Memory.Vector4Array(view.Content).Fill(data as IReadOnlyList<Vector4>);
+                return view;
+            }
+
+            if (typeof(T) == typeof(Quaternion))
+            {
+                new Memory.QuaternionArray(view.Content).Fill(data as IReadOnlyList<Quaternion>);
+                return view;
+            }
+
+            if (typeof(T) == typeof(Matrix4x4))
+            {
+                new Memory.Matrix4x4Array(view.Content).Fill(data as IReadOnlyList<Matrix4x4>);
+                return view;
+            }
+
+            throw new ArgumentException(typeof(T).Name);
+        }
     }
 }

+ 166 - 28
src/SharpGLTF.Toolkit/Schema2/MeshExtensions.cs

@@ -321,6 +321,109 @@ namespace SharpGLTF.Schema2
             return primitive;
         }
 
+        public static unsafe MeshGpuInstancing WithInstanceAccessor<T>(this MeshGpuInstancing instancing, string attribute, IReadOnlyList<T> values)
+            where T : unmanaged
+        {
+            Guard.NotNull(instancing, nameof(instancing));
+            Guard.NotNull(values, nameof(values));
+
+            var root = instancing.LogicalParent.LogicalParent;
+            var view = root.CreateBufferView(values);
+
+            var accessor = root.CreateAccessor();
+
+            if (typeof(T) == typeof(int))
+            {
+                accessor.SetIndexData(view, 0, values.Count, IndexEncodingType.UNSIGNED_INT);
+            }
+            else
+            {
+                var dt = DimensionType.CUSTOM;
+                if (typeof(T) == typeof(Single)) dt = DimensionType.SCALAR;
+                if (typeof(T) == typeof(Vector2)) dt = DimensionType.VEC2;
+                if (typeof(T) == typeof(Vector3)) dt = DimensionType.VEC3;
+                if (typeof(T) == typeof(Vector4)) dt = DimensionType.VEC4;
+                if (typeof(T) == typeof(Quaternion)) dt = DimensionType.VEC4;
+                if (typeof(T) == typeof(Matrix4x4)) dt = DimensionType.MAT4;
+
+                if (dt == DimensionType.CUSTOM) throw new ArgumentException(typeof(T).Name);
+
+                accessor.SetVertexData(view, 0, values.Count, dt, EncodingType.FLOAT, false);
+            }
+
+            instancing.SetAccessor(attribute, accessor);
+
+            return instancing;
+        }
+
+        public static MeshGpuInstancing WithInstanceAccessors(this MeshGpuInstancing instancing, IReadOnlyList<Transforms.AffineTransform> transforms)
+        {
+            Guard.NotNull(instancing, nameof(instancing));
+            Guard.NotNull(transforms, nameof(transforms));
+
+            var xfrms = transforms.Select(item => item.GetDecomposed());
+            var hasS = xfrms.Any(item => item.Scale != Vector3.One);
+            var hasR = xfrms.Any(item => item.Rotation != Quaternion.Identity);
+            var hasT = xfrms.Any(item => item.Translation != Vector3.Zero);
+
+            if (hasS) instancing.WithInstanceAccessor("SCALE", xfrms.Select(item => item.Scale).ToList());
+            if (hasR) instancing.WithInstanceAccessor("ROTATION", xfrms.Select(item => item.Rotation).ToList());
+            if (hasT) instancing.WithInstanceAccessor("TRANSLATION", xfrms.Select(item => item.Translation).ToList());
+
+            return instancing;
+        }
+
+        public static MeshGpuInstancing WithInstanceCustomAccessors(this MeshGpuInstancing instancing, IReadOnlyList<IO.JsonContent> extras)
+        {
+            Guard.NotNull(instancing, nameof(instancing));
+
+            // gather attribute keys
+            var keys = extras
+                .Select(item => item.Content)
+                .OfType<IReadOnlyDictionary<string, Object>>()
+                .SelectMany(item => item.Keys)
+                .Distinct()
+                .Where(item => item.StartsWith("_"));
+
+            foreach (var key in keys)
+            {
+                Object valueGetter(IO.JsonContent extra)
+                {
+                    if (!(extra.Content is IReadOnlyDictionary<string, Object> dict)) return null;
+                    return dict.TryGetValue(key, out var val) ? val : null;
+                }
+
+                var values = extras.Select(valueGetter).ToList();
+
+                instancing.WithInstanceCustomAccessor(key, values);
+            }
+
+            return instancing;
+        }
+
+        public static MeshGpuInstancing WithInstanceCustomAccessor(this MeshGpuInstancing instancing, string attribute, IReadOnlyList<Object> values)
+        {
+            Guard.NotNullOrEmpty(attribute, nameof(attribute));
+
+            attribute = attribute.ToUpper();
+            var expectedType = values.Where(item => item != null).FirstOrDefault()?.GetType();
+            if (expectedType == null) return instancing;
+
+            if (expectedType == typeof(int))
+            {
+                var xValues = values.Select(item => item is int val ? val : 0).ToList();
+                return instancing.WithInstanceAccessor(attribute, xValues);
+            }
+
+            if (expectedType == typeof(Single))
+            {
+                var xValues = values.Select(item => item is float val ? val : 0).ToList();
+                return instancing.WithInstanceAccessor(attribute, xValues);
+            }
+
+            throw new ArgumentException(expectedType.Name);
+        }
+
         #endregion
 
         #region material
@@ -352,14 +455,19 @@ namespace SharpGLTF.Schema2
             var points = prim.GetPointIndices();
             if (!points.Any()) yield break;
 
-            var vertices = prim.GetVertexColumns(xform);
+            var vertices = prim.GetVertexColumns();
             var vtype = vertices.GetCompatibleVertexType();
 
-            foreach (var t in points)
+            foreach (var xinst in Transforms.InstancingTransform.Evaluate(xform))
             {
-                var a = vertices.GetVertex(vtype, t);
+                var xvertices = xinst != null ? vertices.WithTransform(xinst) : vertices;
+
+                foreach (var t in points)
+                {
+                    var a = xvertices.GetVertex(vtype, t);
 
-                yield return (a, prim.Material);
+                    yield return (a, prim.Material);
+                }
             }
         }
 
@@ -378,15 +486,20 @@ namespace SharpGLTF.Schema2
             var lines = prim.GetLineIndices();
             if (!lines.Any()) yield break;
 
-            var vertices = prim.GetVertexColumns(xform);
+            var vertices = prim.GetVertexColumns();
             var vtype = vertices.GetCompatibleVertexType();
 
-            foreach (var (la, lb) in lines)
+            foreach (var xinst in Transforms.InstancingTransform.Evaluate(xform))
             {
-                var va = vertices.GetVertex(vtype, la);
-                var vb = vertices.GetVertex(vtype, lb);
+                var xvertices = xinst != null ? vertices.WithTransform(xinst) : vertices;
 
-                yield return (va, vb, prim.Material);
+                foreach (var (la, lb) in lines)
+                {
+                    var va = xvertices.GetVertex(vtype, la);
+                    var vb = xvertices.GetVertex(vtype, lb);
+
+                    yield return (va, vb, prim.Material);
+                }
             }
         }
 
@@ -402,19 +515,23 @@ namespace SharpGLTF.Schema2
             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);
-            var vtype = vertices.GetCompatibleVertexType();
-
-            foreach (var (ta, tb, tc) in triangles)
+            foreach (var xinst in Transforms.InstancingTransform.Evaluate(xform))
             {
-                var va = vertices.GetVertex(vtype, ta);
-                var vb = vertices.GetVertex(vtype, tb);
-                var vc = vertices.GetVertex(vtype, tc);
+                var xvertices = xinst != null ? vertices.WithTransform(xinst) : vertices;
+                var vtype = vertices.GetCompatibleVertexType();
+
+                foreach (var (ta, tb, tc) in triangles)
+                {
+                    var va = xvertices.GetVertex(vtype, ta);
+                    var vb = xvertices.GetVertex(vtype, tb);
+                    var vc = xvertices.GetVertex(vtype, tc);
 
-                yield return (va, vb, vc, prim.Material);
+                    yield return (va, vb, vc, prim.Material);
+                }
             }
         }
 
@@ -426,10 +543,30 @@ namespace SharpGLTF.Schema2
             if (xform != null && !xform.Visible) mesh = null;
             if (mesh == null) return Enumerable.Empty<(VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, VertexBuilder<TvG, TvM, TvS>, Material)>();
 
+            var primitives = _GatherMeshGeometry<TvG>(mesh);
+
+            return Transforms.InstancingTransform.Evaluate(xform)
+                .SelectMany
+                (
+                    xinst => primitives.SelectMany
+                    (
+                        prim =>
+                        {
+                            var xvertices = xinst != null ? prim.Vertices.WithTransform(xinst) : prim.Vertices;
+                            return _EvaluateTriangles<TvG, TvM, TvS>(prim.Material, xvertices, prim.Triangles);
+                        }
+
+                    )
+                );
+        }
+
+        private static IReadOnlyList<(Material Material, VertexBufferColumns Vertices, IEnumerable<(int, int, int)> Triangles)> _GatherMeshGeometry<TvG>(Mesh mesh)
+            where TvG : struct, IVertexGeometry
+        {
             var primitives = mesh.Primitives
-                .Where(prim => prim.GetTriangleIndices().Any())
-                .Select(prim => (prim.Material, prim.GetVertexColumns(xform), (IEnumerable<(int, int, int)>)prim.GetTriangleIndices().ToList()))
-                .ToList();
+                            .Where(prim => prim.GetTriangleIndices().Any())
+                            .Select(prim => (prim.Material, prim.GetVertexColumns(), (IEnumerable<(int, int, int)>)prim.GetTriangleIndices().ToList()))
+                            .ToList();
 
             bool needsNormals = default(TvG).TryGetNormal(out Vector3 nrm);
             bool needsTangents = default(TvG).TryGetTangent(out Vector4 tgt);
@@ -454,7 +591,7 @@ namespace SharpGLTF.Schema2
                 if (prims.Any()) VertexBufferColumns.CalculateTangents(prims);
             }
 
-            return primitives.SelectMany(prim => _EvaluateTriangles<TvG, TvM, TvS>(prim.Material, prim.Item2, prim.Item3));
+            return primitives;
         }
 
         private static IEnumerable<(VertexBuilder<TvG, TvM, TvS> A, VertexBuilder<TvG, TvM, TvS> B, VertexBuilder<TvG, TvM, TvS> C, Material Material)> _EvaluateTriangles<TvG, TvM, TvS>(Material material, VertexBufferColumns vertices, IEnumerable<(int A, int B, int C)> indices)
@@ -476,7 +613,7 @@ namespace SharpGLTF.Schema2
 
         #region mesh conversion
 
-        public static VertexBufferColumns GetVertexColumns(this MeshPrimitive primitive, MESHXFORM xform = null)
+        public static VertexBufferColumns GetVertexColumns(this MeshPrimitive primitive)
         {
             Guard.NotNull(primitive, nameof(primitive));
 
@@ -490,8 +627,6 @@ namespace SharpGLTF.Schema2
                 _Initialize(morphTarget, columns.AddMorphTarget());
             }
 
-            if (xform != null) columns.ApplyTransform(xform);
-
             return columns;
         }
 
@@ -506,6 +641,8 @@ namespace SharpGLTF.Schema2
 
             if (vertexAccessors.ContainsKey("TEXCOORD_0")) dstColumns.TexCoords0 = vertexAccessors["TEXCOORD_0"].AsVector2Array();
             if (vertexAccessors.ContainsKey("TEXCOORD_1")) dstColumns.TexCoords1 = vertexAccessors["TEXCOORD_1"].AsVector2Array();
+            if (vertexAccessors.ContainsKey("TEXCOORD_2")) dstColumns.TexCoords2 = vertexAccessors["TEXCOORD_2"].AsVector2Array();
+            if (vertexAccessors.ContainsKey("TEXCOORD_3")) dstColumns.TexCoords3 = vertexAccessors["TEXCOORD_3"].AsVector2Array();
 
             if (vertexAccessors.ContainsKey("JOINTS_0")) dstColumns.Joints0 = vertexAccessors["JOINTS_0"].AsVector4Array();
             if (vertexAccessors.ContainsKey("JOINTS_1")) dstColumns.Joints1 = vertexAccessors["JOINTS_1"].AsVector4Array();
@@ -553,10 +690,11 @@ namespace SharpGLTF.Schema2
         /// <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="options">Evaluation options.</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>
         /// <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, Converter<Material, TMaterial> materialFunc, Animation animation, float time)
+        public static MeshBuilder<TMaterial, TvG, TvM, VertexEmpty> ToStaticMeshBuilder<TMaterial, TvG, TvM>(this Scene srcScene, Converter<Material, TMaterial> materialFunc, Runtime.RuntimeOptions options, Animation animation, float time)
             where TvG : struct, IVertexGeometry
             where TvM : struct, IVertexMaterial
         {
@@ -568,7 +706,7 @@ namespace SharpGLTF.Schema2
 
             Guard.NotNull(materialFunc, nameof(materialFunc));
 
-            foreach (var tri in srcScene.EvaluateTriangles<VertexPositionNormal, VertexColor1Texture1>(animation, time))
+            foreach (var tri in srcScene.EvaluateTriangles<VertexPositionNormal, VertexColor1Texture1>(options, animation, time))
             {
                 var material = materialFunc(tri.Item4);
 
@@ -578,7 +716,7 @@ namespace SharpGLTF.Schema2
             return mesh;
         }
 
-        public static MeshBuilder<Materials.MaterialBuilder, TvG, TvM, VertexEmpty> ToStaticMeshBuilder<TvG, TvM>(this Scene srcScene, Animation animation, float time)
+        public static MeshBuilder<Materials.MaterialBuilder, TvG, TvM, VertexEmpty> ToStaticMeshBuilder<TvG, TvM>(this Scene srcScene, Runtime.RuntimeOptions options, Animation animation, float time)
             where TvG : struct, IVertexGeometry
             where TvM : struct, IVertexMaterial
         {
@@ -598,7 +736,7 @@ namespace SharpGLTF.Schema2
                 return materials[srcMaterial] = dstMaterial;
             }
 
-            return srcScene.ToStaticMeshBuilder<Materials.MaterialBuilder, TvG, TvM>(convertMaterial, animation, time);
+            return srcScene.ToStaticMeshBuilder<Materials.MaterialBuilder, TvG, TvM>(convertMaterial, options, animation, time);
         }
 
         public static IMeshBuilder<Materials.MaterialBuilder> ToMeshBuilder(this Mesh srcMesh)

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

@@ -200,15 +200,16 @@ namespace SharpGLTF.Schema2
         /// Yields a collection of triangles representing the geometry in world space.
         /// </summary>
         /// /// <param name="scene">A <see cref="Scene"/> instance.</param>
+        /// <param name="options">Evaluation options.</param>
         /// <param name="animation">An <see cref="Animation"/> instance, or null.</param>
         /// <param name="time">The animation time.</param>
         /// <returns>A collection of triangles in world space.</returns>
-        public static IEnumerable<(IVertexBuilder A, IVertexBuilder B, IVertexBuilder C, Material Material)> EvaluateTriangles(this Scene scene, Animation animation = null, float time = 0)
+        public static IEnumerable<(IVertexBuilder A, IVertexBuilder B, IVertexBuilder C, Material Material)> EvaluateTriangles(this Scene scene, Runtime.RuntimeOptions options = null, Animation animation = null, float time = 0)
         {
             if (scene == null) return Enumerable.Empty<(IVertexBuilder, IVertexBuilder, IVertexBuilder, Material)>();
 
             var instance = Runtime.SceneTemplate
-                .Create(scene)
+                .Create(scene, options)
                 .CreateInstance();
 
             if (animation == null)
@@ -223,7 +224,6 @@ namespace SharpGLTF.Schema2
             var meshes = scene.LogicalParent.LogicalMeshes;
 
             return instance
-                .DrawableInstances
                 .Where(item => item.Transform.Visible)
                 .SelectMany(item => meshes[item.Template.LogicalMeshIndex].EvaluateTriangles(item.Transform));
         }
@@ -234,17 +234,18 @@ namespace SharpGLTF.Schema2
         /// <typeparam name="TvG">The vertex fragment type with Position, Normal and Tangent.</typeparam>
         /// <typeparam name="TvM">The vertex fragment type with Colors and Texture Coordinates.</typeparam>
         /// <param name="scene">A <see cref="Scene"/> instance.</param>
+        /// <param name="options">Evaluation options.</param>
         /// <param name="animation">An <see cref="Animation"/> instance, or null.</param>
         /// <param name="time">The animation time.</param>
         /// <returns>A collection of triangles in world space.</returns>
-        public static IEnumerable<(VertexBuilder<TvG, TvM, VertexEmpty> A, VertexBuilder<TvG, TvM, VertexEmpty> B, VertexBuilder<TvG, TvM, VertexEmpty> C, Material Material)> EvaluateTriangles<TvG, TvM>(this Scene scene, Animation animation = null, float time = 0)
+        public static IEnumerable<(VertexBuilder<TvG, TvM, VertexEmpty> A, VertexBuilder<TvG, TvM, VertexEmpty> B, VertexBuilder<TvG, TvM, VertexEmpty> C, Material Material)> EvaluateTriangles<TvG, TvM>(this Scene scene, Runtime.RuntimeOptions options = null, Animation animation = null, float time = 0)
             where TvG : struct, IVertexGeometry
             where TvM : struct, IVertexMaterial
         {
             if (scene == null) return Enumerable.Empty<(VertexBuilder<TvG, TvM, VertexEmpty>, VertexBuilder<TvG, TvM, VertexEmpty>, VertexBuilder<TvG, TvM, VertexEmpty>, Material)>();
 
             var instance = Runtime.SceneTemplate
-                .Create(scene)
+                .Create(scene, options)
                 .CreateInstance();
 
             if (animation == null)
@@ -259,7 +260,6 @@ namespace SharpGLTF.Schema2
             var meshes = scene.LogicalParent.LogicalMeshes;
 
             return instance
-                .DrawableInstances
                 .Where(item => item.Transform.Visible)
                 .SelectMany(item => meshes[item.Template.LogicalMeshIndex].EvaluateTriangles<TvG, TvM, VertexEmpty>(item.Transform));
         }

BIN
tests/Assets/gltf-GpuMeshInstancing/GrassFieldInstanced.glb


BIN
tests/Assets/gltf-GpuMeshInstancing/InstanceTest.glb


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

@@ -76,6 +76,9 @@ namespace SharpGLTF
             }
             else if (fileName.ToLower().EndsWith(".obj"))
             {
+                // skip exporting to obj if gpu instancing is there
+                if (Node.Flatten(model.DefaultScene).Any(n => n.GetGpuInstancing() != null)) return fileName;                
+
                 fileName = fileName.Replace(" ", "_");
                 model.SaveAsWavefront(fileName);
             }

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

@@ -32,6 +32,12 @@ namespace SharpGLTF
             return new Vector4(rnd.NextSingle(), rnd.NextSingle(), rnd.NextSingle(), rnd.NextSingle());
         }
 
+        public static Quaternion NextQuaternion(this Random rnd)
+        {
+            var r = rnd.NextVector3() * (float)Math.PI*2;
+            return Quaternion.CreateFromYawPitchRoll(r.X, r.Y, r.Z);
+        }
+
         public static float GetAngle(this (Quaternion a, Quaternion b) pair)
         {
             var w = Quaternion.Concatenate(pair.b, Quaternion.Inverse(pair.a)).W;

+ 67 - 59
tests/SharpGLTF.NUnit/TestFiles.cs

@@ -8,90 +8,92 @@ using NUnit.Framework;
 namespace SharpGLTF
 {
     /// <summary>
-    /// Encapsulates the access to test files.
+    /// Centralices the access to test files.
     /// </summary>
     public static class TestFiles
     {
         #region lifecycle
 
-        static TestFiles()
+        private static void _EnsureInitialized()
         {
+            if (_TestFilesDir != null) return;
+
             var wdir = TestContext.CurrentContext.WorkDirectory;
 
-            _ExamplesFound = false;
+            var examplesFound = false;
 
             while (wdir.Length > 3)
             {
-                _RootDir = System.IO.Path.Combine(wdir, "TestFiles");
+                _TestFilesDir = System.IO.Path.Combine(wdir, "TestFiles");
 
-                if (wdir.ToLower().EndsWith("tests") && System.IO.Directory.Exists(_RootDir))
+                if (wdir.ToLower().EndsWith("tests") && System.IO.Directory.Exists(_TestFilesDir))
                 {
-                    _ExamplesFound = true;
+                    examplesFound = true;
                     break;
                 }
 
                 wdir = System.IO.Path.GetDirectoryName(wdir);
             }
 
-            _Check();
+            Assert.IsTrue(examplesFound, "TestFiles directory not found; please, run '1_DownloadTestFiles.cmd' before running the tests.");            
 
-            _SchemaDir = System.IO.Path.Combine(_RootDir, "glTF-Schema");
-            _ValidationDir = System.IO.Path.Combine(_RootDir, "glTF-Validator");
-            _SampleModelsDir = System.IO.Path.Combine(_RootDir, "glTF-Sample-Models");
+            _AssetFilesDir = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(_TestFilesDir), "Assets");
+        }
 
-            _PollyModelsDir = System.IO.Path.Combine(_RootDir, "glTF-Blender-Exporter");
-            _UniVRMModelsDir = System.IO.Path.Combine(_RootDir, "UniVRM");
+        private static string _UsingExternalFiles(params string[] subPath)
+        {
+            _EnsureInitialized();           
+
+            return System.IO.Path.Combine(new string[] { _TestFilesDir }.Concat(subPath).ToArray());
+        }
 
-            _BabylonJsMeshesDir = System.IO.Path.Combine(_RootDir, "BabylonJS-Assets");
-            _BabylonJsPlaygroundDir = System.IO.Path.Combine(_RootDir, "BabylonJS-PlaygroundScenes");
+        private static string _UsingInternalFiles(params string[] subPath)
+        {
+            _EnsureInitialized();
 
-            _GeneratedModelsDir = System.IO.Path.Combine(_RootDir, "GeneratedReferenceModels", "v_0_6_1");
-        }       
+            return System.IO.Path.Combine(new string[] { _AssetFilesDir }.Concat(subPath).ToArray());
+        }
 
         #endregion
 
         #region data
 
-        private static Boolean _ExamplesFound = false;
-
-        private static readonly string _RootDir;
+        /// <summary>
+        /// Path to Tests/Assets/
+        /// </summary>
+        private static string _AssetFilesDir;
 
-        private static readonly string _SchemaDir;
-        private static readonly string _ValidationDir;
-        internal static readonly string _SampleModelsDir;
+        /// <summary>
+        /// Path to Tests/TestFiles/
+        /// </summary>
+        private static string _TestFilesDir;
 
-        private static readonly string _PollyModelsDir;
-        private static readonly string _UniVRMModelsDir;
-        private static readonly string _BabylonJsMeshesDir;
-        private static readonly string _BabylonJsPlaygroundDir;
-        private static readonly string _GeneratedModelsDir;        
+        private static readonly string _SchemaDir = _UsingExternalFiles("glTF-Schema");
+        private static readonly string _ValidationDir = _UsingExternalFiles("glTF-Validator");
+        internal static readonly string _SampleModelsDir = _UsingExternalFiles("glTF-Sample-Models");
+        
+        private static readonly string _BabylonJsMeshesDir = _UsingExternalFiles("BabylonJS-Assets");
+        private static readonly string _GeneratedModelsDir = _UsingExternalFiles("GeneratedReferenceModels", "v_0_6_1");
 
         #endregion
 
-        #region properties
+        #region properties        
 
-        public static string RootDirectory { get { _Check(); return _RootDir; } }
+        public static string KhronosSampleModelsDirectory => _SampleModelsDir;
 
         #endregion
 
         #region API
 
-        private static void _Check()
-        {
-            Assert.IsTrue(_ExamplesFound, "TestFiles directory not found; please, run '1_DownloadTestFiles.cmd' before running the tests.");            
-        }
+        
 
         public static IReadOnlyList<string> GetSchemaExtensionsModelsPaths()
         {
-            _Check();
-
             return GetModelPathsInDirectory(_SchemaDir, "extensions", "2.0");         
         }
 
         public static IEnumerable<string> GetReferenceModelPaths(bool useNegative = false)
         {
-            _Check();
-
             var dirPath = _GeneratedModelsDir;
             if (dirPath.EndsWith(".zip")) dirPath = dirPath.Substring(0, dirPath.Length - 4);
 
@@ -129,8 +131,6 @@ namespace SharpGLTF
 
         public static IReadOnlyList<string> GetSampleModelsPaths()
         {
-            _Check();
-
             var entries = KhronosSampleModel.Load();
 
             var files = entries
@@ -142,8 +142,6 @@ namespace SharpGLTF
 
         public static IReadOnlyList<string> GetKhronosValidationPaths()
         {
-            _Check();
-
             var skip = new string[]
             {
                 "empty_object.gltf", // need to look further
@@ -186,10 +184,8 @@ namespace SharpGLTF
                 .ToList();
         }
 
-        public static IReadOnlyList<string> GetBabylonJSModelsPaths(bool validOrInvalid = true)
+        public static IReadOnlyList<string> GetBabylonJSModelsPaths()
         {
-            _Check();
-
             var skipAlways = new string[]
             {
                 "\\Elf\\Elf.gltf", // validator reports invalid inverse bind matrices.
@@ -197,42 +193,42 @@ namespace SharpGLTF
                 "\\meshes\\KHR_materials_volume_testing.glb", // draco compression-
                 "\\meshes\\Yeti\\MayaExport\\", // validator reports out of bounds accesor
                 "\\meshes\\Demos\\retargeting\\riggedMesh.glb", // validator reports errors
-            };
-
-            var invalid = new string[]
-            {                
-                
-            };
+            };            
 
             var files = GetModelPathsInDirectory(_BabylonJsMeshesDir);
 
             return files
                 .Where(item => !item.ToLower().Contains("gltf-draco"))
                 .Where(item => !item.ToLower().Contains("gltf-meshopt")) // not supported yet
-                .Where(item => skipAlways.All(f => !item.Contains(f)))
-                .Where(item => validOrInvalid == invalid.All(f => !item.EndsWith(f)))
+                .Where(item => skipAlways.All(f => !item.Contains(f)))                
                 .OrderBy(item => item)                
                 .ToList();
         }        
 
         public static string GetPollyFileModelPath()
         {
-            _Check();
-
-            return System.IO.Path.Combine(_PollyModelsDir, "polly", "project_polly.glb");
+            return _UsingExternalFiles("glTF-Blender-Exporter", "polly", "project_polly.glb");
         }
 
         public static string GetUniVRMModelPath()
         {
-            _Check();
+            return _UsingExternalFiles("UniVRM", "AliciaSolid_vrm-0.51.vrm");
+        }
+
+        public static IEnumerable<string> GetMeshIntancingModelPaths()
+        {
+            var fromBabylon = GetBabylonJSModelsPaths()
+                .Where(item => item.ToLower().Contains("teapot"));
+
+            var meshInstPath = _UsingInternalFiles("gltf-GpuMeshInstancing");
 
-            return System.IO.Path.Combine(_UniVRMModelsDir, "AliciaSolid_vrm-0.51.vrm");
+            var fromLocal = System.IO.Directory.GetFiles(meshInstPath, "*.glb", System.IO.SearchOption.AllDirectories);
+
+            return fromBabylon.Concat(fromLocal);
         }
 
         private static IReadOnlyList<string> GetModelPathsInDirectory(params string[] paths)
         {
-            _Check();
-
             var dirPath = System.IO.Path.Combine(paths);
 
             if (dirPath.EndsWith(".zip")) dirPath = dirPath.Substring(0, dirPath.Length-4);
@@ -252,6 +248,8 @@ namespace SharpGLTF
     [System.Diagnostics.DebuggerDisplay("{Name}")]
     class KhronosSampleModel
     {
+        #region loaders
+
         public static KhronosSampleModel[] Load()
         {
             var path = System.IO.Path.Combine(TestFiles._SampleModelsDir, "2.0", "model-index.json");
@@ -270,10 +268,18 @@ namespace SharpGLTF
             return System.Text.Json.JsonSerializer.Deserialize<KhronosSampleModel[]>(json, opts);
         }
 
+        #endregion
+
+        #region data
+
         public string Name { get; set; }
         public string Screenshot { get; set; }
         public Dictionary<string, string> Variants { get; set; } = new Dictionary<string, string>();
 
+        #endregion
+
+        #region API
+
         public IEnumerable<string> GetPaths(params string[] basePath)
         {
             var rootPath = System.IO.Path.Combine(basePath);
@@ -285,5 +291,7 @@ namespace SharpGLTF
                 yield return System.IO.Path.Combine(rootPath, Name, variant.Key, variant.Value);
             }
         }
+
+        #endregion
     }
 }

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

@@ -90,7 +90,7 @@ namespace SharpGLTF.Runtime
                 }
             }
 
-            var worldTriangles = sceneInstance.DrawableInstances.SelectMany(item => evaluateTriangles(item));            
+            var worldTriangles = sceneInstance.SelectMany(item => evaluateTriangles(item));            
 
             var scenePlot = new PlotlyScene();
             scenePlot.AppendTriangles(worldTriangles, c=>c);

+ 4 - 4
tests/SharpGLTF.Tests/Schema2/Authoring/ExtensionsCreationTests.cs

@@ -49,7 +49,7 @@ namespace SharpGLTF.Schema2.Authoring
             TestContext.CurrentContext.AttachShowDirLink();
             TestContext.CurrentContext.AttachGltfValidatorLinks();
 
-            var basePath = System.IO.Path.Combine(TestFiles.RootDirectory, "glTF-Sample-Models", "2.0", "SpecGlossVsMetalRough", "glTF");
+            var basePath = System.IO.Path.Combine(TestFiles.KhronosSampleModelsDirectory, "2.0", "SpecGlossVsMetalRough", "glTF");
 
             // first, create a default material
             var material = new Materials.MaterialBuilder("material1")
@@ -91,7 +91,7 @@ namespace SharpGLTF.Schema2.Authoring
             TestContext.CurrentContext.AttachShowDirLink();
             TestContext.CurrentContext.AttachGltfValidatorLinks();
 
-            var basePath = System.IO.Path.Combine(TestFiles.RootDirectory, "glTF-Sample-Models", "2.0", "SpecGlossVsMetalRough", "glTF");
+            var basePath = System.IO.Path.Combine(TestFiles.KhronosSampleModelsDirectory, "2.0", "SpecGlossVsMetalRough", "glTF");
 
             // first, create a default material
             var material = new Materials.MaterialBuilder("material")
@@ -131,7 +131,7 @@ namespace SharpGLTF.Schema2.Authoring
             TestContext.CurrentContext.AttachShowDirLink();
             TestContext.CurrentContext.AttachGltfValidatorLinks();
 
-            var basePath = System.IO.Path.Combine(TestFiles.RootDirectory, "glTF-Sample-Models", "2.0", "SpecGlossVsMetalRough", "glTF");
+            var basePath = System.IO.Path.Combine(TestFiles.KhronosSampleModelsDirectory, "2.0", "SpecGlossVsMetalRough", "glTF");
             
             var material = new Materials.MaterialBuilder("material")
                 .WithMetallicRoughnessShader()
@@ -168,7 +168,7 @@ namespace SharpGLTF.Schema2.Authoring
             TestContext.CurrentContext.AttachShowDirLink();
             TestContext.CurrentContext.AttachGltfValidatorLinks();
 
-            var basePath = System.IO.Path.Combine(TestFiles.RootDirectory, "glTF-Sample-Models", "2.0", "SpecGlossVsMetalRough", "glTF");
+            var basePath = System.IO.Path.Combine(TestFiles.KhronosSampleModelsDirectory, "2.0", "SpecGlossVsMetalRough", "glTF");
 
             var material = new Materials.MaterialBuilder("material")
                 .WithMetallicRoughnessShader()

+ 37 - 21
tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSampleTests.cs

@@ -118,33 +118,49 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
             foreach (var f in TestFiles.GetBabylonJSModelsPaths())
             {
-                TestContext.Progress.WriteLine(f);
-
                 _LoadModel(f, true);
             }
-        }
+        }        
 
-        [Test]
-        public void LoadInvalidModelsFromBabylonJs()
+        [TestCase("TeapotsGalore.gltf")]
+        [TestCase("GrassFieldInstanced.glb")]
+        [TestCase("InstanceTest.glb")]
+        public void LoadModelsWithGpuMeshInstancingExtension(string fileFilter)
         {
             TestContext.CurrentContext.AttachShowDirLink();
             TestContext.CurrentContext.AttachGltfValidatorLinks();
 
-            foreach (var f in TestFiles.GetBabylonJSModelsPaths(false))
-            {
-                TestContext.Progress.WriteLine(f);
+            var f = TestFiles.GetMeshIntancingModelPaths().FirstOrDefault(item => item.Contains(fileFilter));
+                        
+            var model = _LoadModel(f, false);
 
-                try
-                {
-                    var model = ModelRoot.Load(f, Validation.ValidationMode.Strict);
-                    
-                    Assert.Fail($"{f} Should throw");
-                }
-                catch(Exception ex)
-                {
-                    TestContext.WriteLine(ex.Message);
-                }
-            }
+            var ff = System.IO.Path.GetFileNameWithoutExtension(f);
+
+            model.AttachToCurrentTest($"{ff}.loaded.glb");
+
+            // perform roundtrip
+
+            var roundtripDefault = model.DefaultScene
+                .ToSceneBuilder()                                       // glTF to SceneBuilder
+                .ToGltf2(Scenes.SceneBuilderSchema2Settings.Default);   // SceneBuilder to glTF
+
+            var roundtripInstanced = model.DefaultScene
+                .ToSceneBuilder()                                               // glTF to SceneBuilder
+                .ToGltf2(Scenes.SceneBuilderSchema2Settings.WithGpuInstancing); // SceneBuilder to glTF
+
+            // compare bounding spheres
+
+            var modelBounds = Runtime.MeshDecoder.EvaluateBoundingBox(model.DefaultScene);
+            var rtripDefBounds = Runtime.MeshDecoder.EvaluateBoundingBox(roundtripDefault.DefaultScene);
+            var rtripGpuBounds = Runtime.MeshDecoder.EvaluateBoundingBox(roundtripInstanced.DefaultScene);
+
+            Assert.AreEqual(modelBounds, rtripDefBounds);
+            Assert.AreEqual(modelBounds, rtripGpuBounds);
+
+            // save results
+
+            roundtripDefault.AttachToCurrentTest($"{ff}.roundtrip.default.glb");
+            roundtripInstanced.AttachToCurrentTest($"{ff}.roundtrip.instancing.glb");            
         }
 
         [TestCase("SpecGlossVsMetalRough.gltf")]
@@ -251,7 +267,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
             Assert.NotNull(model);
 
             var triangles = model.DefaultScene
-                .EvaluateTriangles<Geometry.VertexTypes.VertexPosition, Geometry.VertexTypes.VertexEmpty>(null, 0)
+                .EvaluateTriangles<Geometry.VertexTypes.VertexPosition, Geometry.VertexTypes.VertexEmpty>(null, null, 0)
                 .ToArray();
 
             model.AttachToCurrentTest(System.IO.Path.ChangeExtension(System.IO.Path.GetFileName(path), ".obj"));
@@ -355,7 +371,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
                 TestContext.WriteLine($"    Morph Sparse : {msw.Weight0} {msw.Weight1}");
 
                 var triangles = model.DefaultScene
-                    .EvaluateTriangles<Geometry.VertexTypes.VertexPosition, Geometry.VertexTypes.VertexEmpty>(anim, t)
+                    .EvaluateTriangles<Geometry.VertexTypes.VertexPosition, Geometry.VertexTypes.VertexEmpty>(null, anim, t)
                     .ToList();
 
                 var vertices = triangles

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

@@ -57,7 +57,7 @@ namespace SharpGLTF.Schema2.LoadAndSave
             Assert.NotNull(model);
 
             var triangles = model.DefaultScene
-                .EvaluateTriangles<Geometry.VertexTypes.VertexPosition, Geometry.VertexTypes.VertexTexture1>(model.LogicalAnimations[0], 0.5f)
+                .EvaluateTriangles<Geometry.VertexTypes.VertexPosition, Geometry.VertexTypes.VertexTexture1>(null, model.LogicalAnimations[0], 0.5f)
                 .ToList();
 
             // Save as GLB, and also evaluate all triangles and save as Wavefront OBJ            

+ 57 - 23
tests/SharpGLTF.Toolkit.Tests/Scenes/SceneBuilderTests.cs

@@ -20,7 +20,7 @@ namespace SharpGLTF.Scenes
 
 
     [Category("Toolkit.Scenes")]
-    public class SceneBuilderTests
+    public partial class SceneBuilderTests
     {
         [Test(Description ="Creates a simple cube.")]
         public void CreateCubeScene()
@@ -140,8 +140,9 @@ namespace SharpGLTF.Scenes
             scene.AttachToCurrentTest("NonConvexQuads.gltf");
         }
         
-        [Test(Description = "Creates a scene with multiple cubes and spheres.")]
-        public void CreateSceneWithRandomShapes()
+        [TestCase(false)]
+        [TestCase(true)]
+        public void CreateSceneWithRandomShapes(bool useGpuInstancing)
         {
             TestContext.CurrentContext.AttachShowDirLink();
             TestContext.CurrentContext.AttachGltfValidatorLinks();
@@ -149,43 +150,76 @@ namespace SharpGLTF.Scenes
             var rnd = new Random(177);
 
             // create materials
+
             var materials = Enumerable
                 .Range(0, 10)
-                .Select(idx => new Materials.MaterialBuilder()
+                .Select(idx => new MaterialBuilder()
                 .WithChannelParam("BaseColor", new Vector4(rnd.NextVector3(), 1)))
                 .ToList();
-            
+
+            // create meshes
+
+            var sphereMeshes = Enumerable
+                .Range(0, 10)
+                .Select(idx => materials[idx])
+                .Select(mat =>
+                {
+                    var mesh = VPOSNRM.CreateCompatibleMesh("shape");
+                    #if DEBUG
+                    mesh.VertexPreprocessor.SetValidationPreprocessors();
+                    #else
+                    mesh.VertexPreprocessor.SetSanitizerPreprocessors();
+                    #endif
+                    mesh.AddSphere(mat, 0.5f, Matrix4x4.Identity);
+                    mesh.Validate();
+                    return mesh;
+                });
+
+            var cubeMeshes = Enumerable
+                .Range(0, 10)
+                .Select(idx => materials[idx])
+                .Select(mat =>
+                {
+                    var mesh = VPOSNRM.CreateCompatibleMesh("shape");
+                    #if DEBUG
+                    mesh.VertexPreprocessor.SetValidationPreprocessors();
+                    #else
+                    mesh.VertexPreprocessor.SetSanitizerPreprocessors();
+                    #endif
+                    mesh.AddCube(mat, Matrix4x4.Identity);
+                    mesh.Validate();
+                    return mesh;
+                });
+
+            var meshes = sphereMeshes.Concat(cubeMeshes).ToArray();
+
             // create scene            
 
             var scene = new SceneBuilder();
 
             for (int i = 0; i < 100; ++i)
-            {
-                // create mesh
-                var mat = materials[rnd.Next(0, 10)];
-                var mesh = VPOSNRM.CreateCompatibleMesh("shape");
+            {                
+                var mesh = meshes[rnd.Next(0, 20)];
+
+                // create random transform
+                var r = rnd.NextQuaternion();                
+                var t = rnd.NextVector3() * 25;
 
-                #if DEBUG
-                mesh.VertexPreprocessor.SetValidationPreprocessors();
-                #else
-                mesh.VertexPreprocessor.SetSanitizerPreprocessors();
-                #endif
+                scene.AddRigidMesh(mesh, (r, t));
+            }
 
-                if ((i & 1) == 0) mesh.AddCube(mat, Matrix4x4.Identity);
-                else mesh.AddSphere(mat, 0.5f, Matrix4x4.Identity);
+            // collapse to glTF
 
-                mesh.Validate();
+            
+            var gltf = scene.ToGltf2(useGpuInstancing ? SceneBuilderSchema2Settings.WithGpuInstancing : SceneBuilderSchema2Settings.Default);
 
-                // create random transform
-                var r = rnd.NextVector3() * 5;
-                var xform = Matrix4x4.CreateFromYawPitchRoll(r.X, r.Y, r.Z) * Matrix4x4.CreateTranslation(rnd.NextVector3() * 25);
+            var bounds = Runtime.MeshDecoder.EvaluateBoundingBox(gltf.DefaultScene);
 
-                scene.AddRigidMesh(mesh, xform);                
-            }
+            // Assert.AreEqual(defaultBounds,instancedBounds);
 
             // save the model as GLB
 
-            scene.AttachToCurrentTest("shapes.glb");
+            gltf.AttachToCurrentTest("shapes.glb");
             scene.AttachToCurrentTest("shapes.plotly");
         }