Browse Source

Refactored some Image and MaterialBuilders APIs
Removed some Obsolete methods and some code cleanup.
* Some Image and MaterialBuilder APIs might not be compatible with previous version *

Vicente Penades 5 years ago
parent
commit
8165fb67a3

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

@@ -50,28 +50,28 @@ namespace SharpGLTF.Runtime
         private readonly GraphicsDevice _Device;
         private readonly GraphicsDevice _Device;
         private readonly GraphicsResourceTracker _Disposables;
         private readonly GraphicsResourceTracker _Disposables;
 
 
-        private readonly Dictionary<IReadOnlyList<Byte>, Texture2D> _Textures = new Dictionary<IReadOnlyList<byte>, Texture2D>(new ArraySegmentContentComparer());        
+        private readonly Dictionary<Memory.MemoryImage, Texture2D> _Textures = new Dictionary<Memory.MemoryImage, Texture2D>();        
 
 
         #endregion
         #endregion
 
 
         #region API
         #region API
 
 
-        public Texture2D UseTexture(ArraySegment<Byte> data, string name = null)
+        public Texture2D UseTexture(Memory.MemoryImage image, string name = null)
         {
         {
             if (_Device == null) throw new InvalidOperationException();
             if (_Device == null) throw new InvalidOperationException();
 
 
-            if (data.Count == 0) return null;
+            if (!image.IsValid) return null;
 
 
-            if (_Textures.TryGetValue(data, out Texture2D tex)) return tex;
+            if (_Textures.TryGetValue(image, out Texture2D tex)) return tex;
 
 
-            using (var m = new System.IO.MemoryStream(data.Array, data.Offset, data.Count, false))
+            using (var m = image.Open())
             {
             {
                 tex = Texture2D.FromStream(_Device, m);
                 tex = Texture2D.FromStream(_Device, m);
                 _Disposables.AddDisposable(tex);
                 _Disposables.AddDisposable(tex);
 
 
                 tex.Name = name;
                 tex.Name = name;
 
 
-                _Textures[data] = tex;
+                _Textures[image] = tex;
 
 
                 return tex;
                 return tex;
             }
             }
@@ -86,30 +86,7 @@ namespace SharpGLTF.Runtime
             return UseTexture(new ArraySegment<byte>(toBytes), "_InternalSolidWhite");
             return UseTexture(new ArraySegment<byte>(toBytes), "_InternalSolidWhite");
         }
         }
 
 
-        #endregion
-
-        #region types
-
-        private class ArraySegmentContentComparer : IEqualityComparer<IReadOnlyList<Byte>>
-        {
-            public bool Equals(IReadOnlyList<byte> x, IReadOnlyList<byte> y)
-            {
-                return Enumerable.SequenceEqual(x, y);
-            }
-
-            public int GetHashCode(IReadOnlyList<byte> obj)
-            {
-                var h = 0;
-                for (int i = 0; i < obj.Count; ++i)
-                {
-                    h ^= obj[i].GetHashCode();
-                    h *= 17;
-                }
-                return h;
-            }
-        }
-
-        #endregion
+        #endregion        
     }
     }
 
 
     class MaterialFactory
     class MaterialFactory
@@ -327,7 +304,7 @@ namespace SharpGLTF.Runtime
             if (name == null) name = "null";
             if (name == null) name = "null";
             name += "-Diffuse";            
             name += "-Diffuse";            
 
 
-            return _TexFactory.UseTexture(diffuse.Value.Texture?.PrimaryImage?.GetImageContent() ?? default, name);
+            return _TexFactory.UseTexture(diffuse.Value.Texture?.PrimaryImage?.Content ?? default, name);
         }
         }
 
 
         #endregion
         #endregion

+ 21 - 7
src/SharpGLTF.Core/IO/ReadContext.cs

@@ -21,6 +21,8 @@ namespace SharpGLTF.IO
     /// <returns>The file contents as a <see cref="byte"/> array.</returns>
     /// <returns>The file contents as a <see cref="byte"/> array.</returns>
     public delegate BYTES FileReaderCallback(String assetName);
     public delegate BYTES FileReaderCallback(String assetName);
 
 
+    public delegate String UriResolver(String relativeUri);
+
     /// <summary>
     /// <summary>
     /// Context for reading a <see cref="SCHEMA2"/>.
     /// Context for reading a <see cref="SCHEMA2"/>.
     /// </summary>
     /// </summary>
@@ -38,25 +40,28 @@ namespace SharpGLTF.IO
         public static ReadContext CreateFromFile(string filePath)
         public static ReadContext CreateFromFile(string filePath)
         {
         {
             Guard.FilePathMustExist(filePath, nameof(filePath));
             Guard.FilePathMustExist(filePath, nameof(filePath));
-
             var dir = Path.GetDirectoryName(filePath);
             var dir = Path.GetDirectoryName(filePath);
-
             return CreateFromDirectory(dir);
             return CreateFromDirectory(dir);
         }
         }
 
 
         public static ReadContext CreateFromDirectory(string directoryPath)
         public static ReadContext CreateFromDirectory(string directoryPath)
         {
         {
-            BYTES _loadFile(string rawUri)
+            directoryPath = System.IO.Path.GetFullPath(directoryPath);
+
+            string _uriSolver(string rawUri)
             {
             {
                 var path = Uri.UnescapeDataString(rawUri);
                 var path = Uri.UnescapeDataString(rawUri);
-                path = Path.Combine(directoryPath, path);
+                return Path.Combine(directoryPath, path);
+            }
 
 
+            BYTES _loadFile(string rawUri)
+            {
+                var path = _uriSolver(rawUri);
                 var content = File.ReadAllBytes(path);
                 var content = File.ReadAllBytes(path);
-
                 return new BYTES(content);
                 return new BYTES(content);
             }
             }
 
 
-            return new ReadContext(_loadFile);
+            return new ReadContext(_loadFile, _uriSolver);
         }
         }
 
 
         public static ReadContext CreateFromDictionary(IReadOnlyDictionary<string, BYTES> dictionary)
         public static ReadContext CreateFromDictionary(IReadOnlyDictionary<string, BYTES> dictionary)
@@ -64,9 +69,10 @@ namespace SharpGLTF.IO
             return new ReadContext(rawUri => dictionary[rawUri]);
             return new ReadContext(rawUri => dictionary[rawUri]);
         }
         }
 
 
-        private ReadContext(FileReaderCallback reader)
+        private ReadContext(FileReaderCallback reader, UriResolver uriResolver = null)
         {
         {
             _FileReader = reader;
             _FileReader = reader;
+            _UriResolver = uriResolver;
         }
         }
 
 
         internal ReadContext(ReadContext other)
         internal ReadContext(ReadContext other)
@@ -81,6 +87,7 @@ namespace SharpGLTF.IO
         #region data
         #region data
 
 
         private FileReaderCallback _FileReader;
         private FileReaderCallback _FileReader;
+        private UriResolver _UriResolver;
 
 
         /// <summary>
         /// <summary>
         /// When loading a GLB, this represents the internal binary data chunk.
         /// When loading a GLB, this represents the internal binary data chunk.
@@ -91,6 +98,13 @@ namespace SharpGLTF.IO
 
 
         #region API - File System
         #region API - File System
 
 
