Pārlūkot izejas kodu

Fixed some bugs and improved the API of MaterialsBuilder.
Removed a few obsolete methods. (some API breaking changes might pop up).

Vicente Penades 5 gadi atpakaļ
vecāks
revīzija
c5c567fc9f

+ 1 - 1
build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj

@@ -8,7 +8,7 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="LibGit2Sharp" Version="0.26.2" />    
     <PackageReference Include="LibGit2Sharp" Version="0.26.2" />    
-    <PackageReference Include="NJsonSchema.CodeGeneration.CSharp" Version="10.1.16" />
+    <PackageReference Include="NJsonSchema.CodeGeneration.CSharp" Version="10.1.18" />
   </ItemGroup>
   </ItemGroup>
 
 
 </Project>
 </Project>

+ 5 - 0
src/SharpGLTF.Core/IO/ReadContext.cs

@@ -288,6 +288,11 @@ namespace SharpGLTF.IO
                 vcontext.SetError(new Validation.ModelException(null, fex));
                 vcontext.SetError(new Validation.ModelException(null, fex));
                 return (null, vcontext);
                 return (null, vcontext);
             }
             }
+            catch (ArgumentException aex)
+            {
+                vcontext.SetError(new Validation.ModelException(root, aex));
+                return (null, vcontext);
+            }
             catch (Validation.ModelException mex)
             catch (Validation.ModelException mex)
             {
             {
                 vcontext.SetError(mex);
                 vcontext.SetError(mex);

+ 1 - 1
src/SharpGLTF.Core/Memory/MemoryAccessor.Validation.cs

@@ -267,7 +267,7 @@ namespace SharpGLTF.Memory
                     var axisMin = minimum[j];
                     var axisMin = minimum[j];
                     var axisMax = maximum[j];
                     var axisMax = maximum[j];
 
 
-                    if (v < axisMin || v > axisMax) throw new ArgumentException($"Value[{i}] is out of bounds. {axisMin} <= {v} <= {axisMax}", nameof(memory));
+                    if (v < axisMin || v > axisMax) throw new ArgumentOutOfRangeException(nameof(memory), $"Value[{i}] is out of bounds. {axisMin} <= {v} <= {axisMax}");
 
 
                     // if (v < min || v > max) result.AddError(this, $"Item[{j}][{i}] is out of bounds. {min} <= {v} <= {max}");
                     // if (v < min || v > max) result.AddError(this, $"Item[{j}][{i}] is out of bounds. {min} <= {v} <= {max}");
                 }
                 }

+ 72 - 6
src/SharpGLTF.Core/Memory/MemoryImage.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using System.Text;
 
 
 using BYTES = System.ArraySegment<System.Byte>;
 using BYTES = System.ArraySegment<System.Byte>;
@@ -9,8 +10,24 @@ 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>
-    public readonly struct MemoryImage
+    [System.Diagnostics.DebuggerDisplay("{_DebuggerDisplay(),nq}")]
+    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";
@@ -43,6 +60,8 @@ namespace SharpGLTF.Memory
 
 
         public static MemoryImage Empty => default;
         public static MemoryImage Empty => default;
 
 
+        private const string GuardError_MustBeValidImage = "Must be a valid image: Png, Jpg, etc...";
+
         #endregion
         #endregion
 
 
         #region constructor
         #region constructor
@@ -51,14 +70,36 @@ namespace SharpGLTF.Memory
 
 
         public static implicit operator MemoryImage(Byte[] image) { return new MemoryImage(image); }
         public static implicit operator MemoryImage(Byte[] image) { return new MemoryImage(image); }
 
 
-        public MemoryImage(BYTES image) { _Image = image; }
+        public static implicit operator MemoryImage(string filePath) { return new MemoryImage(filePath); }
+
+        public MemoryImage(BYTES image)
+        {
+            Guard.IsTrue(_IsImage(image), nameof(image), GuardError_MustBeValidImage);
+
+            _Image = image;
+        }
+
+        public MemoryImage(Byte[] image)
+        {
+            if (image != null) Guard.IsTrue(_IsImage(image), nameof(image), GuardError_MustBeValidImage);
 
 
-        public MemoryImage(Byte[] image) { _Image = image == null ? default : new BYTES(image); }
+            _Image = image == null ? default : new BYTES(image);
+        }
 
 
         public MemoryImage(string filePath)
         public MemoryImage(string filePath)
         {
         {
-            var data = System.IO.File.ReadAllBytes(filePath);
-            _Image = new BYTES(data);
+            if (string.IsNullOrEmpty(filePath))
+            {
+                _Image = default;
+            }
+            else
+            {
+                var data = System.IO.File.ReadAllBytes(filePath);
+
+                Guard.IsTrue(_IsImage(data), nameof(filePath), GuardError_MustBeValidImage);
+
+                _Image = new BYTES(data);
+            }
         }
         }
 
 
         #endregion
         #endregion
@@ -67,6 +108,29 @@ namespace SharpGLTF.Memory
 
 
         private readonly BYTES _Image;
         private readonly BYTES _Image;
 
 
+        public override int GetHashCode()
+        {
+            // since this object stores the file of an image,
+            // using the file size as a hash is as good as anything else
+
+            return _Image.Count.GetHashCode();
+        }
+
+        public static bool AreEqual(MemoryImage a, MemoryImage b)
+        {
+            if (a.GetHashCode() != b.GetHashCode()) return false;
+            if (a._Image.Equals(b._Image)) return true;
+            return a._Image.AsSpan().SequenceEqual(b._Image);
+        }
+
+        public override bool Equals(object obj) { return obj is MemoryImage other && AreEqual(this, other); }
+
+        public bool Equals(MemoryImage other) { return AreEqual(this, other); }
+
+        public static bool operator ==(MemoryImage left, MemoryImage right) { return AreEqual(left, right); }
+
+        public static bool operator !=(MemoryImage left, MemoryImage right) { return !AreEqual(left, right); }
+
         #endregion
         #endregion
 
 
         #region properties
         #region properties
