Browse Source

Progress with skin and animation evaluation

Vicente Penades 6 years ago
parent
commit
81f0386505

+ 6 - 0
examples/InfiniteSkinnedTentacle/Program.cs

@@ -55,6 +55,12 @@ namespace InfiniteSkinnedTentacle
             RecusiveTentacle(scene, Matrix4x4.CreateTranslation(+25, 0, -25), mesh, Quaternion.CreateFromYawPitchRoll(0.2f, 0f, 0f), 2);            
 
             model.SaveGLB("recursive tentacles.glb");
+
+            model.SaveAsWavefront("recursive tentacles at 000.obj", model.LogicalAnimations[0], 0);
+            model.SaveAsWavefront("recursive tentacles at 025.obj", model.LogicalAnimations[0], 0.25f);
+            model.SaveAsWavefront("recursive tentacles at 050.obj", model.LogicalAnimations[0], 0.50f);
+            model.SaveAsWavefront("recursive tentacles at 075.obj", model.LogicalAnimations[0], 0.75f);
+            model.SaveAsWavefront("recursive tentacles at 100.obj", model.LogicalAnimations[0], 1);
         }
         
         static void RecusiveTentacle(IVisualNodeContainer parent, Matrix4x4 offset, Mesh mesh, Quaternion anim, int repeat)

+ 64 - 0
src/Shared/_Extensions.cs

@@ -160,6 +160,70 @@ namespace SharpGLTF
             return dst;
         }
 
+        internal static (T, T, float) GetSample<T>(this IEnumerable<(float, T)> sequence, float offset)
+        {
+            (float, T)? left = null;
+            (float, T)? right = null;
+            (float, T)? prev = null;
+
+            if (offset < 0) offset = 0;
+
+            foreach (var item in sequence)
+            {
+                if (item.Item1 == offset)
+                {
+                    left = item; continue;
+                }
+
+                if (item.Item1 > offset)
+                {
+                    if (left == null) left = prev;
+                    right = item;
+                    break;
+                }
+
+                prev = item;
+            }
+
+            if (left == null && right == null) return (default(T), default(T), 0);
+            if (left == null) return (right.Value.Item2, right.Value.Item2, 0);
+            if (right == null) return (left.Value.Item2, left.Value.Item2, 0);
+
+            System.Diagnostics.Debug.Assert(left.Value.Item1 < right.Value.Item1);
+
+            var amount = (offset - left.Value.Item1) / (right.Value.Item1 - left.Value.Item1);
+
+            System.Diagnostics.Debug.Assert(amount >= 0 && amount <= 1);
+
+            return (left.Value.Item2, right.Value.Item2, amount);
+        }
+
+        internal static Func<float, Vector3> GetSamplerFunc(this IEnumerable<(float, Vector3)> collection)
+        {
+            if (collection == null) return null;
+
+            Vector3 _sampler(float offset)
+            {
+                var sample = collection.GetSample(offset);
+                return Vector3.Lerp(sample.Item1, sample.Item2, sample.Item3);
+            }
+
+            return _sampler;
+        }
+
+        internal static Func<float, Quaternion> GetSamplerFunc(this IEnumerable<(float, Quaternion)> collection)
+        {
+            if (collection == null) return null;
+
+            Quaternion _sampler(float offset)
+            {
+                var sample = collection.GetSample(offset);
+                return Quaternion.Slerp(sample.Item1, sample.Item2, sample.Item3);
+            }
+
+            return _sampler;
+        }
+
         #endregion
 
         #region linq

+ 13 - 15
src/SharpGLTF.Core/Schema2/gltf.Animations.cs

@@ -133,28 +133,28 @@ namespace SharpGLTF.Schema2
             throw new NotImplementedException();
         }
 
-        public IReadOnlyDictionary<Single, Vector3> FindScaleChannel(Node node)
+        public IEnumerable<(Single, Vector3)> FindScaleChannel(Node node)
         {
             var channel = _channels.FirstOrDefault(item => item.TargetNode == node && item.TargetNodePath == PropertyPath.scale);
             if (channel == null) return null;
 
-            return channel.Sampler.AsVector3KeyFrames();
+            return channel.Sampler.AsLinearVector3KeyFrames();
         }
 