+        public bool TryGetFullPath(string relativeUri, out string fullPath)
+        {
+            if (_UriResolver == null) { fullPath = null; return false; }
+            fullPath = _UriResolver(relativeUri);
+            return true;
+        }
+
         public BYTES ReadAllBytesToEnd(string fileName)
         public BYTES ReadAllBytesToEnd(string fileName)
         {
         {
             if (_BinaryChunk != null)
             if (_BinaryChunk != null)

+ 59 - 26
src/SharpGLTF.Core/Memory/MemoryImage.cs

@@ -10,24 +10,9 @@ namespace SharpGLTF.Memory
     /// <summary>
     /// <summary>
     /// Represents an image file stored as an in-memory byte array
     /// Represents an image file stored as an in-memory byte array
     /// </summary>
     /// </summary>
-    [System.Diagnostics.DebuggerDisplay("{_DebuggerDisplay(),nq}")]
+    [System.Diagnostics.DebuggerDisplay("{DisplayText,nq}")]
     public readonly struct MemoryImage : IEquatable<MemoryImage>
     public readonly struct MemoryImage : IEquatable<MemoryImage>
     {
     {
-        #region debug
-
-        private string _DebuggerDisplay()
-        {
-            if (IsEmpty) return "Empty";
-            if (!IsValid) return $"Unknown {_Image.Count}ᴮʸᵗᵉˢ";
-            if (IsJpg) return $"JPG {_Image.Count}ᴮʸᵗᵉˢ";
-            if (IsPng) return $"PNG {_Image.Count}ᴮʸᵗᵉˢ";
-            if (IsDds) return $"DDS {_Image.Count}ᴮʸᵗᵉˢ";
-            if (IsWebp) return $"WEBP {_Image.Count}ᴮʸᵗᵉˢ";
-            return "Undefined";
-        }
-
-        #endregion
-
         #region constants
         #region constants
 
 
         const string EMBEDDED_OCTET_STREAM = "data:application/octet-stream";
         const string EMBEDDED_OCTET_STREAM = "data:application/octet-stream";
@@ -77,6 +62,7 @@ namespace SharpGLTF.Memory
             Guard.IsTrue(_IsImage(image), nameof(image), GuardError_MustBeValidImage);
             Guard.IsTrue(_IsImage(image), nameof(image), GuardError_MustBeValidImage);
 
 
             _Image = image;
             _Image = image;
+            _SourcePathHint = null;
         }
         }
 
 
         public MemoryImage(Byte[] image)
         public MemoryImage(Byte[] image)
@@ -84,6 +70,7 @@ namespace SharpGLTF.Memory
             if (image != null) Guard.IsTrue(_IsImage(image), nameof(image), GuardError_MustBeValidImage);
             if (image != null) Guard.IsTrue(_IsImage(image), nameof(image), GuardError_MustBeValidImage);
 
 
             _Image = image == null ? default : new BYTES(image);
             _Image = image == null ? default : new BYTES(image);
+            _SourcePathHint = null;
         }
         }
 
 
         public MemoryImage(string filePath)
         public MemoryImage(string filePath)
@@ -91,23 +78,41 @@ namespace SharpGLTF.Memory
             if (string.IsNullOrEmpty(filePath))
             if (string.IsNullOrEmpty(filePath))
             {
             {
                 _Image = default;
                 _Image = default;
+                _SourcePathHint = null;
             }
             }
             else
             else
             {
             {
+                filePath = System.IO.Path.GetFullPath(filePath);
+
                 var data = System.IO.File.ReadAllBytes(filePath);
                 var data = System.IO.File.ReadAllBytes(filePath);
 
 
                 Guard.IsTrue(_IsImage(data), nameof(filePath), GuardError_MustBeValidImage);
                 Guard.IsTrue(_IsImage(data), nameof(filePath), GuardError_MustBeValidImage);
 
 
                 _Image = new BYTES(data);
                 _Image = new BYTES(data);
+                _SourcePathHint = filePath;
             }
             }
         }
         }
 
 
+        internal MemoryImage(BYTES image, string filePath)
+            : this(image)
+        {
+            _SourcePathHint = filePath;
+        }
+
+        internal MemoryImage(Byte[] image, string filePath)
+            : this(image)
+        {
+            _SourcePathHint = filePath;
+        }
+
         #endregion
         #endregion
 
 
         #region data
         #region data
 
 
         private readonly BYTES _Image;
         private readonly BYTES _Image;
 
 
+        private readonly String _SourcePathHint;
+
         public override int GetHashCode()
         public override int GetHashCode()
         {
         {
             // since this object stores the file of an image,
             // since this object stores the file of an image,
@@ -118,6 +123,12 @@ namespace SharpGLTF.Memory
 
 
         public static bool AreEqual(MemoryImage a, MemoryImage b)
         public static bool AreEqual(MemoryImage a, MemoryImage b)
         {
         {
+            // This compares the actual image contents, which means
+            // that _SourcePathHint must not be taken into account.
+            // For example, comparing two images from different paths
+            // that represent the same image byte by byte should
+            // return true.
+
             if (a.GetHashCode() != b.GetHashCode()) return false;
             if (a.GetHashCode() != b.GetHashCode()) return false;
             if (a._Image.Equals(b._Image)) return true;
             if (a._Image.Equals(b._Image)) return true;
             return a._Image.AsSpan().SequenceEqual(b._Image);
             return a._Image.AsSpan().SequenceEqual(b._Image);
@@ -137,6 +148,11 @@ namespace SharpGLTF.Memory
 
 
         public bool IsEmpty => _Image.Count == 0;
         public bool IsEmpty => _Image.Count == 0;
 
 
+        /// <summary>
+        /// Gets the source path of this image, or null if the image cannot be tracked to a file path (as it is the case of embedded images)
+        /// </summary>
+        public string SourcePath => _SourcePathHint;
+
         /// <summary>
         /// <summary>
         /// Gets a value indicating whether this object represents a valid PNG image.
         /// Gets a value indicating whether this object represents a valid PNG image.
         /// </summary>
         /// </summary>
@@ -194,6 +210,21 @@ namespace SharpGLTF.Memory
             }
             }
         }
         }
 
 
+        public string DisplayText
+        {
+            get {
+                if (!string.IsNullOrWhiteSpace(_SourcePathHint)) return System.IO.Path.GetFileName(_SourcePathHint);
+
+                if (IsEmpty) return "Empty";
+                if (!IsValid) return $"Unknown {_Image.Count}ᴮʸᵗᵉˢ";
+                if (IsJpg) return $"JPG {_Image.Count}ᴮʸᵗᵉˢ";
+                if (IsPng) return $"PNG {_Image.Count}ᴮʸᵗᵉˢ";
+                if (IsDds) return $"DDS {_Image.Count}ᴮʸᵗᵉˢ";
+                if (IsWebp) return $"WEBP {_Image.Count}ᴮʸᵗᵉˢ";
+                return "Undefined";
+            }
+        }
+
         #endregion
         #endregion
 
 
         #region API
         #region API
@@ -237,7 +268,7 @@ namespace SharpGLTF.Memory
         /// </summary>
         /// </summary>
         /// <param name="withPrefix">true to prefix the string with a header.</param>
         /// <param name="withPrefix">true to prefix the string with a header.</param>
         /// <returns>A mime64 string.</returns>
         /// <returns>A mime64 string.</returns>
