Browse Source

Added MeshDecoder APIs to easily read mesh data.

Vicente Penades 5 years ago
parent
commit
93def771db

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

@@ -23,7 +23,7 @@ namespace SharpGLTF
 
             // set the node animations for our scene instance-
             if (animation == null) { sceneInstance.SetPoseTransforms(); }
-            else { sceneInstance.SetAnimationFrame(animation.Name, time); }
+            else { sceneInstance.SetAnimationFrame(animation.LogicalIndex, time); }
 
             // keep source meshes.
             var meshes = srcScene.LogicalParent.LogicalMeshes;

+ 3 - 8
examples/SharpGLTF.Runtime.MonoGame/MonoGameModelTemplate.cs

@@ -83,7 +83,6 @@ namespace SharpGLTF.Runtime
         /// </summary>
         private readonly Effect[] _Effects;
 
-
         private readonly SceneTemplate[] _Scenes;
         private readonly BoundingSphere[] _Bounds;
 
@@ -97,19 +96,15 @@ namespace SharpGLTF.Runtime
 
         public IReadOnlyList<Effect> Effects => _Effects;
 
-        public BoundingSphere Bounds => GetBounds(_DefaultSceneIndex);
-
-        public IEnumerable<string> AnimationTracks => GetAnimationTracks(_DefaultSceneIndex);
-
+        public BoundingSphere Bounds => GetBounds(_DefaultSceneIndex);        
+        
         #endregion
 
         #region API
 
         public int IndexOfScene(string sceneName) => Array.FindIndex(_Scenes, item => item.Name == sceneName);
 
-        public BoundingSphere GetBounds(int sceneIndex) => _Bounds[sceneIndex];
-
-        public IEnumerable<string> GetAnimationTracks(int sceneIndex) => _Scenes[sceneIndex].AnimationTracks;
+        public BoundingSphere GetBounds(int sceneIndex) => _Bounds[sceneIndex];        
 
         public MonoGameModelInstance CreateInstance() => CreateInstance(_DefaultSceneIndex);
 

+ 13 - 3
src/SharpGLTF.Core/Collections/NamedList.cs

@@ -11,17 +11,22 @@ namespace SharpGLTF.Collections
     class NamedList<T> : List<T>
     {
         private Dictionary<string, int> _ByName;
+        private List<string> _ByIndex;
 
         public IReadOnlyCollection<String> Names => _ByName != null ? _ByName.Keys : (IReadOnlyCollection<String>)Array.Empty<string>();
 
-        public void SetValue(int index, string name, T value)
+        public void SetName(int index, string name, T value)
         {
             Guard.MustBeGreaterThanOrEqualTo(index, 0, nameof(index));
 
             if (!string.IsNullOrEmpty(name))
             {
                 if (_ByName == null) _ByName = new Dictionary<string, int>();
+                if (_ByIndex == null) _ByIndex = new List<string>();
+                if (_ByIndex.Count <= index) _ByIndex.Add(null);
+
                 _ByName[name] = index;
+                _ByIndex[index] = name;
             }
 
             while (this.Count <= index) this.Add(default);
@@ -32,10 +37,15 @@ namespace SharpGLTF.Collections
         public int IndexOf(string name)
         {
             if (_ByName == null) return default;
-
             if (string.IsNullOrEmpty(name)) return default;
-
             return _ByName.TryGetValue(name, out int index) ? index : -1;
         }
+
+        public string NameOf(int index)
+        {
+            if (_ByIndex == null) return null;
+            if (index < 0 || index >= _ByIndex.Count) return null;
+            return _ByIndex[index];
+        }
     }
 }

+ 1 - 1
src/SharpGLTF.Core/Runtime/AnimatableProperty.cs

@@ -75,7 +75,7 @@ namespace SharpGLTF.Runtime
 
             if (_Animations == null) _Animations = new Collections.NamedList<ICurveSampler<T>>();
 
-            _Animations.SetValue(logicalIndex, name, sampler);
+            _Animations.SetName(logicalIndex, name, sampler);
         }
 
         #endregion

+ 525 - 0
src/SharpGLTF.Core/Runtime/MeshDecoder.Schema2.cs

