Browse Source

Improving scene evaluation;
Added Wavefront Object export.

Vicente Penades 6 years ago
parent
commit
043a9efc47

+ 1 - 1
src/Shared/_Extensions.cs

@@ -230,7 +230,7 @@ namespace SharpGLTF
             return collection.Concat(instances.Where(item => item != null));
         }
 
-        #endregion        
+        #endregion
 
         #region vertex & index accessors
 

+ 6 - 4
src/SharpGLTF.Core/Schema2/glb.Images.cs

@@ -5,6 +5,8 @@ using System.Text;
 
 namespace SharpGLTF.Schema2
 {
+    using BYTES = ArraySegment<Byte>;
+
     [System.Diagnostics.DebuggerDisplay("Image[{LogicalIndex}] {Name}")]
     public sealed partial class Image
     {
@@ -89,13 +91,13 @@ namespace SharpGLTF.Schema2
         /// <summary>
         /// Retrieves the image file as a block of bytes.
         /// </summary>
-        /// <returns>A <see cref="ArraySegment{Byte}"/> block containing the image file.</returns>
-        public ArraySegment<Byte> GetImageContent()
+        /// <returns>A <see cref="BYTES"/> block containing the image file.</returns>
+        public BYTES GetImageContent()
         {
             // the image is stored locally in a temporary buffer
-            if (_SatelliteImageContent != null) return new ArraySegment<byte>(_SatelliteImageContent);
+            if (_SatelliteImageContent != null) return new BYTES(_SatelliteImageContent);
 
-            /// the image is stored in a <see cref="BufferView"/>
+            // the image is stored in a BufferView
             if (this._bufferView.HasValue)
             {
                 var bv = this.LogicalParent.LogicalBufferViews[this._bufferView.Value];

+ 13 - 5
src/SharpGLTF.Core/Schema2/gltf.Materials.cs

@@ -130,6 +130,8 @@ namespace SharpGLTF.Schema2
 
         #region properties
 
+        public Boolean Exists => _Material != null;
+
         public String Key => _Key;
 
         public Texture Texture => _TextureInfoGetter?.Invoke(false) == null ? null : _Material.LogicalParent.LogicalTextures[_TextureInfoGetter(false)._LogicalTextureIndex];
@@ -140,19 +142,21 @@ namespace SharpGLTF.Schema2
 
         public TextureSampler Sampler => Texture?.Sampler;
 
-        public Vector4 Factor => _FactorGetter();
+        public Vector4 Factor => _FactorGetter?.Invoke() ?? Vector4.One;
 
         public TextureTransform Transform => _TextureInfoGetter?.Invoke(false)?.Transform;
 
         #endregion
 
-        #region fluent API
+        #region API
 
-        public void SetFactor(Vector4 value) { _FactorSetter?.Invoke(value); }
+        public void SetFactor(float value) { SetFactor(new Vector4(1, 1, 1, value)); }
 
-        public void SetFactor(float value)
+        public void SetFactor(Vector4 value)
         {
-            SetFactor(new Vector4(1, 1, 1, value));
+            if (_FactorSetter == null) throw new InvalidOperationException();
+
+            _FactorSetter?.Invoke(value);
         }
 
         public void SetTexture(
@@ -165,6 +169,8 @@ namespace SharpGLTF.Schema2
         {
             if (texImg == null) return; // in theory, we should completely remove the TextureInfo
 
+            if (_Material == null) throw new InvalidOperationException();
+
             var sampler = _Material.LogicalParent.UseSampler(mag, min, ws, wt);
             var texture = _Material.LogicalParent.UseTexture(texImg, sampler);
 
@@ -176,6 +182,8 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(tex, nameof(tex));
             Guard.MustShareLogicalParent(_Material, tex, nameof(tex));
 
+            if (_TextureInfoGetter == null) throw new InvalidOperationException();
+
             var texInfo = _TextureInfoGetter(true);
 
             texInfo.TextureSet = texSet;

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

@@ -57,7 +57,7 @@ namespace SharpGLTF.Schema2
         }
 
         /// <summary>
-        /// Sets the cone angles for the <see cref="PunctualLightType.Spot" light/>.
+        /// Sets the cone angles for the <see cref="PunctualLightType.Spot"/> light.
         /// </summary>
         /// <param name="innerConeAngle">
         /// Gets the Angle, in radians, from centre of spotlight where falloff begins.

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

@@ -184,7 +184,6 @@ namespace SharpGLTF.Geometry
 
                 AddTriangle(a, b, c);
             }
-
         }
 
         public void Validate()

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

@@ -20,7 +20,7 @@ namespace SharpGLTF.Geometry
             {
                 foreach (var m in meshBuilders) m.Validate();
             }
-            catch(Exception ex)
+            catch (Exception ex)
             {
                 throw new ArgumentException(ex.Message, nameof(meshBuilders), ex);
             }

+ 47 - 28
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexColumns.cs

@@ -5,48 +5,67 @@ using System.Text;
 
 namespace SharpGLTF.Geometry.VertexTypes
 {
-    class VertexColumns
+    public class VertexColumns
     {
-        #region lifecycle
+        #region columns
 
-        public VertexColumns(IReadOnlyDictionary<string, Accessor> vertexAccessors)
-        {
-            if (vertexAccessors.ContainsKey("POSITION")) Positions = vertexAccessors["POSITION"].AsVector3Array();
-            if (vertexAccessors.ContainsKey("NORMAL")) Normals = vertexAccessors["NORMAL"].AsVector3Array();
-            if (vertexAccessors.ContainsKey("TANGENT")) Tangents = vertexAccessors["TANGENT"].AsVector4Array();
+        public Memory.IEncodedArray<Vector3> Positions { get; set; }
+        public Memory.IEncodedArray<Vector3> Normals { get; set; }
+        public Memory.IEncodedArray<Vector4> Tangents { get; set; }
 
-            if (vertexAccessors.ContainsKey("COLOR_0")) Colors0 = vertexAccessors["COLOR_0"].AsVector4Array();
-            if (vertexAccessors.ContainsKey("COLOR_1")) Colors1 = vertexAccessors["COLOR_1"].AsVector4Array();
+        public Memory.IEncodedArray<Vector4> Colors0 { get; set; }
+        public Memory.IEncodedArray<Vector4> Colors1 { get; set; }
 
-            if (vertexAccessors.ContainsKey("TEXCOORD_0")) Textures0 = vertexAccessors["TEXCOORD_0"].AsVector2Array();
-            if (vertexAccessors.ContainsKey("TEXCOORD_1")) Textures1 = vertexAccessors["TEXCOORD_1"].AsVector2Array();
+        public Memory.IEncodedArray<Vector2> Textures0 { get; set; }
+        public Memory.IEncodedArray<Vector2> Textures1 { get; set; }
 
-            if (vertexAccessors.ContainsKey("JOINTS_0")) Joints0 = vertexAccessors["JOINTS_0"].AsVector4Array();
-            if (vertexAccessors.ContainsKey("JOINTS_1")) Joints1 = vertexAccessors["JOINTS_1"].AsVector4Array();
+        public Memory.IEncodedArray<Vector4> Joints0 { get; set; }
+        public Memory.IEncodedArray<Vector4> Joints1 { get; set; }
 
-            if (vertexAccessors.ContainsKey("WEIGHTS_0")) Weights0 = vertexAccessors["WEIGHTS_0"].AsVector4Array();
-            if (vertexAccessors.ContainsKey("WEIGHTS_1")) Weights1 = vertexAccessors["WEIGHTS_1"].AsVector4Array();
-        }
+        public Memory.IEncodedArray<Vector4> Weights0 { get; set; }
+        public Memory.IEncodedArray<Vector4> Weights1 { get; set; }
 
         #endregion
 
-        #region columns
+        #region API
 
-        public Memory.IEncodedArray<Vector3> Positions { get; private set; }
-        public Memory.IEncodedArray<Vector3> Normals { get; private set; }
-        public Memory.IEncodedArray<Vector4> Tangents { get; private set; }
+        public void SetNormals(IReadOnlyDictionary<Vector3, Vector3> normalsMap)
+        {
+            var data = new Byte[12 * Positions.Count];
+
+            Normals = new Memory.Vector3Array(data, 0, Positions.Count, 0);
 
-        public Memory.IEncodedArray<Vector4> Colors0 { get; private set; }
-        public Memory.IEncodedArray<Vector4> Colors1 { get; private set; }
+            for (int i = 0; i < Normals.Count; ++i)
+            {
+                Normals[i] = normalsMap[Positions[i]];
+            }
+        }
 
-        public Memory.IEncodedArray<Vector2> Textures0 { get; private set; }
-        public Memory.IEncodedArray<Vector2> Textures1 { get; private set; }
+        public TvP GetPositionFragment<TvP>(int index)
+            where TvP : struct, IVertexPosition
+        {
+            var pnt = default(TvP);
 
-        public Memory.IEncodedArray<Vector4> Joints0 { get; private set; }
-        public Memory.IEncodedArray<Vector4> Joints1 { get; private set; }
+            if (Positions != null) pnt.SetPosition(Positions[index]);
+            if (Normals != null) pnt.SetNormal(Normals[index]);
+            if (Tangents != null) pnt.SetTangent(Tangents[index]);
 
-        public Memory.IEncodedArray<Vector4> Weights0 { get; private set; }
-        public Memory.IEncodedArray<Vector4> Weights1 { get; private set; }
+            return pnt;
+        }
+
+        public TvM GetMaterialFragment<TvM>(int index)
+            where TvM : struct, IVertexMaterial
+        {
+            var cctt = default(TvM);
+
+            if (Colors0 != null) cctt.SetColor(0, Colors0[index]);
+            if (Colors1 != null) cctt.SetColor(1, Colors1[index]);
+
+            if (Textures0 != null) cctt.SetTexCoord(0, Textures0[index]);
+            if (Textures1 != null) cctt.SetTexCoord(1, Textures1[index]);
+
+            return cctt;
+        }
 
         #endregion
     }

+ 12 - 1
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexEmpty.cs

@@ -1,11 +1,22 @@
 using System;
 using System.Collections.Generic;
+using System.Numerics;
 using System.Text;
 
 namespace SharpGLTF.Geometry.VertexTypes
 {
     public struct VertexEmpty : IVertexMaterial, IVertexJoints
     {
-        public void Validate() { }
+        void IVertexMaterial.SetColor(int setIndex, Vector4 color)
+        {
+        }
+
+        void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord)
+        {
+        }
+
+        public void Validate()
+        {
+        }
     }
 }

+ 0 - 1
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexJoints.cs

@@ -118,7 +118,6 @@ namespace SharpGLTF.Geometry.VertexTypes
             if (!Weights0._IsReal()) throw new NotFiniteNumberException(nameof(Weights0));
             if (!Weights1._IsReal()) throw new NotFiniteNumberException(nameof(Weights1));
         }
-
     }
 
     /// <summary>

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

@@ -7,6 +7,9 @@ namespace SharpGLTF.Geometry.VertexTypes
 {
     public interface IVertexMaterial
     {
+        void SetColor(int setIndex, Vector4 color);
+        void SetTexCoord(int setIndex, Vector2 coord);
+
         void Validate();
     }
 
@@ -28,6 +31,10 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("COLOR_0", Schema2.EncodingType.UNSIGNED_BYTE, true)]
         public Vector4 Color;
 
+        void IVertexMaterial.SetColor(int setIndex, Vector4 color) { if (setIndex == 0) this.Color = color; }
+
+        void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord) { }
+
         public void Validate()
         {
             if (!Color._IsReal()) throw new NotFiniteNumberException(nameof(Color));
@@ -53,6 +60,10 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("TEXCOORD_0")]
         public Vector2 TexCoord;
 
