Browse Source

Merge pull request #1 from vpenades/master

Pulled in new changes from original repo.
MeltyPlayer 2 years ago
parent
commit
78915d4351

+ 18 - 0
src/Shared/Guard.cs

@@ -74,6 +74,24 @@ namespace SharpGLTF
             throw new ArgumentException(message, parameterName);
             throw new ArgumentException(message, parameterName);
         }
         }
 
 
+        public static void MustExist(System.IO.FileInfo finfo, string parameterName, string message = "")
+        {
+            if (finfo == null) throw new ArgumentNullException(nameof(finfo));
+            if (finfo.Exists) return;
+
+            if (string.IsNullOrWhiteSpace(message)) message = $"{finfo.FullName} is invalid or does not exist.";
+            throw new ArgumentException(message, parameterName);
+        }
+
+        public static void MustExist(System.IO.DirectoryInfo dinfo, string parameterName, string message = "")
+        {
+            if (dinfo == null) throw new ArgumentNullException(nameof(dinfo));
+            if (dinfo.Exists) return;
+
+            if (string.IsNullOrWhiteSpace(message)) message = $"{dinfo.FullName} is invalid or does not exist.";
+            throw new ArgumentException(message, parameterName);
+        }
+
         #endregion
         #endregion
 
 
         #region null / empty
         #region null / empty

+ 36 - 20
src/SharpGLTF.Core/Memory/MemoryImage.cs

@@ -2,7 +2,9 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Text;
 using System.Text;
+
 using BYTES = System.ArraySegment<System.Byte>;
 using BYTES = System.ArraySegment<System.Byte>;
+using LAZYBYTES = System.Lazy<System.ArraySegment<System.Byte>>;
 
 
 namespace SharpGLTF.Memory
 namespace SharpGLTF.Memory
 {
 {
@@ -103,27 +105,20 @@ namespace SharpGLTF.Memory
             return true;
             return true;
         }
         }
 
 
-        public MemoryImage(BYTES image)
-        {
-            Guard.IsTrue(_IsImage(image), nameof(image), GuardError_MustBeValidImage);
-
-            _Image = image;
-            _SourcePathHint = null;
-        }
+        public MemoryImage(BYTES image) 
+            : this(_ToLazy(image), null) { }
 
 
         public MemoryImage(Byte[] image)
         public MemoryImage(Byte[] image)
-        {
-            if (image != null) Guard.IsTrue(_IsImage(image), nameof(image), GuardError_MustBeValidImage);
+            : this(_ToLazy(image), null) { }
 
 
-            _Image = image == null ? default : new BYTES(image);
-            _SourcePathHint = null;
-        }
+        public MemoryImage(Func<BYTES> factory)
+            : this(new LAZYBYTES(factory), null) { }
 
 
         public MemoryImage(string filePath)
         public MemoryImage(string filePath)
         {
         {
             if (string.IsNullOrEmpty(filePath))
             if (string.IsNullOrEmpty(filePath))
             {
             {
-                _Image = default;
+                _LazyImage = _ToLazy(default(BYTES));
                 _SourcePathHint = null;
                 _SourcePathHint = null;
             }
             }
             else
             else
@@ -134,28 +129,49 @@ namespace SharpGLTF.Memory
 
 
                 Guard.IsTrue(_IsImage(data), nameof(filePath), GuardError_MustBeValidImage);
                 Guard.IsTrue(_IsImage(data), nameof(filePath), GuardError_MustBeValidImage);
 
 
-                _Image = new BYTES(data);
+                _LazyImage = _ToLazy(data);
                 _SourcePathHint = filePath;
                 _SourcePathHint = filePath;
             }
             }
-        }
+        }        
+
+        internal MemoryImage(Byte[] image, string filePath)
+            : this(_ToLazy(image), filePath) { }
 
 
         internal MemoryImage(BYTES image, string filePath)
         internal MemoryImage(BYTES image, string filePath)
-            : this(image)
+            : this(_ToLazy(image), filePath) { }
+
+        internal MemoryImage(MemoryImage image, string filePath)
         {
         {
-            _SourcePathHint = filePath;
+            _LazyImage = image._LazyImage;
+            _SourcePathHint = filePath ?? image._SourcePathHint;
         }
         }
 
 