-        public IReadOnlyDictionary<Single, Quaternion> FindRotationChannel(Node node)
+        public IEnumerable<(Single, Quaternion)> FindRotationChannel(Node node)
         {
             var channel = _channels.FirstOrDefault(item => item.TargetNode == node && item.TargetNodePath == PropertyPath.rotation);
             if (channel == null) return null;
 
-            return channel.Sampler.AsQuaternionKeyFrames();
+            return channel.Sampler.AsLinearQuaternionKeyFrames();
         }
 
-        public IReadOnlyDictionary<Single, Vector3> FindTranslationChannel(Node node)
+        public IEnumerable<(Single, Vector3)> FindTranslationChannel(Node node)
         {
             var channel = _channels.FirstOrDefault(item => item.TargetNode == node && item.TargetNodePath == PropertyPath.translation);
             if (channel == null) return null;
 
-            return channel.Sampler.AsVector3KeyFrames();
+            return channel.Sampler.AsLinearVector3KeyFrames();
         }
 
         #endregion
@@ -361,7 +361,9 @@ namespace SharpGLTF.Schema2
 
         private static (Single[], TValue[]) _Split<TValue>(IReadOnlyDictionary<Single, (TValue, TValue, TValue)> keyframes)
         {
-            var sorted = keyframes.OrderBy(item => item.Key).ToList();
+            var sorted = keyframes
+                .OrderBy(item => item.Key)
+                .ToList();
 
             var keys = new Single[sorted.Count];
             var vals = new TValue[sorted.Count * 3];
@@ -405,7 +407,7 @@ namespace SharpGLTF.Schema2
             _output = this._CreateOutputAccessor(kv.Item2).LogicalIndex;
         }
 
-        public IReadOnlyDictionary<Single, Vector3> AsVector3KeyFrames()
+        public IEnumerable<(Single, Vector3)> AsLinearVector3KeyFrames()
         {
             if (this.InterpolationMode == AnimationInterpolationMode.CUBICSPLINE) throw new ArgumentException();
 
@@ -414,21 +416,17 @@ namespace SharpGLTF.Schema2
             var keys = this.Input.AsScalarArray();
             var frames = this.Output.AsVector3Array();
 
-            return keys
-                .Zip(frames, (key, val) => (key, val))
-                .ToDictionary(item => item.key, item => item.val);
+            return keys.Zip(frames, (key, val) => (key, val));
         }
 
-        public IReadOnlyDictionary<Single, Quaternion> AsQuaternionKeyFrames()
+        public IEnumerable<(Single, Quaternion)> AsLinearQuaternionKeyFrames()
         {
             var dict = new Dictionary<Single, Quaternion>();
 
             var keys = this.Input.AsScalarArray();
             var frames = this.Output.AsQuaternionArray();
 
-            return keys
-                .Zip(frames, (key, val) => (key, val))
-                .ToDictionary(item => item.key, item => item.val);
+            return keys.Zip(frames, (key, val) => (key, val));
         }
 
         #endregion

+ 2 - 2
src/SharpGLTF.Core/Schema2/gltf.Mesh.cs

@@ -30,7 +30,7 @@ namespace SharpGLTF.Schema2
 
         public IReadOnlyList<MeshPrimitive> Primitives => _primitives;
 
-        public IReadOnlyList<Single> MorphWeights => _weights.Select(item => (Single)item).ToArray();
+        public IReadOnlyList<Single> MorphWeights => _weights.Count == 0 ? null : _weights.Select(item => (Single)item).ToArray();
 
         #endregion
 
@@ -72,7 +72,7 @@ namespace SharpGLTF.Schema2
         }
 
         #endregion
-        }
+    }
 
     public partial class ModelRoot
     {

+ 71 - 1
src/SharpGLTF.Core/Schema2/gltf.Node.cs

@@ -122,6 +122,23 @@ namespace SharpGLTF.Schema2
             }
         }
 
