Browse Source

Refactored monogame's loader to allow custom vertex formats and effects to be injected while loading.

Vicente Penades 5 years ago
parent
commit
fc0cc7dc29

+ 194 - 0
examples/SharpGLTF.Runtime.MonoGame/LoaderContext.BasicEffect.cs

@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+using GLTFMATERIAL = SharpGLTF.Schema2.Material;
+
+namespace SharpGLTF.Runtime
+{
+    class BasicEffectsLoaderContext : LoaderContext
+    {
+        #region lifecycle
+
+        public BasicEffectsLoaderContext(GraphicsDevice device) : base(device) { }
+
+        #endregion
+
+        #region effects creation
+
+        // Monogame's BasicEffect uses Phong's shading, while glTF uses PBR shading, so
+        // given monogame's limitations, we try to guess the most appropiate values
+        // to have a reasonably good looking renders.
+
+        protected override Effect CreateEffect(GLTFMATERIAL srcMaterial, bool isSkinned)
+        {
+            return isSkinned ? CreateSkinnedEffect(srcMaterial) : CreateRigidEffect(srcMaterial);
+        }
+
+        protected virtual Effect CreateRigidEffect(GLTFMATERIAL srcMaterial)
+        {
+            var dstMaterial = srcMaterial.Alpha == Schema2.AlphaMode.MASK
+                ? CreateAlphaTestEffect(srcMaterial)
+                : CreateBasicEffect(srcMaterial);
+
+            return dstMaterial;
+        }
+
+        protected virtual Effect CreateBasicEffect(GLTFMATERIAL srcMaterial)
+        {
+            var dstMaterial = new BasicEffect(Device);
+
+            dstMaterial.Name = srcMaterial.Name;
+
+            dstMaterial.Alpha = GetAlphaLevel(srcMaterial);
+            dstMaterial.DiffuseColor = GetDiffuseColor(srcMaterial);
+            dstMaterial.SpecularColor = GetSpecularColor(srcMaterial);
+            dstMaterial.SpecularPower = GetSpecularPower(srcMaterial);
+            dstMaterial.EmissiveColor = GeEmissiveColor(srcMaterial);
+            dstMaterial.Texture = UseDiffuseTexture(srcMaterial);
+
+            if (srcMaterial.Unlit)
+            {
+                dstMaterial.EmissiveColor = dstMaterial.DiffuseColor;
+                dstMaterial.SpecularColor = Vector3.Zero;
+                dstMaterial.SpecularPower = 16;
+            }
+
+            dstMaterial.PreferPerPixelLighting = true;
+            dstMaterial.TextureEnabled = dstMaterial.Texture != null;
+
+            return dstMaterial;
+        }
+
+        protected virtual Effect CreateAlphaTestEffect(GLTFMATERIAL srcMaterial)
+        {
+            var dstMaterial = new AlphaTestEffect(Device);
+
+            dstMaterial.Name = srcMaterial.Name;
+
+            dstMaterial.Alpha = GetAlphaLevel(srcMaterial);
+            //dstMaterial.AlphaFunction = CompareFunction.GreaterEqual;
+            dstMaterial.ReferenceAlpha = (int)(srcMaterial.AlphaCutoff * 255);
+
+            dstMaterial.DiffuseColor = GetDiffuseColor(srcMaterial);
+
+            dstMaterial.Texture = UseDiffuseTexture(srcMaterial);
+
+            return dstMaterial;
+        }
+
+        protected virtual Effect CreateSkinnedEffect(GLTFMATERIAL srcMaterial)
+        {
+            var dstMaterial = new SkinnedEffect(Device);            
+
+            dstMaterial.Name = srcMaterial.Name;
+
+            dstMaterial.Alpha = GetAlphaLevel(srcMaterial);
+            dstMaterial.DiffuseColor = GetDiffuseColor(srcMaterial);
+            dstMaterial.SpecularColor = GetSpecularColor(srcMaterial);
+            dstMaterial.SpecularPower = GetSpecularPower(srcMaterial);
+            dstMaterial.EmissiveColor = GeEmissiveColor(srcMaterial);
+            dstMaterial.Texture = UseDiffuseTexture(srcMaterial);
+
+            dstMaterial.WeightsPerVertex = 4;
+            dstMaterial.PreferPerPixelLighting = true;
+
+            // apparently, SkinnedEffect does not support disabling textures, so we set a white texture here.
+            if (dstMaterial.Texture == null) dstMaterial.Texture = UseTexture(null, null); // creates a dummy white texture.
+
+            return dstMaterial;
+        }
+
+        #endregion
+
+        #region meshes creation
+
+        protected override void WriteMeshPrimitive(int logicalMeshIndex, MeshPrimitiveReader srcPrimitive, Effect effect)
+        {
+            if (srcPrimitive.IsSkinned) WriteMeshPrimitive<VertexSkinned>(logicalMeshIndex, effect, srcPrimitive);
+            else WriteMeshPrimitive<VertexPositionNormalTexture>(logicalMeshIndex, effect, srcPrimitive);
+        }
+
+        #endregion
+
+        #region gltf helpers
+
+        private static float GetAlphaLevel(GLTFMATERIAL srcMaterial)
+        {
+            if (srcMaterial.Alpha == Schema2.AlphaMode.OPAQUE) return 1;
+
+            var baseColor = srcMaterial.FindChannel("BaseColor");
+
+            if (baseColor == null) return 1;
+
+            return baseColor.Value.Parameter.W;
+        }
+
+        private static Vector3 GetDiffuseColor(GLTFMATERIAL srcMaterial)
+        {
+            var diffuse = srcMaterial.FindChannel("Diffuse");
+
+            if (diffuse == null) diffuse = srcMaterial.FindChannel("BaseColor");
+
+            if (diffuse == null) return Vector3.One;
+
+            return new Vector3(diffuse.Value.Parameter.X, diffuse.Value.Parameter.Y, diffuse.Value.Parameter.Z);
+        }
+
+        private static Vector3 GetSpecularColor(GLTFMATERIAL srcMaterial)
+        {
+            var mr = srcMaterial.FindChannel("MetallicRoughness");
+
+            if (mr == null) return Vector3.One; // default value 16
+
+            var diffuse = GetDiffuseColor(srcMaterial);
+            var metallic = mr.Value.Parameter.X;
+            var roughness = mr.Value.Parameter.Y;
+
+            var k = Vector3.Zero;
+            k += Vector3.Lerp(diffuse, Vector3.Zero, roughness);
+            k += Vector3.Lerp(diffuse, Vector3.One, metallic);
+            k *= 0.5f;
+
+            return k;
+        }
+
+        private static float GetSpecularPower(GLTFMATERIAL srcMaterial)
+        {
+            var mr = srcMaterial.FindChannel("MetallicRoughness");
+
+            if (mr == null) return 16; // default value = 16
+
+            var metallic = mr.Value.Parameter.X;
+            var roughness = mr.Value.Parameter.Y;
+
+            return 4 + 16 * metallic;
+        }
+
+        private static Vector3 GeEmissiveColor(GLTFMATERIAL srcMaterial)
+        {
+            var emissive = srcMaterial.FindChannel("Emissive");
+
+            if (emissive == null) return Vector3.Zero;
+
+            return new Vector3(emissive.Value.Parameter.X, emissive.Value.Parameter.Y, emissive.Value.Parameter.Z);
+        }
+
+        private Texture2D UseDiffuseTexture(GLTFMATERIAL srcMaterial)
+        {
+            var diffuse = srcMaterial.FindChannel("Diffuse");
+
+            if (diffuse == null) diffuse = srcMaterial.FindChannel("BaseColor");
+            if (diffuse == null) return null;
+
+            return UseTexture(diffuse, null);
+        }
+
+        #endregion
+    }
+}

+ 86 - 78
examples/SharpGLTF.Runtime.MonoGame/LoaderContext.cs

@@ -6,27 +6,24 @@ using Microsoft.Xna.Framework.Graphics;
 
 using SharpGLTF.Schema2;
 