-        internal MemoryImage(Byte[] image, string filePath)
-            : this(image)
+        internal MemoryImage(LAZYBYTES image, string filePath)
         {
         {
+            _LazyImage = image;
             _SourcePathHint = filePath;
             _SourcePathHint = filePath;
+        }        
+
+        private static LAZYBYTES _ToLazy(Byte[] bytes)
+        {
+            return _ToLazy(new BYTES(bytes));
+        }
+        private static LAZYBYTES _ToLazy(BYTES bytes)
+        {
+            #if NETSTANDARD2_0
+            return new LAZYBYTES(()=> bytes);
+            #else
+            return new LAZYBYTES(bytes);
+            #endif
         }
         }
 
 
         #endregion
         #endregion
 
 
         #region data
         #region data
 
 
-        private readonly BYTES _Image;
+        private readonly LAZYBYTES _LazyImage;
+
+        private BYTES _Image => _LazyImage == null ? default : _LazyImage.Value;
 
 
         /// <remarks>
         /// <remarks>
         /// This field must NOT be used for equality checks, it has the same face value as a code comment.
         /// This field must NOT be used for equality checks, it has the same face value as a code comment.

+ 7 - 17
src/SharpGLTF.Core/Schema2/Serialization.ReadContext.cs

@@ -32,29 +32,17 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(callback, nameof(callback));
             Guard.NotNull(callback, nameof(callback));
 
 
             return new ReadContext(callback);
             return new ReadContext(callback);