@@ -105,6 +169,7 @@ namespace SharpGLTF.Memory
         {
         {
             get
             get
             {
             {
+                if (IsEmpty) return null;
                 if (IsPng) return "png";
                 if (IsPng) return "png";
                 if (IsJpg) return "jpg";
                 if (IsJpg) return "jpg";
                 if (IsDds) return "dds";
                 if (IsDds) return "dds";
@@ -120,11 +185,12 @@ namespace SharpGLTF.Memory
         {
         {
             get
             get
             {
             {
+                if (IsEmpty) return null;
                 if (IsPng) return MIME_PNG;
                 if (IsPng) return MIME_PNG;
                 if (IsJpg) return MIME_JPG;
                 if (IsJpg) return MIME_JPG;
                 if (IsDds) return MIME_DDS;
                 if (IsDds) return MIME_DDS;
                 if (IsWebp) return MIME_WEBP;
                 if (IsWebp) return MIME_WEBP;
-                return "raw";
+                throw new NotImplementedException();
             }
             }
         }
         }
 
 

+ 6 - 6
src/SharpGLTF.Core/Schema2/_Extensions.cs

@@ -62,8 +62,8 @@ namespace SharpGLTF.Schema2
         {
         {
             if (value.Equals(defval)) return null;
             if (value.Equals(defval)) return null;
 
 
-            value = Vector2.Min(value, minval);
-            value = Vector2.Max(value, maxval);
+            value = Vector2.Min(value, maxval);
+            value = Vector2.Max(value, minval);
 
 
             return value.Equals(defval) ? (Vector2?)null : value;
             return value.Equals(defval) ? (Vector2?)null : value;
         }
         }
@@ -72,8 +72,8 @@ namespace SharpGLTF.Schema2
         {
         {
             if (value.Equals(defval)) return null;
             if (value.Equals(defval)) return null;
 
 
-            value = Vector3.Min(value, minval);
-            value = Vector3.Max(value, maxval);
+            value = Vector3.Min(value, maxval);
+            value = Vector3.Max(value, minval);
 
 
             return value.Equals(defval) ? (Vector3?)null : value;
             return value.Equals(defval) ? (Vector3?)null : value;
         }
         }
@@ -82,8 +82,8 @@ namespace SharpGLTF.Schema2
         {
         {
             if (value.Equals(defval)) return (Vector4?)null;
             if (value.Equals(defval)) return (Vector4?)null;
 
 
-            value = Vector4.Min(value, minval);
-            value = Vector4.Max(value, maxval);
+            value = Vector4.Min(value, maxval);
+            value = Vector4.Max(value, minval);
 
 
             return value.Equals(defval) ? (Vector4?)null : value;
             return value.Equals(defval) ? (Vector4?)null : value;
         }
         }

+ 4 - 15
src/SharpGLTF.Core/Schema2/gltf.Images.cs

@@ -95,16 +95,6 @@ namespace SharpGLTF.Schema2
 
 
         #region API
         #region API
 
 
-        /// <summary>
-        /// Opens the image file.
-        /// </summary>
-        /// <returns>A <see cref="System.IO.Stream"/> containing the image file.</returns>
-        [Obsolete("Use MemoryImage property")]
-        public System.IO.Stream OpenImageFile()
-        {
-            return this.MemoryImage.Open();
-        }
-
         /// <summary>
         /// <summary>
         /// Retrieves the image file as a segment of bytes.
         /// Retrieves the image file as a segment of bytes.
         /// </summary>
         /// </summary>
@@ -318,19 +308,18 @@ namespace SharpGLTF.Schema2
         /// </summary>
         /// </summary>
         /// <param name="imageContent">An image encoded in PNG, JPEG or DDS</param>
         /// <param name="imageContent">An image encoded in PNG, JPEG or DDS</param>
         /// <returns>A <see cref="Image"/> instance.</returns>
         /// <returns>A <see cref="Image"/> instance.</returns>
-        public Image UseImage(BYTES imageContent)
+        public Image UseImage(Memory.MemoryImage imageContent)
         {
         {
-            Guard.NotNullOrEmpty(imageContent, nameof(imageContent));
-            Guard.IsTrue(new Memory.MemoryImage(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.");
 
 
             foreach (var img in this.LogicalImages)
             foreach (var img in this.LogicalImages)
             {
             {
                 var existingContent = img.GetImageContent();
                 var existingContent = img.GetImageContent();
-                if (Enumerable.SequenceEqual(existingContent, imageContent)) return img;
+                if (Memory.MemoryImage.AreEqual(imageContent, existingContent)) return img;
             }
             }
 
 
             var image = this.CreateImage();
             var image = this.CreateImage();
-            image.SetSatelliteContent(imageContent.ToArray());
+            image.SetSatelliteContent(imageContent.GetBuffer().ToArray());
             return image;
             return image;
         }
         }
 
 

+ 52 - 21
src/SharpGLTF.Toolkit/Materials/ChannelBuilder.cs

@@ -6,16 +6,29 @@ using System.Text;
 
 
 namespace SharpGLTF.Materials
 namespace SharpGLTF.Materials
 {
 {
-    [System.Diagnostics.DebuggerDisplay("{Key} {Parameter}")]
+    [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
     public class ChannelBuilder
     public class ChannelBuilder
     {
     {
+        #region debug
+
+        private string _GetDebuggerDisplay()
+        {
+            var txt = Key.ToString();
+            if (Parameter != _GetDefaultParameter(_Key)) txt += $" {Parameter}";
+
+            var tex = GetValidTexture();
+            if (tex != null) txt += $" 🖼{tex.PrimaryImage.FileExtension}";
+
+            return txt;
+        }
+
+        #endregion
+
         #region lifecycle
         #region lifecycle
 
 
-        internal ChannelBuilder(MaterialBuilder parent, string key)
+        internal ChannelBuilder(MaterialBuilder parent, KnownChannel key)
         {
         {
             Guard.NotNull(parent, nameof(parent));
             Guard.NotNull(parent, nameof(parent));
-            Guard.NotNullOrEmpty(key, nameof(key));
-            Guard.IsTrue(Enum.GetNames(typeof(KnownChannel)).Contains(key), nameof(key), $"{nameof(key)} must be a name of {nameof(KnownChannel)}.");
 
 
             _Parent = parent;
             _Parent = parent;
             _Key = key;
             _Key = key;
@@ -27,9 +40,11 @@ namespace SharpGLTF.Materials
 
 
         #region data
         #region data
 
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly MaterialBuilder _Parent;
         private readonly MaterialBuilder _Parent;
 
 
-        private readonly String _Key;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly KnownChannel _Key;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the <see cref="ChannelBuilder"/> paramenter.
         /// Gets or sets the <see cref="ChannelBuilder"/> paramenter.
@@ -39,7 +54,7 @@ namespace SharpGLTF.Materials
 
 
         public TextureBuilder Texture { get; private set; }
         public TextureBuilder Texture { get; private set; }
 
 
-        public static bool AreEqual(ChannelBuilder a, ChannelBuilder b)
+        public static bool AreEqualByContent(ChannelBuilder a, ChannelBuilder b)
         {
         {
             #pragma warning disable IDE0041 // Use 'is null' check
             #pragma warning disable IDE0041 // Use 'is null' check
             if (Object.ReferenceEquals(a, b)) return true;
             if (Object.ReferenceEquals(a, b)) return true;
@@ -47,13 +62,11 @@ namespace SharpGLTF.Materials
             if (Object.ReferenceEquals(b, null)) return false;
             if (Object.ReferenceEquals(b, null)) return false;
             #pragma warning restore IDE0041 // Use 'is null' check
             #pragma warning restore IDE0041 // Use 'is null' check
 
 
-            if (!Object.ReferenceEquals(a._Parent, b._Parent)) return false;
-
             if (a._Key != b._Key) return false;
             if (a._Key != b._Key) return false;
 
 
             if (a.Parameter != b.Parameter) return false;
             if (a.Parameter != b.Parameter) return false;
 
 
-            if (!TextureBuilder.AreEqual(a.Texture, b.Texture)) return false;
+            if (!TextureBuilder.AreEqualByContent(a.Texture, b.Texture)) return false;
 
 
             return true;
             return true;
         }
         }
@@ -78,14 +91,22 @@ namespace SharpGLTF.Materials
         /// <summary>
         /// <summary>
         /// Gets the <see cref="ChannelBuilder"/> name. It must be a name of <see cref="KnownChannel"/>.
         /// Gets the <see cref="ChannelBuilder"/> name. It must be a name of <see cref="KnownChannel"/>.
         /// </summary>
         /// </summary>
-        public String Key => _Key;
+        public KnownChannel Key => _Key;
 
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         public static IEqualityComparer<ChannelBuilder> ContentComparer => _ContentComparer.Default;
         public static IEqualityComparer<ChannelBuilder> ContentComparer => _ContentComparer.Default;
 
 
         #endregion
         #endregion
 
 
         #region API
         #region API
 
 
+        public TextureBuilder GetValidTexture()
+        {
+            if (Texture == null) return null;
+            if (Texture.PrimaryImage.IsEmpty) return null;
+            return Texture;
+        }
+
         internal void CopyTo(ChannelBuilder other)
         internal void CopyTo(ChannelBuilder other)
         {
         {
             other.Parameter = this.Parameter;
             other.Parameter = this.Parameter;
@@ -102,21 +123,31 @@ namespace SharpGLTF.Materials
 
 
         public void SetDefaultParameter()
         public void SetDefaultParameter()
         {
         {
-            switch (_Key)
+            this.Parameter = _GetDefaultParameter(_Key);
+        }
+
+        private static Vector4 _GetDefaultParameter(KnownChannel key)
+        {
+            switch (key)
             {
             {
-                case "Emissive": Parameter = Vector4.Zero; break;
+                case KnownChannel.Emissive: return Vector4.Zero;
+
+                case KnownChannel.Normal:
+                case KnownChannel.ClearCoatNormal:
+                case KnownChannel.Occlusion:
+                    return  Vector4.UnitX;
 
 
-                case "Normal":
-                case "Occlusion":
-                    Parameter = new Vector4(1, 0, 0, 0); break;
+                case KnownChannel.BaseColor:
+                case KnownChannel.Diffuse:
+                    return Vector4.One;
 
 
-                case "BaseColor":
-                case "Diffuse":
-                    Parameter = Vector4.One; break;
+                case KnownChannel.MetallicRoughness: return new Vector4(1, 1, 0, 0);
+                case KnownChannel.SpecularGlossiness: return Vector4.One;
 
 
-                case "MetalicRoughness": Parameter = new Vector4(1, 1, 0, 0); break;
+                case KnownChannel.ClearCoat: return Vector4.Zero;
+                case KnownChannel.ClearCoatRoughness: return Vector4.Zero;
 
 
-                case "SpecularGlossiness": Parameter = Vector4.One; break;
+                default: throw new NotImplementedException();
             }
             }
         }
         }
 
 
@@ -138,7 +169,7 @@ namespace SharpGLTF.Materials
 
 
             public bool Equals(ChannelBuilder x, ChannelBuilder y)
             public bool Equals(ChannelBuilder x, ChannelBuilder y)
             {
             {
-                return ChannelBuilder.AreEqual(x, y);
+                return ChannelBuilder.AreEqualByContent(x, y);
             }
             }
 
 
             public int GetHashCode(ChannelBuilder obj)
             public int GetHashCode(ChannelBuilder obj)

+ 283 - 83
src/SharpGLTF.Toolkit/Materials/MaterialBuilder.cs

@@ -1,14 +1,51 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Numerics;
 using System.Numerics;
 using System.Text;
 using System.Text;
 
 
+using IMAGEFILE = SharpGLTF.Memory.MemoryImage;
+
 namespace SharpGLTF.Materials
 namespace SharpGLTF.Materials
 {
 {
     [System.Diagnostics.DebuggerDisplay("{Name} {ShaderStyle}")]
     [System.Diagnostics.DebuggerDisplay("{Name} {ShaderStyle}")]
     public class MaterialBuilder
     public class MaterialBuilder
     {
     {
+        #region constants
+
+        public const string SHADERUNLIT = "Unlit";
+        public const string SHADERPBRMETALLICROUGHNESS = "PBRMetallicRoughness";
+        public const string SHADERPBRSPECULARGLOSSINESS = "PBRSpecularGlossiness";
+
+        private static readonly KnownChannel[] _UnlitChannels = new[] { KnownChannel.BaseColor };
+
+        private static readonly KnownChannel[] _MetRouChannels = new[]
+        {
+            KnownChannel.BaseColor,
+            KnownChannel.MetallicRoughness,
+            KnownChannel.Normal,
+            KnownChannel.Occlusion,
+            KnownChannel.Emissive,
+            KnownChannel.ClearCoat,
+            KnownChannel.ClearCoatNormal,
+            KnownChannel.ClearCoatRoughness
+        };
+
+        private static readonly KnownChannel[] _SpeGloChannels = new[]
+        {
+            KnownChannel.Diffuse,
+            KnownChannel.SpecularGlossiness,
+            KnownChannel.Normal,
+            KnownChannel.Occlusion,
+            KnownChannel.Emissive,
+            // KnownChannel.ClearCoat,
+            // KnownChannel.ClearCoatNormal,
+            // KnownChannel.ClearCoatRoughness
+        };
+
+        #endregion
+
         #region lifecycle
         #region lifecycle
 
 
         public MaterialBuilder(string name = null)
         public MaterialBuilder(string name = null)
@@ -49,14 +86,15 @@ namespace SharpGLTF.Materials
 
 
         #region data
         #region data
 
 
-        public const string SHADERUNLIT = "Unlit";
-        public const string SHADERPBRMETALLICROUGHNESS = "PBRMetallicRoughness";
-        public const string SHADERPBRSPECULARGLOSSINESS = "PBRSpecularGlossiness";
-
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly List<ChannelBuilder> _Channels = new List<ChannelBuilder>();
         private readonly List<ChannelBuilder> _Channels = new List<ChannelBuilder>();
 
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private MaterialBuilder _CompatibilityFallbackMaterial;
         private MaterialBuilder _CompatibilityFallbackMaterial;
 
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private string _ShaderStyle = SHADERPBRMETALLICROUGHNESS;
+
         /// <summary>
         /// <summary>
         /// Gets or sets the name of this <see cref="MaterialBuilder"/> instance.
         /// Gets or sets the name of this <see cref="MaterialBuilder"/> instance.
         /// </summary>
         /// </summary>
@@ -71,7 +109,11 @@ namespace SharpGLTF.Materials
         /// </summary>
         /// </summary>
         public Boolean DoubleSided { get; set; } = false;
         public Boolean DoubleSided { get; set; } = false;
 
 
-        public String ShaderStyle { get; set; } = SHADERPBRMETALLICROUGHNESS;
+        public String ShaderStyle
+        {
+            get => _ShaderStyle;
+            set => _SetShader(value);
+        }
 
 
         public static bool AreEqualByContent(MaterialBuilder x, MaterialBuilder y)
         public static bool AreEqualByContent(MaterialBuilder x, MaterialBuilder y)
         {
         {
@@ -91,7 +133,7 @@ namespace SharpGLTF.Materials
             if (x.AlphaMode != y.AlphaMode) return false;
             if (x.AlphaMode != y.AlphaMode) return false;
             if (x.AlphaCutoff != y.AlphaCutoff) return false;
             if (x.AlphaCutoff != y.AlphaCutoff) return false;
             if (x.DoubleSided != y.DoubleSided) return false;
             if (x.DoubleSided != y.DoubleSided) return false;
-            if (x.ShaderStyle != y.ShaderStyle) return false;
+            if (x._ShaderStyle != y._ShaderStyle) return false;
 
 
             if (!AreEqualByContent(x._CompatibilityFallbackMaterial, y._CompatibilityFallbackMaterial)) return false;
             if (!AreEqualByContent(x._CompatibilityFallbackMaterial, y._CompatibilityFallbackMaterial)) return false;
 
 
@@ -107,7 +149,7 @@ namespace SharpGLTF.Materials
                 var xc = x.GetChannel(ckey);
                 var xc = x.GetChannel(ckey);
                 var yc = y.GetChannel(ckey);
                 var yc = y.GetChannel(ckey);
 
 
-                if (!ChannelBuilder.AreEqual(xc, yc)) return false;
+                if (!ChannelBuilder.AreEqualByContent(xc, yc)) return false;
             }
             }
 
 
             return true;
             return true;
@@ -155,72 +197,146 @@ namespace SharpGLTF.Materials
 
 
         #region API
         #region API
 
 
-        /// <summary>
-        /// Sets <see cref="MaterialBuilder.ShaderStyle"/> to use <see cref="SHADERUNLIT"/>.
-        /// </summary>
-        /// <returns>This <see cref="MaterialBuilder"/>.</returns>
-        public MaterialBuilder WithUnlitShader() { return WithShader(SHADERUNLIT); }
+        private void _SetShader(string shader)
+        {
+            Guard.NotNullOrEmpty(shader, nameof(shader));
+            Guard.IsTrue(shader == SHADERUNLIT || shader == SHADERPBRMETALLICROUGHNESS || shader == SHADERPBRSPECULARGLOSSINESS, nameof(shader));
 
 
-        /// <summary>
-        /// Sets <see cref="MaterialBuilder.ShaderStyle"/> to use <see cref="SHADERPBRMETALLICROUGHNESS"/>.
-        /// </summary>
-        /// <returns>This <see cref="MaterialBuilder"/>.</returns>
-        public MaterialBuilder WithMetallicRoughnessShader() { return WithShader(SHADERPBRMETALLICROUGHNESS); }
+            _ShaderStyle = shader;
 
 
-        /// <summary>
-        /// Sets <see cref="MaterialBuilder.ShaderStyle"/> to use <see cref="SHADERPBRSPECULARGLOSSINESS"/>.
-        /// </summary>
-        /// <returns>This <see cref="MaterialBuilder"/>.</returns>
-        public MaterialBuilder WithSpecularGlossinessShader() { return WithShader(SHADERPBRSPECULARGLOSSINESS); }
+            var validChannels = _GetValidChannels();
 
 
-        /// <summary>
-        /// Sets <see cref="MaterialBuilder.ShaderStyle"/>.
-        /// </summary>
-        /// <param name="shader">
-        /// A valid shader style, which can be one of these values:
-        /// <see cref="SHADERUNLIT"/>,
-        /// <see cref="SHADERPBRMETALLICROUGHNESS"/>,
-        /// <see cref="SHADERPBRSPECULARGLOSSINESS"/>
-        /// </param>
-        /// <returns>This <see cref="MaterialBuilder"/>.</returns>
-        public MaterialBuilder WithShader(string shader)
+            // remove incompatible channels.
+            for (int i = _Channels.Count - 1; i >= 0; --i)
+            {
+                var c = _Channels[i];
+                if (!validChannels.Contains(c.Key)) _Channels.RemoveAt(i);
+            }
+        }
+
+        private IReadOnlyList<KnownChannel> _GetValidChannels()
         {
         {
-            Guard.NotNullOrEmpty(shader, nameof(shader));
-            Guard.IsTrue(shader == SHADERUNLIT || shader == SHADERPBRMETALLICROUGHNESS || shader == SHADERPBRSPECULARGLOSSINESS, nameof(shader));
-            ShaderStyle = shader;
-            return this;
+            switch (ShaderStyle)
+            {
+                case SHADERUNLIT: return _UnlitChannels;
+                case SHADERPBRMETALLICROUGHNESS: return _MetRouChannels;
+                case SHADERPBRSPECULARGLOSSINESS: return _SpeGloChannels;
+                default: throw new NotImplementedException();
+            }
         }
         }
 
 
         public ChannelBuilder GetChannel(KnownChannel channelKey)
         public ChannelBuilder GetChannel(KnownChannel channelKey)
         {
         {
-            return GetChannel(channelKey.ToString());
+            return _Channels.FirstOrDefault(item => item.Key == channelKey);
+        }
+
+        public ChannelBuilder UseChannel(KnownChannel channelKey)
+        {
+            Guard.IsTrue(_GetValidChannels().Contains(channelKey), nameof(channelKey));
+
+            var ch = GetChannel(channelKey);
+            if (ch != null) return ch;
+
+            ch = new ChannelBuilder(this, channelKey);
+            _Channels.Add(ch);
+
+            return ch;
         }
         }
 
 
         public ChannelBuilder GetChannel(string channelKey)
         public ChannelBuilder GetChannel(string channelKey)
         {
         {
             Guard.NotNullOrEmpty(channelKey, nameof(channelKey));
             Guard.NotNullOrEmpty(channelKey, nameof(channelKey));
+            var key = (KnownChannel)Enum.Parse(typeof(KnownChannel), channelKey, true);
+
+            return GetChannel(key);
+        }
 
 
-            channelKey = channelKey.ToLowerInvariant();
+        public ChannelBuilder UseChannel(string channelKey)
+        {
+            Guard.NotNullOrEmpty(channelKey, nameof(channelKey));
+            var key = (KnownChannel)Enum.Parse(typeof(KnownChannel), channelKey, true);
 
 
-            return _Channels.FirstOrDefault(item => string.Equals(channelKey, item.Key, StringComparison.OrdinalIgnoreCase));
+            return UseChannel(key);
         }
         }
 
 
-        public ChannelBuilder UseChannel(KnownChannel channelKey)
+        public void RemoveChannel(KnownChannel key)
         {
         {
-            return UseChannel(channelKey.ToString());
+            var idx = _Channels.IndexOf(item => item.Key == key);
+            if (idx < 0) return;
+            _Channels.RemoveAt(idx);
         }
         }
 
 
-        public ChannelBuilder UseChannel(string channelKey)
+        internal void ValidateForSchema2()
         {
         {
-            var ch = GetChannel(channelKey);
-            if (ch != null) return ch;
+            var hasClearCoat = this.GetChannel("ClearCoat") != null
+                || this.GetChannel("ClearCoatRoughness") != null
+                || this.GetChannel("ClearCoatNormal") != null;
 
 
-            ch = new ChannelBuilder(this, channelKey);
-            _Channels.Add(ch);
+            if (this.ShaderStyle == SHADERPBRSPECULARGLOSSINESS)
+            {
+                Guard.IsFalse(hasClearCoat, KnownChannel.ClearCoat.ToString(), "Clear Coat not supported for Specular Glossiness materials.");
 
 
-            return ch;
+                if (this.CompatibilityFallback != null)
+                {
+                    Guard.MustBeNull(this.CompatibilityFallback.CompatibilityFallback, nameof(CompatibilityFallback));
+
+                    Guard.MustBeEqualTo(this.Name, this.CompatibilityFallback.Name, nameof(Name));
+
+                    Guard.IsTrue(this.CompatibilityFallback.ShaderStyle == SHADERPBRMETALLICROUGHNESS, nameof(ShaderStyle));
+
+                    Guard.IsTrue(this.AlphaMode == this.CompatibilityFallback.AlphaMode, nameof(AlphaMode));
+                    Guard.MustBeEqualTo(this.AlphaCutoff, this.CompatibilityFallback.AlphaCutoff, nameof(AlphaCutoff));
+                    Guard.MustBeEqualTo(this.DoubleSided, this.CompatibilityFallback.DoubleSided, nameof(DoubleSided));
+
+                    foreach (var chKey in new[] { KnownChannel.Normal, KnownChannel.Occlusion, KnownChannel.Emissive })
+                    {
+                        var primary = this.GetChannel(chKey);
+                        var fallbck = this.CompatibilityFallback.GetChannel(chKey);
+
+                        Guard.IsTrue(ChannelBuilder.AreEqualByContent(primary, fallbck), chKey.ToString(), "Primary and fallback materials must have the same channel properties");
+                    }
+                }
+            }
+            else
+            {
+                Guard.MustBeNull(this.CompatibilityFallback, nameof(CompatibilityFallback));
+            }
         }
         }
 
 
+        #endregion
+
+        #region API * With
+
+        /// <summary>
+        /// Sets <see cref="MaterialBuilder.ShaderStyle"/>.
+        /// </summary>
+        /// <param name="shader">
+        /// A valid shader style, which can be one of these values:
+        /// <see cref="SHADERUNLIT"/>,
+        /// <see cref="SHADERPBRMETALLICROUGHNESS"/>,
+        /// <see cref="SHADERPBRSPECULARGLOSSINESS"/>
+        /// </param>
+        /// <returns>This <see cref="MaterialBuilder"/>.</returns>
+        public MaterialBuilder WithShader(string shader) { _SetShader(shader); return this; }
+
+        /// <summary>
+        /// Sets <see cref="MaterialBuilder.ShaderStyle"/> to use <see cref="SHADERUNLIT"/>.
+        /// </summary>
+        /// <returns>This <see cref="MaterialBuilder"/>.</returns>
+        public MaterialBuilder WithUnlitShader() { _SetShader(SHADERUNLIT); return this; }
+
+        /// <summary>
+        /// Sets <see cref="MaterialBuilder.ShaderStyle"/> to use <see cref="SHADERPBRMETALLICROUGHNESS"/>.
+        /// </summary>
+        /// <returns>This <see cref="MaterialBuilder"/>.</returns>
+        public MaterialBuilder WithMetallicRoughnessShader() { _SetShader(SHADERPBRMETALLICROUGHNESS); return this; }
+
+        /// <summary>
+        /// Sets <see cref="MaterialBuilder.ShaderStyle"/> to use <see cref="SHADERPBRSPECULARGLOSSINESS"/>.
+        /// </summary>
+        /// <returns>This <see cref="MaterialBuilder"/>.</returns>
+        public MaterialBuilder WithSpecularGlossinessShader() { _SetShader(SHADERPBRSPECULARGLOSSINESS); return this; }
+
         public MaterialBuilder WithAlpha(AlphaMode alphaMode = AlphaMode.OPAQUE, Single alphaCutoff = 0.5f)
         public MaterialBuilder WithAlpha(AlphaMode alphaMode = AlphaMode.OPAQUE, Single alphaCutoff = 0.5f)
         {
         {
             this.AlphaMode = alphaMode;
             this.AlphaMode = alphaMode;
@@ -243,27 +359,32 @@ namespace SharpGLTF.Materials
             return this;
             return this;
         }
         }
 
 
-        public MaterialBuilder WithChannelParam(string channelKey, Vector4 parameter)
+        public MaterialBuilder WithChannelImage(KnownChannel channelKey, IMAGEFILE primaryImage)
         {
         {
-            this.UseChannel(channelKey).Parameter = parameter;
+            if (primaryImage.IsEmpty)
+            {
+                this.GetChannel(channelKey)?.RemoveTexture();
+                return this;
+            }
+
+            this.UseChannel(channelKey)
+                .UseTexture()
+                .WithPrimaryImage(primaryImage);
 
 
             return this;
             return this;
         }
         }
 
 
-        public MaterialBuilder WithChannelImage(KnownChannel channelKey, string primaryImagePath)
+        public MaterialBuilder WithChannelParam(string channelKey, Vector4 parameter)
         {
         {
-            this.UseChannel(channelKey)
-                .UseTexture()
-                .WithPrimaryImage(primaryImagePath);
-
+            this.UseChannel(channelKey).Parameter = parameter;
             return this;
             return this;
         }
         }
 
 
-        public MaterialBuilder WithChannelImage(string channelKey, string primaryImagePath)
+        public MaterialBuilder WithChannelImage(string channelKey, IMAGEFILE primaryImage)
         {
         {
             this.UseChannel(channelKey)
             this.UseChannel(channelKey)
                 .UseTexture()
                 .UseTexture()
-                .WithPrimaryImage(primaryImagePath);
+                .WithPrimaryImage(primaryImage);
 
 
             return this;
             return this;
         }
         }
@@ -283,50 +404,129 @@ namespace SharpGLTF.Materials
             return this;
             return this;
         }
         }
 
 
-        internal void ValidateForSchema2()
+        public MaterialBuilder WithMetallicRoughnessFallback(IMAGEFILE baseColor, Vector4? rgba, IMAGEFILE metallicRoughness, float? metallic, float? roughness)
         {
         {
-            if (this.ShaderStyle == SHADERPBRSPECULARGLOSSINESS)
-            {
-                if (this.CompatibilityFallback != null)
-                {
-                    Guard.MustBeNull(this.CompatibilityFallback.CompatibilityFallback, nameof(this.CompatibilityFallback.CompatibilityFallback));
+            var fallback = this
+                .Clone()
+                .WithMetallicRoughnessShader()
+                .WithBaseColor(baseColor, rgba)
+                .WithMetallicRoughness(metallicRoughness, metallic, roughness);
 
 
-                    Guard.IsTrue(this.CompatibilityFallback.ShaderStyle == SHADERPBRMETALLICROUGHNESS, nameof(CompatibilityFallback.ShaderStyle));
-                }
-            }
-            else
-            {
-                Guard.MustBeNull(this.CompatibilityFallback, nameof(CompatibilityFallback));
-            }
+            return this.WithFallback(fallback);
+        }
+
+        #endregion
+
+        #region API * With for specific channels
+
+        public MaterialBuilder WithNormal(IMAGEFILE imageFile, float scale = 1)
+        {
+            WithChannelImage(KnownChannel.Normal, imageFile);
+            WithChannelParam(KnownChannel.Normal, new Vector4(scale, 0, 0, 0));
+            return this;
+        }
+
+        public MaterialBuilder WithOcclusion(IMAGEFILE imageFile, float strength = 1)
+        {
+            WithChannelImage(KnownChannel.Occlusion, imageFile);
+            WithChannelParam(KnownChannel.Occlusion, new Vector4(strength, 0, 0, 0));
+            return this;
         }
         }
 
 
-        public MaterialBuilder WithNormal(string imageFilePath, float scale = 1)
+        public MaterialBuilder WithEmissive(Vector3 rgb) { return WithChannelParam(KnownChannel.Emissive, new Vector4(rgb, 1)); }
+
+        public MaterialBuilder WithEmissive(IMAGEFILE imageFile, Vector3? rgb = null)
         {
         {
-            WithChannelImage("Normal", imageFilePath);
-            WithChannelParam("Normal", new Vector4(scale, 0, 0, 0));
+            WithChannelImage(KnownChannel.Emissive, imageFile);
+            if (rgb.HasValue) WithEmissive(rgb.Value);
+            return this;
+        }
 
 
+        public MaterialBuilder WithBaseColor(Vector4 rgba) { return WithChannelParam(KnownChannel.BaseColor, rgba); }
+
+        public MaterialBuilder WithBaseColor(IMAGEFILE imageFile, Vector4? rgba = null)
+        {
+            WithChannelImage(KnownChannel.BaseColor, imageFile);
+            if (rgba.HasValue) WithBaseColor(rgba.Value);
             return this;
             return this;
         }
         }
 
 
-        public MaterialBuilder WithOcclusion(string imageFilePath, float strength = 1)
+        public MaterialBuilder WithMetallicRoughness(float? metallic = null, float? roughness = null)
         {
         {
-            WithChannelImage("Occlusion", imageFilePath);
-            WithChannelParam("Occlusion", new Vector4(strength, 0, 0, 0));
+            if (!metallic.HasValue && !roughness.HasValue) return this;
+
+            var channel = UseChannel(KnownChannel.MetallicRoughness);
+            var val = channel.Parameter;
+            if (metallic.HasValue) val.X = metallic.Value;
+            if (roughness.HasValue) val.Y = roughness.Value;
+            channel.Parameter = val;
 
 
             return this;
             return this;
         }
         }
 
 
-        public MaterialBuilder WithEmissive(string imageFilePath, Vector3 emissiveFactor)
+        public MaterialBuilder WithMetallicRoughness(IMAGEFILE imageFile, float? metallic = null, float? roughness = null)
         {
         {
-            WithChannelImage("Emissive", imageFilePath);
-            WithChannelParam("Emissive", new Vector4(emissiveFactor, 0));
+            WithChannelImage(KnownChannel.MetallicRoughness, imageFile);
+            WithMetallicRoughness(metallic, roughness);
+            return this;
+        }
+
+        public MaterialBuilder WithDiffuse(Vector4 rgba) { return WithChannelParam(KnownChannel.Diffuse, rgba); }
 
 
+        public MaterialBuilder WithDiffuse(IMAGEFILE imageFile, Vector4? rgba = null)
+        {
+            WithChannelImage(KnownChannel.Diffuse, imageFile);
+            if (rgba.HasValue) WithDiffuse(rgba.Value);
             return this;
             return this;
         }
         }
 
 
-        public MaterialBuilder WithEmissive(string imageFilePath) { return WithChannelImage("Emissive", imageFilePath); }
+        public MaterialBuilder WithSpecularGlossiness(Vector3? specular = null, float? glossiness = null)
+        {
+            if (!specular.HasValue && !glossiness.HasValue) return this;
+
+            var channel = UseChannel(KnownChannel.SpecularGlossiness);
 
 
-        public MaterialBuilder WithEmissive(Vector3 emissiveFactor) { return WithChannelParam("Emissive", new Vector4(emissiveFactor, 0)); }
+            var val = channel.Parameter;
+            if (specular.HasValue)
+            {
+                val.X = specular.Value.X;
+                val.Y = specular.Value.Y;
+                val.Z = specular.Value.Z;
+            }
+
+            if (glossiness.HasValue) val.W = glossiness.Value;
+
+            channel.Parameter = val;
+
+            return this;
+        }
+
+        public MaterialBuilder WithSpecularGlossiness(IMAGEFILE imageFile, Vector3? specular = null, float? glossiness = null)
+        {
+            WithChannelImage(KnownChannel.SpecularGlossiness, imageFile);
+            WithSpecularGlossiness(specular, glossiness);
+            return this;
+        }
+
+        public MaterialBuilder WithClearCoatNormal(IMAGEFILE imageFile)
+        {
+            WithChannelImage(KnownChannel.ClearCoatNormal, imageFile);
+            return this;
+        }
+
+        public MaterialBuilder WithClearCoat(IMAGEFILE imageFile, float intensity)
+        {
+            WithChannelImage(KnownChannel.ClearCoat, imageFile);
+            WithChannelParam(KnownChannel.ClearCoat, new Vector4(intensity, 0, 0, 0));
+            return this;
+        }
+
+        public MaterialBuilder WithClearCoatRoughness(IMAGEFILE imageFile, float roughness)
+        {
+            WithChannelImage(KnownChannel.ClearCoatRoughness, imageFile);
+            WithChannelParam(KnownChannel.ClearCoatRoughness, new Vector4(roughness, 0, 0, 0));
+            return this;
+        }
 
 
         #endregion
         #endregion
 
 
@@ -338,12 +538,12 @@ namespace SharpGLTF.Materials
 
 
             public bool Equals(MaterialBuilder x, MaterialBuilder y)
             public bool Equals(MaterialBuilder x, MaterialBuilder y)
             {
             {
-                return MaterialBuilder.AreEqualByContent(x, y);
+                return AreEqualByContent(x, y);
             }
             }
 
 
             public int GetHashCode(MaterialBuilder obj)
             public int GetHashCode(MaterialBuilder obj)
             {
             {
-                return MaterialBuilder.GetContentHashCode(obj);
+                return GetContentHashCode(obj);
             }
             }
         }
         }
 
 

+ 25 - 77
src/SharpGLTF.Toolkit/Materials/TextureBuilder.cs

@@ -4,15 +4,14 @@ using System.Linq;
 using System.Numerics;
 using System.Numerics;
 using System.Text;
 using System.Text;
 
 
-using BYTES = System.ArraySegment<byte>;
-
+using IMAGEFILE = SharpGLTF.Memory.MemoryImage;
 using TEXLERP = SharpGLTF.Schema2.TextureInterpolationFilter;
 using TEXLERP = SharpGLTF.Schema2.TextureInterpolationFilter;
 using TEXMIPMAP = SharpGLTF.Schema2.TextureMipMapFilter;
 using TEXMIPMAP = SharpGLTF.Schema2.TextureMipMapFilter;
 using TEXWRAP = SharpGLTF.Schema2.TextureWrapMode;
 using TEXWRAP = SharpGLTF.Schema2.TextureWrapMode;
 
 
 namespace SharpGLTF.Materials
 namespace SharpGLTF.Materials
 {
 {
-    [System.Diagnostics.DebuggerDisplay("Texture {CoordinateSet} {MinFilter} {MagFilter} {WrapS} {WrapT} {Rotation} {Offset} {Scale}")]
+    [System.Diagnostics.DebuggerDisplay("Texture {CoordinateSet} Min:{MinFilter} Mag:{MagFilter} Ws:{WrapS} Wt:{WrapT}")]
     public class TextureBuilder
     public class TextureBuilder
     {
     {
         #region lifecycle
         #region lifecycle
@@ -30,8 +29,8 @@ namespace SharpGLTF.Materials
 
 
         private readonly ChannelBuilder _Parent;
         private readonly ChannelBuilder _Parent;
 
 
-        private BYTES _PrimaryImageContent;
-        private BYTES _FallbackImageContent;
+        private IMAGEFILE _PrimaryImageContent;
+        private IMAGEFILE _FallbackImageContent;
 
 
         public int CoordinateSet { get; set; } = 0;
         public int CoordinateSet { get; set; } = 0;
 
 
@@ -45,7 +44,7 @@ namespace SharpGLTF.Materials
 
 
         private TextureTransformBuilder _Transform;
         private TextureTransformBuilder _Transform;
 
 
-        public static bool AreEqual(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
             if (Object.ReferenceEquals(a, b)) return true;
             if (Object.ReferenceEquals(a, b)) return true;
@@ -53,8 +52,6 @@ namespace SharpGLTF.Materials
             if (Object.ReferenceEquals(b, null)) return false;
             if (Object.ReferenceEquals(b, null)) return false;
             #pragma warning restore IDE0041 // Use 'is null' check
             #pragma warning restore IDE0041 // Use 'is null' check
 
 
-            if (!Object.ReferenceEquals(a._Parent, b._Parent)) return false;
-
             if (a.CoordinateSet != b.CoordinateSet) return false;
             if (a.CoordinateSet != b.CoordinateSet) return false;
 
 
             if (a.MinFilter != b.MinFilter) return false;
             if (a.MinFilter != b.MinFilter) return false;
@@ -62,21 +59,14 @@ namespace SharpGLTF.Materials
             if (a.WrapS != b.WrapS) return false;
             if (a.WrapS != b.WrapS) return false;
             if (a.WrapT != b.WrapT) return false;
             if (a.WrapT != b.WrapT) return false;
 
 
-            if (!_AreArraysContentEqual(a._PrimaryImageContent, b._PrimaryImageContent)) return false;
-            if (!_AreArraysContentEqual(a._FallbackImageContent, b._FallbackImageContent)) return false;
+            if (!IMAGEFILE.AreEqual(a._PrimaryImageContent, b._PrimaryImageContent)) return false;
+            if (!IMAGEFILE.AreEqual(a._FallbackImageContent, b._FallbackImageContent)) return false;
 
 
-            if (TextureTransformBuilder.AreEqual(a._Transform, b._Transform)) return false;
+            if (!TextureTransformBuilder.AreEqualByContent(a._Transform, b._Transform)) return false;
 
 
             return true;
             return true;
         }
         }
 
 
-        private static bool _AreArraysContentEqual(BYTES a, BYTES b)
-        {
-            if (a.Equals(b)) return true;
-
-            return Enumerable.SequenceEqual(a, b);
-        }
-
         public static int GetContentHashCode(TextureBuilder x)
         public static int GetContentHashCode(TextureBuilder x)
         {
         {
             if (x == null) return 0;
             if (x == null) return 0;
@@ -87,8 +77,8 @@ namespace SharpGLTF.Materials
             h ^= x.WrapS.GetHashCode();
             h ^= x.WrapS.GetHashCode();
             h ^= x.WrapT.GetHashCode();
             h ^= x.WrapT.GetHashCode();
 
 
-            h ^= x._PrimaryImageContent.GetContentHashCode(16);
-            h ^= x._FallbackImageContent.GetContentHashCode(16);
+            h ^= x._PrimaryImageContent.GetHashCode();
+            h ^= x._FallbackImageContent.GetHashCode();
 
 
             return h;
             return h;
         }
         }
@@ -101,8 +91,7 @@ namespace SharpGLTF.Materials
         /// Gets or sets the default image bytes to use by this <see cref="TextureBuilder"/>,
         /// Gets or sets the default image bytes to use by this <see cref="TextureBuilder"/>,
         /// Supported formats are: PNG, JPG, DDS and WEBP
         /// Supported formats are: PNG, JPG, DDS and WEBP
         /// </summary>
         /// </summary>
-        [Obsolete("Use PrimaryImage property.")]
-        public BYTES PrimaryImageContent
+        public IMAGEFILE PrimaryImage
         {
         {
             get => _PrimaryImageContent;
             get => _PrimaryImageContent;
             set => WithPrimaryImage(value);
             set => WithPrimaryImage(value);
@@ -112,33 +101,12 @@ namespace SharpGLTF.Materials
         /// Gets or sets the fallback image bytes to use by this <see cref="TextureBuilder"/>,
         /// Gets or sets the fallback image bytes to use by this <see cref="TextureBuilder"/>,
         /// Supported formats are: PNG, JPG.
         /// Supported formats are: PNG, JPG.
         /// </summary>
         /// </summary>
-        [Obsolete("Use FallbackImage property.")]
-        public BYTES FallbackImageContent
+        public IMAGEFILE FallbackImage
         {
         {
             get => _FallbackImageContent;
             get => _FallbackImageContent;
             set => WithFallbackImage(value);
             set => WithFallbackImage(value);
         }
         }
 
 
-        /// <summary>
-        /// Gets or sets the default image bytes to use by this <see cref="TextureBuilder"/>,
-        /// Supported formats are: PNG, JPG, DDS and WEBP
-        /// </summary>
-        public Memory.MemoryImage PrimaryImage
-        {
-            get => new Memory.MemoryImage(_PrimaryImageContent);
-            set => WithPrimaryImage(value.GetBuffer());
-        }
-
-        /// <summary>
-        /// Gets or sets the fallback image bytes to use by this <see cref="TextureBuilder"/>,
-        /// Supported formats are: PNG, JPG.
-        /// </summary>
-        public Memory.MemoryImage FallbackImage
-        {
-            get => new Memory.MemoryImage(_FallbackImageContent);
-            set => WithFallbackImage(value.GetBuffer());
-        }
-
         public TextureTransformBuilder Transform => _Transform;
         public TextureTransformBuilder Transform => _Transform;
 
 
         public static IEqualityComparer<TextureBuilder> ContentComparer => _ContentComparer.Default;
         public static IEqualityComparer<TextureBuilder> ContentComparer => _ContentComparer.Default;
@@ -165,22 +133,7 @@ namespace SharpGLTF.Materials
 
 
         public TextureBuilder WithCoordinateSet(int cset) { CoordinateSet = cset; return this; }
         public TextureBuilder WithCoordinateSet(int cset) { CoordinateSet = cset; return this; }
 
 
-        [Obsolete("Use WithPrimaryImage instead.")]
-        public TextureBuilder WithImage(string imagePath) { return WithPrimaryImage(imagePath); }
-
-        [Obsolete("Use WithPrimaryImage instead,")]
-        public TextureBuilder WithImage(Memory.MemoryImage image) { return WithPrimaryImage(image); }
-
-        public TextureBuilder WithPrimaryImage(string imagePath)
-        {
-            var primary = System.IO.File
-                .ReadAllBytes(imagePath)
-                .Slice(0);
-
-            return WithPrimaryImage(primary);
-        }
-
-        public TextureBuilder WithPrimaryImage(Memory.MemoryImage image)
+        public TextureBuilder WithPrimaryImage(IMAGEFILE image)
         {
         {
             if (!image.IsEmpty)
             if (!image.IsEmpty)
             {
             {
@@ -191,20 +144,11 @@ namespace SharpGLTF.Materials
                 image = default;
                 image = default;
             }
             }
 
 
-            _PrimaryImageContent = image.GetBuffer();
+            _PrimaryImageContent = image;
             return this;
             return this;
         }
         }
 
 
-        public TextureBuilder WithFallbackImage(string imagePath)
-        {
-            var primary = System.IO.File
-                .ReadAllBytes(imagePath)
-                .Slice(0);
-
-            return WithFallbackImage(primary);
-        }
-
-        public TextureBuilder WithFallbackImage(Memory.MemoryImage image)
+        public TextureBuilder WithFallbackImage(IMAGEFILE image)
         {
         {
             if (!image.IsEmpty)
             if (!image.IsEmpty)
             {
             {
@@ -215,7 +159,7 @@ namespace SharpGLTF.Materials
                 image = default;
                 image = default;
             }
             }
 
 
-            _FallbackImageContent = image.GetBuffer();
+            _FallbackImageContent = image;
             return this;
             return this;
         }
         }
 
 
@@ -246,7 +190,7 @@ namespace SharpGLTF.Materials
 
 
         #endregion
         #endregion
 
 
-        #region support types
+        #region nested types
 
 
         sealed class _ContentComparer : IEqualityComparer<TextureBuilder>
         sealed class _ContentComparer : IEqualityComparer<TextureBuilder>
         {
         {
@@ -254,18 +198,19 @@ namespace SharpGLTF.Materials
 
 
             public bool Equals(TextureBuilder x, TextureBuilder y)
             public bool Equals(TextureBuilder x, TextureBuilder y)
             {
             {
-                return TextureBuilder.AreEqual(x, y);
+                return AreEqualByContent(x, y);
             }
             }
 
 
             public int GetHashCode(TextureBuilder obj)
             public int GetHashCode(TextureBuilder obj)
             {
             {
-                return TextureBuilder.GetContentHashCode(obj);
+                return GetContentHashCode(obj);
             }
             }
         }
         }
 
 
         #endregion
         #endregion
     }
     }
 
 
+    [System.Diagnostics.DebuggerDisplay("Transform {Scale} {Rotation} {Offset}")]
     public class TextureTransformBuilder
     public class TextureTransformBuilder
     {
     {
         #region lifecycle
         #region lifecycle
@@ -302,8 +247,11 @@ namespace SharpGLTF.Materials
         /// </summary>
         /// </summary>
         public int? CoordinateSetOverride { get; set; }
         public int? CoordinateSetOverride { get; set; }
 
 
-        public static bool AreEqual(TextureTransformBuilder a, TextureTransformBuilder b)
+        public static bool AreEqualByContent(TextureTransformBuilder a, TextureTransformBuilder b)
         {
         {
+            if (a != null && a.IsDefault) a = null;
+            if (b != null && b.IsDefault) b = null;
+
             #pragma warning disable IDE0041 // Use 'is null' check
             #pragma warning disable IDE0041 // Use 'is null' check
             if (Object.ReferenceEquals(a, b)) return true;
             if (Object.ReferenceEquals(a, b)) return true;
             if (Object.ReferenceEquals(a, null)) return false;
             if (Object.ReferenceEquals(a, null)) return false;
@@ -331,7 +279,7 @@ namespace SharpGLTF.Materials
                 if (Scale != Vector2.One) return false;
                 if (Scale != Vector2.One) return false;
                 if (Rotation != 0) return false;
                 if (Rotation != 0) return false;
                 if (CoordinateSetOverride.HasValue) return false;
                 if (CoordinateSetOverride.HasValue) return false;
-                return false;
+                return true;
             }
             }
         }
         }
 
 

+ 1 - 1
src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs

@@ -141,7 +141,7 @@ namespace SharpGLTF.Scenes
         /// </summary>
         /// </summary>
         public Transforms.AffineTransform LocalTransform
         public Transforms.AffineTransform LocalTransform
         {
         {
-            get => Transforms.AffineTransform.CreateFromAny(_Matrix, _Scale.Value, _Rotation.Value, Translation.Value);
+            get => Transforms.AffineTransform.CreateFromAny(_Matrix, _Scale?.Value, _Rotation?.Value, Translation?.Value);
             set
             set
             {
             {
                 Guard.IsTrue(value.IsValid, nameof(value));
                 Guard.IsTrue(value.IsValid, nameof(value));

+ 54 - 33
src/SharpGLTF.Toolkit/Schema2/MaterialExtensions.cs

@@ -4,6 +4,8 @@ using System.Linq;
 using System.Numerics;
 using System.Numerics;
 using System.Text;
 using System.Text;
 
 
+using SharpGLTF.Materials;
+
 namespace SharpGLTF.Schema2
 namespace SharpGLTF.Schema2
 {
 {
     public static partial class Schema2Toolkit
     public static partial class Schema2Toolkit
@@ -215,44 +217,63 @@ namespace SharpGLTF.Schema2
             }
             }
         }
         }
 
 
-        public static void CopyTo(this Material srcMaterial, Materials.MaterialBuilder dstMaterial)
+        public static void CopyTo(this Material srcMaterial, MaterialBuilder dstMaterial)
         {
         {
-            Guard.NotNull(srcMaterial, nameof(srcMaterial));
-            Guard.NotNull(dstMaterial, nameof(dstMaterial));
+            _CopyDefaultTo(srcMaterial, dstMaterial);
 
 
-            dstMaterial.Name = srcMaterial.Name;
-            dstMaterial.AlphaMode = srcMaterial.Alpha.ToToolkit();
-            dstMaterial.AlphaCutoff = srcMaterial.AlphaCutoff;
-            dstMaterial.DoubleSided = srcMaterial.DoubleSided;
+            if (srcMaterial.Unlit)
+            {
+                dstMaterial.WithUnlitShader();
+                srcMaterial.CopyChannelsTo(dstMaterial, "BaseColor");
+                return;
+            }
 
 
-            srcMaterial.CopyChannelsTo(dstMaterial, "Normal", "Occlusion", "Emissive");
+            if (srcMaterial.FindChannel("Diffuse") != null || srcMaterial.FindChannel("SpecularGlossiness") != null)
+            {
+                dstMaterial.WithSpecularGlossinessShader();
+                srcMaterial.CopyChannelsTo(dstMaterial, "Diffuse", "SpecularGlossiness");
+                // srcMaterial.CopyChannelsTo(dstMaterial, "ClearCoat", "ClearCoatRoughness", "ClearCoatNormal");
+
+                if (srcMaterial.FindChannel("BaseColor") != null || srcMaterial.FindChannel("MetallicRoughness") != null)
+                {
+                    var fallback = new MaterialBuilder(srcMaterial.Name);
 
 
-            if (srcMaterial.Unlit) dstMaterial.WithUnlitShader();
+                    _CopyDefaultTo(srcMaterial, fallback);
+                    _CopyMetallicRoughnessTo(srcMaterial, fallback);
+
+                    dstMaterial.WithFallback(fallback);
+                }
+
+                return;
+            }
 
 
             if (srcMaterial.FindChannel("BaseColor") != null || srcMaterial.FindChannel("MetallicRoughness") != null)
             if (srcMaterial.FindChannel("BaseColor") != null || srcMaterial.FindChannel("MetallicRoughness") != null)
             {
             {
-                dstMaterial.WithMetallicRoughnessShader();
-                srcMaterial.CopyChannelsTo(dstMaterial, "BaseColor", "MetallicRoughness");
-                srcMaterial.CopyChannelsTo(dstMaterial, "ClearCoat", "ClearCoatRoughness", "ClearCoatNormal");
+                _CopyMetallicRoughnessTo(srcMaterial, dstMaterial);
             }
             }
+        }
 
 
-            if (srcMaterial.FindChannel("Diffuse") != null || srcMaterial.FindChannel("SpecularGlossiness") != null)
-            {
-                dstMaterial = new Materials.MaterialBuilder(srcMaterial.Name).WithFallback(dstMaterial);
+        private static void _CopyMetallicRoughnessTo(Material srcMaterial, MaterialBuilder dstMaterial)
+        {
+            dstMaterial.WithMetallicRoughnessShader();
+            srcMaterial.CopyChannelsTo(dstMaterial, "BaseColor", "MetallicRoughness");
+            srcMaterial.CopyChannelsTo(dstMaterial, "ClearCoat", "ClearCoatRoughness", "ClearCoatNormal");
+        }
 
 
-                dstMaterial.Name = srcMaterial.Name;
-                dstMaterial.AlphaMode = srcMaterial.Alpha.ToToolkit();
-                dstMaterial.AlphaCutoff = srcMaterial.AlphaCutoff;
-                dstMaterial.DoubleSided = srcMaterial.DoubleSided;
+        private static void _CopyDefaultTo(Material srcMaterial, MaterialBuilder dstMaterial)
+        {
+            Guard.NotNull(srcMaterial, nameof(srcMaterial));
+            Guard.NotNull(dstMaterial, nameof(dstMaterial));
 
 
-                srcMaterial.CopyChannelsTo(dstMaterial, "Normal", "Occlusion", "Emissive");
+            dstMaterial.Name = srcMaterial.Name;
+            dstMaterial.AlphaMode = srcMaterial.Alpha.ToToolkit();
+            dstMaterial.AlphaCutoff = srcMaterial.AlphaCutoff;
+            dstMaterial.DoubleSided = srcMaterial.DoubleSided;
 
 
-                dstMaterial.WithSpecularGlossinessShader();
-                srcMaterial.CopyChannelsTo(dstMaterial, "Diffuse", "SpecularGlossiness");
-            }
+            srcMaterial.CopyChannelsTo(dstMaterial, "Normal", "Occlusion", "Emissive");
         }
         }
 
 
-        public static void CopyChannelsTo(this Material srcMaterial, Materials.MaterialBuilder dstMaterial, params string[] channelKeys)
+        public static void CopyChannelsTo(this Material srcMaterial, MaterialBuilder dstMaterial, params string[] channelKeys)
         {
         {
             Guard.NotNull(srcMaterial, nameof(srcMaterial));
             Guard.NotNull(srcMaterial, nameof(srcMaterial));
             Guard.NotNull(dstMaterial, nameof(dstMaterial));
             Guard.NotNull(dstMaterial, nameof(dstMaterial));
@@ -271,7 +292,7 @@ namespace SharpGLTF.Schema2
             }
             }
         }
         }
 
 
-        public static void CopyTo(this MaterialChannel srcChannel, Materials.ChannelBuilder dstChannel)
+        public static void CopyTo(this MaterialChannel srcChannel, ChannelBuilder dstChannel)
         {
         {
             Guard.NotNull(srcChannel, nameof(srcChannel));
             Guard.NotNull(srcChannel, nameof(srcChannel));
             Guard.NotNull(dstChannel, nameof(dstChannel));
             Guard.NotNull(dstChannel, nameof(dstChannel));
@@ -303,7 +324,7 @@ namespace SharpGLTF.Schema2
             dstChannel.Texture.FallbackImage = srcChannel.Texture.FallbackImage?.MemoryImage ?? Memory.MemoryImage.Empty;
             dstChannel.Texture.FallbackImage = srcChannel.Texture.FallbackImage?.MemoryImage ?? Memory.MemoryImage.Empty;
         }
         }
 
 
-        public static void CopyTo(this Materials.MaterialBuilder srcMaterial, Material dstMaterial)
+        public static void CopyTo(this MaterialBuilder srcMaterial, Material dstMaterial)
         {
         {
             Guard.NotNull(srcMaterial, nameof(srcMaterial));
             Guard.NotNull(srcMaterial, nameof(srcMaterial));
             Guard.NotNull(dstMaterial, nameof(dstMaterial));
             Guard.NotNull(dstMaterial, nameof(dstMaterial));
@@ -320,11 +341,11 @@ namespace SharpGLTF.Schema2
 
 
             srcMaterial.CopyChannelsTo(dstMaterial, "Normal", "Occlusion", "Emissive");
             srcMaterial.CopyChannelsTo(dstMaterial, "Normal", "Occlusion", "Emissive");
 
 
-            Materials.MaterialBuilder defMaterial = null;
+            MaterialBuilder defMaterial = null;
 
 
             if (srcMaterial.ShaderStyle == "Unlit")
             if (srcMaterial.ShaderStyle == "Unlit")
             {
             {
-                dstMaterial.InitializePBRMetallicRoughness();
+                dstMaterial.InitializeUnlit();
                 srcMaterial.CopyChannelsTo(dstMaterial, "BaseColor");
                 srcMaterial.CopyChannelsTo(dstMaterial, "BaseColor");
                 return;
                 return;
             }
             }
@@ -346,12 +367,12 @@ namespace SharpGLTF.Schema2
             if (defMaterial != null)
             if (defMaterial != null)
             {
             {
                 if (defMaterial.ShaderStyle != "PBRMetallicRoughness") throw new ArgumentException(nameof(srcMaterial.CompatibilityFallback.ShaderStyle));
                 if (defMaterial.ShaderStyle != "PBRMetallicRoughness") throw new ArgumentException(nameof(srcMaterial.CompatibilityFallback.ShaderStyle));
-                srcMaterial.CopyChannelsTo(dstMaterial, "BaseColor", "MetallicRoughness");
-                srcMaterial.CopyChannelsTo(dstMaterial, "ClearCoat", "ClearCoatRoughness", "ClearCoatNormal");
+                defMaterial.CopyChannelsTo(dstMaterial, "BaseColor", "MetallicRoughness");
+                defMaterial.CopyChannelsTo(dstMaterial, "ClearCoat", "ClearCoatRoughness", "ClearCoatNormal");
             }
             }
         }
         }
 
 
-        public static void CopyChannelsTo(this Materials.MaterialBuilder srcMaterial, Material dstMaterial, params string[] channelKeys)
+        public static void CopyChannelsTo(this MaterialBuilder srcMaterial, Material dstMaterial, params string[] channelKeys)
         {
         {
             Guard.NotNull(srcMaterial, nameof(srcMaterial));
             Guard.NotNull(srcMaterial, nameof(srcMaterial));
             Guard.NotNull(dstMaterial, nameof(dstMaterial));
             Guard.NotNull(dstMaterial, nameof(dstMaterial));
@@ -368,14 +389,14 @@ namespace SharpGLTF.Schema2
             }
             }
         }
         }
 
 
-        public static void CopyTo(this Materials.ChannelBuilder srcChannel, MaterialChannel dstChannel)
+        public static void CopyTo(this ChannelBuilder srcChannel, MaterialChannel dstChannel)
         {
         {
             Guard.NotNull(srcChannel, nameof(srcChannel));
             Guard.NotNull(srcChannel, nameof(srcChannel));
             Guard.NotNull(dstChannel, nameof(dstChannel));
             Guard.NotNull(dstChannel, nameof(dstChannel));
 
 
             dstChannel.Parameter = srcChannel.Parameter;
             dstChannel.Parameter = srcChannel.Parameter;
 
 
-            var srcTex = srcChannel.Texture;
+            var srcTex = srcChannel.GetValidTexture();
             if (srcTex == null) return;
             if (srcTex == null) return;
 
 
             Image primary = null;
             Image primary = null;

+ 21 - 2
tests/SharpGLTF.Tests/ExtensionsTests.cs

@@ -1,12 +1,15 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Text;
 using System.Text;
+using System.Numerics;
 
 
 using NUnit.Framework;
 using NUnit.Framework;
 
 
 
 
 namespace SharpGLTF
 namespace SharpGLTF
 {
 {
+    using System.Numerics;
+
     using Schema2;
     using Schema2;
 
 
     [TestFixture]
     [TestFixture]
@@ -45,11 +48,27 @@ namespace SharpGLTF
             Assert.AreEqual(null, 3.AsNullable(3, 1, 5));
             Assert.AreEqual(null, 3.AsNullable(3, 1, 5));
             Assert.AreEqual(4,    4.AsNullable(3, 1, 5));
             Assert.AreEqual(4,    4.AsNullable(3, 1, 5));
             Assert.AreEqual(5,    5.AsNullable(3, 1, 5));
             Assert.AreEqual(5,    5.AsNullable(3, 1, 5));
-            Assert.AreEqual(5,    6.AsNullable(3, 1, 5));
+            Assert.AreEqual(5,    6.AsNullable(3, 1, 5));            
+
+            // vectors
+
+            Assert.AreEqual(null, Vector2.Zero.AsNullable(Vector2.One, Vector2.One, Vector2.One * 2));
+            Assert.AreEqual(new Vector2(2), new Vector2(3).AsNullable(Vector2.One, Vector2.One, Vector2.One * 2));
+
+            Assert.AreEqual(null, Vector3.Zero.AsNullable(Vector3.One, Vector3.One, Vector3.One * 2));
+            Assert.AreEqual(new Vector3(2), new Vector3(3).AsNullable(Vector3.One, Vector3.One, Vector3.One * 2));
+
+            Assert.AreEqual(null, Vector4.Zero.AsNullable(Vector4.One, Vector4.One, Vector4.One * 2));
+            Assert.AreEqual(new Vector4(2), new Vector4(3).AsNullable(Vector4.One, Vector4.One, Vector4.One * 2));
+
 
 
             // special case: default values outside the min-max range should also return null
             // special case: default values outside the min-max range should also return null
             Assert.AreEqual(null,  0.AsNullable( 0, 1, 5));
             Assert.AreEqual(null,  0.AsNullable( 0, 1, 5));
             Assert.AreEqual(null, 10.AsNullable(10, 1, 5));
             Assert.AreEqual(null, 10.AsNullable(10, 1, 5));
-        }
+
+            Assert.AreEqual(null, Vector2.Zero.AsNullable(Vector2.Zero, Vector2.One, Vector2.One * 5));
+            Assert.AreEqual(null, Vector3.Zero.AsNullable(Vector3.Zero, Vector3.One, Vector3.One * 5));
+            Assert.AreEqual(null, Vector4.Zero.AsNullable(Vector4.Zero, Vector4.One, Vector4.One * 5));
+        }        
     }
     }
 }
 }

+ 1 - 1
tests/SharpGLTF.Tests/Validation/InvalidFilesTests.cs

@@ -64,7 +64,7 @@ namespace SharpGLTF.Validation
 
 
                 var gltfJson = f.EndsWith(".gltf") ? System.IO.File.ReadAllText(f) : string.Empty;
                 var gltfJson = f.EndsWith(".gltf") ? System.IO.File.ReadAllText(f) : string.Empty;
 
 
-                var report = ValidationReport.Load(f + ".report.json");
+                var report = ValidationReport.Load($"{f}.report.json");
                 
                 
                 var result = Schema2.ModelRoot.Validate(f);
                 var result = Schema2.ModelRoot.Validate(f);
 
 

+ 140 - 0
tests/SharpGLTF.Toolkit.Tests/Materials/MaterialBuilderTests.cs

@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+
+using NUnit.Framework;
+
+using SharpGLTF.Schema2;
+using SharpGLTF.Validation;
+
+namespace SharpGLTF.Materials
+{
+    [Category("Toolkit.Materials")]
+    public class MaterialBuilderTests
+    {
+        [Test]
+        public void CreateUnlit()
+        {
+            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(Memory.MemoryImage.DefaultPngImage, new Vector4(0.7f, 0, 0f, 0.8f));
+
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, Schema2Roundtrip(srcMaterial)));
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, srcMaterial.Clone()));
+        }
+
+        [Test]
+        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.
+                .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)
+
+                .WithMetallicRoughnessShader()
+                    .WithBaseColor(Memory.MemoryImage.DefaultPngImage, new Vector4(0.7f, 0, 0f, 0.8f))
+                    .WithMetallicRoughness(Memory.MemoryImage.DefaultPngImage, 0.2f, 0.4f);
+
+            // example of setting additional additional parameters for a given channel.
+            srcMaterial.GetChannel(KnownChannel.BaseColor)
+                .Texture
+                .WithCoordinateSet(1)
+                .WithSampler(TextureWrapMode.CLAMP_TO_EDGE, TextureWrapMode.MIRRORED_REPEAT, TextureMipMapFilter.LINEAR_MIPMAP_LINEAR, TextureInterpolationFilter.NEAREST)
+                .WithTransform(Vector2.One*0.2f, Vector2.One*0.3f,0.1f, 2);
+                
+
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, Schema2Roundtrip(srcMaterial)));
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, srcMaterial.Clone()));            
+        }
+
+        [Test]
+        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.
+                .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)
+
+                .WithMetallicRoughnessShader()
+                    .WithBaseColor(Memory.MemoryImage.DefaultPngImage, new Vector4(0.7f, 0, 0f, 0.8f))
+                    .WithMetallicRoughness(Memory.MemoryImage.DefaultPngImage, 0.2f, 0.4f)
+
+                .WithClearCoat(Memory.MemoryImage.DefaultPngImage, 1)
+                .WithClearCoatNormal(Memory.MemoryImage.DefaultPngImage)
+                .WithClearCoatRoughness(Memory.MemoryImage.DefaultPngImage, 1);
+
+
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, Schema2Roundtrip(srcMaterial)));
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, srcMaterial.Clone()));
+        }
+
+        [Test]
+        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.
+                .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)
+
+                .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);
+                
+            
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, Schema2Roundtrip(srcMaterial)));
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, srcMaterial.Clone()));
+        }
+
+        [Test]
+        public void CreateSpecularGlossinessWithFallback()
+        {
+            var primary = new MaterialBuilder("primary")
+
+                // fallback and primary material must have exactly the same properties
+                .WithDoubleSide(true)
+                .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)
+
+                // primary must use Specular Glossiness shader.
+                .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);
+
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(primary, Schema2Roundtrip(primary)));
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(primary, primary.Clone()));
+        }
+
+        private static MaterialBuilder Schema2Roundtrip(MaterialBuilder srcMaterial)
+        {
+            // converts a MaterialBuilder to a Schema2.Material and back to a MaterialBuilder
+
+            var dstModel = Schema2.ModelRoot.CreateModel();
+            var dstMaterial = dstModel.CreateMaterial(srcMaterial.Name);
+
+            srcMaterial.CopyTo(dstMaterial); // copy MaterialBuilder to Schema2.Material.
+
+            var ctx = new ValidationResult(dstModel,ValidationMode.Strict, true);
+            dstModel.ValidateReferences(ctx.GetContext());
+            dstModel.ValidateContent(ctx.GetContext());
+
+            var rtpMaterial = new MaterialBuilder(dstMaterial.Name);
+
+            dstMaterial.CopyTo(rtpMaterial);// copy Schema2.Material to MaterialBuilder.
+
+            return rtpMaterial;
+        }
+    }
+}