@@ -0,0 +1,525 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Numerics;
+using System.Globalization;
+
+using SCHEMA2ACCESSOR = SharpGLTF.Schema2.Accessor;
+using SCHEMA2PRIMITIVE = SharpGLTF.Schema2.MeshPrimitive;
+
+using XY = System.Numerics.Vector2;
+using XYZ = System.Numerics.Vector3;
+using XYZW = System.Numerics.Vector4;
+
+namespace SharpGLTF.Runtime
+{
+    sealed class _MeshDecoder<TMaterial> : IMeshDecoder<TMaterial>
+        where TMaterial : class
+    {
+        #region lifecycle
+
+        public _MeshDecoder(Schema2.Mesh srcMesh)
+        {
+            Guard.NotNull(srcMesh, nameof(srcMesh));
+
+            _Name = srcMesh.Name;
+
+            _LogicalIndex = srcMesh.LogicalIndex;
+
+            _Primitives = srcMesh
+                .Primitives
+                .Select(item => new _MeshPrimitiveDecoder<TMaterial>(item))
+                .ToArray();
+
+            _Extras = srcMesh.Extras;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly string _Name;
+        private readonly int _LogicalIndex;
+        private readonly _MeshPrimitiveDecoder<TMaterial>[] _Primitives;
+
+        private readonly Object _Extras;
+
+        #endregion
+
+        #region properties
+
+        public string Name => _Name;
+        public int LogicalIndex => _LogicalIndex;
+        public IReadOnlyList<IMeshPrimitiveDecoder<TMaterial>> Primitives => _Primitives;
+
+        #endregion
+
+        #region API
+
+        public void GenerateNormalsAndTangents()
+        {
+            if (_Primitives.Length == 0) return;
+
+            var geometries = _Primitives.Select(item => item._Geometry);
+            VertexNormalsFactory.CalculateSmoothNormals(geometries);
+            VertexTangentsFactory.CalculateTangents(geometries);
+
+            var morphTargetsCount = _Primitives.Min(item => item.MorphTargetsCount);
+
+            for (int i = 0; i < morphTargetsCount; ++i)
+            {
+                var targets = _Primitives.Select(item => item._MorphTargets[i]);
+                VertexNormalsFactory.CalculateSmoothNormals(targets);
+                VertexTangentsFactory.CalculateTangents(targets);
+            }
+        }
+
+        #endregion
+    }
+
+    [System.Diagnostics.DebuggerDisplay("{_GetDebugString(),nq}")]
+    sealed class _MeshPrimitiveDecoder<TMaterial>
+        : _MeshPrimitiveDecoder
+        , IMeshPrimitiveDecoder<TMaterial>
+        where TMaterial : class
+    {
+        #region lifecycle
+
+        internal _MeshPrimitiveDecoder(SCHEMA2PRIMITIVE srcPrim)
+            : base(srcPrim)
+        {
+            _Material = srcPrim.Material as TMaterial;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly TMaterial _Material;
+
+        #endregion
+
+        #region properties
+
+        public TMaterial Material => _Material;
+
+        #endregion
+    }
+
+    [System.Diagnostics.DebuggerDisplay("{_GetDebugString(),nq}")]
+    class _MeshPrimitiveDecoder : IMeshPrimitiveDecoder
+    {
+        #region debug
+
+        protected virtual string _GetDebugString()
+        {
+            var vCount = _Geometry.VertexCount;
+            var tCount = TriangleIndices.Count();
+
+            return $"Primitive Vertices:{vCount} Triangles:{tCount}";
+        }
+
+        #endregion
+
+        #region lifecycle
+
+        internal _MeshPrimitiveDecoder(SCHEMA2PRIMITIVE srcPrim)
+        {
+            _Extras = srcPrim.Extras;
+
+            // indices (points, lines or triangles)
+
+            _PrimitiveType = srcPrim.DrawPrimitiveType;
+            _PrimitiveIndices = srcPrim.GetIndices() as IReadOnlyList<uint>;
+
+            // base geometry (position + normal + tangent).
+            _Geometry = new _MeshGeometryDecoder(this, srcPrim);
+
+            // morph targets (positionDelta + normalDelta + tangentDelta)
+            for (int i = 0; i < srcPrim.MorphTargetsCount; ++i)
+            {
+                var morphTarget = new _MorphTargetDecoder(_Geometry, srcPrim, i);
+                _MorphTargets.Add(morphTarget);
+            }
+
+            // additional vertex attributes (color + UVs + Skinning)
+
+            _Color0 = srcPrim.GetVertexAccessor("COLOR_0")?.AsColorArray() as IReadOnlyList<XYZW>;
+            _TexCoord0 = srcPrim.GetVertexAccessor("TEXCOORD_0")?.AsVector2Array() as IReadOnlyList<XY>;
+            _TexCoord1 = srcPrim.GetVertexAccessor("TEXCOORD_1")?.AsVector2Array() as IReadOnlyList<XY>;
+
+            _Joints0 = srcPrim.GetVertexAccessor("JOINTS_0")?.AsVector4Array() as IReadOnlyList<XYZW>;
+            _Joints1 = srcPrim.GetVertexAccessor("JOINTS_1")?.AsVector4Array() as IReadOnlyList<XYZW>;
+            _Weights0 = srcPrim.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array() as IReadOnlyList<XYZW>;
+            _Weights1 = srcPrim.GetVertexAccessor("WEIGHTS_1")?.AsVector4Array() as IReadOnlyList<XYZW>;
+
+            if (_Joints0 == null || _Weights0 == null) { _Joints0 = _Joints1 = _Weights0 = _Weights1 = null; }
+            if (_Joints1 == null || _Weights1 == null) { _Joints1 = _Weights1 = null; }
+
+            if (_Weights0 != null)
+            {
+                var wwww = _Weights0.ToArray(); // isolate memory to prevent overwriting source glTF.
+
+                for (int i = 0; i < _Weights0.Count; ++i)
+                {
+                    var r = XYZW.Dot(_Weights0[i], XYZW.One);
+                    wwww[i] /= r;
+                }
+
+                _Weights0 = wwww;
+            }
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly Schema2.PrimitiveType _PrimitiveType;
+        private readonly IReadOnlyList<uint> _PrimitiveIndices;
+
+        internal readonly _MeshGeometryDecoder _Geometry;
+
+        internal readonly List<_MorphTargetDecoder> _MorphTargets = new List<_MorphTargetDecoder>();
+
+        private readonly IReadOnlyList<XYZW> _Color0;
+
+        private readonly IReadOnlyList<XY> _TexCoord0;
+        private readonly IReadOnlyList<XY> _TexCoord1;
+
+        private readonly IReadOnlyList<XYZW> _Joints0;
+        private readonly IReadOnlyList<XYZW> _Joints1;
+
+        private readonly IReadOnlyList<XYZW> _Weights0;
+        private readonly IReadOnlyList<XYZW> _Weights1;
+
+        private readonly Object _Extras;
+
+        #endregion
+
+        #region properties
+
+        public int VertexCount => _Geometry.VertexCount;
+
+        public int ColorsCount => _Color0 != null ? 1 : 0;
+
+        public int TexCoordsCount => (_TexCoord0 != null ? 1 : 0) + (_TexCoord1 != null ? 1 : 0);
+
+        public int JointsWeightsCount => (_Joints0 != null ? 4 : 0) + (_Joints1 != null ? 4 : 0);
+
+        public int MorphTargetsCount => _MorphTargets.Count;
+
+        public IEnumerable<(int A, int B, int C)> TriangleIndices
+        {
+            get
+            {
+                if (this._PrimitiveType.GetPrimitiveVertexSize() != 3) return Enumerable.Empty<(int, int, int)>();
+
+                if (this._PrimitiveIndices == null) return this._PrimitiveType.GetTrianglesIndices(VertexCount);
+
+                return this._PrimitiveType.GetTrianglesIndices(this._PrimitiveIndices);
+            }
+        }
+
+        #endregion
+
+        #region API
+
+        public XYZ GetPosition(int vertexIndex) { return _Geometry.GetPosition(vertexIndex); }
+
+        public IReadOnlyList<XYZ> GetPositionDeltas(int vertexIndex)
+        {
+            return MorphTargetsCount > 0
+                ? (IReadOnlyList<XYZ>)new _MorphTargetPositionSlice(_MorphTargets, vertexIndex)
+                : Array.Empty<XYZ>();
+        }
+
+        public XYZ GetNormal(int vertexIndex) { return _Geometry.GetNormal(vertexIndex); }
+
+        public IReadOnlyList<XYZ> GetNormalDeltas(int vertexIndex)
+        {
+            return MorphTargetsCount > 0
+                ? (IReadOnlyList<XYZ>)new _MorphTargetNormalSlice(_MorphTargets, vertexIndex)
+                : Array.Empty<XYZ>();
+        }
+
+        public XYZW GetTangent(int vertexIndex) { return _Geometry.GetTangent(vertexIndex); }
+
+        public IReadOnlyList<XYZ> GetTangentDeltas(int vertexIndex)
+        {
+            return MorphTargetsCount > 0
+                ? (IReadOnlyList<XYZ>)new _MorphTargetTangentSlice(_MorphTargets, vertexIndex)
+                : Array.Empty<XYZ>();
+        }
+
+        public XY GetTextureCoord(int vertexIndex, int set)
+        {
+            if (set == 0 && _TexCoord0 != null) return _TexCoord0[vertexIndex];
+            if (set == 1 && _TexCoord1 != null) return _TexCoord1[vertexIndex];
+
+            return XY.Zero;
+        }
+
+        public XYZW GetColor(int vertexIndex, int set)
+        {
+            if (set == 0 && _Color0 != null) return _Color0[vertexIndex];
+
+            return XYZW.One;
+        }
+
+        public XYZW GetJoints(int vertexIndex)
+        {
+            if (_Joints0 != null) return _Joints0[vertexIndex];
+            return XYZW.Zero;
+        }
+
+        public XYZW GetWeights(int vertexIndex)
+        {
+            if (_Weights0 != null) return _Weights0[vertexIndex];
+            return XYZW.UnitX;
+        }
+
+        public Transforms.SparseWeight8 GetSkinWeights(int vertexIndex)
+        {
+            if (_Weights0 == null) return default;
+            if (_Weights1 == null) return new Transforms.SparseWeight8(_Joints0[vertexIndex], _Weights0[vertexIndex]);
+            return new Transforms.SparseWeight8(_Joints0[vertexIndex], _Joints1[vertexIndex], _Weights0[vertexIndex], _Weights1[vertexIndex]);
+        }
+
+        #endregion
+    }
+
+    [System.Diagnostics.DebuggerDisplay("Vertices: {VertexCount}")]
+    sealed class _MeshGeometryDecoder
+        : VertexNormalsFactory.IMeshPrimitive
+        , VertexTangentsFactory.IMeshPrimitive
+    {
+        #region  lifecycle
+
+        public _MeshGeometryDecoder(_MeshPrimitiveDecoder owner, SCHEMA2PRIMITIVE srcPrim)
+        {
+            _Owner = owner;
+
+            _Positions = srcPrim.GetVertexAccessor("POSITION")?.AsVector3Array() as IReadOnlyList<XYZ>;
+            _Normals = srcPrim.GetVertexAccessor("NORMAL")?.AsVector3Array() as IReadOnlyList<XYZ>;
+            _Tangents = srcPrim.GetVertexAccessor("TANGENT")?.AsVector4Array() as IReadOnlyList<XYZW>;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly _MeshPrimitiveDecoder _Owner;
+
+        internal readonly IReadOnlyList<XYZ> _Positions;
+        private IReadOnlyList<XYZ> _Normals;
+        private IReadOnlyList<XYZW> _Tangents;
+
+        private XYZ[] _GeneratedNormals;
+        private XYZW[] _GeneratedTangents;
+
+        #endregion
+
+        #region properties
+
+        public int VertexCount => _Positions?.Count ?? 0;
+
+        #endregion
+
+        #region API
+
+        public XYZ GetPosition(int vertexIndex) { return _Positions[vertexIndex]; }
+
+        public XYZ GetNormal(int vertexIndex) { return _Normals[vertexIndex]; }
+
+        public XYZW GetTangent(int vertexIndex) { return _Tangents[vertexIndex]; }
+
+        public XY GetTextureCoord(int vertexIndex, int set) { return _Owner.GetTextureCoord(vertexIndex, set); }
+
+        #endregion
+
+        #region Support methods for VertexNormalsFactory and VertexTangentsFactory
+
+        IEnumerable<(int A, int B, int C)> VertexNormalsFactory.IMeshPrimitive.GetTriangleIndices() { return _Owner.TriangleIndices; }
+
+        IEnumerable<(int A, int B, int C)> VertexTangentsFactory.IMeshPrimitive.GetTriangleIndices() { return _Owner.TriangleIndices; }
+
+        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)
+        {
+            if (_Normals == null) _Normals = _GeneratedNormals = new XYZ[VertexCount];
+            if (_GeneratedNormals != null) _GeneratedNormals[idx] = normal;
+        }
+
+        void VertexTangentsFactory.IMeshPrimitive.SetVertexTangent(int idx, XYZW tangent)
+        {
+            if (_Tangents == null) _Tangents = _GeneratedTangents = new XYZW[VertexCount];
+            if (_GeneratedTangents != null) _GeneratedTangents[idx] = tangent;
+        }
+
+        #endregion
+    }
+
+    [System.Diagnostics.DebuggerDisplay("Vertices: {VertexCount}")]
+    sealed class _MorphTargetDecoder
+        : VertexNormalsFactory.IMeshPrimitive
+        , VertexTangentsFactory.IMeshPrimitive
+    {
+        #region  lifecycle
+
+        public _MorphTargetDecoder(_MeshGeometryDecoder geometry, SCHEMA2PRIMITIVE srcPrim, int morphTargetIndex)
+        {
+            _Geometry = geometry;
+
+            // get morph deltas and apply them to our base geometry copy.
+
+            var morphs = srcPrim.GetMorphTargetAccessors(morphTargetIndex);
+
+            if (morphs.TryGetValue("POSITION", out SCHEMA2ACCESSOR pAccessor))
+            {
+                _PositionsDeltas = pAccessor.AsVector3Array() as IReadOnlyList<Vector3>;
+            }
+
+            if (morphs.TryGetValue("NORMAL", out SCHEMA2ACCESSOR nAccessor))
+            {
+                _NormalsDeltas = nAccessor.AsVector3Array() as IReadOnlyList<Vector3>;
+            }
+
+            if (morphs.TryGetValue("TANGENT", out SCHEMA2ACCESSOR tAccessor))
+            {
+                _TangentsDeltas = tAccessor.AsVector3Array() as IReadOnlyList<Vector3>;
+            }
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly _MeshGeometryDecoder _Geometry;
+
+        internal readonly IReadOnlyList<XYZ> _PositionsDeltas;
+        private IReadOnlyList<XYZ> _NormalsDeltas;
+        private IReadOnlyList<XYZ> _TangentsDeltas;
+
+        private XYZ[] _GeneratedNormals;
+        private XYZ[] _GeneratedTangents;
+
+        #endregion
+
+        #region properties
+
+        public int VertexCount => _PositionsDeltas?.Count ?? 0;
+
+        #endregion
+
+        #region API
+
+        public XYZ GetPositionBase(int vertexIndex) { return _Geometry.GetPosition(vertexIndex); }
+
+        public XYZ GetPositionDelta(int vertexIndex) { return _PositionsDeltas[vertexIndex]; }
+
+        public XYZ GetNormalBase(int vertexIndex) { return _Geometry.GetNormal(vertexIndex); }
+
+        public XYZ GetNormalDelta(int vertexIndex) { return _NormalsDeltas[vertexIndex]; }
+
+        public XYZW GetTangentBase(int vertexIndex) { return _Geometry.GetTangent(vertexIndex); }
+
+        public XYZ GetTangentDelta(int vertexIndex) { return _TangentsDeltas[vertexIndex]; }
+
+        public XY GetTextureCoord(int vertexIndex, int set) { return _Geometry.GetTextureCoord(vertexIndex, set); }
+
+        #endregion
+
+        #region Support methods for VertexNormalsFactory and VertexTangentsFactory
+
+        IEnumerable<(int A, int B, int C)> VertexNormalsFactory.IMeshPrimitive.GetTriangleIndices()
+        {
+            return ((VertexNormalsFactory.IMeshPrimitive)_Geometry).GetTriangleIndices();
+        }
+
+        IEnumerable<(int A, int B, int C)> VertexTangentsFactory.IMeshPrimitive.GetTriangleIndices()
+        {
+            return ((VertexTangentsFactory.IMeshPrimitive)_Geometry).GetTriangleIndices();
+        }
+
+        XYZ VertexNormalsFactory.IMeshPrimitive.GetVertexPosition(int idx) { return GetPositionBase(idx) + GetPositionDelta(idx); }
+        XYZ VertexTangentsFactory.IMeshPrimitive.GetVertexPosition(int idx) { return GetPositionBase(idx) + GetPositionDelta(idx); }
+        XYZ VertexTangentsFactory.IMeshPrimitive.GetVertexNormal(int idx) { return GetNormalBase(idx) + GetNormalDelta(idx); }
+        XY VertexTangentsFactory.IMeshPrimitive.GetVertexTexCoord(int idx) { return GetTextureCoord(idx, 0); }
+
+        void VertexNormalsFactory.IMeshPrimitive.SetVertexNormal(int idx, XYZ normal)
+        {
+            if (_NormalsDeltas == null) _NormalsDeltas = _GeneratedNormals = new XYZ[VertexCount];
+            if (_GeneratedNormals == null) return;
+            _GeneratedNormals[idx] = normal - GetNormalBase(idx);
+        }
+
+        void VertexTangentsFactory.IMeshPrimitive.SetVertexTangent(int idx, XYZW tangent)
+        {
+            if (_TangentsDeltas == null) _TangentsDeltas = _GeneratedTangents = new XYZ[VertexCount];
+            if (_GeneratedTangents != null) return;
+            var t = tangent - GetTangentBase(idx);
+            _GeneratedTangents[idx] = new XYZ(t.X, t.Y, t.Z);
+        }
+
+        #endregion
+    }
+
+    [System.Diagnostics.DebuggerDisplay("Vertex {_VertexIndex} Positions deltas")]
+    readonly struct _MorphTargetPositionSlice : IReadOnlyList<XYZ>
+    {
+        public _MorphTargetPositionSlice(IReadOnlyList<_MorphTargetDecoder> ggg, int idx)
+        {
+            _Geometries = ggg;
+            _VertexIndex = idx;
+        }
+
+        private readonly IReadOnlyList<_MorphTargetDecoder> _Geometries;
+        private readonly int _VertexIndex;
+
+        public XYZ this[int index] => _Geometries[index].GetPositionDelta(_VertexIndex);
+        public int Count => _Geometries.Count;
+        public IEnumerator<XYZ> GetEnumerator() { throw new NotImplementedException(); }
+        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw new NotImplementedException(); }
+    }
+
+    [System.Diagnostics.DebuggerDisplay("Vertex {_VertexIndex} Normals deltas")]
+    readonly struct _MorphTargetNormalSlice : IReadOnlyList<XYZ>
+    {
+        public _MorphTargetNormalSlice(IReadOnlyList<_MorphTargetDecoder> ggg, int idx)
+        {
+            _Geometries = ggg;
+            _VertexIndex = idx;
+        }
+
+        private readonly IReadOnlyList<_MorphTargetDecoder> _Geometries;
+        private readonly int _VertexIndex;
+
+        public XYZ this[int index] => _Geometries[index].GetNormalDelta(_VertexIndex);
+        public int Count => _Geometries.Count;
+        public IEnumerator<XYZ> GetEnumerator() { throw new NotImplementedException(); }
+        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw new NotImplementedException(); }
+    }
+
+    [System.Diagnostics.DebuggerDisplay("Vertex {_VertexIndex} Tangents deltas")]
+    readonly struct _MorphTargetTangentSlice : IReadOnlyList<XYZ>
+    {
+        public _MorphTargetTangentSlice(IReadOnlyList<_MorphTargetDecoder> ggg, int idx)
+        {
+            _Geometries = ggg;
+            _VertexIndex = idx;
+        }
+
+        private readonly IReadOnlyList<_MorphTargetDecoder> _Geometries;
+        private readonly int _VertexIndex;
+
+        public XYZ this[int index] => _Geometries[index].GetTangentDelta(_VertexIndex);
+        public int Count => _Geometries.Count;
+        public IEnumerator<XYZ> GetEnumerator() { throw new NotImplementedException(); }
+        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw new NotImplementedException(); }
+    }
+}

+ 291 - 0
src/SharpGLTF.Core/Runtime/MeshDecoder.cs

@@ -0,0 +1,291 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using XY = System.Numerics.Vector2;
+using XYZ = System.Numerics.Vector3;
+using XYZW = System.Numerics.Vector4;
+
+namespace SharpGLTF.Runtime
+{
+    public interface IMeshDecoder<TMaterial>
+        where TMaterial : class
+    {
+        string Name { get; }
+        int LogicalIndex { get; }
+        IReadOnlyList<IMeshPrimitiveDecoder<TMaterial>> Primitives { get; }
+    }
+
+    public interface IMeshPrimitiveDecoder
+    {
+        #region properties
+
+        int VertexCount { get; }
+
+        int MorphTargetsCount { get; }
+
+        int ColorsCount { get; }
+
+        int TexCoordsCount { get; }
+
+        int JointsWeightsCount { get; }
+
+        IEnumerable<(int A, int B, int C)> TriangleIndices { get; }
+
+        #endregion
+
+        #region API
+
+        XYZ GetPosition(int vertexIndex);
+
+        XYZ GetNormal(int vertexIndex);
+
+        XYZW GetTangent(int vertexIndex);
+
+        IReadOnlyList<XYZ> GetPositionDeltas(int vertexIndex);
+
+        IReadOnlyList<XYZ> GetNormalDeltas(int vertexIndex);
+
+        IReadOnlyList<XYZ> GetTangentDeltas(int vertexIndex);
+
+        XY GetTextureCoord(int vertexIndex, int textureSetIndex);
+
+        XYZW GetColor(int vertexIndex, int colorSetIndex);
+
+        XYZW GetJoints(int vertexIndex);
+
+        XYZW GetWeights(int vertexIndex);
+
+        Transforms.SparseWeight8 GetSkinWeights(int vertexIndex);
+
+        #endregion
+    }
+
+    public interface IMeshPrimitiveDecoder<TMaterial> : IMeshPrimitiveDecoder
+        where TMaterial : class
+    {
+        TMaterial Material { get; }
+    }
+
+    /// <summary>
+    /// Utility methods to help decode Meshes.
+    /// </summary>
+    public static class MeshDecoder
+    {
+        public static IMeshDecoder<Schema2.Material> Decode(this Schema2.Mesh mesh)
+        {
+            if (mesh == null) return null;
+
+            var meshDecoder = new _MeshDecoder<Schema2.Material>(mesh);
+
+            meshDecoder.GenerateNormalsAndTangents();
+
+            return meshDecoder;
+        }
+
+        public static IMeshDecoder<Schema2.Material>[] Decode(this IReadOnlyList<Schema2.Mesh> meshes)
+        {
+            Guard.NotNull(meshes, nameof(meshes));
+            return meshes.Select(item => item.Decode()).ToArray();
+        }
+
+        public static XYZ GetPosition(this IMeshPrimitiveDecoder primitive, int idx, Transforms.IGeometryTransform xform)
+        {
+            Guard.NotNull(primitive, nameof(primitive));
+            Guard.MustBeBetweenOrEqualTo(idx, 0, primitive.VertexCount + 1, nameof(idx));
+            Guard.NotNull(xform, nameof(xform));
+
+            var p = primitive.GetPosition(idx);
+            var d = primitive.GetPositionDeltas(idx);
+            var w = primitive.GetSkinWeights(idx);
+
+            return xform.TransformPosition(p, d, w);
+        }
+
+        public static XYZ GetNormal(this IMeshPrimitiveDecoder primitive, int idx, Transforms.IGeometryTransform xform)
+        {
+            Guard.NotNull(primitive, nameof(primitive));
+            Guard.MustBeBetweenOrEqualTo(idx, 0, primitive.VertexCount + 1, nameof(idx));
+            Guard.NotNull(xform, nameof(xform));
+
+            var n = primitive.GetNormal(idx);
+            var d = primitive.GetNormalDeltas(idx);
+            var w = primitive.GetSkinWeights(idx);
+
+            return xform.TransformNormal(n, d, w);
+        }
+
+        public static XYZW GetTangent(this IMeshPrimitiveDecoder primitive, int idx, Transforms.IGeometryTransform xform)
+        {
+            Guard.NotNull(primitive, nameof(primitive));
+            Guard.MustBeBetweenOrEqualTo(idx, 0, primitive.VertexCount + 1, nameof(idx));
+            Guard.NotNull(xform, nameof(xform));
+
+            var t = primitive.GetTangent(idx);
+            var d = primitive.GetTangentDeltas(idx);
+            var w = primitive.GetSkinWeights(idx);
+
+            return xform.TransformTangent(t, d, w);
+        }
+
+        public static (XYZ Min, XYZ Max) EvaluateBoundingBox(this Schema2.Scene scene, float samplingTimeStep = 1.0f)
+        {
+            Guard.NotNull(scene, nameof(scene));
+
+            var decodedMeshes = scene.LogicalParent.LogicalMeshes.Decode();
+            var sceneTemplate = SceneTemplate.Create(scene, false);
+            var sceneInstance = sceneTemplate.CreateInstance();
+
+            if (sceneInstance.AnimationTracksCount == 0)
+            {
+                sceneInstance.SetPoseTransforms();
+                return sceneInstance.EvaluateBoundingBox(decodedMeshes);
+            }
+
+            var min = new XYZ(float.PositiveInfinity);
+            var max = new XYZ(float.NegativeInfinity);
+
+            for (int trackIdx = 0; trackIdx < sceneInstance.AnimationTracksCount; ++trackIdx)
+            {
+                var duration = sceneInstance.GetAnimationDuration(trackIdx);
+
+                for (float time = 0; time < duration; time += samplingTimeStep)
+                {
+                    sceneInstance.SetAnimationFrame(trackIdx, time);
+                    var (fMin, fMax) = sceneInstance.EvaluateBoundingBox(decodedMeshes);
+
+                    min = XYZ.Min(min, fMin);
+                    max = XYZ.Max(max, fMax);
+                }
+            }
+
+            return (min, max);
+        }
+
+        public static (XYZ Center, Single Radius) EvaluateBoundingSphere(this Schema2.Scene scene, float samplingTimeStep = 1.0f)
+        {
+            Guard.NotNull(scene, nameof(scene));
+
+            var decodedMeshes = scene.LogicalParent.LogicalMeshes.Decode();
+            var sceneTemplate = SceneTemplate.Create(scene, false);
+            var sceneInstance = sceneTemplate.CreateInstance();
+
+            if (sceneInstance.AnimationTracksCount == 0)
+            {
+                sceneInstance.SetPoseTransforms();
+                return sceneInstance.EvaluateBoundingSphere(decodedMeshes);
+            }
+
+            var center = XYZ.Zero;
+            float radius = -1f;
+
+            for (int trackIdx = 0; trackIdx < sceneInstance.AnimationTracksCount; ++trackIdx)
+            {
+                var duration = sceneInstance.GetAnimationDuration(trackIdx);
+
+                for (float time = 0; time < duration; time += samplingTimeStep)
+                {
+                    sceneInstance.SetAnimationFrame(trackIdx, time);
+                    var (fc, fr) = sceneInstance.EvaluateBoundingSphere(decodedMeshes);
+
+                    if (radius < 0) { center = fc; radius = fr; continue; }
+
+                    // combine spheres
+
+                    var direction = fc - center;
+                    var distance = direction.Length();
+
+                    // check if current frame is already contained in master sphere.
+                    if (radius >= (fr + distance)) continue;
+
+                    // check if master sphere is already contained in current frame.
+                    if (fr >= (radius + distance)) { center = fc; radius = fr; continue; }
+
+                    // combine
+                    direction = XYZ.Normalize(direction);
+                    var p0 = center - (direction * radius);
+                    var p1 = fc + (direction * fr);
+
+                    center = (p0 + p1) / 2;
+                    radius = (p0 - p1).Length() / 2;
+                }
+            }
+
+            return (center, radius);
+        }
+
+        public static (XYZ Min, XYZ Max) EvaluateBoundingBox<TMaterial>(this SceneInstance instance, IReadOnlyList<IMeshDecoder<TMaterial>> meshes)
+            where TMaterial : class
+        {
+            Guard.NotNull(instance, nameof(instance));
+            Guard.NotNull(meshes, nameof(meshes));
+
+            var min = new XYZ(float.PositiveInfinity);
+            var max = new XYZ(float.NegativeInfinity);
+
+            foreach (var pos in instance.GetWorldVertices(meshes))
+            {
+                min = XYZ.Min(min, pos);
+                max = XYZ.Max(max, pos);
+            }
+
+            return (min, max);
+        }
+
+        public static (XYZ Center, Single Radius) EvaluateBoundingSphere<TMaterial>(this SceneInstance instance, IReadOnlyList<IMeshDecoder<TMaterial>> meshes)
+            where TMaterial : class
+        {
+            Guard.NotNull(instance, nameof(instance));
+            Guard.NotNull(meshes, nameof(meshes));
+
+            var center = XYZ.Zero;
+            var radius = -1f;
+
+            foreach (var p1 in instance.GetWorldVertices(meshes))
+            {
+                if (radius < 0) { center = p1; radius = 0; continue; }
+
+                var dir = XYZ.Normalize(p1 - center);
+                var p2 = center - (dir * radius);
+
+                center = (p1 + p2) / 2;
+                radius = (p1 - p2).Length() / 2;
+            }
+
+            return (center, radius);
+        }
+
+        public static IEnumerable<XYZ> GetWorldVertices<TMaterial>(this SceneInstance instance, IReadOnlyList<IMeshDecoder<TMaterial>> meshes)
+            where TMaterial : class
+        {
+            Guard.NotNull(instance, nameof(instance));
+            Guard.NotNull(meshes, nameof(meshes));
+
+            for (int i = 0; i < meshes.Count; ++i)
+            {
+                Guard.MustBeEqualTo(meshes[i].LogicalIndex, i, nameof(meshes) + $"[{i}]");
+            }
+
+            return instance
+                .DrawableInstances
+                .Where(item => item.Transform.Visible)
+                .SelectMany(item => meshes[item.Template.LogicalMeshIndex].GetWorldVertices(item.Transform));
+        }
+
+        public static IEnumerable<XYZ> GetWorldVertices<TMaterial>(this IMeshDecoder<TMaterial> mesh, Transforms.IGeometryTransform xform)
+            where TMaterial : class
+        {
+            Guard.NotNull(mesh, nameof(mesh));
+            Guard.NotNull(xform, nameof(xform));
+
+            foreach (var primitive in mesh.Primitives)
+            {
+                for (int i = 0; i < primitive.VertexCount; ++i)
+                {
+                    yield return primitive.GetPosition(i, xform);
+                }
+            }
+        }
+    }
+}

+ 14 - 42
src/SharpGLTF.Core/Runtime/SceneInstance.cs

@@ -72,15 +72,9 @@ namespace SharpGLTF.Runtime
         public IEnumerable<NodeInstance> VisualNodes => _NodeInstances.Where(item => item.VisualParent == null);
 
         /// <summary>
-        /// Gets all the names of the animations tracks.
+        /// Gets the total number of animation tracks for this instance.
         /// </summary>
-        public IEnumerable<String> AnimationTracks => _AnimationTracks.Names;
-
-        /// <summary>
-        /// Gets the number of drawable references.
-        /// </summary>
-        [Obsolete("Use DrawableInstancesCount")]
-        public int DrawableReferencesCount => _DrawableTransforms.Length;
+        public int AnimationTracksCount => _AnimationTracks.Count;
 
         /// <summary>
         /// Gets the number of drawable instances.
@@ -88,35 +82,13 @@ namespace SharpGLTF.Runtime
         public int DrawableInstancesCount => _DrawableTransforms.Length;
 
         /// <summary>
-        /// Gets a collection of drawable references, where:
-        /// <list type="bullet">
-        /// <item>
-        /// <term>MeshIndex</term>
-        /// <description>The logical Index of a <see cref="Schema2.Mesh"/> in <see cref="Schema2.ModelRoot.LogicalMeshes"/>.</description>
-        /// </item>
-        /// <item>
-        /// <term>Transform</term>
-        /// <description>An <see cref="IGeometryTransform"/> that can be used to transform the <see cref="Schema2.Mesh"/> into world space.</description>
-        /// </item>
-        /// </list>
+        /// Gets the current sequence of drawing commands.
         /// </summary>
-        [Obsolete("Use DrawableInstances.")]
-        public IEnumerable<(int MeshIndex, IGeometryTransform Transform)> DrawableReferences
-        {
-            get
-            {
-                for (int i = 0; i < _DrawableTransforms.Length; ++i)
-                {
-                    yield return GetDrawableReference(i);
-                }
-            }
-        }
-
         public IEnumerable<DrawableInstance> DrawableInstances
         {
             get
             {
-                for (int i = 0; i < _DrawableTransforms.Length; ++i)
+                for (int i = 0; i < _DrawableReferences.Length; ++i)
                 {
                     yield return GetDrawableInstance(i);
                 }
@@ -146,6 +118,16 @@ namespace SharpGLTF.Runtime
             foreach (var n in _NodeInstances) n.SetPoseTransform();
         }
 
+        public string NameOfTrack(int trackIndex)
+        {
+            return _AnimationTracks.NameOf(trackIndex);
+        }
+
+        public int IndexOfTrack(string name)
+        {
+            return _AnimationTracks.IndexOf(name);
+        }
+
         public float GetAnimationDuration(int trackLogicalIndex)
         {
             if (trackLogicalIndex < 0) return 0;
@@ -154,11 +136,6 @@ namespace SharpGLTF.Runtime
             return _AnimationTracks[trackLogicalIndex];
         }
 
-        public float GetAnimationDuration(string trackName)
-        {
-            return GetAnimationDuration(_AnimationTracks.IndexOf(trackName));
-        }
-
         public void SetAnimationFrame(int trackLogicalIndex, float time, bool looped = true)
         {
             if (looped)
@@ -170,11 +147,6 @@ namespace SharpGLTF.Runtime
             foreach (var n in _NodeInstances) n.SetAnimationFrame(trackLogicalIndex, time);
         }
 
-        public void SetAnimationFrame(string trackName, float time, bool looped = true)
-        {
-            SetAnimationFrame(_AnimationTracks.IndexOf(trackName), time, looped);
-        }
-
         public void SetAnimationFrame(params (int TrackIdx, float Time, float Weight)[] blended)
         {
             SetAnimationFrame(_NodeInstances, blended);

+ 2 - 7
src/SharpGLTF.Core/Runtime/SceneTemplate.cs

@@ -87,7 +87,7 @@ namespace SharpGLTF.Runtime
 
                 float duration = dstTracks.Count <= index ? 0 : dstTracks[index];
                 duration = Math.Max(duration, anim.Duration);
-                dstTracks.SetValue(index, name, anim.Duration);
+                dstTracks.SetName(index, name, anim.Duration);
             }
 
             return new SceneTemplate(srcScene.Name, dstNodes, drawables, dstTracks);
@@ -122,12 +122,7 @@ namespace SharpGLTF.Runtime
         /// Gets the unique indices of <see cref="Schema2.Mesh"/> instances in <see cref="Schema2.ModelRoot.LogicalMeshes"/>
         /// </summary>
         public IEnumerable<int> LogicalMeshIds => _DrawableReferences.Select(item => item.LogicalMeshIndex).Distinct();
-
-        /// <summary>
-        /// Gets A collection of animation track names.
-        /// </summary>
-        public IEnumerable<string> AnimationTracks => _AnimationTracks.Names;
-
+        
         #endregion
 
         #region API

+ 85 - 0
src/SharpGLTF.Core/Runtime/VertexNormalsFactory.cs

@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.Runtime
+{
+    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();
+        }
+
+        public static void CalculateSmoothNormals<T>(IEnumerable<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;
+        }
+    }
+}