-        }
-
-        [Obsolete("Use CreateFromDirectory")]
-        public static ReadContext CreateFromFile(string filePath)
-        {
-            Guard.NotNull(filePath, nameof(filePath));
-            Guard.FilePathMustExist(filePath, nameof(filePath));
+        }        
 
 
-            var dir = Path.GetDirectoryName(filePath);
-            return CreateFromDirectory(dir);
-        }
-
-        public static ReadContext CreateFromDirectory(string directoryPath)
+        public static ReadContext CreateFromDirectory(DirectoryInfo dinfo)
         {
         {
-            Guard.NotNull(directoryPath, nameof(directoryPath));
-
-            if (string.IsNullOrEmpty(directoryPath)) directoryPath = Environment.CurrentDirectory;
-            else directoryPath = System.IO.Path.GetFullPath(directoryPath);
+            Guard.NotNull(dinfo, nameof(dinfo));
+            Guard.MustExist(dinfo, nameof(dinfo));
 
 
             string _uriSolver(string rawUri)
             string _uriSolver(string rawUri)
             {
             {
                 var path = Uri.UnescapeDataString(rawUri);
                 var path = Uri.UnescapeDataString(rawUri);
-                return Path.Combine(directoryPath, path);
+                return Path.Combine(dinfo.FullName, path);
             }
             }
 
 
             BYTES _loadFile(string rawUri)
             BYTES _loadFile(string rawUri)
@@ -156,6 +144,7 @@ namespace SharpGLTF.Schema2
         public Validation.ValidationResult Validate(string resourceName)
         public Validation.ValidationResult Validate(string resourceName)
         {
         {
             Guard.FilePathMustBeValid(resourceName, nameof(resourceName));
             Guard.FilePathMustBeValid(resourceName, nameof(resourceName));
+            if (System.IO.Path.IsPathRooted(resourceName)) throw new ArgumentException("path must be relative", nameof(resourceName));
 
 
             var root = this.ReadAllBytesToEnd(resourceName);
             var root = this.ReadAllBytesToEnd(resourceName);
 
 
@@ -178,6 +167,7 @@ namespace SharpGLTF.Schema2
         public MODEL ReadSchema2(string resourceName)
         public MODEL ReadSchema2(string resourceName)
         {
         {
             Guard.FilePathMustBeValid(resourceName, nameof(resourceName));
             Guard.FilePathMustBeValid(resourceName, nameof(resourceName));
+            if (System.IO.Path.IsPathRooted(resourceName)) throw new ArgumentException("path must be relative", nameof(resourceName));
 
 
             var root = this.ReadAllBytesToEnd(resourceName);
             var root = this.ReadAllBytesToEnd(resourceName);
 
 

+ 12 - 11
src/SharpGLTF.Core/Schema2/Serialization.ReadSettings.cs

@@ -91,14 +91,12 @@ namespace SharpGLTF.Schema2
         public static Validation.ValidationResult Validate(string filePath)
         public static Validation.ValidationResult Validate(string filePath)
         {
         {
             Guard.NotNull(filePath, nameof(filePath));
             Guard.NotNull(filePath, nameof(filePath));
-            Guard.FilePathMustExist(filePath, nameof(filePath));
-
-            var dir = Path.GetDirectoryName(filePath);
-            filePath = System.IO.Path.GetFileName(filePath);
-
-            var context = ReadContext.CreateFromDirectory(dir);
+            var finfo = new System.IO.FileInfo(filePath);
+            Guard.MustExist(finfo, nameof(filePath));
 
 
-            return context.Validate(filePath);
+            return ReadContext
+                .CreateFromDirectory(finfo.Directory)
+                .Validate(finfo.Name);
         }
         }
 
 
         #endregion
         #endregion
@@ -122,16 +120,19 @@ namespace SharpGLTF.Schema2
 
 
             if (!(settings is ReadContext context))
             if (!(settings is ReadContext context))
             {
             {
-                Guard.FilePathMustExist(filePath, nameof(filePath));
+                var finfo = new System.IO.FileInfo(filePath);
 
 
-                var dir = Path.GetDirectoryName(filePath);
-                filePath = System.IO.Path.GetFileName(filePath);
+                Guard.MustExist(finfo, nameof(filePath));
 
 
                 context = ReadContext
                 context = ReadContext
-                    .CreateFromDirectory(dir)
+                    .CreateFromDirectory(finfo.Directory)
                     .WithSettingsFrom(settings);
                     .WithSettingsFrom(settings);
+
+                filePath = finfo.Name;
             }
             }
 
 
+            // at this point, filePath must be a path "relative to context"            
+
             return context.ReadSchema2(filePath);
             return context.ReadSchema2(filePath);
         }
         }
 
 

+ 18 - 12
src/SharpGLTF.Core/Schema2/Serialization.WriteContext.cs

@@ -45,22 +45,20 @@ namespace SharpGLTF.Schema2
             return context;
             return context;
         }
         }
 
 
+        [Obsolete("Use CreateFromDirectory", true)]
         public static WriteContext CreateFromFile(string filePath)
         public static WriteContext CreateFromFile(string filePath)
         {
         {
             Guard.FilePathMustBeValid(filePath, nameof(filePath));
             Guard.FilePathMustBeValid(filePath, nameof(filePath));
 
 
-            if (!Path.IsPathRooted(filePath)) filePath = Path.GetFullPath(filePath);
+            var finfo = new System.IO.FileInfo(filePath);
 
 
-            var dir = Path.GetDirectoryName(filePath);
-
-            return CreateFromDirectory(dir);
+            return CreateFromDirectory(finfo.Directory);
         }
         }
 
 
-        public static WriteContext CreateFromDirectory(string directoryPath)
+        public static WriteContext CreateFromDirectory(DirectoryInfo dinfo)
         {
         {
-            Guard.DirectoryPathMustExist(directoryPath, nameof(directoryPath));
-
-            var dinfo = new DirectoryInfo(directoryPath);
+            Guard.NotNull(dinfo, nameof(dinfo));
+            Guard.MustExist(dinfo, nameof(dinfo));            
 
 
             void _saveFile(string rawUri, BYTES data)
             void _saveFile(string rawUri, BYTES data)
             {
             {
@@ -105,10 +103,7 @@ namespace SharpGLTF.Schema2
         public WriteContext WithTextSettings()
         public WriteContext WithTextSettings()
         {
         {
             if (ImageWriting == ResourceWriteMode.Default) ImageWriting = ResourceWriteMode.SatelliteFile;
             if (ImageWriting == ResourceWriteMode.Default) ImageWriting = ResourceWriteMode.SatelliteFile;
-            if (ImageWriting == ResourceWriteMode.BufferView) ImageWriting = ResourceWriteMode.SatelliteFile;
-
-            MergeBuffers = true;
-            JsonIndented = false;
+            if (ImageWriting == ResourceWriteMode.BufferView) ImageWriting = ResourceWriteMode.SatelliteFile;            
 
 
             return this;
             return this;
         }
         }
@@ -120,7 +115,10 @@ namespace SharpGLTF.Schema2
             if (ImageWriting == ResourceWriteMode.Default) ImageWriting = ResourceWriteMode.BufferView;
             if (ImageWriting == ResourceWriteMode.Default) ImageWriting = ResourceWriteMode.BufferView;
             if (ImageWriting == ResourceWriteMode.EmbeddedAsBase64) ImageWriting = ResourceWriteMode.BufferView;
             if (ImageWriting == ResourceWriteMode.EmbeddedAsBase64) ImageWriting = ResourceWriteMode.BufferView;
 
 
+            // merging buffers is mandatory for GLB since the format only supports a single buffer.
             MergeBuffers = true;
             MergeBuffers = true;
+
+            // there's no point in writing an indented json that's going to be written into a binary file.
             JsonIndented = false;
             JsonIndented = false;
 
 
             return this;
             return this;
@@ -203,6 +201,9 @@ namespace SharpGLTF.Schema2
         public void WriteTextSchema2(string baseName, MODEL model)
         public void WriteTextSchema2(string baseName, MODEL model)
         {
         {
             Guard.NotNullOrEmpty(baseName, nameof(baseName));
             Guard.NotNullOrEmpty(baseName, nameof(baseName));
+            Guard.FilePathMustBeValid(baseName, nameof(baseName));
+            if (System.IO.Path.IsPathRooted(baseName)) throw new ArgumentException("path must be relative", nameof(baseName));
+
             Guard.NotNull(model, nameof(model));
             Guard.NotNull(model, nameof(model));
 
 
             // merge images when explicitly requested.
             // merge images when explicitly requested.
@@ -235,8 +236,13 @@ namespace SharpGLTF.Schema2
         public void WriteBinarySchema2(string baseName, MODEL model)
         public void WriteBinarySchema2(string baseName, MODEL model)
         {
         {
             Guard.NotNullOrEmpty(baseName, nameof(baseName));
             Guard.NotNullOrEmpty(baseName, nameof(baseName));
+            Guard.FilePathMustBeValid(baseName, nameof(baseName));
+            if (System.IO.Path.IsPathRooted(baseName)) throw new ArgumentException("path must be relative", nameof(baseName));
+
             Guard.NotNull(model, nameof(model));
             Guard.NotNull(model, nameof(model));
 
 
+            
+
             // merge images for all cases except for satellite files
             // merge images for all cases except for satellite files
             var mergeImages = this.ImageWriting != ResourceWriteMode.SatelliteFile;
             var mergeImages = this.ImageWriting != ResourceWriteMode.SatelliteFile;
 
 

+ 18 - 6
src/SharpGLTF.Core/Schema2/Serialization.WriteSettings.cs

@@ -172,17 +172,23 @@ namespace SharpGLTF.Schema2
         /// <param name="settings">Optional settings.</param>
         /// <param name="settings">Optional settings.</param>
         public void SaveGLB(string filePath, WriteSettings settings = null)
         public void SaveGLB(string filePath, WriteSettings settings = null)
         {
         {
+            Guard.NotNull(filePath, nameof(filePath));
+
             if (!(settings is WriteContext context))
             if (!(settings is WriteContext context))
             {
             {
+                var finfo = new System.IO.FileInfo(filePath);
+
                 context = WriteContext
                 context = WriteContext
-                    .CreateFromFile(filePath)
+                    .CreateFromDirectory(finfo.Directory)
                     .WithSettingsFrom(settings);
                     .WithSettingsFrom(settings);
-            }
 
 
-            context.WithBinarySettings();
+                filePath = finfo.Name;
+            }
 
 
             var name = Path.GetFileNameWithoutExtension(filePath);
             var name = Path.GetFileNameWithoutExtension(filePath);
 
 
+            context.WithBinarySettings();
+
             context.WriteBinarySchema2(name, this);
             context.WriteBinarySchema2(name, this);
         }
         }
 
 
@@ -196,17 +202,23 @@ namespace SharpGLTF.Schema2
         /// </remarks>
         /// </remarks>
         public void SaveGLTF(string filePath, WriteSettings settings = null)
         public void SaveGLTF(string filePath, WriteSettings settings = null)
         {
         {
+            Guard.NotNull(filePath, nameof(filePath));
+
             if (!(settings is WriteContext context))
             if (!(settings is WriteContext context))
             {
             {
+                var finfo = new System.IO.FileInfo(filePath);
+
                 context = WriteContext
                 context = WriteContext
-                    .CreateFromFile(filePath)
+                    .CreateFromDirectory(finfo.Directory)
                     .WithSettingsFrom(settings);
                     .WithSettingsFrom(settings);
-            }
 
 
-            context.WithTextSettings();
+                filePath = finfo.Name;
+            }
 
 
             var name = Path.GetFileNameWithoutExtension(filePath);
             var name = Path.GetFileNameWithoutExtension(filePath);
 
 
+            context.WithTextSettings();
+            
             context.WriteTextSchema2(name, this);
             context.WriteTextSchema2(name, this);
         }
         }
 
 

+ 7 - 5
src/SharpGLTF.Core/Schema2/gltf.Images.cs

@@ -55,15 +55,17 @@ namespace SharpGLTF.Schema2
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// When set to a FileName or a relative File Path, it will be used to write the texture.
+        /// When set to a FileName or a relative File Path,
+        /// it will be used as the file name of the texture being written.<br/>
+        /// When null, a default file name will be used.
         /// </summary>
         /// </summary>
-        /// <remarks>
+        /// <remarks>        
         /// <para>
         /// <para>
-        /// When null, the default file name will be used.
+        /// if not sure about the image extension, using .* as extension will replace
+        /// the extension with the appropiate one at the time of writing.
         /// </para>
         /// </para>
         /// <para>
         /// <para>
-        /// if not sure about the image extension, using "name.*" as extension will replace
-        /// the extension with the appropiate one before writing.
+        /// For more advanced scenarios, you can also use: <see cref="WriteSettings.ImageWriteCallback"/>
         /// </para>
         /// </para>
         /// </remarks>
         /// </remarks>
         public String AlternateWriteFileName { get; set; }
         public String AlternateWriteFileName { get; set; }

+ 4 - 2
src/SharpGLTF.Core/Schema2/gltf.Root.cs

@@ -71,12 +71,14 @@ namespace SharpGLTF.Schema2
             rcontext.Validation = Validation.ValidationMode.Skip;
             rcontext.Validation = Validation.ValidationMode.Skip;
             var cloned = rcontext.ReadSchema2("$$$deepclone$$$.gltf");
             var cloned = rcontext.ReadSchema2("$$$deepclone$$$.gltf");
 
 
-            // Restore MemoryImage source URIs (they're not cloned as part of the serialization)
+            // Restore MemoryImage's source URIs hints
+            // and Image's AlternateWriteFileName
+            // (they're not cloned as part of the serialization)
             foreach (var srcImg in this.LogicalImages)
             foreach (var srcImg in this.LogicalImages)
             {
             {
                 var dstImg = cloned.LogicalImages[srcImg.LogicalIndex];
                 var dstImg = cloned.LogicalImages[srcImg.LogicalIndex];
                 var img = dstImg.Content;
                 var img = dstImg.Content;
-                dstImg.Content = new Memory.MemoryImage(img._GetBuffer(), srcImg.Content.SourcePath);
+                dstImg.Content = new Memory.MemoryImage(img, srcImg.Content.SourcePath);
                 dstImg.AlternateWriteFileName = srcImg.AlternateWriteFileName;
                 dstImg.AlternateWriteFileName = srcImg.AlternateWriteFileName;
             }
             }
 
 

+ 67 - 62
src/SharpGLTF.Toolkit/IO/WavefrontWriter.cs

@@ -1,10 +1,12 @@
 using System;
 using System;
 using System.Collections;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Numerics;
 using System.Numerics;
 using System.Text;
 using System.Text;
 
 
+using SharpGLTF.Memory;
 using SharpGLTF.Schema2;
 using SharpGLTF.Schema2;
 
 
 using static System.FormattableString;
 using static System.FormattableString;
@@ -55,14 +57,13 @@ namespace SharpGLTF.IO
         {
         {
             Guard.NotNullOrEmpty(filePath, nameof(filePath));
             Guard.NotNullOrEmpty(filePath, nameof(filePath));
 
 
-            var files = GetFiles(System.IO.Path.GetFileNameWithoutExtension(filePath));
-
             var dir = System.IO.Path.GetDirectoryName(filePath);
             var dir = System.IO.Path.GetDirectoryName(filePath);
 
 
-            foreach (var f in files)
+            foreach (var fileNameAndGenerator in _GetFileGenerators(System.IO.Path.GetFileNameWithoutExtension(filePath)))
             {
             {
-                var fpath = System.IO.Path.Combine(dir, f.Key);
-                System.IO.File.WriteAllBytes(fpath, f.Value.ToArray());
+                var fpath = System.IO.Path.Combine(dir, fileNameAndGenerator.Key);
+                using var fs = File.OpenWrite(fpath);
+                fileNameAndGenerator.Value(fs);
             }
             }
         }
         }
 
 
@@ -81,17 +82,33 @@ namespace SharpGLTF.IO
             Guard.IsFalse(baseName.Any(c => char.IsWhiteSpace(c)), nameof(baseName), "Whitespace characters not allowed in filename");
             Guard.IsFalse(baseName.Any(c => char.IsWhiteSpace(c)), nameof(baseName), "Whitespace characters not allowed in filename");
 
 
             var files = new Dictionary<String, BYTES>();
             var files = new Dictionary<String, BYTES>();
+            foreach (var fileNameAndGenerator in _GetFileGenerators(baseName)) 
+            {
+                using var mem = new MemoryStream();
+                fileNameAndGenerator.Value(mem);
 
 
-            var materials = _WriteMaterials(files, baseName, _Mesh.Primitives.Select(item => item.Material));
-
-            var geocontent = _GetGeometryContent(materials, baseName + ".mtl");
+                mem.TryGetBuffer(out var bytes);
 
 
-            _WriteTextContent(files, baseName + ".obj", geocontent);
+                files[fileNameAndGenerator.Key] = bytes;
+            }
 
 
             return files;
             return files;
         }
         }
 
 
-        private static IReadOnlyDictionary<Material, string> _WriteMaterials(IDictionary<String, BYTES> files, string baseName, IEnumerable<Material> materials)
+        private IReadOnlyDictionary<String, Action<Stream>> _GetFileGenerators(string baseName) 
+        {
+            Guard.IsFalse(baseName.Any(c => char.IsWhiteSpace(c)), nameof(baseName), "Whitespace characters not allowed in filename");
+
+            var fileGenerators = new Dictionary<String, Action<Stream>>();
+
+            var materials = _GetMaterialFileGenerator(fileGenerators, baseName, _Mesh.Primitives.Select(item => item.Material));
+
+            fileGenerators[baseName + ".obj"] = fs => _GetGeometryContent(new StreamWriter(fs), materials, baseName + ".mtl");
+
+            return fileGenerators;
+        }
+
+        private static IReadOnlyDictionary<Material, string> _GetMaterialFileGenerator(IDictionary<String, Action<Stream>> fileGenerators, string baseName, IEnumerable<Material> materials)
         {
         {
             // write all image files
             // write all image files
             var images = materials
             var images = materials
@@ -101,76 +118,81 @@ namespace SharpGLTF.IO
 
 
             bool firstImg = true;
             bool firstImg = true;
 
 
+            var imageNameByImage = new Dictionary<MemoryImage, string>();
             foreach (var img in images)
             foreach (var img in images)
             {
             {
                 var imgName = firstImg
                 var imgName = firstImg
                     ? $"{baseName}.{img.FileExtension}"
                     ? $"{baseName}.{img.FileExtension}"
-                    : $"{baseName}_{files.Count}.{img.FileExtension}";
+                    : $"{baseName}_{fileGenerators.Count}.{img.FileExtension}";
 
 
-                files[imgName] = new BYTES(img.Content.ToArray());
+                fileGenerators[imgName] = fs => {
+                    var bytes = img.Content.ToArray();
+                    fs.Write(bytes, 0, bytes.Length);
+                };
                 firstImg = false;
                 firstImg = false;
+
+                imageNameByImage[img] = imgName;
             }
             }
 
 
             // write materials
             // write materials
 
 
             var mmap = new Dictionary<Material, string>();
             var mmap = new Dictionary<Material, string>();
-
-            var sb = new StringBuilder();
-
-            foreach (var m in materials)
+            foreach (var m in materials) 
             {
             {
                 mmap[m] = $"Material_{mmap.Count}";
                 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}"));
-                sb.AppendLine(Invariant($"Ks {m.SpecularColor.X} {m.SpecularColor.Y} {m.SpecularColor.Z}"));
-
-                if (m.DiffuseTexture.IsValid)
-                {
-                    var imgName = files.FirstOrDefault(kvp => new Memory.MemoryImage(kvp.Value) == m.DiffuseTexture ).Key;
-                    sb.AppendLine($"map_Kd {imgName}");
-                }
-
-                sb.AppendLine();
             }
             }
 
 
             // write material library
             // write material library
-            _WriteTextContent(files, baseName + ".mtl", sb);
+            fileGenerators[baseName + ".mtl"] = fs =>
+            {
+                var sw = new StreamWriter(fs);
+                foreach (var m in materials) 
+                {
+                    sw.WriteLine($"newmtl {mmap[m]}");
+                    sw.WriteLine("illum 2");
+                    sw.WriteLine(Invariant($"Ka {m.DiffuseColor.X} {m.DiffuseColor.Y} {m.DiffuseColor.Z}"));
+                    sw.WriteLine(Invariant($"Kd {m.DiffuseColor.X} {m.DiffuseColor.Y} {m.DiffuseColor.Z}"));
+                    sw.WriteLine(Invariant($"Ks {m.SpecularColor.X} {m.SpecularColor.Y} {m.SpecularColor.Z}"));
+
+                    if (m.DiffuseTexture.IsValid) {
+                        var imgName = imageNameByImage[m.DiffuseTexture];
+                        sw.WriteLine($"map_Kd {imgName}");
+                    }
+
+                    sw.WriteLine();
+                }
+            };
 
 
             return mmap;
             return mmap;
         }
         }
 
 
-        private StringBuilder _GetGeometryContent(IReadOnlyDictionary<Material, string> materials, string mtlLib)
+        private void _GetGeometryContent(StreamWriter sw, IReadOnlyDictionary<Material, string> materials, string mtlLib)
         {
         {
-            var sb = new StringBuilder();
-
-            sb.AppendLine($"mtllib {mtlLib}");
+            sw.WriteLine($"mtllib {mtlLib}");
 
 
-            sb.AppendLine();
+            sw.WriteLine();
 
 
             foreach (var p in _Mesh.Primitives)
             foreach (var p in _Mesh.Primitives)
             {
             {
                 foreach (var v in p.Vertices)
                 foreach (var v in p.Vertices)
                 {
                 {
                     var pos = v.Position;
                     var pos = v.Position;
-                    sb.AppendLine(Invariant($"v {pos.X} {pos.Y} {pos.Z}"));
+                    sw.WriteLine(Invariant($"v {pos.X} {pos.Y} {pos.Z}"));
                 }
                 }
             }
             }
 
 
-            sb.AppendLine();
+            sw.WriteLine();
 
 
             foreach (var p in _Mesh.Primitives)
             foreach (var p in _Mesh.Primitives)
             {
             {
                 foreach (var v in p.Vertices)
                 foreach (var v in p.Vertices)
                 {
                 {
                     var nrm = v.Geometry.Normal;
                     var nrm = v.Geometry.Normal;
-                    sb.AppendLine(Invariant($"vn {nrm.X} {nrm.Y} {nrm.Z}"));
+                    sw.WriteLine(Invariant($"vn {nrm.X} {nrm.Y} {nrm.Z}"));
                 }
                 }
             }
             }
 
 
-            sb.AppendLine();
+            sw.WriteLine();
 
 
             foreach (var p in _Mesh.Primitives)
             foreach (var p in _Mesh.Primitives)
             {
             {
@@ -179,13 +201,13 @@ namespace SharpGLTF.IO
                     var uv = v.Material.TexCoord;
                     var uv = v.Material.TexCoord;
                     uv.Y = 1 - uv.Y;
                     uv.Y = 1 - uv.Y;
 
 
-                    sb.AppendLine(Invariant($"vt {uv.X} {uv.Y}"));
+                    sw.WriteLine(Invariant($"vt {uv.X} {uv.Y}"));
                 }
                 }
             }
             }
 
 
-            sb.AppendLine();
+            sw.WriteLine();
 
 
-            sb.AppendLine("g default");
+            sw.WriteLine("g default");
 
 
             var baseVertexIndex = 1;
             var baseVertexIndex = 1;
 
 
@@ -193,7 +215,7 @@ namespace SharpGLTF.IO
             {
             {
                 var mtl = materials[p.Material];
                 var mtl = materials[p.Material];
 
 
-                sb.AppendLine($"usemtl {mtl}");
+                sw.WriteLine($"usemtl {mtl}");
 
 
                 foreach (var t in p.Triangles)
                 foreach (var t in p.Triangles)
                 {
                 {
@@ -201,28 +223,11 @@ namespace SharpGLTF.IO
                     var b = t.B + baseVertexIndex;
                     var b = t.B + baseVertexIndex;
                     var c = t.C + baseVertexIndex;
                     var c = t.C + baseVertexIndex;
 
 
-                    sb.AppendLine(Invariant($"f {a}/{a}/{a} {b}/{b}/{b} {c}/{c}/{c}"));
+                    sw.WriteLine(Invariant($"f {a}/{a}/{a} {b}/{b}/{b} {c}/{c}/{c}"));
                 }
                 }
 
 
                 baseVertexIndex += p.Vertices.Count;
                 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;
-            }
         }
         }
 
 
         #endregion
         #endregion

+ 1 - 14
tests/SharpGLTF.Core.Tests/IO/JsonContentTests.cs

@@ -258,18 +258,5 @@ namespace SharpGLTF.IO
 
 
             Assert.IsTrue(roundtripModel.LogicalImages[0].Content.IsPng);
             Assert.IsTrue(roundtripModel.LogicalImages[0].Content.IsPng);
         }
         }
-    }
-
-    [Category("Core.IO")]
-    public class ContextTests
-    {
-        [Test]
-        public void TestCurrentDirectoryLoad()
-        {
-            // for some reason, System.IO.Path.GetFullPath() does not recogninze an empty string as the current directory.
-
-            var currDirContext0 = Schema2.ReadContext.CreateFromDirectory(string.Empty);
-            Assert.NotNull(currDirContext0);            
-        }
-    }
+    }    
 }
 }