-#if USINGMONOGAMEMODEL
-using MODELMESH = Microsoft.Xna.Framework.Graphics.ModelMesh;
-using MODELMESHPART = Microsoft.Xna.Framework.Graphics.ModelMeshPart;
-#else
-using MODELMESH = SharpGLTF.Runtime.ModelMeshReplacement;
-using MODELMESHPART = SharpGLTF.Runtime.ModelMeshPartReplacement;
-#endif
+using SRCMESH = SharpGLTF.Schema2.Mesh;
+using SRCPRIM = SharpGLTF.Schema2.MeshPrimitive;
+using SRCMATERIAL = SharpGLTF.Schema2.Material;
+
+using MODELMESH = SharpGLTF.Runtime.RuntimeModelMesh;
 
 namespace SharpGLTF.Runtime
 {
     /// <summary>
-    /// Helper class used to import a glTF model into MonoGame
+    /// Helper class used to import a glTF meshes and materials into MonoGame
     /// </summary>
-    class LoaderContext
+    public abstract class LoaderContext
     {
         #region lifecycle
 
         public LoaderContext(GraphicsDevice device)
         {
             _Device = device;
-            _MatFactory = new MaterialFactory(device, _Disposables);
         }
 
         #endregion
@@ -35,23 +32,56 @@ namespace SharpGLTF.Runtime
 
         private GraphicsDevice _Device;
 
-        private readonly GraphicsResourceTracker _Disposables = new GraphicsResourceTracker();
-        private readonly MaterialFactory _MatFactory;        
-
-        private readonly Dictionary<Mesh, MODELMESH> _RigidMeshes = new Dictionary<Mesh, MODELMESH>();
-        private readonly Dictionary<Mesh, MODELMESH> _SkinnedMeshes = new Dictionary<Mesh, MODELMESH>();
+        private GraphicsResourceTracker _Disposables;
         
+        private EffectsFactory _MatFactory;
+
+        // gathers all meshes using shared vertex and index buffers whenever possible.
+        private MeshPrimitiveWriter _MeshWriter;
+
+        // used as a container to a default material;
+        private SharpGLTF.Schema2.ModelRoot _DummyModel; 
+
         #endregion
 
         #region properties
 
-        public IReadOnlyList<GraphicsResource> Disposables => _Disposables.Disposables;
+        protected GraphicsDevice Device => _Device;
+
+        internal IReadOnlyList<GraphicsResource> Disposables => _Disposables.Disposables;
+
+        #endregion
+
+        #region API
+
+        internal void Reset()
+        {
+            _Disposables = new GraphicsResourceTracker();
+            _MatFactory = new EffectsFactory(_Device, _Disposables);
+            _MeshWriter = new MeshPrimitiveWriter();
+        }
 
         #endregion
 
         #region Mesh API
 
-        private static IEnumerable<Schema2.MeshPrimitive> GetValidPrimitives(Schema2.Mesh srcMesh)
+        internal void _WriteMesh(SRCMESH srcMesh)
+        {
+            if (_Device == null) throw new InvalidOperationException();            
+
+            var srcPrims = _GetValidPrimitives(srcMesh)
+                .ToDictionary(item => item, item => new MeshPrimitiveReader(item, item.Material?.DoubleSided ?? false));
+
+            VertexNormalsFactory.CalculateSmoothNormals(srcPrims.Values.ToList());
+            VertexTangentsFactory.CalculateTangents(srcPrims.Values.ToList());
+            
+            foreach (var srcPrim in srcPrims)
+            {
+                _WriteMeshPrimitive(srcMesh.LogicalIndex, srcPrim.Value, srcPrim.Key.Material);                
+            }            
+        }
+
+        private static IEnumerable<SRCPRIM> _GetValidPrimitives(SRCMESH srcMesh)
         {
             foreach (var srcPrim in srcMesh.Primitives)
             {
@@ -67,90 +97,68 @@ namespace SharpGLTF.Runtime
             }
         }
 
-        public MODELMESH CreateMesh(Schema2.Mesh srcMesh, int maxBones = 72)
+        private void _WriteMeshPrimitive(int logicalMeshIndex, MeshPrimitiveReader srcPrim, SRCMATERIAL srcMaterial)
         {
-            if (_Device == null) throw new InvalidOperationException();            
-
-            var srcPrims = GetValidPrimitives(srcMesh).ToList();            
-
-            var dstMesh = new MODELMESH(_Device, Enumerable.Range(0, srcPrims.Count).Select(item => new MODELMESHPART()).ToList());
+            if (srcMaterial == null) srcMaterial = GetDefaultMaterial();
 
-            dstMesh.Name = srcMesh.Name;
-            dstMesh.BoundingSphere = srcMesh.CreateBoundingSphere();
+            var effect = _MatFactory.GetMaterial(srcMaterial, srcPrim.IsSkinned);
 
-            var srcNormals = new MeshNormalsFallback(srcMesh);
-
-            var idx = 0;
-            foreach (var srcPrim in srcPrims)
+            if (effect == null)
             {
-                CreateMeshPart(dstMesh.MeshParts[idx++], srcPrim, srcNormals, maxBones);
+                effect = CreateEffect(srcMaterial, srcPrim.IsSkinned);
+                _MatFactory.Register(srcMaterial, srcPrim.IsSkinned, effect);
             }
 
-            return dstMesh;
-        }
-
-        private void CreateMeshPart(MODELMESHPART dstPart, MeshPrimitive srcPart, MeshNormalsFallback normalsFunc, int maxBones)
-        {
-            var doubleSided = srcPart.Material?.DoubleSided ?? false;
+            WriteMeshPrimitive(logicalMeshIndex, srcPrim, effect);
+        }        
 
-            var srcGeometry = new MeshPrimitiveReader(srcPart, doubleSided, normalsFunc);
+        protected abstract void WriteMeshPrimitive(int logicalMeshIndex, MeshPrimitiveReader srcPrimitive, Effect effect);
 
-            var eff = srcGeometry.IsSkinned ? _MatFactory.UseSkinnedEffect(srcPart.Material) : _MatFactory.UseRigidEffect(srcPart.Material);
+        protected void WriteMeshPrimitive<TVertex>(int logicalMeshIndex, Effect effect, MeshPrimitiveReader primitive)
+            where TVertex : unmanaged, IVertexType
+        {
+            _MeshWriter.WriteMeshPrimitive<TVertex>(logicalMeshIndex, effect, primitive);
+        }
 
-            dstPart.Effect = eff;            
+        #endregion
 
-            var vb = srcGeometry.IsSkinned ? CreateVertexBuffer(srcGeometry.ToXnaSkinned()) : CreateVertexBuffer(srcGeometry.ToXnaRigid());
+        #region EFfects API
 
-            dstPart.VertexBuffer = vb;
-            dstPart.NumVertices = srcGeometry.VertexCount;
-            dstPart.VertexOffset = 0;
+        /// <summary>
+        /// Called when finding a new material that needs to be converted to an Effect.
+        /// </summary>
+        /// <param name="srcMaterial">The material to convert.</param>
+        /// <param name="isSkinned">Indicates that the material is used in a skinned mesh.</param>
+        /// <returns>An effect to be used in place of <paramref name="srcMaterial"/>. </returns>
+        protected abstract Effect CreateEffect(Material srcMaterial, bool isSkinned);
 
-            dstPart.IndexBuffer = CreateIndexBuffer(srcGeometry.TriangleIndices);
-            dstPart.PrimitiveCount = srcGeometry.TriangleIndices.Length;
-            dstPart.StartIndex = 0;
+        protected virtual Texture2D UseTexture(MaterialChannel? channel, string name)
+        {
+            return _MatFactory.UseTexture(channel, name);
         }
-        
+
         #endregion
 
         #region resources API
 
-        private VertexBuffer CreateVertexBuffer<T>(T[] dstVertices) where T:struct, IVertexType
+        internal IReadOnlyDictionary<int, MODELMESH> CreateRuntimeModels()
         {
-            var vb = new VertexBuffer(_Device, typeof(T), dstVertices.Length, BufferUsage.None);
-            _Disposables.AddDisposable(vb);
-
-            vb.SetData(dstVertices);
-            return vb;
+            return _MeshWriter.GetRuntimeMeshes(_Device, _Disposables);
         }
 
-        private IndexBuffer CreateIndexBuffer(IEnumerable<(int A, int B, int C)> triangles)
+        private Material GetDefaultMaterial()
         {
-            var sequence32 = triangles
-                .SelectMany(item => new[] { (UInt32)item.C, (UInt32)item.B, (UInt32)item.A })
-                .ToArray();
-
-            var max = sequence32.Max();
-
-            if (max > 65535)
+            if (_DummyModel != null)
             {
-                var indices = new IndexBuffer(_Device, typeof(UInt32), sequence32.Length, BufferUsage.None);
-                _Disposables.AddDisposable(indices);
-
-                indices.SetData(sequence32);
-                return indices;
+                _DummyModel = ModelRoot.CreateModel();
+                _DummyModel.CreateMaterial("Default");
             }
-            else
-            {
-                var sequence16 = sequence32.Select(item => (UInt16)item).ToArray();                
-
-                var indices = new IndexBuffer(_Device, typeof(UInt16), sequence16.Length, BufferUsage.None);
-                _Disposables.AddDisposable(indices);
-
-                indices.SetData(sequence16);
-                return indices;
-            }
-        }        
+            
+            return _DummyModel.LogicalMaterials[0];
+        }           
 
         #endregion
-    }    
+    }
+
+    
 }

+ 0 - 63
examples/SharpGLTF.Runtime.MonoGame/MeshNormalsFallback.cs