+        void IVertexMaterial.SetColor(int setIndex, Vector4 color) { }
+
+        void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord) { if (setIndex == 0) this.TexCoord = coord; }
+
         public void Validate()
         {
             if (!TexCoord._IsReal()) throw new NotFiniteNumberException(nameof(TexCoord));
@@ -76,6 +87,10 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("TEXCOORD_0")]
         public Vector2 TexCoord;
 
+        void IVertexMaterial.SetColor(int setIndex, Vector4 color) { if (setIndex == 0) this.Color = color; }
+
+        void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord) { if (setIndex == 0) this.TexCoord = coord; }
+
         public void Validate()
         {
             if (!Color._IsReal()) throw new NotFiniteNumberException(nameof(Color));

+ 22 - 0
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexPosition.cs

@@ -7,6 +7,10 @@ namespace SharpGLTF.Geometry.VertexTypes
 {
     public interface IVertexPosition
     {
+        void SetPosition(Vector3 position);
+        void SetNormal(Vector3 normal);
+        void SetTangent(Vector4 tangent);
+
         void Transform(Matrix4x4 xform);
 
         void Validate();
@@ -35,6 +39,12 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("POSITION")]
         public Vector3 Position;
 
+        void IVertexPosition.SetPosition(Vector3 position) { this.Position = position; }
+
+        void IVertexPosition.SetNormal(Vector3 normal) { }
+
+        void IVertexPosition.SetTangent(Vector4 tangent) { }
+
         public void Transform(Matrix4x4 xform)
         {
             Position = Vector3.Transform(Position, xform);
@@ -69,6 +79,12 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("NORMAL")]
         public Vector3 Normal;
 
+        void IVertexPosition.SetPosition(Vector3 position) { this.Position = position; }
+
+        void IVertexPosition.SetNormal(Vector3 normal) { this.Normal = normal; }
+
+        void IVertexPosition.SetTangent(Vector4 tangent) { }
+
         public void Transform(Matrix4x4 xform)
         {
             Position = Vector3.Transform(Position, xform);
@@ -103,6 +119,12 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("TANGENT")]
         public Vector4 Tangent;
 
+        void IVertexPosition.SetPosition(Vector3 position) { this.Position = position; }
+
+        void IVertexPosition.SetNormal(Vector3 normal) { this.Normal = normal; }
+
+        void IVertexPosition.SetTangent(Vector4 tangent) { this.Tangent = tangent; }
+
         public void Transform(Matrix4x4 xform)
         {
             Position = Vector3.Transform(Position, xform);

+ 88 - 0
src/SharpGLTF.Toolkit/IO/EvaluationUtils.cs

@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.IO
+{
+    using Schema2;
+
+    using VERTEX = ValueTuple<Geometry.VertexTypes.VertexPositionNormal, Geometry.VertexTypes.VertexTexture1>;
+
+    /// <summary>
+    /// Utility class that hierarchicaly traverses a gltf model, evaluates the geometry and yields a collection of worldspace triangles.
+    /// </summary>
+    static class EvaluationUtils
+    {
+        /// <summary>
+        /// Yields all the triangles in the model's default scene, in world space
+        /// </summary>
+        /// <param name="model">The input model</param>
+        /// <returns>A collection of worldspace triangles</returns>
+        public static IEnumerable<(VERTEX, VERTEX, VERTEX, Material)> Triangulate(this ModelRoot model)
+        {
+            return model.DefaultScene.Triangulate();
+        }
+
+        /// <summary>
+        /// Yields all the triangles in the scene, in world space
+        /// </summary>
+        /// <param name="scene">The input scene</param>
+        /// <returns>A collection of worldspace triangles</returns>
+        public static IEnumerable<(VERTEX, VERTEX, VERTEX, Material)> Triangulate(this Scene scene)
+        {
+            return Node.Flatten(scene).SelectMany(item => item.Triangulate(true));
+        }
+
+        /// <summary>
+        /// Yields all the triangles in the node
+        /// </summary>
+        /// <param name="node">The input node</param>
+        /// <param name="inWorldSpace">true if we want to transform the local triangles to worldspace</param>
+        /// <returns>A collection of triangles in the node's space, or in worldspace</returns>
+        public static IEnumerable<(VERTEX, VERTEX, VERTEX, Material)> Triangulate(this Node node, bool inWorldSpace)
+        {
+            var mesh = node.Mesh;
+            if (mesh == null) return Enumerable.Empty<(VERTEX, VERTEX, VERTEX, Material)>();
+
+            var xform = inWorldSpace ? node.WorldMatrix : Matrix4x4.Identity;
+
+            var normals = mesh.ComputeNormals();
+
+            return mesh.Primitives.SelectMany(item => item.Triangulate(normals, xform));
+        }
+
+        /// <summary>
+        /// Yields all the triangles in the mesh
+        /// </summary>
+        /// <param name="prim">The input primitive</param>
+        /// <param name="normals">A dictionary mapping positions to normals</param>
+        /// <param name="xform">The transform matrix</param>
+        /// <returns>A collection of triangles transformed by <paramref name="xform"/> </returns>
+        public static IEnumerable<(VERTEX, VERTEX, VERTEX, Material)> Triangulate(this MeshPrimitive prim, IReadOnlyDictionary<Vector3, Vector3> normals, Matrix4x4 xform)
+        {
+            var vertices = prim.GetVertexColumns();
+            if (vertices.Normals == null) vertices.SetNormals(normals);
+
+            var triangles = prim.GetTriangleIndices();
+
+            foreach (var t in triangles)
+            {
+                var ap = vertices.GetPositionFragment<Geometry.VertexTypes.VertexPositionNormal>(t.Item1);
+                var bp = vertices.GetPositionFragment<Geometry.VertexTypes.VertexPositionNormal>(t.Item2);
+                var cp = vertices.GetPositionFragment<Geometry.VertexTypes.VertexPositionNormal>(t.Item3);
+
+                ap.Transform(xform);
+                bp.Transform(xform);
+                cp.Transform(xform);
+
+                var at = vertices.GetMaterialFragment<Geometry.VertexTypes.VertexTexture1>(t.Item1);
+                var bt = vertices.GetMaterialFragment<Geometry.VertexTypes.VertexTexture1>(t.Item2);
+                var ct = vertices.GetMaterialFragment<Geometry.VertexTypes.VertexTexture1>(t.Item3);
+
+                yield return ((ap, at), (bp, bt), (cp, ct), prim.Material);
+            }
+        }
+    }
+}

+ 240 - 0
src/SharpGLTF.Toolkit/IO/WavefrontWriter.cs

@@ -0,0 +1,240 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+using static System.FormattableString;
+
+namespace SharpGLTF.IO
+{
+    using BYTES = ArraySegment<Byte>;
+
+    using POSITION = Geometry.VertexTypes.VertexPositionNormal;
+    using TEXCOORD = Geometry.VertexTypes.VertexTexture1;
+
+    using VERTEX = ValueTuple<Geometry.VertexTypes.VertexPositionNormal, Geometry.VertexTypes.VertexTexture1>;
+
+    /// <summary>
+    /// Tiny wavefront object writer
+    /// </summary>
+    /// <see href="https://www.fileformat.info/format/wavefrontobj/egff.htm"/>
+    class WavefrontWriter
+    {
+        #region data
+
+        public struct Material
+        {
+            public Vector4 DiffuseColor;
+            public BYTES DiffuseTexture;
+        }
+
+        private readonly Geometry.MeshBuilder<Material, POSITION, TEXCOORD> _Mesh = new Geometry.MeshBuilder<Material, POSITION, TEXCOORD>();
+
+        #endregion
+
+        #region API
+
+        public void AddTriangle(Material material, VERTEX a, VERTEX b, VERTEX c)
+        {
+            _Mesh.UsePrimitive(material).AddTriangle(a, b, c);
+        }
+
+        public void WriteFiles(string filePath)
+        {
+            var dir = System.IO.Path.GetDirectoryName(filePath);
+
+            var files = GetFiles(System.IO.Path.GetFileNameWithoutExtension(filePath));
+
+            foreach (var f in files)
+            {
+                var fpath = System.IO.Path.Combine(dir, f.Key);
+                System.IO.File.WriteAllBytes(fpath, f.Value.ToArray());
+            }
+        }
+
+        public IReadOnlyDictionary<String, BYTES> GetFiles(string baseName)
+        {
+            var files = new Dictionary<String, BYTES>();
+
+            var materials = _WriteMaterials(files, baseName, _Mesh.Primitives.Select(item => item.Material));
+
+            var geocontent = _GetGeometryContent(materials, baseName + ".mtl");
+
+            _WriteTextContent(files, baseName + ".obj", geocontent);
+
+            return files;
+        }
+
+        private IReadOnlyDictionary<Material, string> _WriteMaterials(IDictionary<String, BYTES> files, string baseName, IEnumerable<Material> materials)
+        {
+            // write all image files
+            var images = materials.Select(item => item.DiffuseTexture);
+
+            foreach (var img in images.Distinct())
+            {
+                if (img.Array == null) continue;
+
+                var imgName = $"{baseName}_{files.Count}";
+
+                if (_IsPng(img)) files[imgName + ".png"] = img;
+                if (_IsJpeg(img)) files[imgName + ".jpg"] = img;
+            }
+
+            // write materials
+
+            var mmap = new Dictionary<Material, string>();
+
+            var sb = new StringBuilder();
+
+            foreach (var m in materials)
+            {
+                mmap[m] = $"Material_{mmap.Count}";
+
+                sb.AppendLine($"newmtl {mmap[m]}");
+                sb.AppendLine("illum 2");
+                sb.AppendLine(Invariant($"Ka {m.DiffuseColor.X} {m.DiffuseColor.Y} {m.DiffuseColor.Z}"));
+                sb.AppendLine(Invariant($"Kd {m.DiffuseColor.X} {m.DiffuseColor.Y} {m.DiffuseColor.Z}"));
+
+                if (m.DiffuseTexture.Array != null)
+                {
+                    var imgName = files.FirstOrDefault(kvp => kvp.Value == m.DiffuseTexture).Key;
+                    sb.AppendLine($"map_Kd {imgName}");
+                }
+
+                sb.AppendLine();
+            }
+
+            // write material library
+            _WriteTextContent(files, baseName + ".mtl", sb);
+
+            return mmap;
+        }
+
+        private StringBuilder _GetGeometryContent(IReadOnlyDictionary<Material, string> materials, string mtlLib)
+        {
+            var sb = new StringBuilder();
+
+            sb.AppendLine($"mtllib {mtlLib}");
+
+            sb.AppendLine();
+
+            foreach (var p in _Mesh.Primitives)
+            {
+                foreach (var v in p.Vertices)
+                {
+                    var pos = v.Item1.Position;
+                    sb.AppendLine(Invariant($"v {pos.X} {pos.Y} {pos.Z}"));
+                }
+            }
+
+            sb.AppendLine();
+
+            foreach (var p in _Mesh.Primitives)
+            {
+                foreach (var v in p.Vertices)
+                {
+                    var nrm = v.Item1.Normal;
+                    sb.AppendLine(Invariant($"vn {nrm.X} {nrm.Y} {nrm.Z}"));
+                }
+            }
+
+            sb.AppendLine();
+
+            foreach (var p in _Mesh.Primitives)
+            {
+                foreach (var v in p.Vertices)
+                {
+                    var uv = v.Item2.TexCoord;
+                    uv.Y = 1 - uv.Y;
+
+                    sb.AppendLine(Invariant($"vt {uv.X} {uv.Y}"));
+                }
+            }
+
+            sb.AppendLine();
+
+            sb.AppendLine("g default");
+
+            var baseVertexIndex = 1;
+
+            foreach (var p in _Mesh.Primitives)
+            {
+                var mtl = materials[p.Material];
+
+                sb.AppendLine($"usemtl {mtl}");
+
+                foreach (var t in p.Triangles)
+                {
+                    var a = t.Item1 + baseVertexIndex;
+                    var b = t.Item2 + baseVertexIndex;
+                    var c = t.Item3 + baseVertexIndex;
+
+                    sb.AppendLine(Invariant($"f {a}/{a}/{a} {b}/{b}/{b} {c}/{c}/{c}"));
+                }
+
+                baseVertexIndex += p.Vertices.Count;
+            }
+
+            return sb;
+        }
+
+        private static void _WriteTextContent(IDictionary<string, BYTES> files, string fileName, StringBuilder sb)
+        {
+            using (var mem = new System.IO.MemoryStream())
+            {
+                using (var tex = new System.IO.StreamWriter(mem))
+                {
+                    tex.Write(sb.ToString());
+                }
+
+                mem.TryGetBuffer(out BYTES content);
+
+                files[fileName] = content;
+            }
+        }
+
+        private static bool _IsPng(IReadOnlyList<Byte> data)
+        {
+            if (data[0] != 0x89) return false;
+            if (data[1] != 0x50) return false;
+            if (data[2] != 0x4e) return false;
+            if (data[3] != 0x47) return false;
+
+            return true;
+        }
+
+        private static bool _IsJpeg(IReadOnlyList<Byte> data)
+        {
+            if (data[0] != 0xff) return false;
+            if (data[1] != 0xd8) return false;
+
+            return true;
+        }
+
+        #endregion
+
+        #region schema2 API
+
+        public void AddModel(Schema2.ModelRoot model)
+        {
+            foreach (var triangle in model.Triangulate())
+            {
+                var dstMaterial = new Material();
+
+                var srcMaterial = triangle.Item4;
+                if (srcMaterial != null)
+                {
+                    var baseColor = srcMaterial.FindChannel("BaseColor");
+                    dstMaterial.DiffuseColor = baseColor.Factor;
+                    dstMaterial.DiffuseTexture = baseColor.Image?.GetImageContent() ?? default;
+                }
+
+                this.AddTriangle(dstMaterial, triangle.Item1, triangle.Item2, triangle.Item3);
+            }
+        }
+
+        #endregion
+    }
+}

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

@@ -8,7 +8,7 @@ namespace SharpGLTF.Schema2
     public static partial class Toolkit
     {
         /// <summary>
-        /// Sets the cone angles for the <see cref="PunctualLightType.Spot" light/>.
+        /// Sets the cone angles for the <see cref="PunctualLightType.Spot"/> light.
         /// </summary>
         /// <param name="light">This <see cref="PunctualLight"/> instance.</param>
         /// <param name="innerConeAngle">

+ 2 - 1
src/SharpGLTF.Toolkit/Schema2/MaterialExtensions.cs

@@ -62,7 +62,8 @@ namespace SharpGLTF.Schema2
             return material;
         }
 
-        public static Material WithPBRMetallicRoughness(this Material material,
+        public static Material WithPBRMetallicRoughness(
+            this Material material,
             Vector4 baseColorFactor, string baseColorImageFilePath,
             float metallicFactor = 1, string metallicImageFilePath = null,
             float roughnessFactor = 1, string roughtnessImageFilePath = null

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

@@ -236,5 +236,98 @@ namespace SharpGLTF.Schema2
         }
 
         #endregion
+
+        #region evaluation
+
+        public static Geometry.VertexTypes.VertexColumns GetVertexColumns(this MeshPrimitive primitive)
+        {
+            var vertexAccessors = primitive.VertexAccessors;
+
+            var columns = new Geometry.VertexTypes.VertexColumns();
+
+            if (vertexAccessors.ContainsKey("POSITION")) columns.Positions = vertexAccessors["POSITION"].AsVector3Array();
+            if (vertexAccessors.ContainsKey("NORMAL")) columns.Normals = vertexAccessors["NORMAL"].AsVector3Array();
+            if (vertexAccessors.ContainsKey("TANGENT")) columns.Tangents = vertexAccessors["TANGENT"].AsVector4Array();
+
+            if (vertexAccessors.ContainsKey("COLOR_0")) columns.Colors0 = vertexAccessors["COLOR_0"].AsVector4Array();
+            if (vertexAccessors.ContainsKey("COLOR_1")) columns.Colors1 = vertexAccessors["COLOR_1"].AsVector4Array();
+
+            if (vertexAccessors.ContainsKey("TEXCOORD_0")) columns.Textures0 = vertexAccessors["TEXCOORD_0"].AsVector2Array();
+            if (vertexAccessors.ContainsKey("TEXCOORD_1")) columns.Textures1 = vertexAccessors["TEXCOORD_1"].AsVector2Array();
+
+            if (vertexAccessors.ContainsKey("JOINTS_0")) columns.Joints0 = vertexAccessors["JOINTS_0"].AsVector4Array();
+            if (vertexAccessors.ContainsKey("JOINTS_1")) columns.Joints1 = vertexAccessors["JOINTS_1"].AsVector4Array();
+
+            if (vertexAccessors.ContainsKey("WEIGHTS_0")) columns.Weights0 = vertexAccessors["WEIGHTS_0"].AsVector4Array();
+            if (vertexAccessors.ContainsKey("WEIGHTS_1")) columns.Weights1 = vertexAccessors["WEIGHTS_1"].AsVector4Array();
+
+            return columns;
+        }
+
+        public static IEnumerable<(int, int, int)> GetTriangleIndices(this MeshPrimitive primitive)
+        {
+            if (primitive.DrawPrimitiveType == PrimitiveType.POINTS) yield break;
+            if (primitive.DrawPrimitiveType == PrimitiveType.LINES) yield break;
+            if (primitive.DrawPrimitiveType == PrimitiveType.LINE_LOOP) yield break;
+            if (primitive.DrawPrimitiveType == PrimitiveType.LINE_STRIP) yield break;
+
+            var indices = primitive.IndexAccessor != null
+                ?
+                primitive.IndexAccessor.AsIndicesArray()
+                :
+                EncodedArrayUtils.IndicesRange(0, primitive.GetVertexAccessor("POSITION").Count);
+
+            if (primitive.DrawPrimitiveType == PrimitiveType.TRIANGLES)
+            {
+                for (int i = 2; i < indices.Count; i += 3)
+                {
+                    yield return ((int)indices[i - 2], (int)indices[i - 1], (int)indices[i]);
+                }
+            }
+        }
+
+        public static Dictionary<Vector3, Vector3> ComputeNormals(this Mesh mesh)
+        {
+            var posnrm = new Dictionary<Vector3, Vector3>();
+
+            void addDirection(Dictionary<Vector3, Vector3> dict, Vector3 pos, Vector3 dir)
+            {
+                if (!dir._IsReal()) return;
+                if (!dict.TryGetValue(pos, out Vector3 n)) n = Vector3.Zero;
+                dict[pos] = n + dir;
+            }
+
+            foreach (var p in mesh.Primitives)
+            {
+                var positions = p.GetVertexAccessor("POSITION").AsVector3Array();
+
+                foreach (var t in p.GetTriangleIndices())
+                {
+                    var p1 = positions[t.Item1];
+                    var p2 = positions[t.Item2];
+                    var p3 = positions[t.Item3];
+                    var d = Vector3.Cross(p2 - p1, p3 - p1);
+                    addDirection(posnrm, p1, d);
+                    addDirection(posnrm, p2, d);
+                    addDirection(posnrm, p3, d);
+                }
+            }
+
+            foreach (var pos in posnrm.Keys.ToList())
+            {
+                posnrm[pos] = Vector3.Normalize(posnrm[pos]);
+            }
+
+            return posnrm;
+        }
+
+        public static void SaveAsWavefront(this ModelRoot model, string filePath)
+        {
+            var wf = new IO.WavefrontWriter();
+            wf.AddModel(model);
+            wf.WriteFiles(filePath);
+        }
+
+        #endregion
     }
 }

+ 15 - 4
tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSampleTests.cs

@@ -38,22 +38,33 @@ namespace SharpGLTF.Schema2.LoadAndSave
             {
                 if (!f.Contains(section)) continue;
 
+                var perf = System.Diagnostics.Stopwatch.StartNew();                
+
                 var model = GltfUtils.LoadModel(f);
                 Assert.NotNull(model);
 
-                // evaluate and save all the triangles to a Wavefront Object
-                model.AttachToCurrentTest(System.IO.Path.ChangeExtension(System.IO.Path.GetFileName(f), ".obj"));
-                model.AttachToCurrentTest(System.IO.Path.ChangeExtension(System.IO.Path.GetFileName(f), ".glb"));
-                
+                var perf_load = perf.ElapsedMilliseconds;
+
                 // do a model clone and compare it
                 _AssertAreEqual(model, model.DeepClone());
 
+                var perf_clone= perf.ElapsedMilliseconds;
+
                 // check extensions used
                 if (!model.ExtensionsUsed.Contains("EXT_lights_image_based"))
                 {
                     var detectedExtensions = model.RetrieveUsedExtensions().ToArray();
                     CollectionAssert.AreEquivalent(model.ExtensionsUsed, detectedExtensions);
                 }
+                
+                // evaluate and save all the triangles to a Wavefront Object
+                model.AttachToCurrentTest(System.IO.Path.ChangeExtension(System.IO.Path.GetFileName(f), ".obj"));
+                var perf_wavefront = perf.ElapsedMilliseconds;
+
+                model.AttachToCurrentTest(System.IO.Path.ChangeExtension(System.IO.Path.GetFileName(f), ".glb"));
+                var perf_glb = perf.ElapsedMilliseconds;
+
+                TestContext.Progress.WriteLine($"processed {f.ToShortDisplayPath()} - Load:{perf_load}ms Clone:{perf_clone}ms S.obj:{perf_wavefront}ms S.glb:{perf_glb}ms");
             }
         }
 

+ 0 - 94
tests/SharpGLTF.Tests/Schema2/ModelDumpUtils.cs

@@ -1,94 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Numerics;
-using System.Text;
-
-namespace SharpGLTF.Schema2
-{
-    /// <summary>
-    /// Utility class that hierarchicaly traverses a gltf model, evaluates the geometry and yields a collection of worldspace triangles.
-    /// </summary>
-    static class ModelDumpUtils
-    {
-        public static WavefrontWriter ToWavefrontWriter(this ModelRoot model)
-        {
-            var writer = new WavefrontWriter();
-
-            foreach(var triangle in model.Triangulate())
-            {
-                writer.AddTriangle(triangle.Item1, triangle.Item2, triangle.Item3);
-            }
-
-            return writer;
-        }
-
-        /// <summary>
-        /// Yields all the triangles in the model's default scene, in world space
-        /// </summary>
-        /// <param name="model"The input model</param>
-        /// <returns>A collection of worldspace triangles</returns>
-        public static IEnumerable<(Vector3,Vector3,Vector3)> Triangulate(this ModelRoot model)
-        {
-            return model.DefaultScene.Triangulate();
-        }
-
-        /// <summary>
-        /// Yields all the triangles in the scene, in world space
-        /// </summary>
-        /// <param name="scene"The input scene</param>
-        /// <returns>A collection of worldspace triangles</returns>
-        public static IEnumerable<(Vector3, Vector3, Vector3)> Triangulate(this Scene scene)
-        {
-            return Node.Flatten(scene).SelectMany(item => item.Triangulate(true));
-        }
-
-        /// <summary>
-        /// Yields all the triangles in the node
-        /// </summary>
-        /// <param name="node">The input node</param>
-        /// <param name="inWorldSpace">true if we want to transform the local triangles to worldspace</param>
-        /// <returns>A collection of triangles in the node's space, or in worldspace</returns>
-        public static IEnumerable<(Vector3,Vector3,Vector3)> Triangulate(this Node node, bool inWorldSpace)
-        {
-            var mesh = node.Mesh;
-            if (mesh == null) return Enumerable.Empty<(Vector3, Vector3, Vector3)>();
-
-            var xform = inWorldSpace ? node.WorldMatrix : Matrix4x4.Identity;
-
-            return mesh.Primitives.SelectMany(item => item.Triangulate(xform));
-        }
-
-        /// <summary>
-        /// Yields all the triangles in the mesh
-        /// </summary>
-        /// <param name="prim">The input primitive</param>
-        /// <param name="xform">The transform matrix</param>
-        /// <returns>A collection of triangles transformed by <paramref name="xform"/> </returns>
-        public static IEnumerable<(Vector3, Vector3, Vector3)> Triangulate(this MeshPrimitive prim, Matrix4x4 xform)
-        {
-            var positions = prim.GetVertexAccessor("POSITION").AsVector3Array();
-
-            var indices = prim.IndexAccessor != null
-                ?
-                prim.IndexAccessor.AsIndicesArray()
-                :
-                Memory.EncodedArrayUtils.IndicesRange(0,positions.Count);
-
-            if (prim.DrawPrimitiveType != PrimitiveType.TRIANGLES) yield break;
-
-            for(int i=0; i< indices.Count; i+=3)
-            {
-                var a = (int)indices[i + 0];
-                var b = (int)indices[i + 1];
-                var c = (int)indices[i + 2];
-
-                var aa = Vector3.Transform(positions[a], xform);
-                var bb = Vector3.Transform(positions[b], xform);
-                var cc = Vector3.Transform(positions[c], xform);
-
-                yield return (aa, bb, cc);
-            }
-        }
-    }
-}

+ 1 - 3
tests/SharpGLTF.Tests/TestUtils.cs

@@ -54,9 +54,7 @@ namespace SharpGLTF
             }
             else if (fileName.ToLower().EndsWith(".obj"))
             {
-                // evaluate all triangles of the model
-                var wavefront = Schema2.ModelDumpUtils.ToWavefrontWriter(model).ToString();
-                System.IO.File.WriteAllText(fileName, wavefront);                
+                Schema2.Toolkit.SaveAsWavefront(model, fileName);
             }
 
             // Attach the saved file to the current test