+ 80 - 0
tests/SharpGLTF.ThirdParty.Tests/MeltyPlayerTests.cs

@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+
+using NUnit.Framework;
+
+using SharpGLTF.Schema2;
+
+namespace SharpGLTF.ThirdParty
+{
+    [ResourcePathFormat("*\\Assets")]
+    [AttachmentPathFormat("*\\?", true)]
+    internal class MeltyPlayerTests
+    {
+        /// <summary>
+        /// Regression: The only way to write models that exceed 2gb is to disable MergeBuffers.
+        /// </summary>
+        /// <remarks>
+        /// <see href="https://github.com/vpenades/SharpGLTF/issues/165">#165</see>
+        /// </remarks>
+        [Test]        
+        public void CreateSceneWithMultipleBuffers()
+        {
+            TestContext.CurrentContext.AttachGltfValidatorLinks();
+
+            // create model
+            var model = ModelRoot.CreateModel();
+
+            // create scene
+            var scene = model.DefaultScene = model.UseScene("Default");
+
+            // create material
+            var material = model.CreateMaterial("Default")
+                .WithDefault(new Vector4(0, 1, 0, 1))
+                .WithDoubleSide(true);
+
+            void addNodeMesh(string name, Matrix4x4 xform)
+            {
+                // create node
+                var rnode = scene.CreateNode(name);
+
+                // create mesh
+                var rmesh = rnode.Mesh = model.CreateMesh($"{name} Triangle Mesh");
+
+                // create the vertex positions
+                var positions = new[]
+                {
+                    Vector3.Transform(new Vector3(0, 10, 0), xform),
+                    Vector3.Transform(new Vector3(-10, -10, 0), xform),
+                    Vector3.Transform(new Vector3(10, -10, 0), xform),
+                };
+
+                // create an index buffer and fill it            
+                var indices = new[] { 0, 1, 2 };
+
+                // create mesh primitive
+                var primitive = rmesh.CreatePrimitive()
+                    .WithVertexAccessor("POSITION", positions)
+                    .WithIndicesAccessor(PrimitiveType.TRIANGLES, indices)
+                    .WithMaterial(material);
+            }
+
+            addNodeMesh("Node1", Matrix4x4.Identity);
+            addNodeMesh("Node2", Matrix4x4.CreateTranslation(20, 0, 0));
+
+            var ws = new WriteSettings();
+            ws.MergeBuffers = false;
+            ws.JsonIndented = true;
+
+            var resultPath0 = AttachmentInfo.From("result0.gltf").WriteObject(f => model.Save(f, ws));
+
+            var satellites = ModelRoot.GetSatellitePaths(resultPath0.FullName);
+
+            Assert.AreEqual(4, satellites.Length);
+        }
+    }
+}