+        public Transforms.AffineTransform GetLocalTransform(Animation animation, float time)
+        {
+            Guard.MustShareLogicalParent(this, animation, nameof(animation));
+
+            var xform = this.LocalTransform;
+
+            var sfunc = animation.FindScaleChannel(this).GetSamplerFunc();
+            var rfunc = animation.FindRotationChannel(this).GetSamplerFunc();
+            var tfunc = animation.FindTranslationChannel(this).GetSamplerFunc();
+
+            if (sfunc != null) xform.Scale = sfunc(time);
+            if (rfunc != null) xform.Rotation = rfunc(time);
+            if (tfunc != null) xform.Translation = tfunc(time);
+
+            return xform;
+        }
+
         /// <summary>
         /// Gets or sets the world transform <see cref="Matrix4x4"/> of this <see cref="Node"/>.
         /// </summary>
@@ -139,6 +156,59 @@ namespace SharpGLTF.Schema2
             }
         }
 
+        public Matrix4x4 GetWorldMatrix(Animation animation, float time)
+        {
+            if (animation == null) return this.WorldMatrix;
+
+            var vs = VisualParent;
+            var lm = GetLocalTransform(animation, time).Matrix;
+            return vs == null ? lm : Transforms.AffineTransform.LocalToWorld(vs.GetWorldMatrix(animation, time), lm);
+        }
+
+        /// <summary>
+        /// Creates a <see cref="Transforms.ITransform"/> object, based on the current
+        /// transform state, that can be used to transform the <see cref="Mesh"/>
+        /// vertices to world space.
+        /// </summary>
+        /// <returns>A <see cref="Transforms.ITransform"/> object</returns>
+        public Transforms.ITransform GetMeshWorldTransform() { return GetMeshWorldTransform(null, 0); }
+
+        /// <summary>
+        /// Creates a <see cref="Transforms.ITransform"/> object, based on the current
+        /// transform state, and the given <see cref="Animation"/>, that can be used
+        /// to transform the <see cref="Mesh"/> vertices to world space.
+        /// </summary>
+        /// <param name="animation">The <see cref="Animation"/> to use.</param>
+        /// <param name="time">The time within <paramref name="animation"/>.</param>
+        /// <returns>A <see cref="Transforms.ITransform"/> object</returns>
+        public Transforms.ITransform GetMeshWorldTransform(Animation animation, float time)
+        {
+            float[] weights = null; // this.MorphWeights.ToArray();
+
+            if (weights != null)
+            {
+                Guard.MustShareLogicalParent(this, animation, nameof(animation));
+
+                // TODO: get input only (time) channel, and create
+                // var mfunc = animation.FindMorphingChannel(this).GetSamplerFunc();
+
+            }
+
+            if (this.Skin == null) return new Transforms.StaticTransform(this.GetWorldMatrix(animation, time), weights);
+
+            var jointXforms = new Matrix4x4[this.Skin.JointsCount];
+            var invBindings = new Matrix4x4[this.Skin.JointsCount];
+
+            for (int i = 0; i < this.Skin.JointsCount; ++i)
+            {
+                var j = this.Skin.GetJoint(i);
+                jointXforms[i] = j.Key.GetWorldMatrix(animation, time);
+                invBindings[i] = j.Value;
+            }
+
+            return new Transforms.SkinTransform(invBindings, jointXforms, weights);
+        }
+
         #endregion
 
         #region properties - content