@@ -1,63 +0,0 @@
-using System.Collections.Generic;
-
-using XYZ = System.Numerics.Vector3;
-
-namespace SharpGLTF.Runtime
-{
-    /// <summary>
-    /// Helper class used to calculate smooth Normals on glTF meshes with missing normals.
-    /// </summary>
-    class MeshNormalsFallback
-    {
-        #region lifecycle
-
-        public MeshNormalsFallback(Schema2.Mesh mesh)
-        {
-            foreach (var srcPrim in mesh.Primitives)
-            {
-                var accessor = srcPrim.GetVertexAccessor("POSITION");
-                if (accessor == null) continue;
-
-                var positions = accessor.AsVector3Array();
-
-                foreach (var srcTri in srcPrim.GetTriangleIndices())
-                {
-                    var a = positions[srcTri.A];
-                    var b = positions[srcTri.B];
-                    var c = positions[srcTri.C];
-                    var d = XYZ.Cross(b - a, c - a);
-
-                    AddWeightedNormal(a, d);
-                    AddWeightedNormal(b, d);
-                    AddWeightedNormal(c, d);
-                }
-            }
-        }
-
-        #endregion
-
-        #region data
-        
-        private readonly Dictionary<XYZ, XYZ> _WeightedNormals = new Dictionary<XYZ, XYZ>();
-
-        #endregion
-
-        #region API
-
-        private void AddWeightedNormal(XYZ p, XYZ d)
-        {
-            if (_WeightedNormals.TryGetValue(p, out XYZ ddd)) ddd += d;
-            else ddd = d;
-
-            _WeightedNormals[p] = ddd;
-        }
-
-        public XYZ GetNormal(XYZ position)
-        {
-            if (!_WeightedNormals.TryGetValue(position, out XYZ normal)) normal = position;
-            return normal == XYZ.Zero ? XYZ.UnitX : XYZ.Normalize(normal);
-        }
-        
-        #endregion
-    }
-}

+ 396 - 42
examples/SharpGLTF.Runtime.MonoGame/MeshPrimitiveReader.cs

@@ -1,7 +1,11 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices.WindowsRuntime;
 
+using Microsoft.Xna.Framework;
 using Microsoft.Xna.Framework.Graphics;
 
 using SharpGLTF.Schema2;
@@ -13,27 +17,20 @@ using XYZW = System.Numerics.Vector4;
 namespace SharpGLTF.Runtime
 {
     /// <summary>
-    /// Reads the content of a glTF <see cref="MeshPrimitive"/> object into a structure that's easier to consume.
+    /// Reads the content of a glTF <see cref="MeshPrimitive"/> object into a structure that's easier to consume by MonoGame.
     /// </summary>
-    class MeshPrimitiveReader
+    public sealed class MeshPrimitiveReader
+        : VertexNormalsFactory.IMeshPrimitive
+        , VertexTangentsFactory.IMeshPrimitive
     {
         #region lifecycle
 
-        public MeshPrimitiveReader(MeshPrimitive srcPrim, bool doubleSided, MeshNormalsFallback fallbackNormals)
+        internal MeshPrimitiveReader(MeshPrimitive srcPrim, bool doubleSided)
         {
             _Positions = srcPrim.GetVertexAccessor("POSITION")?.AsVector3Array();
             _Normals = srcPrim.GetVertexAccessor("NORMAL")?.AsVector3Array();
-
-            if (_Normals == null)
-            {
-                _Normals = new XYZ[_Positions.Count];
-
-                for (int i = 0; i < _Normals.Count; ++i)
-                {
-                    _Normals[i] = fallbackNormals.GetNormal(_Positions[i]);
-                }
-            }
-
+            _Tangents = srcPrim.GetVertexAccessor("TANGENT")?.AsVector4Array();
+            
             _Color0 = srcPrim.GetVertexAccessor("COLOR_0")?.AsColorArray();
             _TexCoord0 = srcPrim.GetVertexAccessor("TEXCOORD_0")?.AsVector2Array();
 
@@ -56,28 +53,30 @@ namespace SharpGLTF.Runtime
                 }
             }
 
+            _TrianglesSource = srcPrim.GetTriangleIndices().ToArray();
+
             if (doubleSided) // Monogame's effect material does not support double sided materials, so we simulate it by adding reverse faces
             {
-                var front = srcPrim.GetTriangleIndices();
-                var back = front.Select(item => (item.A, item.C, item.B));
-                _Triangles = front.Concat(back).ToArray();
+                var back = _TrianglesSource.Select(item => (item.A, item.C, item.B));
+                _Triangles = _TrianglesSource.Concat(back).ToArray();
             }
             else
             {
-                _Triangles = srcPrim.GetTriangleIndices().ToArray();
-            }
-
-            
+                _Triangles = _TrianglesSource;
+            }            
         }
 
         #endregion
 
         #region data
 
-        private readonly (int, int, int)[] _Triangles;
+        private readonly (int A, int B, int C)[] _TrianglesSource;
+
+        private readonly (int A, int B, int C)[] _Triangles;
 
         private readonly IList<XYZ> _Positions;
-        private readonly IList<XYZ> _Normals;
+        private IList<XYZ> _Normals;
+        private IList<XYZW> _Tangents;
 
         private readonly IList<XYZW> _Color0;
         private readonly IList<XY> _TexCoord0;
@@ -90,49 +89,404 @@ namespace SharpGLTF.Runtime
 
         #endregion
 
-        #region properties
+        #region properties        
 
         public bool IsSkinned => _Joints0 != null;
-
         public int VertexCount => _Positions?.Count ?? 0;
-
-        public (int A, int B, int C)[] TriangleIndices => _Triangles;
+        public (int A, int B, int C)[] TriangleIndices => _Triangles;        
 
         #endregion
 
-        #region API        
+        #region API
+
+        public XYZ GetPosition(int idx) { return _Positions[idx]; }
 
-        public VertexPositionNormalTexture[] ToXnaRigid()
+        public XYZ GetNormal(int idx) { return _Normals[idx]; }
+
+        public XYZW GetTangent(int idx) { return _Tangents[idx]; }
+
+        public XY GetTextureCoord(int idx, int set)
         {
-            var dst = new VertexPositionNormalTexture[_Positions.Count];
+            if (set == 0 && _TexCoord0 != null) return _TexCoord0[idx];
+
+            return XY.Zero;
+        }
+
+        public XYZW GetColor(int idx, int set)
+        {
+            if (set == 0 && _Color0 != null) return _Color0[idx];
+
+            return XYZW.One;
+        }
+
+        public XYZW GetIndices(int idx)
+        {
+            if (_Joints0 != null) return _Joints0[idx];
+            return XYZW.Zero;
+        }
+
+        public XYZW GetWeights(int idx)
+        {
+            if (_Weights0 != null) return _Weights0[idx];
+            return XYZW.UnitX;
+        }
+
+        /// <summary>
+        /// Gets the current Vertex attributes as an array of <see cref="{TVertex}"/> vertices.
+        /// </summary>
+        /// <typeparam name="TVertex">A Vertex type implementing <see cref="IVertexType"/>.</typeparam>
+        /// <returns>A <see cref="{TVertex}"/> array</returns>
+        public unsafe TVertex[] ToXnaVertices<TVertex>()
+            where TVertex:unmanaged, IVertexType
+        {
+            var declaration = default(TVertex).VertexDeclaration;
+
+            if (sizeof(TVertex) != declaration.VertexStride) throw new ArgumentException(nameof(TVertex));
+
+            var dst = new TVertex[_Positions.Count];
 
             for (int i = 0; i < dst.Length; ++i)
             {
-                dst[i].Position = _Positions[i].ToXna();
-                dst[i].Normal = _Normals[i].ToXna();                
+                var v = _VertexWriter.CreateFromArray(dst, i);                
 
-                if (_TexCoord0 != null) dst[i].TextureCoordinate = _TexCoord0[i].ToXna();
+                foreach(var element in declaration.GetVertexElements())
+                {
+                    switch(element.VertexElementUsage)
+                    {
+                        case VertexElementUsage.Position: v.SetValue(element, GetPosition(i)); break;
+                        case VertexElementUsage.Normal: v.SetValue(element, GetNormal(i)); break;
+                        case VertexElementUsage.Tangent: v.SetValue(element, GetTangent(i), true); break;
+
+                        case VertexElementUsage.TextureCoordinate: v.SetValue(element, GetTextureCoord(i,element.UsageIndex)); break;
+                        case VertexElementUsage.Color: v.SetValue(element, GetColor(i, element.UsageIndex) , true); break;
+
+                        case VertexElementUsage.BlendIndices: v.SetValue(element, GetIndices(i), false); break;
+                        case VertexElementUsage.BlendWeight: v.SetValue(element, GetWeights(i), true); break;
+                    }                            
+                }                
             }
 
             return dst;
+        }        
+
+        #endregion
+
+        #region nested types
+
+        readonly ref struct _VertexWriter 
+        {
+            #region constructor
+            public static _VertexWriter CreateFromArray<TVertex>(TVertex[] vvv, int idx)
+                where TVertex : unmanaged, IVertexType
+            {
+                var v = vvv.AsSpan().Slice(idx, 1);
+
+                var d = System.Runtime.InteropServices.MemoryMarshal.Cast<TVertex, Byte>(v);
+
+                return new _VertexWriter(d);
+            }
+
+            public _VertexWriter(Span<Byte> vertex)
+            {
+                _Vertex = vertex;
+            }
+
+            #endregion
+
+            #region data
+
+            private readonly Span<Byte> _Vertex;
+
+            #endregion
+
+            #region API            
+
+            public unsafe void SetValue(VertexElement element, XY value)
+            {
+                if (element.VertexElementFormat == VertexElementFormat.Vector2)
+                {
+                    var dst = _Vertex.Slice(element.Offset, sizeof(XY));
+                    System.Runtime.InteropServices.MemoryMarshal.Write(dst, ref value);
+                    return;
+                }
+
+                throw new NotImplementedException();
+            }
+
+            public unsafe void SetValue(VertexElement element, XYZ value)
+            {
+                if (element.VertexElementFormat == VertexElementFormat.Vector3)
+                {
+                    var dst = _Vertex.Slice(element.Offset, sizeof(XYZ));
+                    System.Runtime.InteropServices.MemoryMarshal.Write(dst, ref value);
+                    return;
+                }
+
+                throw new NotImplementedException();
+            }
+
+            public unsafe void SetValue(VertexElement element, XYZW value, bool valueIsUnitLength)
+            {
+                var dst = _Vertex.Slice(element.Offset);
+
+                switch (element.VertexElementFormat)
+                {
+                    case VertexElementFormat.Vector4:                        
+                        System.Runtime.InteropServices.MemoryMarshal.Write(dst, ref value);
+                        return;
+
+                    case VertexElementFormat.Byte4:
+                        if (valueIsUnitLength)
+                        {
+                            SetValue(element, new Microsoft.Xna.Framework.Graphics.PackedVector.NormalizedByte4(value.ToXna()));
+                        }
+                        else
+                        {
+                            SetValue(element, new Microsoft.Xna.Framework.Graphics.PackedVector.Byte4(value.ToXna()));
+                        }
+                        
+                        return;
+                    
+                    case VertexElementFormat.Short4:
+                        SetValue(element, new Microsoft.Xna.Framework.Graphics.PackedVector.Short4(value.ToXna()));
+                        return;
+
+                    case VertexElementFormat.NormalizedShort4:
+                        SetValue(element, new Microsoft.Xna.Framework.Graphics.PackedVector.NormalizedShort4(value.ToXna()));
+                        return;
+                }
+
+                throw new NotImplementedException();
+            }
+
+            public unsafe void SetValue(VertexElement element, Microsoft.Xna.Framework.Graphics.PackedVector.Byte4 value)
+            {
+                if (element.VertexElementFormat != VertexElementFormat.Byte4) throw new ArgumentException(nameof(element));
+                
+                var dst = _Vertex.Slice(element.Offset, sizeof(Microsoft.Xna.Framework.Graphics.PackedVector.Byte4));
+                System.Runtime.InteropServices.MemoryMarshal.Write(dst, ref value);                
+            }
+
+            public unsafe void SetValue(VertexElement element, Microsoft.Xna.Framework.Graphics.PackedVector.NormalizedByte4 value)
+            {
+                if (element.VertexElementFormat != VertexElementFormat.Byte4) throw new ArgumentException(nameof(element));
+
+                var dst = _Vertex.Slice(element.Offset, sizeof(Microsoft.Xna.Framework.Graphics.PackedVector.Byte4));
+                System.Runtime.InteropServices.MemoryMarshal.Write(dst, ref value);
+            }
+
+            public unsafe void SetValue(VertexElement element, Microsoft.Xna.Framework.Graphics.PackedVector.Short4 value)
+            {
+                if (element.VertexElementFormat != VertexElementFormat.Short4) throw new ArgumentException(nameof(element));
+
+                var dst = _Vertex.Slice(element.Offset, sizeof(Microsoft.Xna.Framework.Graphics.PackedVector.Short4));
+                System.Runtime.InteropServices.MemoryMarshal.Write(dst, ref value);
+            }
+
+            public unsafe void SetValue(VertexElement element, Microsoft.Xna.Framework.Graphics.PackedVector.NormalizedShort4 value)
+            {
+                if (element.VertexElementFormat != VertexElementFormat.NormalizedShort4) throw new ArgumentException(nameof(element));
+                
+                var dst = _Vertex.Slice(element.Offset, sizeof(Microsoft.Xna.Framework.Graphics.PackedVector.NormalizedShort4));
+                System.Runtime.InteropServices.MemoryMarshal.Write(dst, ref value);                
+            }
+
+            #endregion
         }
 