+ 142 - 0
src/SharpGLTF.Core/Runtime/VertexTangentsFactory.cs

@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.Runtime
+{
+    using VERTEXKEY = System.ValueTuple<Vector3, Vector3, Vector2>;
+
+    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();
+        }
+
+        public static void CalculateTangents<T>(IEnumerable<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);
+        }
+    }
+}

+ 6 - 0
src/SharpGLTF.Core/Runtime/vertexIndex.cs

@@ -0,0 +1,6 @@
+namespace SharpGLTF.Runtime
+{
+    public class vertexIndex
+    {
+    }
+}

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

@@ -10,14 +10,14 @@ namespace SharpGLTF.Transforms
     {
         #region constructor
 
+        public static implicit operator IndexWeight((int Index, float Weight) pair) { return new IndexWeight(pair.Index, pair.Weight); }
+
         public IndexWeight((int Index, float Weight) pair)
         {
             Index = pair.Index;
             Weight = pair.Weight;
         }
 
-        public static implicit operator IndexWeight((int Index, float Weight) pair) { return new IndexWeight(pair.Index, pair.Weight); }
-
         public IndexWeight(int i, float w)
         {
             Index = i;

+ 52 - 29
src/SharpGLTF.Core/Transforms/MeshTransforms.cs

@@ -32,11 +32,34 @@ namespace SharpGLTF.Transforms
         /// </remarks>
         bool FlipFaces { get; }
 
-        V3 TransformPosition(V3 position, V3[] morphTargets, in SparseWeight8 skinWeights);
-        V3 TransformNormal(V3 normal, V3[] morphTargets, in SparseWeight8 skinWeights);
-        V4 TransformTangent(V4 tangent, V3[] morphTargets, in SparseWeight8 skinWeights);
+        /// <summary>
+        /// Transforms a vertex position from local mesh space to world space.
+        /// </summary>
+        /// <param name="position">The local position of the vertex.</param>
+        /// <param name="positionDeltas">The local position deltas of the vertex, one for each morph target, or null.</param>
+        /// <param name="skinWeights">The skin weights of the vertex, or default.</param>
+        /// <returns>A position in world space.</returns>
+        V3 TransformPosition(V3 position, IReadOnlyList<V3> positionDeltas, in SparseWeight8 skinWeights);
+
+        /// <summary>
+        /// Transforms a vertex normal from local mesh space to world space.
+        /// </summary>
+        /// <param name="normal">The local normal of the vertex.</param>
+        /// <param name="normalDeltas">The local normal deltas of the vertex, one for each morph target, or null.</param>
+        /// <param name="skinWeights">The skin weights of the vertex, or default.</param>
+        /// <returns>A normal in world space.</returns>
+        V3 TransformNormal(V3 normal, IReadOnlyList<V3> normalDeltas, in SparseWeight8 skinWeights);
 
-        V4 MorphColors(V4 color, V4[] morphTargets);
+        /// <summary>
+        /// Transforms a vertex tangent from local mesh space to world space.
+        /// </summary>
+        /// <param name="tangent">The tangent normal of the vertex.</param>
+        /// <param name="tangentDeltas">The local tangent deltas of the vertex, one for each morph target, or null.</param>
+        /// <param name="skinWeights">The skin weights of the vertex, or default.</param>
+        /// <returns>A tangent in world space.</returns>
+        V4 TransformTangent(V4 tangent, IReadOnlyList<V3> tangentDeltas, in SparseWeight8 skinWeights);
+
+        V4 MorphColors(V4 color, IReadOnlyList<V4> morphTargets);
     }
 
     public abstract class MorphTransform
@@ -103,39 +126,39 @@ namespace SharpGLTF.Transforms
             _Weights = morphWeights.GetNormalizedWithComplement(COMPLEMENT_INDEX);
         }
 