-        public string ToMime64(bool withPrefix = true)
+        internal string ToMime64(bool withPrefix = true)
         {
         {
             if (!this.IsValid) return null;
             if (!this.IsValid) return null;
 
 
@@ -259,19 +290,21 @@ namespace SharpGLTF.Memory
         /// Tries to parse a Mime64 string to a Byte array.
         /// Tries to parse a Mime64 string to a Byte array.
         /// </summary>
         /// </summary>
         /// <param name="mime64content">The Mime64 string source.</param>
         /// <param name="mime64content">The Mime64 string source.</param>
-        /// <returns>A byte array representing an image file, or null if the image was not identified.</returns>
-        public static Byte[] TryParseBytes(string mime64content)
+        /// <param name="data">if decoding succeeds, it will contain the decoded data</param>
+        /// <returns>true if decoding succeeded.</returns>
+        internal static bool TryParseMime64(string mime64content, out Byte[] data)
         {
         {
-            if (mime64content == null) return null;
+            if (mime64content == null) { data = null; return false; }
 
 
-            var bytes = mime64content.TryParseBase64Unchecked(_EmbeddedHeaders);
+            data = mime64content.TryParseBase64Unchecked(_EmbeddedHeaders);
+            if (data == null) return false;
 
 
-            if (mime64content.StartsWith(EMBEDDED_PNG_BUFFER, StringComparison.Ordinal) && !_IsPngImage(bytes)) throw new ArgumentException("Invalid PNG Content", nameof(mime64content));
-            if (mime64content.StartsWith(EMBEDDED_JPEG_BUFFER, StringComparison.Ordinal) && !_IsJpgImage(bytes)) throw new ArgumentException("Invalid JPG Content", nameof(mime64content));
-            if (mime64content.StartsWith(EMBEDDED_DDS_BUFFER, StringComparison.Ordinal) && !_IsDdsImage(bytes)) throw new ArgumentException("Invalid DDS Content", nameof(mime64content));
-            if (mime64content.StartsWith(EMBEDDED_WEBP_BUFFER, StringComparison.Ordinal) && !_IsWebpImage(bytes)) throw new ArgumentException("Invalid WEBP Content", nameof(mime64content));
+            if (mime64content.StartsWith(EMBEDDED_PNG_BUFFER, StringComparison.Ordinal) && !_IsPngImage(data)) throw new ArgumentException("Invalid PNG Content", nameof(mime64content));
+            if (mime64content.StartsWith(EMBEDDED_JPEG_BUFFER, StringComparison.Ordinal) && !_IsJpgImage(data)) throw new ArgumentException("Invalid JPG Content", nameof(mime64content));
+            if (mime64content.StartsWith(EMBEDDED_DDS_BUFFER, StringComparison.Ordinal) && !_IsDdsImage(data)) throw new ArgumentException("Invalid DDS Content", nameof(mime64content));
+            if (mime64content.StartsWith(EMBEDDED_WEBP_BUFFER, StringComparison.Ordinal) && !_IsWebpImage(data)) throw new ArgumentException("Invalid WEBP Content", nameof(mime64content));
 
 
-            return bytes;
+            return true;
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 63 - 76
src/SharpGLTF.Core/Schema2/gltf.Images.cs

@@ -7,9 +7,18 @@ using BYTES = System.ArraySegment<byte>;
 
 
 namespace SharpGLTF.Schema2
 namespace SharpGLTF.Schema2
 {
 {
-    [System.Diagnostics.DebuggerDisplay("Image[{LogicalIndex}] {Name}")]
+    [System.Diagnostics.DebuggerDisplay("{_DebuggerDisplay(),nq}")]
     public sealed partial class Image
     public sealed partial class Image
     {
     {
+        #region debug
+
+        internal string _DebuggerDisplay()
+        {
+            return $"Image[{LogicalIndex}] {Name} = {Content.DisplayText}";
+        }
+
+        #endregion
+
         #region lifecycle
         #region lifecycle
 
 
         internal Image() { }
         internal Image() { }
@@ -28,7 +37,7 @@ namespace SharpGLTF.Schema2
         /// fields are briefly reassigned so the JSON can be serialized correctly.
         /// fields are briefly reassigned so the JSON can be serialized correctly.
         /// After serialization <see cref="Image._uri"/> and <see cref="Image._mimeType"/> fields are set back to null.
         /// After serialization <see cref="Image._uri"/> and <see cref="Image._mimeType"/> fields are set back to null.
         /// </remarks>
         /// </remarks>
-        private Byte[] _SatelliteImageContent;
+        private Memory.MemoryImage? _SatelliteContent;
 
 
         #endregion
         #endregion
 
 
@@ -40,44 +49,23 @@ namespace SharpGLTF.Schema2
         public int LogicalIndex => this.LogicalParent.LogicalImages.IndexOfReference(this);
         public int LogicalIndex => this.LogicalParent.LogicalImages.IndexOfReference(this);
 
 
         /// <summary>
         /// <summary>
-        /// Gets a value indicating whether the contained image is stored in a satellite file when loaded or saved.
-        /// </summary>
-        public bool IsSatelliteFile => _SatelliteImageContent != null;
-
-        /// <summary>
-        /// Gets the in-memory representation of the image file.
-        /// </summary>
-        public Memory.MemoryImage MemoryImage => new Memory.MemoryImage(GetImageContent());
-
-        /// <summary>
-        /// Gets a value indicating whether the contained image is a PNG image.
-        /// </summary>
-        [Obsolete("Use MemoryImage property")]
-        public bool IsPng => this.MemoryImage.IsPng;
-
-        /// <summary>
-        /// Gets a value indicating whether the contained image is a JPEG image.
-        /// </summary>
-        [Obsolete("Use MemoryImage property")]
-        public bool IsJpeg => this.MemoryImage.IsJpg;
-
-        /// <summary>
-        /// Gets a value indicating whether the contained image is a DDS image.
+        /// Gets or sets the in-memory representation of the image file.
         /// </summary>
         /// </summary>
-        [Obsolete("Use MemoryImage property")]
-        public bool IsDds => this.MemoryImage.IsDds;
-
-        /// <summary>
-        /// Gets a value indicating whether the contained image is a WEBP image.
-        /// </summary>
-        [Obsolete("Use MemoryImage property")]
-        public bool IsWebp => this.MemoryImage.IsWebp;
+        [Obsolete("Use Content property instead.")]
+        public Memory.MemoryImage MemoryImage
+        {
+            get => Content;
+            set => Content = value;
+        }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the filename extension of the image that can be retrieved with <see cref="GetImageContent"/>
+        /// Gets or sets the in-memory representation of the image file.
         /// </summary>
         /// </summary>
-        [Obsolete("Use MemoryImage property")]
-        public string FileExtension => this.MemoryImage.FileExtension;
+        public Memory.MemoryImage Content
+        {
+            get => GetSatelliteContent();
+            set => SetSatelliteContent(value);
+        }
 
 
         internal int _SourceBufferViewIndex => _bufferView.AsValue(-1);
         internal int _SourceBufferViewIndex => _bufferView.AsValue(-1);
 
 
@@ -86,8 +74,7 @@ namespace SharpGLTF.Schema2
             get
             get
             {
             {
                 if (_bufferView != null) return true;
                 if (_bufferView != null) return true;
-                if (_SatelliteImageContent != null) return true;
-                return false;
+                return _SatelliteContent?.IsValid ?? false;
             }
             }
         }
         }
 
 
@@ -99,10 +86,10 @@ namespace SharpGLTF.Schema2
         /// Retrieves the image file as a segment of bytes.
         /// Retrieves the image file as a segment of bytes.
         /// </summary>
         /// </summary>
         /// <returns>A <see cref="BYTES"/> segment containing the image file, which can be a PNG, JPG, DDS or WEBP format.</returns>
         /// <returns>A <see cref="BYTES"/> segment containing the image file, which can be a PNG, JPG, DDS or WEBP format.</returns>
-        public BYTES GetImageContent()
+        private Memory.MemoryImage GetSatelliteContent()
         {
         {
             // the image is stored locally in a temporary buffer
             // the image is stored locally in a temporary buffer
-            if (_SatelliteImageContent != null) return new BYTES(_SatelliteImageContent);
+            if (_SatelliteContent.HasValue) return _SatelliteContent.Value;
 
 
             // the image is stored in a BufferView
             // the image is stored in a BufferView
             if (this._bufferView.HasValue)
             if (this._bufferView.HasValue)
@@ -119,30 +106,17 @@ namespace SharpGLTF.Schema2
             throw new InvalidOperationException();
             throw new InvalidOperationException();
         }
         }
 
 
-        /// <summary>
-        /// Initializes this <see cref="Image"/> with an image loaded from a file.
-        /// </summary>
-        /// <param name="filePath">A valid path to an image file.</param>
-        public void SetSatelliteFile(string filePath)
-        {
-            var content = System.IO.File.ReadAllBytes(filePath);
-            SetSatelliteContent(content);
-        }
-
         /// <summary>
         /// <summary>
         /// Initializes this <see cref="Image"/> with an image stored in a <see cref="Byte"/> array.
         /// Initializes this <see cref="Image"/> with an image stored in a <see cref="Byte"/> array.
         /// </summary>
         /// </summary>
         /// <param name="content">A <see cref="Byte"/> array containing a PNG or JPEG image.</param>
         /// <param name="content">A <see cref="Byte"/> array containing a PNG or JPEG image.</param>
-        public void SetSatelliteContent(Byte[] content)
+        private void SetSatelliteContent(Memory.MemoryImage content)
         {
         {
-            Guard.NotNull(content, nameof(content));
-
-            var imimg = new Memory.MemoryImage(content);
-            if (!imimg.IsValid) throw new ArgumentException($"{nameof(content)} must be a PNG, JPG, DDS or WEBP image", nameof(content));
+            if (!content.IsValid) throw new ArgumentException($"{nameof(content)} must be a PNG, JPG, DDS or WEBP image", nameof(content));
 
 
             _DiscardContent();
             _DiscardContent();
 
 
-            this._SatelliteImageContent = content;
+            this._SatelliteContent = content.GetBuffer();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -151,16 +125,16 @@ namespace SharpGLTF.Schema2
         /// </summary>
         /// </summary>
         internal void TransferToInternalBuffer()
         internal void TransferToInternalBuffer()
         {
         {
-            if (this._SatelliteImageContent == null) return;
+            if (!this._SatelliteContent.HasValue) return;
 
 
             // transfer the external image content to a buffer.
             // transfer the external image content to a buffer.
             this._bufferView = this.LogicalParent
             this._bufferView = this.LogicalParent
-                .UseBufferView(this._SatelliteImageContent)
+                .UseBufferView(this._SatelliteContent.Value.GetBuffer())
                 .LogicalIndex;
                 .LogicalIndex;
 
 
             this._uri = null;
             this._uri = null;
             this._mimeType = null;
             this._mimeType = null;
-            this._SatelliteImageContent = null;
+            this._SatelliteContent = default;
         }
         }
 
 
         #endregion
         #endregion
@@ -169,18 +143,30 @@ namespace SharpGLTF.Schema2
 
 
         internal void _ResolveUri(IO.ReadContext context)
         internal void _ResolveUri(IO.ReadContext context)
         {
         {
+            // No uri to decode.
             if (String.IsNullOrWhiteSpace(_uri)) return;
             if (String.IsNullOrWhiteSpace(_uri)) return;
 
 
-            var data = Memory.MemoryImage.TryParseBytes(_uri);
-
-            if (data == null)
+            // Try decode Base64 embedded image.
+            if (Memory.MemoryImage.TryParseMime64(_uri, out byte[] data))
             {
             {
-                data = context
-                    .ReadAllBytesToEnd(_uri)
-                    .ToUnderlayingArray();
+                _SatelliteContent = data;
             }
             }
 
 
-            _SatelliteImageContent = data;
+            // Then it's a regular URI
+            else
+            {
+                // try resolve the full path
+                if (context.TryGetFullPath(_uri, out string fullPath))
+                {
+                    _SatelliteContent = fullPath;
+                }
+
+                // full path could not be resolved, use direct load instead.
+                else
+                {
+                    _SatelliteContent = context.ReadAllBytesToEnd(_uri);
+                }
+            }
 
 
             _uri = null;
             _uri = null;
             _mimeType = null;
             _mimeType = null;
@@ -191,7 +177,7 @@ namespace SharpGLTF.Schema2
             this._uri = null;
             this._uri = null;
             this._mimeType = null;
             this._mimeType = null;
             this._bufferView = null;
             this._bufferView = null;
-            this._SatelliteImageContent = null;
+            this._SatelliteContent = null;
         }
         }
 
 
         #endregion
         #endregion
@@ -203,9 +189,10 @@ namespace SharpGLTF.Schema2
         /// </summary>
         /// </summary>
         internal void _WriteToInternal()
         internal void _WriteToInternal()
         {
         {
-            if (_SatelliteImageContent == null) { _WriteAsBufferView(); return; }
+            if (!_SatelliteContent.HasValue) { _WriteAsBufferView(); return; }
 
 
-            var imimg = new Memory.MemoryImage(_SatelliteImageContent);
+            var imimg = _SatelliteContent.Value;
+            if (!imimg.IsValid) throw new InvalidOperationException();
 
 
             _uri = imimg.ToMime64();
             _uri = imimg.ToMime64();
             _mimeType = imimg.MimeType;
             _mimeType = imimg.MimeType;
@@ -218,13 +205,13 @@ namespace SharpGLTF.Schema2
         /// <param name="satelliteUri">A local satellite URI</param>
         /// <param name="satelliteUri">A local satellite URI</param>
         internal void _WriteToSatellite(IO.WriteContext writer, string satelliteUri)
         internal void _WriteToSatellite(IO.WriteContext writer, string satelliteUri)
         {
         {
-            if (_SatelliteImageContent == null)
+            if (!_SatelliteContent.HasValue)
             {
             {
                 _WriteAsBufferView();
                 _WriteAsBufferView();
                 return;
                 return;
             }
             }
 
 
-            var imimg = new Memory.MemoryImage(_SatelliteImageContent);
+            var imimg = _SatelliteContent.Value;
             if (!imimg.IsValid) throw new InvalidOperationException();
             if (!imimg.IsValid) throw new InvalidOperationException();
 
 
             satelliteUri = System.IO.Path.ChangeExtension(satelliteUri, imimg.FileExtension);
             satelliteUri = System.IO.Path.ChangeExtension(satelliteUri, imimg.FileExtension);
@@ -239,7 +226,7 @@ namespace SharpGLTF.Schema2
         {
         {
             Guard.IsTrue(_bufferView.HasValue, nameof(_bufferView));
             Guard.IsTrue(_bufferView.HasValue, nameof(_bufferView));
 
 
-            var imimg = this.MemoryImage;
+            var imimg = this.Content;
             if (!imimg.IsValid) throw new InvalidOperationException();
             if (!imimg.IsValid) throw new InvalidOperationException();
 
 
             _uri = null;
             _uri = null;
@@ -279,7 +266,7 @@ namespace SharpGLTF.Schema2
                 validate.IsTrue("BufferView", bv.IsDataBuffer, "is a GPU target.");
                 validate.IsTrue("BufferView", bv.IsDataBuffer, "is a GPU target.");
             }
             }
 
 
-            validate.IsTrue("MemoryImage", MemoryImage.IsValid, "Invalid image");
+            validate.IsTrue("MemoryImage", Content.IsValid, "Invalid image");
         }
         }
 
 
         #endregion
         #endregion
@@ -312,14 +299,14 @@ namespace SharpGLTF.Schema2
         {
         {
             Guard.IsTrue(imageContent.IsValid, nameof(imageContent), $"{nameof(imageContent)} must be a valid image byte stream.");
             Guard.IsTrue(imageContent.IsValid, nameof(imageContent), $"{nameof(imageContent)} must be a valid image byte stream.");
 
 
+            // If we find an image with the same content, let's reuse it.
             foreach (var img in this.LogicalImages)
             foreach (var img in this.LogicalImages)
             {
             {
-                var existingContent = img.GetImageContent();
-                if (Memory.MemoryImage.AreEqual(imageContent, existingContent)) return img;
+                if (img.Content.Equals(imageContent)) return img;
             }
             }
 
 
             var image = this.CreateImage();
             var image = this.CreateImage();
-            image.SetSatelliteContent(imageContent.GetBuffer().ToArray());
+            image.Content = imageContent;
             return image;
             return image;
         }
         }
 
 

+ 7 - 7
src/SharpGLTF.Core/Schema2/gltf.Textures.cs

@@ -87,7 +87,7 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(primaryImage, nameof(primaryImage));
             Guard.NotNull(primaryImage, nameof(primaryImage));
             Guard.MustShareLogicalParent(this, primaryImage, nameof(primaryImage));
             Guard.MustShareLogicalParent(this, primaryImage, nameof(primaryImage));
 
 
-            if (primaryImage.MemoryImage.IsDds || primaryImage.MemoryImage.IsWebp)
+            if (primaryImage.Content.IsDds || primaryImage.Content.IsWebp)
             {
             {
                 var fallback = LogicalParent.UseImage(Memory.MemoryImage.DefaultPngImage.Slice(0));
                 var fallback = LogicalParent.UseImage(Memory.MemoryImage.DefaultPngImage.Slice(0));
                 SetImages(primaryImage, fallback);
                 SetImages(primaryImage, fallback);
@@ -105,17 +105,17 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(fallbackImage, nameof(fallbackImage));
             Guard.NotNull(fallbackImage, nameof(fallbackImage));
             Guard.MustShareLogicalParent(this, primaryImage, nameof(primaryImage));
             Guard.MustShareLogicalParent(this, primaryImage, nameof(primaryImage));
             Guard.MustShareLogicalParent(this, fallbackImage, nameof(fallbackImage));
             Guard.MustShareLogicalParent(this, fallbackImage, nameof(fallbackImage));
-            Guard.IsTrue(primaryImage.MemoryImage.IsDds || primaryImage.MemoryImage.IsWebp, "Primary image must be DDS or WEBP");
-            Guard.IsTrue(fallbackImage.MemoryImage.IsJpg || fallbackImage.MemoryImage.IsPng, nameof(fallbackImage), "Fallback image must be PNG or JPEG");
+            Guard.IsTrue(primaryImage.Content.IsDds || primaryImage.Content.IsWebp, "Primary image must be DDS or WEBP");
+            Guard.IsTrue(fallbackImage.Content.IsJpg || fallbackImage.Content.IsPng, nameof(fallbackImage), "Fallback image must be PNG or JPEG");
 
 
             ClearImages();
             ClearImages();
 
 
-            if (primaryImage.MemoryImage.IsDds)
+            if (primaryImage.Content.IsDds)
             {
             {
                 _UseDDSTexture().Image = primaryImage;
                 _UseDDSTexture().Image = primaryImage;
             }
             }
 
 
-            if (primaryImage.MemoryImage.IsWebp)
+            if (primaryImage.Content.IsWebp)
             {
             {
                 _UseWEBPTexture().Image = primaryImage;
                 _UseWEBPTexture().Image = primaryImage;
             }
             }
@@ -181,7 +181,7 @@ namespace SharpGLTF.Schema2
                 if (value != null)
                 if (value != null)
                 {
                 {
                     Guard.MustShareLogicalParent(_Parent, value, nameof(value));
                     Guard.MustShareLogicalParent(_Parent, value, nameof(value));
-                    Guard.IsTrue(value.MemoryImage.IsDds, nameof(value));
+                    Guard.IsTrue(value.Content.IsDds, nameof(value));
                 }
                 }
 
 
                 _source = value?.LogicalIndex;
                 _source = value?.LogicalIndex;
@@ -206,7 +206,7 @@ namespace SharpGLTF.Schema2
                 if (value != null)
                 if (value != null)
                 {
                 {
                     Guard.MustShareLogicalParent(_Parent, value, nameof(value));
                     Guard.MustShareLogicalParent(_Parent, value, nameof(value));
-                    Guard.IsTrue(value.MemoryImage.IsWebp, nameof(value));
+                    Guard.IsTrue(value.Content.IsWebp, nameof(value));
                 }
                 }
 
 
                 _source = value?.LogicalIndex;
                 _source = value?.LogicalIndex;

+ 1 - 0
src/SharpGLTF.Core/Transforms/AffineTransform.cs

@@ -18,6 +18,7 @@ namespace SharpGLTF.Transforms
     /// represented by a <see cref="AffineTransform"/>.
     /// represented by a <see cref="AffineTransform"/>.
     /// </remarks>
     /// </remarks>
     /// <see href="https://github.com/vpenades/SharpGLTF/issues/41"/>
     /// <see href="https://github.com/vpenades/SharpGLTF/issues/41"/>
+    [System.Diagnostics.DebuggerDisplay("AffineTransform 𝐒:{Scale} 𝐑:{Rotation} 𝚻:{Translation}")]
     public struct AffineTransform
     public struct AffineTransform
     {
     {
         #region lifecycle
         #region lifecycle

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

@@ -31,7 +31,7 @@ namespace SharpGLTF.IO
         {
         {
             public Vector3 DiffuseColor;
             public Vector3 DiffuseColor;
             public Vector3 SpecularColor;
             public Vector3 SpecularColor;
-            public BYTES DiffuseTexture;
+            public Memory.MemoryImage DiffuseTexture;
         }
         }
 
 
         private readonly Geometry.MeshBuilder<Material, VGEOMETRY, VMATERIAL, VEMPTY> _Mesh = new Geometry.MeshBuilder<Material, VGEOMETRY, VMATERIAL, VEMPTY>();
         private readonly Geometry.MeshBuilder<Material, VGEOMETRY, VMATERIAL, VEMPTY> _Mesh = new Geometry.MeshBuilder<Material, VGEOMETRY, VMATERIAL, VEMPTY>();
@@ -71,46 +71,20 @@ namespace SharpGLTF.IO
             return files;
             return files;
         }
         }
 
 
-        // internal class to compare the content of two array segments
-        private class _ArraySegmentEqualityComparer<T> : IEqualityComparer<ArraySegment<T>>
-        {
-            public bool Equals(ArraySegment<T> x, ArraySegment<T> y)
-            {
-                return x.SequenceEqual(y);
-            }
-
-            public int GetHashCode(ArraySegment<T> obj)
-            {
-                int h = 0;
-
-                foreach (var item in obj)
-                {
-                    h ^= obj.GetHashCode();
-                    h *= 17;
-                }
-
-                return h;
-            }
-        }
-
         private static IReadOnlyDictionary<Material, string> _WriteMaterials(IDictionary<String, BYTES> files, string baseName, IEnumerable<Material> materials)
         private static IReadOnlyDictionary<Material, string> _WriteMaterials(IDictionary<String, BYTES> files, string baseName, IEnumerable<Material> materials)
         {
         {
             // write all image files
             // write all image files
             var images = materials
             var images = materials
                 .Select(item => item.DiffuseTexture)
                 .Select(item => item.DiffuseTexture)
-                .Where(item => item.Array != null)
-                .Distinct(new _ArraySegmentEqualityComparer<Byte>() );
+                .Where(item => item.IsValid)
+                .Distinct();
 
 
             bool firstImg = true;
             bool firstImg = true;
 
 
             foreach (var img in images)
             foreach (var img in images)
             {
             {
-                var imimg = new Memory.MemoryImage(img);
-
-                var imgName = firstImg ? baseName : $"{baseName}_{files.Count}.{imimg.FileExtension}";
-
-                files[imgName] = img;
-
+                var imgName = firstImg ? baseName : $"{baseName}_{files.Count}.{img.FileExtension}";
+                files[imgName] = img.GetBuffer();
                 firstImg = false;
                 firstImg = false;
             }
             }
 
 
@@ -130,9 +104,9 @@ namespace SharpGLTF.IO
                 sb.AppendLine(Invariant($"Kd {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}"));
                 sb.AppendLine(Invariant($"Ks {m.SpecularColor.X} {m.SpecularColor.Y} {m.SpecularColor.Z}"));
 
 
-                if (m.DiffuseTexture.Array != null)
+                if (m.DiffuseTexture.IsValid)
                 {
                 {
-                    var imgName = files.FirstOrDefault(kvp => kvp.Value.SequenceEqual(m.DiffuseTexture) ).Key;
+                    var imgName = files.FirstOrDefault(kvp => new Memory.MemoryImage(kvp.Value) == m.DiffuseTexture ).Key;
                     sb.AppendLine($"map_Kd {imgName}");
                     sb.AppendLine($"map_Kd {imgName}");
                 }
                 }
 
 
@@ -236,21 +210,7 @@ namespace SharpGLTF.IO
         {
         {
             foreach (var triangle in Schema2Toolkit.EvaluateTriangles<VGEOMETRY, VMATERIAL>(model.DefaultScene))
             foreach (var triangle in Schema2Toolkit.EvaluateTriangles<VGEOMETRY, VMATERIAL>(model.DefaultScene))
             {
             {
-                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;
-                }
-
+                var dstMaterial = GetMaterialFromTriangle(triangle.Material);
                 this.AddTriangle(dstMaterial, triangle.A, triangle.B, triangle.C);
                 this.AddTriangle(dstMaterial, triangle.A, triangle.B, triangle.C);
             }
             }
         }
         }
@@ -259,23 +219,27 @@ namespace SharpGLTF.IO
         {
         {
             foreach (var triangle in Schema2Toolkit.EvaluateTriangles<VGEOMETRY, VMATERIAL>(model.DefaultScene, animation, time))
             foreach (var triangle in Schema2Toolkit.EvaluateTriangles<VGEOMETRY, VMATERIAL>(model.DefaultScene, animation, time))
             {
             {
-                var dstMaterial = default(Material);
+                var dstMaterial = GetMaterialFromTriangle(triangle.Material);
+                this.AddTriangle(dstMaterial, triangle.A, triangle.B, triangle.C);
+            }
+        }
 
 
-                var srcMaterial = triangle.Item4;
-                if (srcMaterial != null)
-                {
-                    // https://stackoverflow.com/questions/36510170/how-to-calculate-specular-contribution-in-pbr
+        private static Material GetMaterialFromTriangle(Schema2.Material srcMaterial)
+        {
+            if (srcMaterial == null) return default;
 
 
-                    var diffuse = srcMaterial.GetDiffuseColor(Vector4.One);
+            // https://stackoverflow.com/questions/36510170/how-to-calculate-specular-contribution-in-pbr
 
 
-                    dstMaterial.DiffuseColor = new Vector3(diffuse.X, diffuse.Y, diffuse.Z);
-                    dstMaterial.SpecularColor = new Vector3(0.2f);
+            var diffuse = srcMaterial.GetDiffuseColor(Vector4.One);
 
 
-                    dstMaterial.DiffuseTexture = srcMaterial.GetDiffuseTexture()?.PrimaryImage?.GetImageContent() ?? default;
-                }
+            var dstMaterial = default(Material);
 
 
-                this.AddTriangle(dstMaterial, triangle.A, triangle.B, triangle.C);
-            }
+            dstMaterial.DiffuseColor = new Vector3(diffuse.X, diffuse.Y, diffuse.Z);
+            dstMaterial.SpecularColor = new Vector3(0.2f);
+
+            dstMaterial.DiffuseTexture = srcMaterial.GetDiffuseTexture()?.PrimaryImage?.Content ?? default;
+
+            return dstMaterial;
         }
         }
 
 
         #endregion
         #endregion

+ 41 - 4
src/SharpGLTF.Toolkit/Materials/ChannelBuilder.cs

@@ -14,10 +14,47 @@ namespace SharpGLTF.Materials
         private string _GetDebuggerDisplay()
         private string _GetDebuggerDisplay()
         {
         {
             var txt = Key.ToString();
             var txt = Key.ToString();
-            if (Parameter != _GetDefaultParameter(_Key)) txt += $" {Parameter}";
+
+            var hasParam = false;
+
+            if (Parameter != _GetDefaultParameter(_Key))
+            {
+                hasParam = true;
+
+                var rgb = $"𝐑 {Parameter.X} 𝐆 {Parameter.Y} 𝐁 {Parameter.Z}";
+                var rgba = $"{rgb} 𝐀 {Parameter.W}";
+
+                switch (Key)
+                {
+                    case KnownChannel.Normal:
+                    case KnownChannel.ClearCoatNormal:
+                    case KnownChannel.Occlusion:
+                        txt += $" {Parameter.X}"; break;
+
+                    case KnownChannel.Emissive:
+                        txt += $" ({rgb})"; break;
+
+                    case KnownChannel.Diffuse:
+                    case KnownChannel.BaseColor:
+                        txt += $" ({rgba})"; break;
+
+                    case KnownChannel.MetallicRoughness:
+                        txt += $" 𝐌 {Parameter.X} 𝐑 {Parameter.Y}"; break;
+
+                    case KnownChannel.SpecularGlossiness:
+                        txt += $" 𝐒 ({rgb}) 𝐆 {Parameter.Y}"; break;
+
+                    default:
+                        txt += $" {Parameter}"; break;
+                }
+            }
 
 
             var tex = GetValidTexture();
             var tex = GetValidTexture();
-            if (tex != null) txt += $" 🖼{tex.PrimaryImage.FileExtension}";
+            if (tex != null)
+            {
+                if (hasParam) txt += " ×";
+                txt += $" {tex.PrimaryImage.DisplayText}";
+            }
 
 
             return txt;
             return txt;
         }
         }
@@ -169,12 +206,12 @@ namespace SharpGLTF.Materials
 
 
             public bool Equals(ChannelBuilder x, ChannelBuilder y)
             public bool Equals(ChannelBuilder x, ChannelBuilder y)
             {
             {
-                return ChannelBuilder.AreEqualByContent(x, y);
+                return AreEqualByContent(x, y);
             }
             }
 
 
             public int GetHashCode(ChannelBuilder obj)
             public int GetHashCode(ChannelBuilder obj)
             {
             {
-                return ChannelBuilder.GetContentHashCode(obj);
+                return GetContentHashCode(obj);
             }
             }
         }
         }
 
 

+ 20 - 1
src/SharpGLTF.Toolkit/Materials/MaterialBuilder.cs

@@ -9,9 +9,27 @@ using IMAGEFILE = SharpGLTF.Memory.MemoryImage;
 
 
 namespace SharpGLTF.Materials
 namespace SharpGLTF.Materials
 {
 {
-    [System.Diagnostics.DebuggerDisplay("{Name} {ShaderStyle}")]
+    [System.Diagnostics.DebuggerDisplay("{_DebuggerDisplay(),nq}")]
     public class MaterialBuilder
     public class MaterialBuilder
     {
     {
+        #region debug
+
+        internal string _DebuggerDisplay()
+        {
+            var txt = "MatBuilder ";
+            if (!string.IsNullOrWhiteSpace(Name)) txt += $" \"{Name}\"";
+
+            txt += $" {_ShaderStyle}";
+
+            if (AlphaMode == AlphaMode.BLEND) txt += " AlphaBlend";
+            if (AlphaMode == AlphaMode.MASK) txt += $" AlphaMask({AlphaCutoff})";
+            if (DoubleSided) txt += " DoubleSided";
+
+            return txt;
+        }
+
+        #endregion
+
         #region constants
         #region constants
 
 
         public const string SHADERUNLIT = "Unlit";
         public const string SHADERUNLIT = "Unlit";
@@ -179,6 +197,7 @@ namespace SharpGLTF.Materials
 
 
         #region properties
         #region properties
 
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         public IReadOnlyCollection<ChannelBuilder> Channels => _Channels;
         public IReadOnlyCollection<ChannelBuilder> Channels => _Channels;
 
 
         public MaterialBuilder CompatibilityFallback
         public MaterialBuilder CompatibilityFallback

+ 31 - 4
src/SharpGLTF.Toolkit/Materials/TextureBuilder.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Numerics;
 using System.Numerics;
@@ -11,9 +12,30 @@ using TEXWRAP = SharpGLTF.Schema2.TextureWrapMode;
 
 
 namespace SharpGLTF.Materials
 namespace SharpGLTF.Materials
 {
 {
-    [System.Diagnostics.DebuggerDisplay("Texture {CoordinateSet} Min:{MinFilter} Mag:{MagFilter} Ws:{WrapS} Wt:{WrapT}")]
+    [System.Diagnostics.DebuggerDisplay("{_DebuggerDisplay(),nq}")]
     public class TextureBuilder
     public class TextureBuilder
     {
     {
+        #region Debug
+
+        internal string _DebuggerDisplay()
+        {
+            var txt = "Texture ";
+            if (CoordinateSet != 0) txt += $" {CoordinateSet}ˢᵉᵗ";
+
+            if (MinFilter != TEXMIPMAP.DEFAULT) txt += $" {MinFilter}ᴹⁱⁿ";
+            if (MagFilter != TEXLERP.DEFAULT) txt += $" {MagFilter}ᴹᵃᵍ";
+
+            if (WrapS != TEXWRAP.REPEAT) txt += $" {WrapS}↔";
+            if (WrapT != TEXWRAP.REPEAT) txt += $" {WrapT}↕";
+
+            if (_PrimaryImageContent.IsValid) txt += $" {_PrimaryImageContent.DisplayText}";
+            if (_FallbackImageContent.IsValid) txt += $" => {_FallbackImageContent.DisplayText}";
+
+            return txt;
+        }
+
+        #endregion
+
         #region lifecycle
         #region lifecycle
 
 
         internal TextureBuilder(ChannelBuilder parent)
         internal TextureBuilder(ChannelBuilder parent)
@@ -27,11 +49,18 @@ namespace SharpGLTF.Materials
 
 
         #region data
         #region data
 
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly ChannelBuilder _Parent;
         private readonly ChannelBuilder _Parent;
 
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private IMAGEFILE _PrimaryImageContent;
         private IMAGEFILE _PrimaryImageContent;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private IMAGEFILE _FallbackImageContent;
         private IMAGEFILE _FallbackImageContent;
 
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private TextureTransformBuilder _Transform;
+
         public int CoordinateSet { get; set; } = 0;
         public int CoordinateSet { get; set; } = 0;
 
 
         public TEXMIPMAP MinFilter { get; set; } = TEXMIPMAP.DEFAULT;
         public TEXMIPMAP MinFilter { get; set; } = TEXMIPMAP.DEFAULT;
@@ -42,8 +71,6 @@ namespace SharpGLTF.Materials
 
 
         public TEXWRAP WrapT { get; set; } = TEXWRAP.REPEAT;
         public TEXWRAP WrapT { get; set; } = TEXWRAP.REPEAT;
 
 
-        private TextureTransformBuilder _Transform;
-
         public static bool AreEqualByContent(TextureBuilder a, TextureBuilder b)
         public static bool AreEqualByContent(TextureBuilder a, TextureBuilder b)
         {
         {
             #pragma warning disable IDE0041 // Use 'is null' check
             #pragma warning disable IDE0041 // Use 'is null' check
@@ -210,7 +237,7 @@ namespace SharpGLTF.Materials
         #endregion
         #endregion
     }
     }
 
 
-    [System.Diagnostics.DebuggerDisplay("Transform {Scale} {Rotation} {Offset}")]
+    [System.Diagnostics.DebuggerDisplay("Transform 𝐒:{Scale} 𝐑:{Rotation} 𝚻:{Offset}")]
     public class TextureTransformBuilder
     public class TextureTransformBuilder
     {
     {
         #region lifecycle
         #region lifecycle

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

@@ -320,8 +320,8 @@ namespace SharpGLTF.Schema2
                 dstChannel.Texture.WithTransform(srcXform.Offset, srcXform.Scale, srcXform.Rotation, srcXform.TextureCoordinateOverride);
                 dstChannel.Texture.WithTransform(srcXform.Offset, srcXform.Scale, srcXform.Rotation, srcXform.TextureCoordinateOverride);
             }
             }
 
 
-            dstChannel.Texture.PrimaryImage = srcChannel.Texture.PrimaryImage?.MemoryImage ?? Memory.MemoryImage.Empty;
-            dstChannel.Texture.FallbackImage = srcChannel.Texture.FallbackImage?.MemoryImage ?? Memory.MemoryImage.Empty;
+            dstChannel.Texture.PrimaryImage = srcChannel.Texture.PrimaryImage?.Content ?? Memory.MemoryImage.Empty;
+            dstChannel.Texture.FallbackImage = srcChannel.Texture.FallbackImage?.Content ?? Memory.MemoryImage.Empty;
         }
         }
 
 
         public static void CopyTo(this MaterialBuilder srcMaterial, Material dstMaterial)
         public static void CopyTo(this MaterialBuilder srcMaterial, Material dstMaterial)

+ 31 - 0
tests/SharpGLTF.Tests/Memory/MemoryImageTests.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+using NUnit.Framework;
+
+namespace SharpGLTF.Memory
+{
+    [Category("Core Memory")]
+    public class MemoryImageTests
+    {
+        [Test]
+        public void TestImageEquality()
+        {
+            // two images that are equal byte by byte, loaded from different sources
+            // must be considered equal.
+
+            var image1 = new MemoryImage(MemoryImage.DefaultPngImage, "first_reference.png");
+            var image2 = new MemoryImage(MemoryImage.DefaultPngImage, "second_reference.png");
+            var image3 = MemoryImage.Empty;
+
+            Assert.AreEqual(image1.GetHashCode(), image2.GetHashCode());
+            Assert.AreEqual(image1, image2);
+            Assert.IsTrue(MemoryImage.AreEqual(image1, image2));
+
+            Assert.AreNotEqual(image1.GetHashCode(), image3.GetHashCode());
+            Assert.AreNotEqual(image1, image3);
+            Assert.IsFalse(MemoryImage.AreEqual(image1, image3));
+        }
+    }
+}

+ 1 - 1
tests/SharpGLTF.Tests/Schema2/Authoring/ExtensionsCreationTests.cs

@@ -52,7 +52,7 @@ namespace SharpGLTF.Schema2.Authoring
             var basePath = System.IO.Path.Combine(TestFiles.RootDirectory, "glTF-Sample-Models", "2.0", "SpecGlossVsMetalRough", "glTF");
             var basePath = System.IO.Path.Combine(TestFiles.RootDirectory, "glTF-Sample-Models", "2.0", "SpecGlossVsMetalRough", "glTF");
 
 
             // first, create a default material
             // first, create a default material
-            var material = new Materials.MaterialBuilder("material1 fallback")
+            var material = new Materials.MaterialBuilder("material1")
                 .WithMetallicRoughnessShader()
                 .WithMetallicRoughnessShader()
                 .WithChannelImage(Materials.KnownChannel.Normal, System.IO.Path.Combine(basePath, "WaterBottle_normal.png"))
                 .WithChannelImage(Materials.KnownChannel.Normal, System.IO.Path.Combine(basePath, "WaterBottle_normal.png"))
                 .WithChannelImage(Materials.KnownChannel.Emissive, System.IO.Path.Combine(basePath, "WaterBottle_emissive.png"))
                 .WithChannelImage(Materials.KnownChannel.Emissive, System.IO.Path.Combine(basePath, "WaterBottle_emissive.png"))

+ 96 - 34
tests/SharpGLTF.Toolkit.Tests/Materials/MaterialBuilderTests.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Numerics;
 using System.Numerics;
 using System.Text;
 using System.Text;
@@ -13,14 +14,55 @@ namespace SharpGLTF.Materials
     [Category("Toolkit.Materials")]
     [Category("Toolkit.Materials")]
     public class MaterialBuilderTests
     public class MaterialBuilderTests
     {
     {
+        [Test]
+        public void TestMaterialEquality()
+        {
+            // Checking if two materials are the same or not is conceptually ambiguous.
+            // The static method AreEqualByContent allows to check if two materials represent
+            // the same physical material, even if they're two different references.
+            // ... And we could use it for general equality checks, but then, since
+            // MaterialBuilder is NOT inmutable, it can mean that two materials can be equal
+            // at a given time, and non equal at another. Furthermore, it would imply having
+            // a hash code that changes over time. As a consequence, it could be impossible
+            // to use MaterialBuilder as a dictionary Key.
+            
+            var assetsPath = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Assets");
+            var tex1 = System.IO.Path.Combine(assetsPath, "shannon.png");
+
+            var srcMaterial = new MaterialBuilder()
+                .WithDoubleSide(true) // notice that DoubleSide enables double face rendering. This is an example, but it's usually NOT NECCESARY.
+                .WithAlpha(AlphaMode.MASK, 0.7f)
+                .WithUnlitShader()
+                .WithBaseColor(tex1, new Vector4(0.7f, 0, 0f, 0.8f));
+
+            var clnMaterial = srcMaterial.Clone();
+
+            // srcMaterial and clnMaterial are two different objects, so plain equality checks must apply to reference checks
+            Assert.IsFalse(srcMaterial == clnMaterial);
+            Assert.AreNotEqual(srcMaterial, clnMaterial);
+            Assert.AreNotEqual(srcMaterial.GetHashCode(), clnMaterial.GetHashCode());
+
+            // checking the materials represent the same "material" must be made with AreEqualByContent method.
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, clnMaterial));
+
+            var bag = new HashSet<MaterialBuilder>();
+            bag.Add(srcMaterial);
+            bag.Add(clnMaterial);
+
+            Assert.AreEqual(2, bag.Count);
+        }
+
         [Test]
         [Test]
         public void CreateUnlit()
         public void CreateUnlit()
         {
         {
+            var assetsPath = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Assets");
+            var tex1 = System.IO.Path.Combine(assetsPath, "shannon.png");
+
             var srcMaterial = new MaterialBuilder()
             var srcMaterial = new MaterialBuilder()
                 .WithDoubleSide(true) // notice that DoubleSide enables double face rendering. This is an example, but it's usually NOT NECCESARY.
                 .WithDoubleSide(true) // notice that DoubleSide enables double face rendering. This is an example, but it's usually NOT NECCESARY.
                 .WithAlpha(AlphaMode.MASK, 0.7f)
                 .WithAlpha(AlphaMode.MASK, 0.7f)
                 .WithUnlitShader()
                 .WithUnlitShader()
-                .WithBaseColor(Memory.MemoryImage.DefaultPngImage, new Vector4(0.7f, 0, 0f, 0.8f));
+                .WithBaseColor(tex1, new Vector4(0.7f, 0, 0f, 0.8f));
 
 
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, Schema2Roundtrip(srcMaterial)));
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, Schema2Roundtrip(srcMaterial)));
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, srcMaterial.Clone()));
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, srcMaterial.Clone()));
@@ -29,18 +71,20 @@ namespace SharpGLTF.Materials
         [Test]
         [Test]
         public void CreateMetallicRoughness()
         public void CreateMetallicRoughness()
         {
         {
-            var srcMaterial = new MaterialBuilder()
-                .WithDoubleSide(true) // notice that DoubleSide enables double face rendering. This is an example, but it's usually NOT NECCESARY.
+            var assetsPath = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Assets");
+            var tex1 = System.IO.Path.Combine(assetsPath, "shannon.png");
+
+            var srcMaterial = new MaterialBuilder()                
                 .WithAlpha(AlphaMode.MASK, 0.6f)
                 .WithAlpha(AlphaMode.MASK, 0.6f)
-                .WithEmissive(Memory.MemoryImage.DefaultPngImage, new Vector3(0.2f, 0.3f, 0.1f))
-                .WithNormal(Memory.MemoryImage.DefaultPngImage, 0.3f)
-                .WithOcclusion(Memory.MemoryImage.DefaultPngImage, 0.4f)
+                .WithEmissive(tex1, new Vector3(0.2f, 0.3f, 0.1f))
+                .WithNormal(tex1, 0.3f)
+                .WithOcclusion(tex1, 0.4f)
 
 
                 .WithMetallicRoughnessShader()
                 .WithMetallicRoughnessShader()
-                    .WithBaseColor(Memory.MemoryImage.DefaultPngImage, new Vector4(0.7f, 0, 0f, 0.8f))
-                    .WithMetallicRoughness(Memory.MemoryImage.DefaultPngImage, 0.2f, 0.4f);
+                    .WithBaseColor(tex1, new Vector4(0.7f, 0, 0f, 0.8f))
+                    .WithMetallicRoughness(tex1, 0.2f, 0.4f);
 
 
-            // example of setting additional additional parameters for a given channel.
+            // example of setting additional parameters for a given channel.
             srcMaterial.GetChannel(KnownChannel.BaseColor)
             srcMaterial.GetChannel(KnownChannel.BaseColor)
                 .Texture
                 .Texture
                 .WithCoordinateSet(1)
                 .WithCoordinateSet(1)
@@ -55,20 +99,22 @@ namespace SharpGLTF.Materials
         [Test]
         [Test]
         public void CreateClearCoat()
         public void CreateClearCoat()
         {
         {
-            var srcMaterial = new MaterialBuilder()
-                .WithDoubleSide(true) // notice that DoubleSide enables double face rendering. This is an example, but it's usually NOT NECCESARY.
+            var assetsPath = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Assets");
+            var tex1 = System.IO.Path.Combine(assetsPath, "shannon.png");
+
+            var srcMaterial = new MaterialBuilder()                
                 .WithAlpha(AlphaMode.MASK, 0.6f)
                 .WithAlpha(AlphaMode.MASK, 0.6f)
-                .WithEmissive(Memory.MemoryImage.DefaultPngImage, new Vector3(0.2f, 0.3f, 0.1f))
-                .WithNormal(Memory.MemoryImage.DefaultPngImage, 0.3f)
-                .WithOcclusion(Memory.MemoryImage.DefaultPngImage, 0.4f)
+                .WithEmissive(tex1, new Vector3(0.2f, 0.3f, 0.1f))
+                .WithNormal(tex1, 0.3f)
+                .WithOcclusion(tex1, 0.4f)
 
 
                 .WithMetallicRoughnessShader()
                 .WithMetallicRoughnessShader()
-                    .WithBaseColor(Memory.MemoryImage.DefaultPngImage, new Vector4(0.7f, 0, 0f, 0.8f))
-                    .WithMetallicRoughness(Memory.MemoryImage.DefaultPngImage, 0.2f, 0.4f)
+                    .WithBaseColor(tex1, new Vector4(0.7f, 0, 0f, 0.8f))
+                    .WithMetallicRoughness(tex1, 0.2f, 0.4f)
 
 
-                .WithClearCoat(Memory.MemoryImage.DefaultPngImage, 1)
-                .WithClearCoatNormal(Memory.MemoryImage.DefaultPngImage)
-                .WithClearCoatRoughness(Memory.MemoryImage.DefaultPngImage, 1);
+                .WithClearCoat(tex1, 1)
+                .WithClearCoatNormal(tex1)
+                .WithClearCoatRoughness(tex1, 1);
 
 
 
 
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, Schema2Roundtrip(srcMaterial)));
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, Schema2Roundtrip(srcMaterial)));
@@ -78,16 +124,18 @@ namespace SharpGLTF.Materials
         [Test]
         [Test]
         public void CreateSpecularGlossiness()
         public void CreateSpecularGlossiness()
         {
         {
-            var srcMaterial = new MaterialBuilder()
-                .WithDoubleSide(true) // notice that DoubleSide enables double face rendering. This is an example, but it's usually NOT NECCESARY.
+            var assetsPath = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Assets");
+            var tex1 = System.IO.Path.Combine(assetsPath, "shannon.png");
+
+            var srcMaterial = new MaterialBuilder()                
                 .WithAlpha(AlphaMode.MASK, 0.6f)
                 .WithAlpha(AlphaMode.MASK, 0.6f)
-                .WithEmissive(Memory.MemoryImage.DefaultPngImage, new Vector3(0.2f, 0.3f, 0.1f))
-                .WithNormal(Memory.MemoryImage.DefaultPngImage, 0.3f)
-                .WithOcclusion(Memory.MemoryImage.DefaultPngImage, 0.4f)
+                .WithEmissive(tex1, new Vector3(0.2f, 0.3f, 0.1f))
+                .WithNormal(tex1, 0.3f)
+                .WithOcclusion(tex1, 0.4f)
 
 
                 .WithSpecularGlossinessShader()
                 .WithSpecularGlossinessShader()
-                    .WithDiffuse(Memory.MemoryImage.DefaultPngImage, new Vector4(0.7f, 0, 0f, 0.8f))
-                    .WithSpecularGlossiness(Memory.MemoryImage.DefaultPngImage, new Vector3(0.7f, 0, 0f), 0.8f);
+                    .WithDiffuse(tex1, new Vector4(0.7f, 0, 0f, 0.8f))
+                    .WithSpecularGlossiness(tex1, new Vector3(0.7f, 0, 0f), 0.8f);
                 
                 
             
             
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, Schema2Roundtrip(srcMaterial)));
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, Schema2Roundtrip(srcMaterial)));
@@ -97,22 +145,36 @@ namespace SharpGLTF.Materials
         [Test]
         [Test]
         public void CreateSpecularGlossinessWithFallback()
         public void CreateSpecularGlossinessWithFallback()
         {
         {
+            var assetsPath = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Assets");
+            var tex1 = System.IO.Path.Combine(assetsPath, "shannon.webp");
+            var tex2 = System.IO.Path.Combine(assetsPath, "shannon.png");
+
             var primary = new MaterialBuilder("primary")
             var primary = new MaterialBuilder("primary")
 
 
                 // fallback and primary material must have exactly the same properties
                 // fallback and primary material must have exactly the same properties
                 .WithDoubleSide(true)
                 .WithDoubleSide(true)
                 .WithAlpha(AlphaMode.MASK, 0.75f)
                 .WithAlpha(AlphaMode.MASK, 0.75f)
-                .WithEmissive(Memory.MemoryImage.DefaultPngImage, new Vector3(0.2f, 0.3f, 0.1f))
-                .WithNormal(Memory.MemoryImage.DefaultPngImage, 0.3f)
-                .WithOcclusion(Memory.MemoryImage.DefaultPngImage, 0.4f)
+                .WithEmissive(tex1, new Vector3(0.2f, 0.3f, 0.1f))
+                .WithNormal(tex1, 0.3f)
+                .WithOcclusion(tex1, 0.4f)
 
 
                 // primary must use Specular Glossiness shader.
                 // primary must use Specular Glossiness shader.
                 .WithSpecularGlossinessShader()
                 .WithSpecularGlossinessShader()
-                    .WithDiffuse(Memory.MemoryImage.DefaultPngImage, new Vector4(0.7f, 0, 0f, 1.0f))
-                    .WithSpecularGlossiness(Memory.MemoryImage.DefaultPngImage, new Vector3(0.7f, 0, 0f), 0.8f)                
-
-                .WithMetallicRoughnessFallback(Memory.MemoryImage.DefaultPngImage, new Vector4(0.7f, 0, 0, 1), String.Empty, 0.6f, 0.7f);
-
+                    .WithDiffuse(tex1, new Vector4(0.7f, 0, 0f, 1.0f))
+                    .WithSpecularGlossiness(tex1, new Vector3(0.7f, 0, 0f), 0.8f);
+            
+            // set fallback textures for engines that don't support WEBP texture format
+            primary.GetChannel(KnownChannel.Normal).Texture.FallbackImage = tex2;
+            primary.GetChannel(KnownChannel.Emissive).Texture.FallbackImage = tex2;
+            primary.GetChannel(KnownChannel.Occlusion).Texture.FallbackImage = tex2;
+            primary.GetChannel(KnownChannel.Diffuse).Texture.FallbackImage = tex2;
+            primary.GetChannel(KnownChannel.SpecularGlossiness).Texture.FallbackImage = tex2;
+
+            // set fallback material for engines that don't support Specular Glossiness shader.
+            primary.WithMetallicRoughnessFallback(tex1, new Vector4(0.7f, 0, 0, 1), String.Empty, 0.6f, 0.7f);
+            primary.CompatibilityFallback.GetChannel(KnownChannel.BaseColor).Texture.FallbackImage = tex2;
+
+            // check
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(primary, Schema2Roundtrip(primary)));
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(primary, Schema2Roundtrip(primary)));
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(primary, primary.Clone()));
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(primary, primary.Clone()));
         }
         }