-        public VertexSkinned[] ToXnaSkinned()
+        #endregion
+
+        #region Support methods for VertexNormalsFactory and VertexTangentsFactory
+
+        IEnumerable<(int A, int B, int C)> VertexNormalsFactory.IMeshPrimitive.GetTriangleIndices() { return _TrianglesSource; }
+
+        IEnumerable<(int A, int B, int C)> VertexTangentsFactory.IMeshPrimitive.GetTriangleIndices() { return _TrianglesSource; }
+
+        XYZ VertexNormalsFactory.IMeshPrimitive.GetVertexPosition(int idx) { return GetPosition(idx); }
+        XYZ VertexTangentsFactory.IMeshPrimitive.GetVertexPosition(int idx) { return GetPosition(idx); }
+        XYZ VertexTangentsFactory.IMeshPrimitive.GetVertexNormal(int idx) { return GetNormal(idx); }
+        XY VertexTangentsFactory.IMeshPrimitive.GetVertexTexCoord(int idx) { return GetTextureCoord(idx, 0); }
+
+        void VertexNormalsFactory.IMeshPrimitive.SetVertexNormal(int idx, XYZ normal)
         {
-            var dst = new VertexSkinned[_Positions.Count];
+            if (_Normals == null) _Normals = new XYZ[VertexCount];
+            if (!(_Normals is XYZ[])) return; // if it's not a plain array, it's a glTF source, so we prevent writing existing normals.            
+            _Normals[idx] = normal;
+        }
 