@@ -185,7 +255,7 @@ namespace SharpGLTF.Schema2
         /// </summary>
         public IReadOnlyList<Single> MorphWeights
         {
-            get => _weights == null ? Mesh?.MorphWeights : _weights.Select(item => (float)item).ToArray();
+            get => _weights.Count == 0 ? Mesh?.MorphWeights : _weights.Select(item => (float)item).ToArray();
             set
             {
                 _weights.Clear();

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

@@ -0,0 +1,224 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+
+using TRANSFORM = System.Numerics.Matrix4x4;
+using V3 = System.Numerics.Vector3;
+using V4 = System.Numerics.Vector4;
+
+namespace SharpGLTF.Transforms
+{
+    public interface ITransform
+    {
+        V3 TransformPosition(V3 position, params (int, float)[] skinWeights);
+        V3 TransformNormal(V3 normal, params (int, float)[] skinWeights);
+        V4 TransformTangent(V4 tangent, params (int, float)[] skinWeights);
+
+        V3 TransformPosition(V3[] positions, params (int, float)[] skinWeights);
+        V3 TransformNormal(V3[] normals, params (int, float)[] skinWeights);
+        V4 TransformTangent(V4[] tangents, params (int, float)[] skinWeights);
+    }
+
+    public abstract class MorphTransform
+    {
+        #region constructor
+
+        protected MorphTransform(float[] morphWeights)
+        {
+            if (morphWeights == null || morphWeights.Length == 0)
+            {
+                _MorphWeights = _NoWeights;
+                return;
+            }
+
+            _MorphWeights = new float[morphWeights.Length];
+            morphWeights.CopyTo(_MorphWeights, 0);
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly float[] _MorphWeights;
+
+        private static readonly float[] _NoWeights = new float[] { 1 };
+
+        #endregion
+
+        #region properties
+
+        public IReadOnlyList<float> MorphWeights => _MorphWeights;
+
+        #endregion
+
+        #region API
+
+        protected V3 MorphPositions(V3[] positions)
+        {
+            if (_MorphWeights == null) return positions[0];
+            Guard.IsTrue(_MorphWeights.Length == positions.Length, nameof(positions));
+
+            var p = V3.Zero;
+            for (int i = 0; i < _MorphWeights.Length; ++i)
+            {
+                p += positions[i] * _MorphWeights[i];
+            }
+
+            return p;
+        }
+
+        protected V3 MorphNormals(V3[] normals)
+        {
+            if (_MorphWeights == null) return normals[0];
+            Guard.IsTrue(_MorphWeights.Length == normals.Length, nameof(normals));
+
+            var n = V3.Zero;
+            for (int i = 0; i < _MorphWeights.Length; ++i)
+            {
+                n += normals[i] * _MorphWeights[i];
+            }
+
+            return V3.Normalize(n);
+        }
+
+        protected V4 MorphTangents(V4[] tangents)
+        {
+            if (_MorphWeights == null) return tangents[0];
+            Guard.IsTrue(_MorphWeights.Length == tangents.Length, nameof(tangents));
+
+            var t = V4.Zero;
+            for (int i = 0; i < _MorphWeights.Length; ++i)
+            {
+                t += tangents[i] * _MorphWeights[i];
+            }
+
+            return t;
+        }
+
+        #endregion
+    }
+
+    public class StaticTransform : MorphTransform , ITransform
+    {
+        public StaticTransform(TRANSFORM xform, params float[] morphWeights)
+            : base(morphWeights)
+        {
+            _Transform = xform;
+        }
+
+        private readonly TRANSFORM _Transform;
+
+        public V3 TransformPosition(V3 position, params (int, float)[] skinWeights)
+        {
+            return V3.Transform(position, _Transform);
+        }
+
+        public V3 TransformNormal(V3 normal, params (int, float)[] skinWeights)
+        {
+            return V3.Normalize(V3.Transform(normal, _Transform));
+        }
+
+        public V4 TransformTangent(V4 tangent, params (int, float)[] skinWeights)
+        {
+            return V4.Transform(tangent, _Transform);
+        }
+
+        public V3 TransformPosition(V3[] positions, params (int, float)[] skinWeights)
+        {
+            var position = MorphPositions(positions);
+
+            return V3.Transform(position, _Transform);
+        }
+
+        public V3 TransformNormal(V3[] normals, params (int, float)[] skinWeights)
+        {
+            var normal = MorphNormals(normals);
+
+            return V3.Normalize(V3.TransformNormal(normal, _Transform));
+        }
+
+        public V4 TransformTangent(V4[] tangents, params (int, float)[] skinWeights)
+        {
+            var tangent = MorphTangents(tangents);
+
+            var tangentV = new V3(tangent.X, tangent.Y, tangent.Z);
+
+            tangentV = V3.TransformNormal(tangentV, _Transform);
+
+            return new V4(tangentV, tangent.W);
+        }
+    }
+
+    public class SkinTransform : MorphTransform , ITransform
+    {
+        public SkinTransform(TRANSFORM[] invBindings, TRANSFORM[] xforms, params float[] morphWeights)
+            : base(morphWeights)
+        {
+            Guard.NotNull(invBindings, nameof(invBindings));
+            Guard.NotNull(xforms, nameof(xforms));
+            Guard.IsTrue(invBindings.Length == xforms.Length, nameof(xforms), $"{invBindings} and {xforms} length mismatch.");
+
+            _JointTransforms = new TRANSFORM[invBindings.Length];
+
+            for (int i = 0; i < _JointTransforms.Length; ++i)
+            {
+                _JointTransforms[i] = invBindings[i] * xforms[i];
+            }
+        }
+
+        private readonly TRANSFORM[] _JointTransforms;
+
+        public V3 TransformPosition(V3 localPosition, params (int, float)[] skinWeights)
+        {
+            var worldPosition = V3.Zero;
+
+            foreach (var jw in skinWeights)
+            {
+                worldPosition += V3.Transform(localPosition, _JointTransforms[jw.Item1]) * jw.Item2;
+            }
+
+            return worldPosition;
+        }
+
+        public V3 TransformNormal(V3 localNormal, params (int, float)[] skinWeights)
+        {
+            var worldNormal = V3.Zero;
+
+            foreach (var jw in skinWeights)
+            {
+                worldNormal += V3.TransformNormal(localNormal, _JointTransforms[jw.Item1]) * jw.Item2;
+            }
+
+            return V3.Normalize(localNormal);
+        }
+
+        public V4 TransformTangent(V4 localTangent, params (int, float)[] skinWeights)
+        {
+            var localTangentV = new V3(localTangent.X, localTangent.Y, localTangent.Z);
+            var worldTangent = V3.Zero;
+
+            foreach (var jw in skinWeights)
+            {
+                worldTangent += V3.TransformNormal(localTangentV, _JointTransforms[jw.Item1]) * jw.Item2;
+            }
+
+            return new V4(worldTangent, localTangentV.Z);
+        }
+
+        public V3 TransformPosition(V3[] positions, params (int, float)[] skinWeights)
+        {
+            return TransformPosition(MorphPositions(positions));
+        }
+
+        public V3 TransformNormal(V3[] normals, params (int, float)[] skinWeights)
+        {
+            return TransformNormal(MorphNormals(normals));
+        }
+
+        public V4 TransformTangent(V4[] tangents, params (int, float)[] skinWeights)
+        {
+            return TransformTangent(MorphTangents(tangents));
+        }
+    }
+}

+ 48 - 3
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexColumns.cs

@@ -34,9 +34,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public void SetNormals(IReadOnlyDictionary<Vector3, Vector3> normalsMap)
         {
-            var data = new Byte[12 * Positions.Count];
-
-            Normals = new Memory.Vector3Array(data, 0, Positions.Count, 0);
+            Normals = new Vector3[Positions.Count];
 
             for (int i = 0; i < Normals.Count; ++i)
             {
@@ -47,6 +45,53 @@ namespace SharpGLTF.Geometry.VertexTypes
             }
         }
 
+        public void ApplyTransform(Transforms.ITransform transform)
+        {
+            var newPos = new Vector3[Positions.Count];
+            var newNrm = Normals == null || Normals.Count < newPos.Length ? null : new Vector3[newPos.Length];
+            var newTgt = Tangents == null || Tangents.Count < newPos.Length ? null : new Vector4[newPos.Length];
+
+            var jw0 = Joints0 != null && Joints0.Count == newPos.Length && Weights0 != null && Weights0.Count == newPos.Length;
+            var jw1 = Joints1 != null && Joints1.Count == newPos.Length && Weights1 != null && Weights1.Count == newPos.Length;
+            var jjww = new (int, float)[8];
+
+            for (int i = 0; i < newPos.Length; ++i)
+            {
+                if (jw0)
+                {
+                    var j = Joints0[i];
+                    var w = Weights0[i];
+                    jjww[0] = ((int)j.X, w.X);
+                    jjww[1] = ((int)j.Y, w.Y);
+                    jjww[2] = ((int)j.Z, w.Z);
+                    jjww[3] = ((int)j.W, w.W);
+                }
+
+                if (jw1)
+                {
+                    var j = Joints1[i];
+                    var w = Weights1[i];
+                    jjww[4] = ((int)j.X, w.X);
+                    jjww[5] = ((int)j.Y, w.Y);
+                    jjww[6] = ((int)j.Z, w.Z);
+                    jjww[7] = ((int)j.W, w.W);
+                }
+
+                newPos[i] = transform.TransformPosition(Positions[i], jjww);
+                if (newNrm != null) newNrm[i] = transform.TransformNormal(Normals[i], jjww);
+                if (newTgt != null) newTgt[i] = transform.TransformTangent(Tangents[i], jjww);
+            }
+
+            Positions = newPos;
+            Normals = newNrm;
+            Tangents = newTgt;
+
+            Joints0 = null;
+            Joints1 = null;
+            Weights0 = null;
+            Weights1 = null;
+        }
+
         public TvP GetPositionFragment<TvP>(int index)
             where TvP : struct, IVertexGeometry
         {

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

@@ -12,7 +12,7 @@ using static System.FormattableString;
 namespace SharpGLTF.IO
 {
     using BYTES = ArraySegment<Byte>;
-    using VERTEX = ValueTuple<Geometry.VertexTypes.VertexPositionNormal, Geometry.VertexTypes.VertexTexture1, Geometry.VertexTypes.VertexEmpty>;
+    using VERTEX = Geometry.VertexBuilder<Geometry.VertexTypes.VertexPositionNormal, Geometry.VertexTypes.VertexTexture1, Geometry.VertexTypes.VertexEmpty>;
     using VGEOMETRY = Geometry.VertexTypes.VertexPositionNormal;
     using VMATERIAL = Geometry.VertexTypes.VertexTexture1;
     using VSKINNING = Geometry.VertexTypes.VertexEmpty;
@@ -254,6 +254,29 @@ namespace SharpGLTF.IO
             }
         }
 
+        public void AddModel(ModelRoot model, Animation animation, float time)
+        {
+            foreach (var triangle in Schema2Toolkit.Triangulate<VGEOMETRY, VMATERIAL>(model.DefaultScene, animation, time))
+            {
+                var dstMaterial = default(Material);
+
+                var srcMaterial = triangle.Item4;
+                if (srcMaterial != null)
+                {
+                    // https://stackoverflow.com/questions/36510170/how-to-calculate-specular-contribution-in-pbr
+
+                    var diffuse = srcMaterial.GetDiffuseColor(Vector4.One);
+
+                    dstMaterial.DiffuseColor = new Vector3(diffuse.X, diffuse.Y, diffuse.Z);
+                    dstMaterial.SpecularColor = new Vector3(0.2f);
+
+                    dstMaterial.DiffuseTexture = srcMaterial.GetDiffuseTexture()?.PrimaryImage?.GetImageContent() ?? default;
+                }
+
+                this.AddTriangle(dstMaterial, triangle.Item1, triangle.Item2, triangle.Item3);
+            }
+        }
+
         #endregion
     }
 }

+ 53 - 0
src/SharpGLTF.Toolkit/Schema2/MeshExtensions.cs

@@ -275,6 +275,15 @@ namespace SharpGLTF.Schema2
             return mesh.Primitives.SelectMany(item => item.Triangulate<TvP, TvM, TvS>(xform, normals));
         }
 