-        protected V3 MorphVectors(V3 value, V3[] morphTargets)
+        protected V3 MorphVectors(V3 value, IReadOnlyList<V3> morphTargets)
         {
-            if (_Weights.Index0 == COMPLEMENT_INDEX && _Weights.Weight0 == 1) return value;
+            if (morphTargets == null || morphTargets.Count == 0) return value;
 
-            if (morphTargets == null) return value;
+            if (_Weights.Index0 == COMPLEMENT_INDEX && _Weights.Weight0 == 1) return value;
 
             var p = V3.Zero;
 
             if (_AbsoluteMorphTargets)
             {
-                foreach (var pair in _Weights.GetNonZeroWeights())
+                foreach (var (index, weight) in _Weights.GetNonZeroWeights())
                 {
-                    var val = pair.Index == COMPLEMENT_INDEX ? value : morphTargets[pair.Index];
-                    p += val * pair.Weight;
+                    var val = index == COMPLEMENT_INDEX ? value : morphTargets[index];
+                    p += val * weight;
                 }
             }
             else
             {
-                foreach (var pair in _Weights.GetNonZeroWeights())
+                foreach (var (index, weight) in _Weights.GetNonZeroWeights())
                 {
-                    var val = pair.Index == COMPLEMENT_INDEX ? value : value + morphTargets[pair.Index];
-                    p += val * pair.Weight;
+                    var val = index == COMPLEMENT_INDEX ? value : value + morphTargets[index];
+                    p += val * weight;
                 }
             }
 
             return p;
         }
 