-            for (int i = 0; i < dst.Length; ++i)
+        void VertexTangentsFactory.IMeshPrimitive.SetVertexTangent(int idx, XYZW tangent)
+        {
+            if (_Tangents == null) _Tangents = new XYZW[VertexCount];
+            if (!(_Tangents is XYZW[])) return; // if it's not a plain array, it's a glTF source, so we prevent writing existing tangents.            
+            _Tangents[idx] = tangent;
+        }        
+
+        #endregion
+    }
+
+    sealed class MeshPrimitiveWriter
+    {
+        #region data
+
+        // shared buffers
+        private readonly Dictionary<Type, IPrimitivesBuffers> _Buffers = new Dictionary<Type, IPrimitivesBuffers>();
+
+        // primitives
+        private readonly List<_MeshPrimitive> _MeshPrimitives = new List<_MeshPrimitive>();
+
+        #endregion
+
+        #region API
+
+        public void WriteMeshPrimitive<TVertex>(int logicalMeshIndex, Effect effect, MeshPrimitiveReader primitive)
+            where TVertex : unmanaged, IVertexType
+        {
+            if (!_Buffers.TryGetValue(typeof(TVertex), out IPrimitivesBuffers pb))
+            {
+                _Buffers[typeof(TVertex)] = pb = new _PrimitivesBuffers<TVertex>();
+            }
+
+            var part = (pb as _PrimitivesBuffers<TVertex>).Append(logicalMeshIndex, effect, primitive);
+
+            _MeshPrimitives.Add(part);
+        }
+
+        internal IReadOnlyDictionary<int, RuntimeModelMesh> GetRuntimeMeshes(GraphicsDevice device, GraphicsResourceTracker disposables)
+        {
+            // create shared vertex/index buffers
+
+            var vbuffers = _Buffers.Values.ToDictionary(key => key, val => val.CreateVertexBuffer(device));
+            var ibuffers = _Buffers.Values.ToDictionary(key => key, val => val.CreateIndexBuffer(device));
+
+            foreach (var vb in vbuffers.Values) disposables.AddDisposable(vb);
+            foreach (var ib in ibuffers.Values) disposables.AddDisposable(ib);
+
+            // create RuntimeModelMesh
+
+            RuntimeModelMesh _convert(IEnumerable<_MeshPrimitive> srcParts)
             {
-                dst[i].Position = _Positions[i].ToXna();
-                dst[i].Normal = _Normals[i].ToXna();
+                var dstMesh = new RuntimeModelMesh(device);
 
-                if (_TexCoord0 != null) dst[i].TextureCoordinate = _TexCoord0[i].ToXna();
+                foreach(var srcPart in srcParts)
+                {
+                    var vb = vbuffers[srcPart.PrimitiveBuffers];
+                    var ib = ibuffers[srcPart.PrimitiveBuffers];
+
+                    var dstPart = dstMesh.CreateMeshPart();
+                    dstPart.Effect = srcPart.PrimitiveEffect;
+                    dstPart.SetVertexBuffer(vb, srcPart.VertexOffset, srcPart.VertexCount);
+                    dstPart.SetIndexBuffer(ib, srcPart.TriangleOffset * 3, srcPart.TriangleCount);                    
+                }
 
-                dst[i].BlendIndices = new Microsoft.Xna.Framework.Graphics.PackedVector.Byte4(_Joints0[i].ToXna());
-                dst[i].BlendWeight = _Weights0[i].ToXna();
+                return dstMesh;
             }
 
-            return dst;
+            return _MeshPrimitives
+                .GroupBy(item => item.LogicalMeshIndex)
+                .ToDictionary(k => k.Key, v => _convert(v));
+        }
+
+        #endregion
+
+        #region nested types
+
+        interface IPrimitivesBuffers
+        {
+            VertexBuffer CreateVertexBuffer(GraphicsDevice device);
+            IndexBuffer CreateIndexBuffer(GraphicsDevice device);
+        }
+
+        /// <summary>
+        /// Contains the shared vertex/index buffers of all the mesh primitive that share the same vertex type.
+        /// </summary>
+        /// <typeparam name="TVertex"></typeparam>
+        sealed class _PrimitivesBuffers<TVertex> : IPrimitivesBuffers
+            where TVertex : unmanaged, IVertexType
+        {
+            #region data
+
+            private readonly List<TVertex> _Vertices = new List<TVertex>();
+            private readonly List<(int,int,int)> _Triangles = new List<(int, int, int)>();
+
+            #endregion
+
+            #region API
+
+            public _MeshPrimitive Append(int meshKey, Effect effect, MeshPrimitiveReader primitive)
+            {
+                var partVertices = primitive.ToXnaVertices<TVertex>();
+                var partTriangles = primitive.TriangleIndices;
+
+                var part = new _MeshPrimitive
+                {
+                    LogicalMeshIndex = meshKey,
+                    PrimitiveEffect = effect,
+                    PrimitiveBuffers = this,
+                    VertexOffset = _Vertices.Count,
+                    VertexCount = partVertices.Length,
+                    TriangleOffset = _Triangles.Count,
+                    TriangleCount = partTriangles.Length
+                };
+
+                _Vertices.AddRange(partVertices);
+                _Triangles.AddRange(partTriangles);
+
+                return part;
+            }
+
+            public VertexBuffer CreateVertexBuffer(GraphicsDevice device)
+            {
+                var data = new VertexBuffer(device, typeof(TVertex), _Vertices.Count, BufferUsage.None);
+                data.SetData(_Vertices.ToArray());
+                return data;
+            }
+
+            public IndexBuffer CreateIndexBuffer(GraphicsDevice device)
+            {
+                return CreateIndexBuffer(device, _Triangles);
+            }
+
+            private static IndexBuffer CreateIndexBuffer(GraphicsDevice device, IEnumerable<(int A, int B, int C)> triangles)
+            {
+                var sequence32 = triangles
+                    .SelectMany(item => new[] { (UInt32)item.C, (UInt32)item.B, (UInt32)item.A })
+                    .ToArray();
+
+                var max = sequence32.Max();
+
+                if (max > 65535)
+                {
+                    var indices = new IndexBuffer(device, typeof(UInt32), sequence32.Length, BufferUsage.None);                    
+
+                    indices.SetData(sequence32);
+                    return indices;
+                }
+                else
+                {
+                    var sequence16 = sequence32.Select(item => (UInt16)item).ToArray();
+
+                    var indices = new IndexBuffer(device, typeof(UInt16), sequence16.Length, BufferUsage.None);
+
+                    indices.SetData(sequence16);
+                    return indices;
+                }
+            }
+
+            #endregion
+        }
+
+        /// <summary>
+        /// Represents a mesh primitive
+        /// </summary>
+        struct _MeshPrimitive
+        {
+            public int LogicalMeshIndex;
+            public Effect PrimitiveEffect;
+            public IPrimitivesBuffers PrimitiveBuffers;
+            public int VertexOffset;
+            public int VertexCount;
+            public int TriangleOffset;
+            public int TriangleCount;
         }
 
         #endregion

+ 113 - 39
examples/SharpGLTF.Runtime.MonoGame/ModelMeshReplacement.cs

@@ -9,13 +9,38 @@ namespace SharpGLTF.Runtime
 {
     /// <summary>
     /// Replaces <see cref="ModelMeshPart"/>.
-    /// </summary>
-    sealed class ModelMeshPartReplacement
+    /// </summary>    
+    sealed class RuntimeModelMeshPart
     {
-        internal ModelMeshReplacement _Parent;
+        #region lifecycle
+
+        internal RuntimeModelMeshPart(RuntimeModelMesh parent)
+        {
+            _Parent = parent;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly RuntimeModelMesh _Parent;
 
         private Effect _Effect;
 
+        private IndexBuffer _IndexBuffer;
+        private int _IndexOffset;
+        private int _PrimitiveCount;        
+
+        private VertexBuffer _VertexBuffer;
+        private int _VertexOffset;
+        private int _VertexCount;
+
+        public object Tag { get; set; }
+
+        #endregion
+
+        #region properties
+
         public Effect Effect
         {
             get => _Effect;
@@ -23,52 +48,85 @@ namespace SharpGLTF.Runtime
             {
                 if (_Effect == value) return;
                 _Effect = value;
-                _Parent.InvalidateEffectsCollection(); // if we change this property, we need to invalidate the parent's effect collection.
+                _Parent.InvalidateEffectCollection(); // if we change this property, we need to invalidate the parent's effect collection.
             }
         }
 
-        public IndexBuffer IndexBuffer { get; set; }
+        public GraphicsDevice Device => _Parent._GraphicsDevice;
 
-        public int NumVertices { get; set; }
+        #endregion
 
-        public int PrimitiveCount { get; set; }
+        #region API
 
-        public int StartIndex { get; set; }
+        public void SetVertexBuffer(VertexBuffer vb, int offset, int count)
+        {
+            this._VertexBuffer = vb;
+            this._VertexOffset = offset;
+            this._VertexCount = count;            
+        }
 
-        public object Tag { get; set; }
+        public void SetIndexBuffer(IndexBuffer ib, int offset, int count)
+        {
+            this._IndexBuffer = ib;
+            this._IndexOffset = offset;
+            this._PrimitiveCount = count;            
+        }
 
-        public VertexBuffer VertexBuffer { get; set; }
+        public void Draw(GraphicsDevice device)
+        {
+            if (_PrimitiveCount > 0)
+            {
+                device.SetVertexBuffer(_VertexBuffer);
+                device.Indices = _IndexBuffer;
 
-        public int VertexOffset { get; set; }
+                for (int j = 0; j < _Effect.CurrentTechnique.Passes.Count; j++)
+                {
+                    _Effect.CurrentTechnique.Passes[j].Apply();
+                    device.DrawIndexedPrimitives(PrimitiveType.TriangleList, _VertexOffset, _IndexOffset, _PrimitiveCount);
+                }
+            }
+        }
+
+        #endregion
     }
 
     /// <summary>
     /// Replaces <see cref="ModelMesh"/>
     /// </summary>
-    sealed class ModelMeshReplacement
+    sealed class RuntimeModelMesh
     {
-        private GraphicsDevice graphicsDevice;
+        #region lifecycle
 
-        public ModelMeshReplacement(GraphicsDevice graphicsDevice, List<ModelMeshPartReplacement> parts)
+        public RuntimeModelMesh(GraphicsDevice graphicsDevice)
         {
-            // TODO: Complete member initialization
-            this.graphicsDevice = graphicsDevice;
+            this._GraphicsDevice = graphicsDevice;
+        }
 
-            MeshParts = parts.ToArray();
+        #endregion
 
-            foreach (var mp in MeshParts) mp._Parent = this;
-        }
+        #region data        
+
+        internal GraphicsDevice _GraphicsDevice;
+
+        private readonly List<RuntimeModelMeshPart> _Primitives = new List<RuntimeModelMeshPart>();
 
         private IReadOnlyList<Effect> _Effects;
 
+        private Microsoft.Xna.Framework.BoundingSphere? _Sphere;
+
+        #endregion
+
+        #region  properties
+
         public IReadOnlyCollection<Effect> Effects
         {
             get
             {
                 if (_Effects != null) return _Effects;
 
-                // effects collection has changed since last call, so we reconstruct the collection.
-                _Effects = MeshParts
+                // Create the shared effects collection on demand.
+
+                _Effects = _Primitives
                     .Select(item => item.Effect)
                     .Distinct()
                     .ToArray();
@@ -77,9 +135,20 @@ namespace SharpGLTF.Runtime
             }
         }
 
-        public Microsoft.Xna.Framework.BoundingSphere BoundingSphere { get; set; }
+        public Microsoft.Xna.Framework.BoundingSphere BoundingSphere
+        {
+            set => _Sphere = value;
+
+            get
+            {
+                if (_Sphere.HasValue) return _Sphere.Value;
+
+                return default;
+            }
+            
+        }
 
-        public IList<ModelMeshPartReplacement> MeshParts { get; set; }
+        public IReadOnlyList<RuntimeModelMeshPart> MeshParts => _Primitives;
 
         public string Name { get; set; }
 
@@ -87,27 +156,32 @@ namespace SharpGLTF.Runtime
 
         public object Tag { get; set; }
 
-        internal void InvalidateEffectsCollection() { _Effects = null; }
+        #endregion
+
+        #region API
+
+        internal void InvalidateEffectCollection() { _Effects = null; }
+
+        public RuntimeModelMeshPart CreateMeshPart()
+        {
+            var primitive = new RuntimeModelMeshPart(this);
+
+            _Primitives.Add(primitive);
+            InvalidateEffectCollection();
+
+            _Sphere = null;
+
+            return primitive;
+        }
 
         public void Draw()
         {
-            for (int i = 0; i < MeshParts.Count; i++)
+            for (int i = 0; i < _Primitives.Count; i++)
             {
-                var part = MeshParts[i];
-                var effect = part.Effect;
-
-                if (part.PrimitiveCount > 0)
-                {
-                    this.graphicsDevice.SetVertexBuffer(part.VertexBuffer);
-                    this.graphicsDevice.Indices = part.IndexBuffer;
-
-                    for (int j = 0; j < effect.CurrentTechnique.Passes.Count; j++)
-                    {
-                        effect.CurrentTechnique.Passes[j].Apply();
-                        graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, part.VertexOffset, part.StartIndex, part.PrimitiveCount);
-                    }
-                }
+                _Primitives[i].Draw(_GraphicsDevice);
             }
         }
+
+        #endregion
     }
 }

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