+        public static IEnumerable<((TvP, TvM), (TvP, TvM), (TvP, TvM), Material)> Triangulate<TvP, TvM>(this Mesh mesh, Transforms.ITransform xform)
+            where TvP : struct, Geometry.VertexTypes.IVertexGeometry
+            where TvM : struct, Geometry.VertexTypes.IVertexMaterial
+        {
+            var normals = mesh.GetComputedNormals();
+
+            return mesh.Primitives.SelectMany(item => item.Triangulate<TvP, TvM>(xform, normals));
+        }
+
         public static IEnumerable<((TvP, TvM, TvS), (TvP, TvM, TvS), (TvP, TvM, TvS), Material)> Triangulate<TvP, TvM, TvS>(this MeshPrimitive prim, Matrix4x4 xform, IReadOnlyDictionary<Vector3, Vector3> defaultNormals)
             where TvP : struct, Geometry.VertexTypes.IVertexGeometry
             where TvM : struct, Geometry.VertexTypes.IVertexMaterial
@@ -307,6 +316,43 @@ namespace SharpGLTF.Schema2
             }
         }
 
+        public static IEnumerable<((TvP, TvM), (TvP, TvM), (TvP, TvM), Material)> Triangulate<TvP, TvM>(this MeshPrimitive prim, Transforms.ITransform xform, IReadOnlyDictionary<Vector3, Vector3> defaultNormals)
+            where TvP : struct, Geometry.VertexTypes.IVertexGeometry
+            where TvM : struct, Geometry.VertexTypes.IVertexMaterial
+        {
+            var vertices = prim.GetVertexColumns();
+            if (vertices.Normals == null && defaultNormals != null) vertices.SetNormals(defaultNormals);
+
+            vertices.ApplyTransform(xform);
+
+            var triangles = prim.GetTriangleIndices();
+
+            var jointweights = new (int, float)[8];
+
+            foreach (var t in triangles)
+            {
+                var ap = vertices.GetPositionFragment<TvP>(t.Item1);
+                var bp = vertices.GetPositionFragment<TvP>(t.Item2);
+                var cp = vertices.GetPositionFragment<TvP>(t.Item3);
+
+                var am = vertices.GetMaterialFragment<TvM>(t.Item1);
+                var bm = vertices.GetMaterialFragment<TvM>(t.Item2);
+                var cm = vertices.GetMaterialFragment<TvM>(t.Item3);
+
+                yield return ((ap, am), (bp, bm), (cp, cm), prim.Material);
+            }
+        }
+
+        private static TvP _Transform<TvP>(TvP p, (int, float)[] jointweights, Transforms.ITransform xform)
+            where TvP : struct, Geometry.VertexTypes.IVertexGeometry
+        {
+            p.SetPosition(xform.TransformPosition(p.GetPosition(), jointweights));
+            if (p.TryGetNormal(out Vector3 n)) p.SetNormal(xform.TransformNormal(n, jointweights));
+            if (p.TryGetTangent(out Vector4 t)) p.SetTangent(xform.TransformTangent(t, jointweights));
+
+            return p;
+        }
+
         public static Geometry.VertexTypes.VertexColumns GetVertexColumns(this MeshPrimitive primitive)
         {
             var vertexAccessors = primitive.VertexAccessors;
@@ -404,6 +450,13 @@ namespace SharpGLTF.Schema2
             wf.WriteFiles(filePath);
         }
 