-        protected V4 MorphVectors(V4 value, V4[] morphTargets)
+        protected V4 MorphVectors(V4 value, IReadOnlyList<V4> morphTargets)
         {
-            if (_Weights.Index0 == COMPLEMENT_INDEX && _Weights.Weight0 == 1) return value;
+            if (morphTargets == null || morphTargets.Count == 0) return value;
 
-            if (morphTargets == null) return value;
+            if (_Weights.Index0 == COMPLEMENT_INDEX && _Weights.Weight0 == 1) return value;
 
             var p = V4.Zero;
 
@@ -159,7 +182,7 @@ namespace SharpGLTF.Transforms
             return p;
         }
 
-        public V4 MorphColors(V4 color, V4[] morphTargets)
+        public V4 MorphColors(V4 color, IReadOnlyList<V4> morphTargets)
         {
             return MorphVectors(color, morphTargets);
         }
@@ -228,21 +251,21 @@ namespace SharpGLTF.Transforms
             _FlipFaces = determinant3x3 < 0;
         }
 
-        public V3 TransformPosition(V3 position, V3[] morphTargets, in SparseWeight8 skinWeights)
+        public V3 TransformPosition(V3 position, IReadOnlyList<V3> morphTargets, in SparseWeight8 skinWeights)
         {
             position = MorphVectors(position, morphTargets);
 
             return V3.Transform(position, _WorldMatrix);
         }
 