@@ -9,8 +9,8 @@ using Microsoft.Xna.Framework.Graphics;
 using MODELMESH = Microsoft.Xna.Framework.Graphics.ModelMesh;
 using MODELMESHPART = Microsoft.Xna.Framework.Graphics.ModelMeshPart;
 #else
-using MODELMESH = SharpGLTF.Runtime.ModelMeshReplacement;
-using MODELMESHPART = SharpGLTF.Runtime.ModelMeshPartReplacement;
+using MODELMESH = SharpGLTF.Runtime.RuntimeModelMesh;
+using MODELMESHPART = SharpGLTF.Runtime.RuntimeModelMeshPart;
 #endif
 
 namespace SharpGLTF.Runtime
@@ -106,8 +106,20 @@ namespace SharpGLTF.Runtime
 
                 skin.SetBoneTransforms(skinTransforms);
             }
+
+            if (effect is IEffectBones iskin && skinTransforms != null)
+            {
+                var xposed = skinTransforms.Select(item => Matrix.Transpose(item)).ToArray();
+
+                iskin.SetBoneTransforms(skinTransforms);
+            }            
         }
 
         #endregion
     }
+
+    public interface IEffectBones // it could be great if SkinnedEffect implemented this.
+    {        
+        void SetBoneTransforms(Matrix[] boneTransforms);
+    }
 }

+ 21 - 13
examples/SharpGLTF.Runtime.MonoGame/MonoGameModelTemplate.cs

@@ -9,8 +9,8 @@ using Microsoft.Xna.Framework.Graphics;
 using MODELMESH = Microsoft.Xna.Framework.Graphics.ModelMesh;
 using MODELMESHPART = Microsoft.Xna.Framework.Graphics.ModelMeshPart;
 #else
-using MODELMESH = SharpGLTF.Runtime.ModelMeshReplacement;
-using MODELMESHPART = SharpGLTF.Runtime.ModelMeshPartReplacement;
+using MODELMESH = SharpGLTF.Runtime.RuntimeModelMesh;
+using MODELMESHPART = SharpGLTF.Runtime.RuntimeModelMeshPart;
 #endif
 
 namespace SharpGLTF.Runtime