+        public static void SaveAsWavefront(this ModelRoot model, string filePath, Animation animation, float time)
+        {
+            var wf = new IO.WavefrontWriter();
+            wf.AddModel(model, animation, time);
+            wf.WriteFiles(filePath);
+        }
+
         #endregion
     }
 }

+ 20 - 0
src/SharpGLTF.Toolkit/Schema2/SceneExtensions.cs

@@ -128,6 +128,13 @@ namespace SharpGLTF.Schema2
             return Node.Flatten(scene).SelectMany(item => item.Triangulate<TvP, TvM, TvS>(true));
         }
 
+        public static IEnumerable<((TvP, TvM), (TvP, TvM), (TvP, TvM), Material)> Triangulate<TvP, TvM>(this Scene scene, Animation animation, float time)
+            where TvP : struct, Geometry.VertexTypes.IVertexGeometry
+            where TvM : struct, Geometry.VertexTypes.IVertexMaterial
+        {
+            return Node.Flatten(scene).SelectMany(item => item.Triangulate<TvP, TvM>(animation, time));
+        }
+
         /// <summary>
         /// Yield a collection of triangles representing the geometry
         /// of the input <see cref="Node"/> in local or world space.
@@ -151,6 +158,19 @@ namespace SharpGLTF.Schema2
             return mesh.Triangulate<TvP, TvM, TvS>(xform);
         }
 
+        public static IEnumerable<((TvP, TvM), (TvP, TvM), (TvP, TvM), Material)> Triangulate<TvP, TvM>(this Node node, Animation animation, float time)
+            where TvP : struct, Geometry.VertexTypes.IVertexGeometry
+            where TvM : struct, Geometry.VertexTypes.IVertexMaterial
+        {
+            var mesh = node.Mesh;
+
+            if (mesh == null) return Enumerable.Empty<((TvP, TvM), (TvP, TvM), (TvP, TvM), Material)>();
+
+            var xform = node.GetMeshWorldTransform(animation, time);
+
+            return mesh.Triangulate<TvP, TvM>(xform);
+        }
+
         #endregion
     }
 }