+ 0 - 72
tests/SharpGLTF.Tests/WavefrontWriter.cs

@@ -1,72 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Numerics;
-using System.Text;
-
-using static System.FormattableString;
-
-namespace SharpGLTF
-{
-    using POSITION = Geometry.VertexTypes.VertexPositionNormal;
-    using TEXCOORD = Geometry.VertexTypes.VertexEmpty;
-    using MATERIAL = Vector4;
-
-    /// <summary>
-    /// Tiny wavefront object writer
-    /// </summary>
-    class WavefrontWriter
-    {
-        #region data
-
-        private readonly Geometry.MeshBuilder<MATERIAL, POSITION, TEXCOORD> _Mesh = new Geometry.MeshBuilder<MATERIAL, POSITION, TEXCOORD>();
-        
-        #endregion
-
-        #region API        
-
-        public void AddTriangle(Vector3 a, Vector3 b, Vector3 c)
-        {
-            var aa = new POSITION { Position = a };
-            var bb = new POSITION { Position = b };
-            var cc = new POSITION { Position = c };
-
-            _Mesh.UsePrimitive(Vector4.One).AddTriangle(aa, bb, cc);
-        }
-
-        public override string ToString()
-        {
-            var sb = new StringBuilder();
-
-            sb.AppendLine();
-
-            foreach (var p in _Mesh.Primitives)
-            {
-                foreach (var v in p.Vertices)
-                {
-                    sb.AppendLine(Invariant($"v {v.Item1.Position.X} {v.Item1.Position.Y} {v.Item1.Position.Z}"));
-                }
-            }
-
-            sb.AppendLine();
-
-            sb.AppendLine("g default");
-
-            var baseVertexIndex = 1;
-
-            foreach(var p in _Mesh.Primitives)
-            {
-                foreach (var t in p.Triangles)
-                {
-                    sb.AppendLine(Invariant($"f {t.Item1 + baseVertexIndex} {t.Item2 + baseVertexIndex} {t.Item3 + baseVertexIndex}"));
-                }
-
-                baseVertexIndex += p.Vertices.Count;
-            }
-
-            return sb.ToString();
-        }
-
-        #endregion
-    }
-}