@@ -19,28 +19,36 @@ namespace SharpGLTF.Runtime
     {
         #region lifecycle
 
-        public static MonoGameDeviceContent<MonoGameModelTemplate> LoadDeviceModel(GraphicsDevice device, string filePath)
+        public static MonoGameDeviceContent<MonoGameModelTemplate> LoadDeviceModel(GraphicsDevice device, string filePath, LoaderContext context = null)
         {
             var model = Schema2.ModelRoot.Load(filePath, Validation.ValidationMode.TryFix);
 
-            return CreateDeviceModel(device, model);
+            return CreateDeviceModel(device, model, context);
         }
 
-        public static MonoGameDeviceContent<MonoGameModelTemplate> CreateDeviceModel(GraphicsDevice device, Schema2.ModelRoot srcModel)
+        public static MonoGameDeviceContent<MonoGameModelTemplate> CreateDeviceModel(GraphicsDevice device, Schema2.ModelRoot srcModel, LoaderContext context = null)
         {
-            srcModel.FixTextureSampler();
+            if (context == null) context = new BasicEffectsLoaderContext(device);
+
+            context.Reset();
 
             var templates = srcModel.LogicalScenes
                 .Select(item => SceneTemplate.Create(item, true))
-                .ToArray();
-            
-            var context = new LoaderContext(device);
+                .ToArray();            
+
+            var srcMeshes = templates
+                .SelectMany(item => item.LogicalMeshIds)
+                .Distinct()
+                .Select(idx => srcModel.LogicalMeshes[idx]);
+
+            foreach(var srcMesh in srcMeshes)
+            {
+                context._WriteMesh(srcMesh);
+            }
 
-            var meshes = templates
-                .SelectMany(item => item.LogicalMeshIds)                
-                .ToDictionary(k => k, k => context.CreateMesh(srcModel.LogicalMeshes[k]));            
+            var dstMeshes = context.CreateRuntimeModels();
 
-            var mdl = new MonoGameModelTemplate(templates,srcModel.DefaultScene.LogicalIndex, meshes);
+            var mdl = new MonoGameModelTemplate(templates,srcModel.DefaultScene.LogicalIndex, dstMeshes);
 
             return new MonoGameDeviceContent<MonoGameModelTemplate>(mdl, context.Disposables.ToArray());
         }

+ 231 - 0
examples/SharpGLTF.Runtime.MonoGame/NormalTangentFactories.cs

@@ -0,0 +1,231 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Schema;
+
+namespace SharpGLTF.Runtime
+{
+    using VERTEXKEY = System.ValueTuple<Vector3, Vector3, Vector2>;
+
+    static class VertexNormalsFactory
+    {
+        #pragma warning disable CA1034 // Nested types should not be visible
+        public interface IMeshPrimitive
+        #pragma warning restore CA1034 // Nested types should not be visible
+        {
+            int VertexCount { get; }
+
+            Vector3 GetVertexPosition(int idx);
+
+            void SetVertexNormal(int idx, Vector3 normal);
+
+            IEnumerable<(int A, int B, int C)> GetTriangleIndices();
+        }
+
+        private static bool _IsFinite(this float value) { return !float.IsNaN(value) && !float.IsInfinity(value); }
+
+        private static bool _IsFinite(this Vector3 value) { return value.X._IsFinite() && value.Y._IsFinite() && value.Z._IsFinite(); }
+
+        public static void CalculateSmoothNormals<T>(IReadOnlyList<T> primitives)
+            where T : IMeshPrimitive
+        {
+            // Guard.NotNull(primitives, nameof(primitives));
+
+            var normalMap = new Dictionary<Vector3, Vector3>();
+
+            // calculate
+
+            foreach (var primitive in primitives)
+            {
+                foreach (var (ta, tb, tc) in primitive.GetTriangleIndices())
+                {
+                    var p1 = primitive.GetVertexPosition(ta);
+                    var p2 = primitive.GetVertexPosition(tb);
+                    var p3 = primitive.GetVertexPosition(tc);
+
+                    var d = Vector3.Cross(p2 - p1, p3 - p1);
+
+                    _AddDirection(normalMap, p1, d);
+                    _AddDirection(normalMap, p2, d);
+                    _AddDirection(normalMap, p3, d);
+                }
+            }
+
+            // normalize
+
+            foreach (var pos in normalMap.Keys.ToList())
+            {
+                var nrm = Vector3.Normalize(normalMap[pos]);
+
+                normalMap[pos] = nrm._IsFinite() && nrm.LengthSquared() > 0.5f ? nrm : Vector3.UnitZ;
+            }
+
+            // apply
+
+            foreach (var primitive in primitives)
+            {
+                for (int i = 0; i < primitive.VertexCount; ++i)
+                {
+                    var pos = primitive.GetVertexPosition(i);
+
+                    if (normalMap.TryGetValue(pos, out Vector3 nrm))
+                    {
+                        primitive.SetVertexNormal(i, nrm);
+                    }
+                    else
+                    {
+                        primitive.SetVertexNormal(i, Vector3.UnitZ);
+                    }
+                }
+            }
+        }
+
+        private static void _AddDirection(Dictionary<Vector3, Vector3> dict, Vector3 pos, Vector3 dir)
+        {
+            if (!dir._IsFinite()) return;
+            if (!dict.TryGetValue(pos, out Vector3 n)) n = Vector3.Zero;
+            dict[pos] = n + dir;
+        }
+    }    
+
+    static class VertexTangentsFactory
+    {
+        // https://gamedev.stackexchange.com/questions/128023/how-does-mikktspace-work-for-calculating-the-tangent-space-during-normal-mapping
+        // https://stackoverflow.com/questions/25349350/calculating-per-vertex-tangents-for-glsl
+        // https://github.com/buildaworldnet/IrrlichtBAW/wiki/How-to-Normal-Detail-Bump-Derivative-Map,-why-Mikkelsen-is-slightly-wrong-and-why-you-should-give-up-on-calculating-per-vertex-tangents
+        // https://gamedev.stackexchange.com/questions/68612/how-to-compute-tangent-and-bitangent-vectors
+        // https://www.marti.works/calculating-tangents-for-your-mesh/
+        // https://www.html5gamedevs.com/topic/34364-gltf-support-and-mikkt-space/
+
+        
+
+        /// <summary>
+        /// this interface must be defined by the input primitive to which we want to add tangents
+        /// </summary>
+        public interface IMeshPrimitive
+        {
+            int VertexCount { get; }
+
+            Vector3 GetVertexPosition(int idx);
+            Vector3 GetVertexNormal(int idx);
+            Vector2 GetVertexTexCoord(int idx);
+
+            void SetVertexTangent(int idx, Vector4 tangent);
+
+            IEnumerable<(int A, int B, int C)> GetTriangleIndices();
+        }
+
+        private static bool _IsFinite(this float value) { return !float.IsNaN(value) && !float.IsInfinity(value); }
+
+        private static bool _IsFinite(this Vector3 value) { return value.X._IsFinite() && value.Y._IsFinite() && value.Z._IsFinite(); }
+
+        public static void CalculateTangents<T>(IReadOnlyList<T> primitives)
+            where T : IMeshPrimitive
+        {
+            // Guard.NotNull(primitives, nameof(primitives));
+
+            var tangentsMap = new Dictionary<VERTEXKEY, (Vector3 u, Vector3 v)>();
+
+            // calculate
+
+            foreach (var primitive in primitives)
+            {
+                foreach (var (i1, i2, i3) in primitive.GetTriangleIndices())
+                {
+                    var p1 = primitive.GetVertexPosition(i1);
+                    var p2 = primitive.GetVertexPosition(i2);
+                    var p3 = primitive.GetVertexPosition(i3);
+
+                    // check for degenerated triangle
+                    if (p1 == p2 || p1 == p3 || p2 == p3) continue;
+
+                    var uv1 = primitive.GetVertexTexCoord(i1);
+                    var uv2 = primitive.GetVertexTexCoord(i2);
+                    var uv3 = primitive.GetVertexTexCoord(i3);
+
+                    // check for degenerated triangle
+                    if (uv1 == uv2 || uv1 == uv3 || uv2 == uv3) continue;
+
+                    var n1 = primitive.GetVertexNormal(i1);
+                    var n2 = primitive.GetVertexNormal(i2);
+                    var n3 = primitive.GetVertexNormal(i3);
+
+                    // calculate tangents
+
+                    var svec = p2 - p1;
+                    var tvec = p3 - p1;
+
+                    var stex = uv2 - uv1;
+                    var ttex = uv3 - uv1;
+
+                    float sx = stex.X;
+                    float tx = ttex.X;
+                    float sy = stex.Y;
+                    float ty = ttex.Y;
+
+                    var r = 1.0F / ((sx * ty) - (tx * sy));
+
+                    if (!r._IsFinite()) continue;
+
+                    var sdir = new Vector3((ty * svec.X) - (sy * tvec.X), (ty * svec.Y) - (sy * tvec.Y), (ty * svec.Z) - (sy * tvec.Z)) * r;
+                    var tdir = new Vector3((sx * tvec.X) - (tx * svec.X), (sx * tvec.Y) - (tx * svec.Y), (sx * tvec.Z) - (tx * svec.Z)) * r;
+
+                    if (!sdir._IsFinite()) continue;
+                    if (!tdir._IsFinite()) continue;
+
+                    // accumulate tangents
+
+                    _AddTangent(tangentsMap, (p1, n1, uv1), (sdir, tdir));
+                    _AddTangent(tangentsMap, (p2, n2, uv2), (sdir, tdir));
+                    _AddTangent(tangentsMap, (p3, n3, uv3), (sdir, tdir));
+                }
+            }
+
+            // normalize
+
+            foreach (var key in tangentsMap.Keys.ToList())
+            {
+                var val = tangentsMap[key];
+
+                // Gram-Schmidt orthogonalize
+                val.u = Vector3.Normalize(val.u - (key.Item2 * Vector3.Dot(key.Item2, val.u)));
+                val.v = Vector3.Normalize(val.v - (key.Item2 * Vector3.Dot(key.Item2, val.v)));
+
+                tangentsMap[key] = val;
+            }
+
+            // apply
+
+            foreach (var primitive in primitives)
+            {
+                for (int i = 0; i < primitive.VertexCount; ++i)
+                {
+                    var p = primitive.GetVertexPosition(i);
+                    var n = primitive.GetVertexNormal(i);
+                    var t = primitive.GetVertexTexCoord(i);
+
+                    if (tangentsMap.TryGetValue((p, n, t), out (Vector3 u, Vector3 v) tangents))
+                    {
+                        var handedness = Vector3.Dot(Vector3.Cross(tangents.u, n), tangents.v) < 0 ? -1.0f : 1.0f;
+
+                        primitive.SetVertexTangent(i, new Vector4(tangents.u, handedness));
+                    }
+                    else
+                    {
+                        primitive.SetVertexTangent(i, new Vector4(1, 0, 0, 1));
+                    }
+                }
+            }
+        }
+
+        private static void _AddTangent(Dictionary<VERTEXKEY, (Vector3, Vector3)> dict, VERTEXKEY key, (Vector3 tu, Vector3 tv) alpha)
+        {
+            dict.TryGetValue(key, out (Vector3 tu, Vector3 tv) beta);
+
+            dict[key] = (alpha.tu + beta.tu, alpha.tv + beta.tv);
+        }
+    }
+}

+ 31 - 183
examples/SharpGLTF.Runtime.MonoGame/ResourceManager.cs

@@ -5,8 +5,12 @@ using System.Linq;
 using Microsoft.Xna.Framework;
 using Microsoft.Xna.Framework.Graphics;
 
+using SharpGLTF.Schema2;
+
 namespace SharpGLTF.Runtime
 {
+    // tracks all the disposable objects of a model;
+    // vertex buffers, index buffers, effects and textures.
     class GraphicsResourceTracker
     {
         #region data
@@ -21,8 +25,7 @@ namespace SharpGLTF.Runtime
 
         #endregion
 
-        #region API
-
+        #region API        
         public void AddDisposable(GraphicsResource resource)
         {
             if (resource == null) throw new ArgumentNullException();
@@ -89,11 +92,11 @@ namespace SharpGLTF.Runtime
         #endregion        
     }
 
-    class MaterialFactory
+    class EffectsFactory
     {
         #region lifecycle
 
-        public MaterialFactory(GraphicsDevice device, GraphicsResourceTracker disposables)
+        public EffectsFactory(GraphicsDevice device, GraphicsResourceTracker disposables)
         {
             _Device = device;
             _TexFactory = new TextureFactory(device, disposables);
@@ -110,201 +113,46 @@ namespace SharpGLTF.Runtime
 
         private readonly Dictionary<Object, Effect> _RigidEffects = new Dictionary<Object, Effect>();
         private readonly Dictionary<Object, SkinnedEffect> _SkinnedEffects = new Dictionary<Object, SkinnedEffect>();
-
-        private BasicEffect _DefaultRigid;
-        private SkinnedEffect _DefaultSkinned;
-
-        #endregion        
+        
+        #endregion
 
         #region API - Schema
 
-        // Monogame's BasicEffect uses Phong's shading, while glTF uses PBR shading, so
-        // given monogame's limitations, we try to guess the most appropiate values
-        // to have a reasonably good looking renders.
-
-        public Effect UseRigidEffect(Schema2.Material srcMaterial)
+        public void Register(Object key, bool isSkinned, Effect effect)
         {
-            if (_Device == null) throw new InvalidOperationException();
+            if (key == null) throw new ArgumentNullException(nameof(key));
+            if (effect == null) throw new ArgumentNullException(nameof(effect));
 
-            if (srcMaterial == null)
-            {
-                if (_DefaultRigid == null)
-                {
-                    _DefaultRigid = new BasicEffect(_Device);
-                    _Disposables.AddDisposable(_DefaultRigid);
-                }
+            if (isSkinned && effect is SkinnedEffect skEffect) { _SkinnedEffects[key] = skEffect; }
+            else { _RigidEffects[key] = effect; }
+        }        
 
-                return _DefaultRigid;
-            }
-
-            if (_RigidEffects.TryGetValue(srcMaterial, out Effect dstMaterial)) return dstMaterial;
-
-            dstMaterial = srcMaterial.Alpha == Schema2.AlphaMode.MASK ? CreateAlphaTestEffect(srcMaterial) : CreateBasicEffect(srcMaterial);
-
-            _RigidEffects[srcMaterial] = dstMaterial;
-
-            return dstMaterial;
-        }
-
-        private Effect CreateBasicEffect(Schema2.Material srcMaterial)
+        public Effect GetMaterial(Schema2.Material srcMaterial, bool isSkinned)
         {
-            var dstMaterial = new BasicEffect(_Device);
-            _Disposables.AddDisposable(dstMaterial);           
-
-
-            dstMaterial.Name = srcMaterial.Name;
-
-            dstMaterial.Alpha = GetAlphaLevel(srcMaterial);
-            dstMaterial.DiffuseColor = GetDiffuseColor(srcMaterial);
-            dstMaterial.SpecularColor = GetSpecularColor(srcMaterial);
-            dstMaterial.SpecularPower = GetSpecularPower(srcMaterial);
-            dstMaterial.EmissiveColor = GeEmissiveColor(srcMaterial);
-            dstMaterial.Texture = GetDiffuseTexture(srcMaterial);
-
-            if (srcMaterial.Unlit)
+            if (isSkinned)
             {
-                dstMaterial.EmissiveColor = dstMaterial.DiffuseColor;
-                dstMaterial.SpecularColor = Vector3.Zero;
-                dstMaterial.SpecularPower = 16;
+                if (_SkinnedEffects.TryGetValue(srcMaterial, out SkinnedEffect dstMaterial)) return dstMaterial;
             }
-
-            dstMaterial.PreferPerPixelLighting = true;
-            dstMaterial.TextureEnabled = dstMaterial.Texture != null;
-
-            return dstMaterial;
-        }
-
-        private Effect CreateAlphaTestEffect(Schema2.Material srcMaterial)
-        {
-            var dstMaterial = new AlphaTestEffect(_Device);
-            _Disposables.AddDisposable(dstMaterial);
-
-            dstMaterial.Name = srcMaterial.Name;
-
-            dstMaterial.Alpha = GetAlphaLevel(srcMaterial);
-            //dstMaterial.AlphaFunction = CompareFunction.GreaterEqual;
-            dstMaterial.ReferenceAlpha = (int)(srcMaterial.AlphaCutoff * 255);
-            
-
-            dstMaterial.DiffuseColor = GetDiffuseColor(srcMaterial);
-            
-            dstMaterial.Texture = GetDiffuseTexture(srcMaterial);            
-
-            return dstMaterial;
-        }
-
-        public Effect UseSkinnedEffect(Schema2.Material srcMaterial)
-        {
-            if (_Device == null) throw new InvalidOperationException();
-
-            if (srcMaterial == null)
+            else
             {
-                if (_DefaultSkinned == null)
-                {
-                    _DefaultSkinned = new SkinnedEffect(_Device);
-                    _Disposables.AddDisposable(_DefaultRigid);
-                }
-
-                return _DefaultSkinned;
+                if (_RigidEffects.TryGetValue(srcMaterial, out Effect dstMaterial)) return dstMaterial;
             }
 
-            if (_SkinnedEffects.TryGetValue(srcMaterial, out SkinnedEffect dstMaterial)) return dstMaterial;
-
-            dstMaterial = new SkinnedEffect(_Device);
-            _SkinnedEffects[srcMaterial] = dstMaterial;
-            _Disposables.AddDisposable(dstMaterial);
-
-            dstMaterial.Name = srcMaterial.Name;
+            return null;
+        }        
 
-            dstMaterial.Alpha = GetAlphaLevel(srcMaterial);
-            dstMaterial.DiffuseColor = GetDiffuseColor(srcMaterial);
-            dstMaterial.SpecularColor = GetSpecularColor(srcMaterial);
-            dstMaterial.SpecularPower = GetSpecularPower(srcMaterial);
-            dstMaterial.EmissiveColor = GeEmissiveColor(srcMaterial);
-            dstMaterial.Texture = GetDiffuseTexture(srcMaterial);
-
-            dstMaterial.WeightsPerVertex = 4;
-            dstMaterial.PreferPerPixelLighting = true;
-            // apparently, SkinnedEffect does not support disabling textures, so we set a white texture here.
-            if (dstMaterial.Texture == null) dstMaterial.Texture = _TexFactory.UseWhiteImage();
-
-            return dstMaterial;
-        }
-
-        private static float GetAlphaLevel(Schema2.Material srcMaterial)
-        {
-            if (srcMaterial.Alpha == Schema2.AlphaMode.OPAQUE) return 1;
-
-            var baseColor = srcMaterial.FindChannel("BaseColor");
-
-            if (baseColor == null) return 1;
-
-            return baseColor.Value.Parameter.W;
-        }
-
-        private static Vector3 GetDiffuseColor(Schema2.Material srcMaterial)
+        internal Texture2D UseTexture(MaterialChannel? channel, string name)
         {
-            var diffuse = srcMaterial.FindChannel("Diffuse");
-
-            if (diffuse == null) diffuse = srcMaterial.FindChannel("BaseColor");
-
-            if (diffuse == null) return Vector3.One;
-
-            return new Vector3(diffuse.Value.Parameter.X, diffuse.Value.Parameter.Y, diffuse.Value.Parameter.Z);
-        }
-
-        private static Vector3 GetSpecularColor(Schema2.Material srcMaterial)
-        {
-            var mr = srcMaterial.FindChannel("MetallicRoughness");
-
-            if (mr == null) return Vector3.One; // default value 16
-
-            var diffuse = GetDiffuseColor(srcMaterial);
-            var metallic = mr.Value.Parameter.X;
-            var roughness = mr.Value.Parameter.Y;
-
-            var k = Vector3.Zero;
-            k += Vector3.Lerp(diffuse, Vector3.Zero, roughness);
-            k += Vector3.Lerp(diffuse, Vector3.One, metallic);
-            k *= 0.5f;
+            if (!channel.HasValue) return _TexFactory.UseWhiteImage();
 
-            return k;
-        }
-
-        private static float GetSpecularPower(Schema2.Material srcMaterial)
-        {
-            var mr = srcMaterial.FindChannel("MetallicRoughness");
-
-            if (mr == null) return 16; // default value = 16
-
-            var metallic = mr.Value.Parameter.X;
-            var roughness = mr.Value.Parameter.Y;
-
-            return 4 + 16 * metallic;
-        }
-
-        private static Vector3 GeEmissiveColor(Schema2.Material srcMaterial)
-        {
-            var emissive = srcMaterial.FindChannel("Emissive");
-
-            if (emissive == null) return Vector3.Zero;
-
-            return new Vector3(emissive.Value.Parameter.X, emissive.Value.Parameter.Y, emissive.Value.Parameter.Z);
-        }
-
-        private Texture2D GetDiffuseTexture(Schema2.Material srcMaterial)
-        {
-            var diffuse = srcMaterial.FindChannel("Diffuse");
-
-            if (diffuse == null) diffuse = srcMaterial.FindChannel("BaseColor");
-
-            if (diffuse == null) return null;
-
-            var name = srcMaterial.Name;
-            if (name == null) name = "null";
-            name += "-Diffuse";            
+            if (channel.HasValue && name == null)
+            {
+                name = channel.Value.LogicalParent.Name;
+                if (name == null) name = "null";
+                name += $"-{channel.Value.Key}";
+            }            
 
-            return _TexFactory.UseTexture(diffuse.Value.Texture?.PrimaryImage?.Content ?? default, name);
+            return _TexFactory.UseTexture(channel.Value.Texture?.PrimaryImage?.Content ?? default, name);
         }
 
         #endregion

+ 2 - 1
examples/SharpGLTF.Runtime.MonoGame/SharpGLTF.Runtime.MonoGame.csproj

@@ -3,7 +3,8 @@
   <PropertyGroup>
     <TargetFramework>net471</TargetFramework>
     <RootNamespace>SharpGLTF.Runtime</RootNamespace>
-    <LangVersion>7.1</LangVersion>
+    <LangVersion>7.3</LangVersion>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
   <ItemGroup>

+ 14 - 12
examples/SharpGLTF.Runtime.MonoGame/VertexSkinned.cs

@@ -10,19 +10,9 @@ namespace SharpGLTF.Runtime
 {
     struct VertexSkinned : IVertexType
     {
-        #region data
+        #region static
 
-        public Vector3 Position;
-        public Vector3 Normal;
-        public Vector2 TextureCoordinate;
-        public Microsoft.Xna.Framework.Graphics.PackedVector.Byte4 BlendIndices;
-        public Vector4 BlendWeight;
-        
-        #endregion
-
-        #region API
-
-        public VertexDeclaration VertexDeclaration => CreateVertexDeclaration();
+        private static VertexDeclaration _VDecl = CreateVertexDeclaration();
 
         public static VertexDeclaration CreateVertexDeclaration()
         {
@@ -47,5 +37,17 @@ namespace SharpGLTF.Runtime
         }
 
         #endregion
+
+        #region data
+
+        public VertexDeclaration VertexDeclaration => _VDecl;
+
+        public Vector3 Position;
+        public Vector3 Normal;
+        public Vector2 TextureCoordinate;
+        public Microsoft.Xna.Framework.Graphics.PackedVector.Byte4 BlendIndices;
+        public Vector4 BlendWeight;
+        
+        #endregion
     }
 }

+ 1 - 22
examples/SharpGLTF.Runtime.MonoGame/_Extensions.cs

@@ -32,28 +32,7 @@ namespace SharpGLTF.Runtime
                 m.M31, m.M32, m.M33, m.M34,
                 m.M41, m.M42, m.M43, m.M44
                 );
-        }
-
-        public static void FixTextureSampler(this Schema2.ModelRoot root)
-        {
-            // SharpGLTF 1.0.0-Alpha10 has an issue with TextureSamplers, it's fixed in newer versions
-
-            foreach(var t in root.LogicalTextures)
-            {
-                if (t.Sampler == null)
-                {
-                    var sampler = root.UseTextureSampler
-                        (
-                        Schema2.TextureWrapMode.REPEAT,
-                        Schema2.TextureWrapMode.REPEAT,
-                        Schema2.TextureMipMapFilter.DEFAULT,
-                        Schema2.TextureInterpolationFilter.LINEAR
-                        );
-
-                    t.Sampler = sampler;
-                }
-            }
-        }
+        }       
 
         public static BoundingSphere CreateBoundingSphere(this Schema2.Mesh mesh)
         {