-        public V3 TransformNormal(V3 normal, V3[] morphTargets, in SparseWeight8 skinWeights)
+        public V3 TransformNormal(V3 normal, IReadOnlyList<V3> morphTargets, in SparseWeight8 skinWeights)
         {
             normal = MorphVectors(normal, morphTargets);
 
             return V3.Normalize(V3.TransformNormal(normal, _WorldMatrix));
         }
 
-        public V4 TransformTangent(V4 tangent, V3[] morphTargets, in SparseWeight8 skinWeights)
+        public V4 TransformTangent(V4 tangent, IReadOnlyList<V3> morphTargets, in SparseWeight8 skinWeights)
         {
             var t = MorphVectors(new V3(tangent.X, tangent.Y, tangent.Z), morphTargets);
 
@@ -322,7 +345,7 @@ namespace SharpGLTF.Transforms
 
         public bool FlipFaces => false;
 
-        public V3 TransformPosition(V3 localPosition, V3[] morphTargets, in SparseWeight8 skinWeights)
+        public V3 TransformPosition(V3 localPosition, IReadOnlyList<V3> morphTargets, in SparseWeight8 skinWeights)
         {
             Guard.NotNull(skinWeights, nameof(skinWeights));
 
@@ -332,15 +355,15 @@ namespace SharpGLTF.Transforms
 
             var wnrm = 1.0f / skinWeights.WeightSum;
 
-            foreach (var jw in skinWeights.GetIndexedWeights())
+            foreach (var (jidx, jweight) in skinWeights.GetIndexedWeights())
             {
-                worldPosition += V3.Transform(localPosition, _SkinTransforms[jw.Index]) * jw.Weight * wnrm;
+                worldPosition += V3.Transform(localPosition, _SkinTransforms[jidx]) * jweight * wnrm;
             }
 
             return worldPosition;
         }
 
-        public V3 TransformNormal(V3 localNormal, V3[] morphTargets, in SparseWeight8 skinWeights)
+        public V3 TransformNormal(V3 localNormal, IReadOnlyList<V3> morphTargets, in SparseWeight8 skinWeights)
         {
             Guard.NotNull(skinWeights, nameof(skinWeights));
 
@@ -348,15 +371,15 @@ namespace SharpGLTF.Transforms
 
             var worldNormal = V3.Zero;
 
-            foreach (var jw in skinWeights.GetIndexedWeights())
+            foreach (var (jidx, jweight) in skinWeights.GetIndexedWeights())
             {
-                worldNormal += V3.TransformNormal(localNormal, _SkinTransforms[jw.Index]) * jw.Weight;
+                worldNormal += V3.TransformNormal(localNormal, _SkinTransforms[jidx]) * jweight;
             }
 
             return V3.Normalize(localNormal);
         }
 
-        public V4 TransformTangent(V4 localTangent, V3[] morphTargets, in SparseWeight8 skinWeights)
+        public V4 TransformTangent(V4 localTangent, IReadOnlyList<V3> morphTargets, in SparseWeight8 skinWeights)
         {
             Guard.NotNull(skinWeights, nameof(skinWeights));
 
@@ -364,9 +387,9 @@ namespace SharpGLTF.Transforms
 
             var worldTangent = V3.Zero;
 
-            foreach (var jw in skinWeights.GetIndexedWeights())
+            foreach (var (jidx, jweight) in skinWeights.GetIndexedWeights())
             {
-                worldTangent += V3.TransformNormal(localTangentV, _SkinTransforms[jw.Index]) * jw.Weight;
+                worldTangent += V3.TransformNormal(localTangentV, _SkinTransforms[jidx]) * jweight;
             }
 
             worldTangent = V3.Normalize(worldTangent);

+ 31 - 8
src/SharpGLTF.Core/Transforms/SparseWeight8.cs

@@ -16,6 +16,7 @@ namespace SharpGLTF.Transforms
     /// - As an animation key in morph targets; a mesh can have many morph targets, but realistically and due to GPU limitations, only up to 8 morph targets can be blended at the same time.
     /// </remarks>
     [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
+    [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
     public readonly struct SparseWeight8
     {
         #region debug
@@ -228,21 +229,27 @@ namespace SharpGLTF.Transforms
         #region data
 
         public readonly int Index0;
-        public readonly int Index1;
-        public readonly int Index2;
-        public readonly int Index3;
-        public readonly int Index4;
-        public readonly int Index5;
-        public readonly int Index6;
-        public readonly int Index7;
-
         public readonly float Weight0;
+
+        public readonly int Index1;
         public readonly float Weight1;
+
+        public readonly int Index2;
         public readonly float Weight2;
+
+        public readonly int Index3;
         public readonly float Weight3;
+
+        public readonly int Index4;
         public readonly float Weight4;
+
+        public readonly int Index5;
         public readonly float Weight5;
+
+        public readonly int Index6;
         public readonly float Weight6;
+
+        public readonly int Index7;
         public readonly float Weight7;
 
         public static bool AreWeightsEqual(in SparseWeight8 x, in SparseWeight8 y)
@@ -460,6 +467,22 @@ namespace SharpGLTF.Transforms
             return r;
         }
 
+        public SparseWeight8 GetReducedWeights(int maxWeights)
+        {
+            Span<IndexWeight> entries = stackalloc IndexWeight[8];
+
+            IndexWeight.CopyTo(this, entries);
+            IndexWeight.BubbleSortByWeight(entries);
+
+            for (int i = maxWeights; i < entries.Length; ++i) entries[i] = default;
+
+            var reduced = new SparseWeight8(entries);
+
+            var scale = reduced.WeightSum == 0f ? 0f : this.WeightSum / reduced.WeightSum;
+
+            return Multiply(reduced, scale);
+        }
+
         public override string ToString()
         {
             var sb = new StringBuilder();

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

@@ -223,7 +223,7 @@ namespace SharpGLTF.Schema2
             }
             else
             {
-                instance.SetAnimationFrame(animation.Name, time);
+                instance.SetAnimationFrame(animation.LogicalIndex, time);
             }
 
             var meshes = scene.LogicalParent.LogicalMeshes;
@@ -259,7 +259,7 @@ namespace SharpGLTF.Schema2
             }
             else
             {
-                instance.SetAnimationFrame(animation.Name, time);
+                instance.SetAnimationFrame(animation.LogicalIndex, time);
             }
 
             var meshes = scene.LogicalParent.LogicalMeshes;

+ 19 - 0
tests/SharpGLTF.Tests/Runtime/SceneTemplateTests.cs

@@ -47,5 +47,24 @@ namespace SharpGLTF.Runtime
 
             return (template, new WeakReference<Schema2.ModelRoot>(model));
         }
+
+        [Test]
+        public static void TestMeshDecoding()
+        {
+            var modelPath = TestFiles.GetSampleModelsPaths()
+                                .FirstOrDefault(item => item.Contains("BrainStem.glb"));
+
+            var model = Schema2.ModelRoot.Load(modelPath);
+
+            var (center, radius) = model.DefaultScene.EvaluateBoundingSphere(1.0f);
+            
+            // precission needs to be fairly low because calculation results
+            // in NetCore and NetFramework are amazingly different.
+            Assert.AreEqual(-0.07429607f, center.X, 0.0001f);
+            Assert.AreEqual( 0.8432209f, center.Y, 0.0001f);
+            Assert.AreEqual(-0.04639983f, center.Z, 0.0001f);
+            Assert.AreEqual( 2.528468f, radius, 0.0001f);
+        }
+
     }
 }

+ 14 - 0
tests/SharpGLTF.Tests/Transforms/SparseWeight8Tests.cs

@@ -212,5 +212,19 @@ namespace SharpGLTF.Transforms
             Assert.AreEqual(lr[6], cr[6], 0.000001f);
             Assert.AreEqual(lr[7], cr[7], 0.000001f);
         }
+
+        [Test]
+        public void TestSparseWeightReduction()
+        {
+            var a = SparseWeight8.Create(5, 3, 2, 4, 0, 4, 2);
+
+            var b = a.GetReducedWeights(4);
+
+            Assert.AreEqual(0, b.Weight4);
+            Assert.AreEqual(0, b.Weight5);
+            Assert.AreEqual(0, b.Weight6);
+            Assert.AreEqual(0, b.Weight7);
+            Assert.AreEqual(a.WeightSum, b.WeightSum, 0.00001f);
+        }
     }
 }