Browse Source

Added KHR_Materials_Volume
Refactored Material Channel Parameters API, so now the Channel parameters can be set separately instead o using a merged Vector4.

Vicente Penades 4 years ago
parent
commit
cd38b918e1

+ 8 - 2
examples/SharpGLTF.Plotly/PlotlyToolkit.cs

@@ -85,10 +85,16 @@ namespace SharpGLTF
 
         private static int GetMaterialColor(MaterialBuilder material)
         {
-            var color = (material.GetChannel(KnownChannel.BaseColor) ?? material.GetChannel(KnownChannel.Diffuse))?.Parameter ?? Vector4.One * 0.8f;
+            var color = Vector4.One;
 
-            color *= 255;
+            var baseColor = material.GetChannel(KnownChannel.BaseColor);
+            if (baseColor != null) color = (Vector4)baseColor.Parameters[KnownProperty.RGBA];
+
+            var diffuseColor = material.GetChannel(KnownChannel.Diffuse);
+            if (diffuseColor != null) color = (Vector4)diffuseColor.Parameters[KnownProperty.RGBA];
 
+            color *= 0.8f;
+            color *= 255;
             var ccc = color.X * 65536 + color.Y * 256 + color.Z;
 
             return (int)ccc;

+ 1 - 0
src/SharpGLTF.Core/Schema2/gltf.ExtensionsFactory.cs

@@ -34,6 +34,7 @@ namespace SharpGLTF.Schema2
             RegisterExtension<Material, MaterialSpecular>("KHR_materials_specular");
             RegisterExtension<Material, MaterialClearCoat>("KHR_materials_clearcoat");
             RegisterExtension<Material, MaterialTransmission>("KHR_materials_transmission");
+            RegisterExtension<Material, MaterialVolume>("KHR_materials_volume");
             RegisterExtension<Material, MaterialPBRSpecularGlossiness>("KHR_materials_pbrSpecularGlossiness");
 
             RegisterExtension<TextureInfo, TextureTransform>("KHR_texture_transform");

+ 148 - 46
src/SharpGLTF.Core/Schema2/gltf.MaterialChannel.cs

@@ -1,6 +1,10 @@
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using System.Numerics;
 
+using PARAMETER = System.Object;
+
 namespace SharpGLTF.Schema2
 {
     /// <summary>
@@ -17,40 +21,7 @@ namespace SharpGLTF.Schema2
     {
         #region lifecycle
 
-        internal MaterialChannel(Material m, String key, Func<Boolean, TextureInfo> texInfo, Single defval, Func<Single> cgetter, Action<Single> csetter)
-            : this(m, key, texInfo)
-        {
-            Guard.NotNull(cgetter, nameof(cgetter));
-            Guard.NotNull(csetter, nameof(csetter));
-
-            _ParameterDefVal = new Vector4(defval, 0, 0, 0);
-            _ParameterGetter = () => new Vector4(cgetter(), 0, 0, 0);
-            _ParameterSetter = value => csetter(value.X);
-        }
-
-        internal MaterialChannel(Material m, String key, Func<Boolean, TextureInfo> texInfo, Vector3 defval, Func<Vector3> cgetter, Action<Vector3> csetter)
-            : this(m, key, texInfo)
-        {
-            Guard.NotNull(cgetter, nameof(cgetter));
-            Guard.NotNull(csetter, nameof(csetter));
-
-            _ParameterDefVal = new Vector4(defval, 0);
-            _ParameterGetter = () => new Vector4(cgetter(), 0);
-            _ParameterSetter = value => csetter(new Vector3(value.X, value.Y, value.Z));
-        }
-
-        internal MaterialChannel(Material m, String key, Func<Boolean, TextureInfo> texInfo, Vector4 defval, Func<Vector4> cgetter, Action<Vector4> csetter)
-            : this(m, key, texInfo)
-        {
-            Guard.NotNull(cgetter, nameof(cgetter));
-            Guard.NotNull(csetter, nameof(csetter));
-
-            _ParameterDefVal = defval;
-            _ParameterGetter = cgetter;
-            _ParameterSetter = csetter;
-        }
-
-        private MaterialChannel(Material m, String key, Func<Boolean, TextureInfo> texInfo)
+        internal MaterialChannel(Material m, String key, Func<Boolean, TextureInfo> texInfo, params MaterialParameter[] parameters)
         {
             Guard.NotNull(m, nameof(m));
             Guard.NotNullOrEmpty(key, nameof(key));
@@ -61,24 +32,24 @@ namespace SharpGLTF.Schema2
             _Material = m;
 
             _TextureInfo = texInfo;
-
-            _ParameterDefVal = Vector4.Zero;
-            _ParameterGetter = null;
-            _ParameterSetter = null;
+            _Parameters = parameters;
         }
 
         #endregion
 
         #region data
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly String _Key;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly Material _Material;
 
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly Func<Boolean, TextureInfo> _TextureInfo;
 
-        private readonly Vector4 _ParameterDefVal;
-        private readonly Func<Vector4> _ParameterGetter;
-        private readonly Action<Vector4> _ParameterSetter;
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+        private readonly IReadOnlyList<MaterialParameter> _Parameters;
 
         public override int GetHashCode()
         {
@@ -102,12 +73,16 @@ namespace SharpGLTF.Schema2
         /// The meaning of the <see cref="Vector4.X"/>, <see cref="Vector4.Y"/>. <see cref="Vector4.Z"/> and <see cref="Vector4.W"/>
         /// depend on the type of channel.
         /// </summary>
+        [Obsolete("Use Parameters[]")]
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         public Vector4 Parameter
         {
-            get => _ParameterGetter();
-            set => _ParameterSetter(value);
+            get => MaterialParameter.Combine(_Parameters);
+            set => MaterialParameter.Apply(_Parameters, value);
         }
 
+        public IReadOnlyList<MaterialParameter> Parameters => _Parameters;
+
         /// <summary>
         /// Gets the <see cref="Texture"/> instance used by this Material, or null.
         /// </summary>
@@ -189,12 +164,139 @@ namespace SharpGLTF.Schema2
 
         private bool _CheckHasDefaultContent()
         {
-            if (this.Parameter != _ParameterDefVal) return false;
             if (this.Texture != null) return false;
+            if (!this._Parameters.All(item => item.IsDefault)) return false;
+            return true;
+        }
 
-            // there's no point in keep checking because if there's no texture, all other elements are irrelevant.
+        #endregion
+    }
 
-            return true;
+    [System.Diagnostics.DebuggerDisplay("[{Name}, {Value}]")]
+    public readonly struct MaterialParameter
+    {
+        #region constants
+
+        internal enum Key
+        {
+            RGB,
+            RGBA,
+
+            NormalScale,
+            OcclusionStrength,
+
+            MetallicFactor,
+            RoughnessFactor,
+            SpecularFactor,
+            GlossinessFactor,
+            ClearCoatFactor,
+            ThicknessFactor,
+            TransmissionFactor,
+            AttenuationDistance,
+        }
+
+        #endregion
+
+        #region constructors
+
+        internal MaterialParameter(Key key, double defval, Func<double?> getter, Action<double?> setter, double min = double.MinValue, double max = double.MaxValue)
+        {
+            _Key = key;
+            _ValueDefault = defval;
+            _ValueGetter = () => (PARAMETER)(float)getter().AsValue(defval);
+            _ValueSetter = value => { double v = (float)value; setter(v.AsNullable(defval, min, max)); };
+        }
+
+        internal MaterialParameter(Key key, float defval, Func<float> getter, Action<float> setter)
+        {
+            _Key = key;
+            _ValueDefault = defval;
+            _ValueGetter = () => (PARAMETER)getter();
+            _ValueSetter = value => setter((float)value);
+        }
+
+        internal MaterialParameter(Key key, Vector2 defval, Func<Vector2> getter, Action<Vector2> setter)
+        {
+            _Key = key;
+            _ValueDefault = defval;
+            _ValueGetter = () => (PARAMETER)getter();
+            _ValueSetter = value => setter((Vector2)value);
+        }
+
+        internal MaterialParameter(Key key, Vector3 defval, Func<Vector3> getter, Action<Vector3> setter)
+        {
+            _Key = key;
+            _ValueDefault = defval;
+            _ValueGetter = () => (PARAMETER)getter();
+            _ValueSetter = value => setter((Vector3)value);
+        }
+
+        internal MaterialParameter(Key key, Vector4 defval, Func<Vector4> getter, Action<Vector4> setter)
+        {
+            _Key = key;
+            _ValueDefault = defval;
+            _ValueGetter = () => (PARAMETER)getter();
+            _ValueSetter = value => setter((Vector4)value);
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly Key _Key;
+        private readonly Object _ValueDefault;
+        private readonly Func<PARAMETER> _ValueGetter;
+        private readonly Action<PARAMETER> _ValueSetter;
+
+        #endregion
+
+        #region properties
+
+        public string Name => _Key.ToString();
+
+        public bool IsDefault => Object.Equals(Value, _ValueDefault);
+
+        public PARAMETER Value
+        {
+            get => _ValueGetter();
+            set => _ValueSetter(value);
+        }
+
+        #endregion
+
+        #region helpers
+        internal static Vector4 Combine(IReadOnlyList<MaterialParameter> parameters)
+        {
+            Span<float> tmp = stackalloc float[4];
+            int idx = 0;
+
+            foreach (var p in parameters)
+            {
+                if (p.Value is Single v1) { tmp[idx++] = v1; }
+                if (p.Value is Vector2 v2) { tmp[idx++] = v2.X; tmp[idx++] = v2.Y; }
+                if (p.Value is Vector3 v3) { tmp[idx++] = v3.X; tmp[idx++] = v3.Y; tmp[idx++] = v3.Z; }
+                if (p.Value is Vector4 v4) { tmp[idx++] = v4.X; tmp[idx++] = v4.Y; tmp[idx++] = v4.Z; tmp[idx++] = v4.W; }
+            }
+
+            return new Vector4(tmp[0], tmp[1], tmp[2], tmp[3]);
+        }
+        internal static void Apply(IReadOnlyList<MaterialParameter> parameters, Vector4 value)
+        {
+            Span<float> tmp = stackalloc float[4];
+            tmp[0] = value.X;
+            tmp[1] = value.Y;
+            tmp[2] = value.Z;
+            tmp[3] = value.W;
+
+            int idx = 0;
+
+            foreach (var p in parameters)
+            {
+                if (p.Value is Single) { p.Value = tmp[idx++]; }
+                if (p.Value is Vector2) { p.Value = new Vector2(tmp[idx + 0], tmp[idx + 1]); idx += 2; }
+                if (p.Value is Vector3) { p.Value = new Vector3(tmp[idx + 0], tmp[idx + 1], tmp[idx + 2]); idx += 3; }
+                if (p.Value is Vector4) { p.Value = new Vector4(tmp[idx + 0], tmp[idx + 1], tmp[idx + 2], tmp[idx + 3]); idx += 4; }
+            }
         }
 
         #endregion

+ 230 - 348
src/SharpGLTF.Core/Schema2/gltf.MaterialsFactory.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Numerics;
 using System.Text;
 
@@ -20,6 +19,7 @@ namespace SharpGLTF.Schema2
             this.RemoveExtensions<MaterialClearCoat>();
             this.RemoveExtensions<MaterialSpecular>();
             this.RemoveExtensions<MaterialTransmission>();
+            this.RemoveExtensions<MaterialVolume>();
             this.RemoveExtensions<MaterialPBRSpecularGlossiness>();
         }
 
@@ -53,6 +53,7 @@ namespace SharpGLTF.Schema2
                 if (extn == "Specular") this.UseExtension<MaterialSpecular>();
                 if (extn == "ClearCoat") this.UseExtension<MaterialClearCoat>();
                 if (extn == "Transmission") this.UseExtension<MaterialTransmission>();
+                if (extn == "Volume") this.UseExtension<MaterialVolume>();
             }
         }
 
@@ -81,79 +82,45 @@ namespace SharpGLTF.Schema2
 
         private IEnumerable<MaterialChannel> _GetChannels()
         {
-            if (_pbrMetallicRoughness != null)
-            {
-                var channels = _pbrMetallicRoughness.GetChannels(this);
-                foreach (var c in channels) yield return c;
-            }
+            var channels = _pbrMetallicRoughness?.GetChannels(this);
+            if (channels != null) { foreach (var c in channels) yield return c; }
 
-            var pbrSpecGloss = this.GetExtension<MaterialPBRSpecularGlossiness>();
-            if (pbrSpecGloss != null)
-            {
-                var channels = pbrSpecGloss.GetChannels(this);
-                foreach (var c in channels) yield return c;
-            }
+            channels = this.GetExtension<MaterialPBRSpecularGlossiness>()?.GetChannels(this);
+            if (channels != null) { foreach (var c in channels) yield return c; }
 
-            var clearCoat = this.GetExtension<MaterialClearCoat>();
-            if (clearCoat != null)
-            {
-                var channels = clearCoat.GetChannels(this);
-                foreach (var c in channels) yield return c;
-            }
+            channels = this.GetExtension<MaterialClearCoat>()?.GetChannels(this);
+            if (channels != null) {foreach (var c in channels) yield return c; }
 
-            var transmission = this.GetExtension<MaterialTransmission>();
-            if (transmission != null)
-            {
-                var channels = transmission.GetChannels(this);
-                foreach (var c in channels) yield return c;
-            }
+            channels = this.GetExtension<MaterialTransmission>()?.GetChannels(this);
+            if (channels != null) { foreach (var c in channels) yield return c; }
 
-            var sheen = this.GetExtension<MaterialSheen>();
-            if (sheen != null)
-            {
-                var channels = sheen.GetChannels(this);
-                foreach (var c in channels) yield return c;
-            }
+            channels = this.GetExtension<MaterialSheen>()?.GetChannels(this);
+            if (channels != null) { foreach (var c in channels) yield return c; }
 
-            var specular = this.GetExtension<MaterialSpecular>();
-            if (specular != null)
-            {
-                var channels = specular.GetChannels(this);
-                foreach (var c in channels) yield return c;
-            }
+            channels = this.GetExtension<MaterialSpecular>()?.GetChannels(this);
+            if (channels != null) { foreach (var c in channels) yield return c; }
 
-            yield return new MaterialChannel
-                (
-                this, "Normal",
-                _GetNormalTexture,
+            channels = this.GetExtension<MaterialVolume>()?.GetChannels(this);
+            if (channels != null) { foreach (var c in channels) yield return c; }
+
+            var normalParam = new MaterialParameter(MaterialParameter.Key.NormalScale,
                 MaterialNormalTextureInfo.ScaleDefault,
                 () => _GetNormalTexture(false)?.Scale ?? MaterialNormalTextureInfo.ScaleDefault,
-                value => _GetNormalTexture(true).Scale = value
-                );
+                value => _GetNormalTexture(true).Scale = value);
 
-            yield return new MaterialChannel
-                (
-                this, "Occlusion",
-                _GetOcclusionTexture,
+            var occlusionParam = new MaterialParameter(MaterialParameter.Key.OcclusionStrength,
                 MaterialOcclusionTextureInfo.StrengthDefault,
                 () => _GetOcclusionTexture(false)?.Strength ?? MaterialOcclusionTextureInfo.StrengthDefault,
-                value => _GetOcclusionTexture(true).Strength = value
-                );
+                value => _GetOcclusionTexture(true).Strength = value);
 
-            yield return new MaterialChannel
-                (
-                this, "Emissive",
-                _GetEmissiveTexture,
-                Vector4.Zero,
-                () => this._EmissiveColor,
-                value => this._EmissiveColor = value
-                );
-        }
+            var emissiveParam = new MaterialParameter(MaterialParameter.Key.RGB,
+                _emissiveFactorDefault,
+                () => this._emissiveFactor.AsValue(_emissiveFactorDefault),
+                value => this._emissiveFactor = value.AsNullable(_emissiveFactorDefault, Vector3.Zero, Vector3.One));
 
-        private Vector4 _EmissiveColor
-        {
-            get => new Vector4(_emissiveFactor.AsValue(_emissiveFactorDefault), 1);
-            set => _emissiveFactor = new Vector3(value.X, value.Y, value.Z).AsNullable(_emissiveFactorDefault, Vector3.Zero, Vector3.One);
+            yield return new MaterialChannel(this, "Normal", _GetNormalTexture, normalParam);
+            yield return new MaterialChannel(this, "Occlusion", _GetOcclusionTexture, occlusionParam);
+            yield return new MaterialChannel(this, "Emissive", _GetEmissiveTexture, emissiveParam);
         }
 
         private MaterialNormalTextureInfo _GetNormalTexture(bool create)
@@ -184,6 +151,24 @@ namespace SharpGLTF.Schema2
             return base.GetLogicalChildren().ConcatElements(_baseColorTexture, _metallicRoughnessTexture);
         }
 
+        protected override void OnValidateContent(ValidationContext validate)
+        {
+            base.OnValidateContent(validate);
+
+            if (_baseColorFactor.HasValue)
+            {
+                Guard.MustBeBetweenOrEqualTo(_baseColorFactor.Value.X, 0, 1, nameof(_baseColorFactor));
+                Guard.MustBeBetweenOrEqualTo(_baseColorFactor.Value.Y, 0, 1, nameof(_baseColorFactor));
+                Guard.MustBeBetweenOrEqualTo(_baseColorFactor.Value.Z, 0, 1, nameof(_baseColorFactor));
+                Guard.MustBeBetweenOrEqualTo(_baseColorFactor.Value.W, 0, 1, nameof(_baseColorFactor));
+            }
+
+            if (_metallicFactor.HasValue)
+            {
+                Guard.MustBeBetweenOrEqualTo(_metallicFactor.Value, _metallicFactorMinimum, _metallicFactorMaximum, nameof(_metallicFactor));
+            }
+        }
+
         private TextureInfo _GetBaseTexture(bool create)
         {
             if (create && _baseColorTexture == null) _baseColorTexture = new TextureInfo();
@@ -202,65 +187,26 @@ namespace SharpGLTF.Schema2
             set => _baseColorFactor = value.AsNullable(_baseColorFactorDefault);
         }
 
-        public static Vector4 ParameterDefault => new Vector4((float)_metallicFactorDefault, (float)_roughnessFactorDefault, 0, 0);
-
-        public Vector4 Parameter
+        public float MetallicFactor
         {
-            get
-            {
-                return new Vector4
-                    (
-                    (float)_metallicFactor.AsValue( _metallicFactorDefault),
-                    (float)_roughnessFactor.AsValue(_roughnessFactorDefault),
-                    0,
-                    0
-                    );
-            }
-            set
-            {
-                _metallicFactor  = ((double)value.X).AsNullable( _metallicFactorDefault,  _metallicFactorMinimum,  _metallicFactorMaximum);
-                _roughnessFactor = ((double)value.Y).AsNullable(_roughnessFactorDefault, _roughnessFactorMinimum, _roughnessFactorMaximum);
-            }
+            get => (float)_metallicFactor.AsValue(_metallicFactorDefault);
+            set => _metallicFactor = ((double)value).AsNullable(_metallicFactorDefault, _metallicFactorMinimum, _metallicFactorMaximum);
         }
 
-        public IEnumerable<MaterialChannel> GetChannels(Material material)
+        public float RoughnessFactor
         {
-            yield return new MaterialChannel
-                (
-                material, "BaseColor",
-                _GetBaseTexture,
-                _baseColorFactorDefault,
-                () => this.Color,
-                value => this.Color = value
-                );
-
-            yield return new MaterialChannel
-                (
-                material,
-                "MetallicRoughness",
-                _GetMetallicTexture,
-                ParameterDefault,
-                () => this.Parameter,
-                value => this.Parameter = value
-                );
+            get => (float)_roughnessFactor.AsValue(_roughnessFactorDefault);
+            set => _roughnessFactor = ((double)value).AsNullable(_roughnessFactorDefault, _roughnessFactorMinimum, _roughnessFactorMaximum);
         }
 
-        protected override void OnValidateContent(ValidationContext validate)
+        public IEnumerable<MaterialChannel> GetChannels(Material material)
         {
-            base.OnValidateContent(validate);
-
-            if (_baseColorFactor.HasValue)
-            {
-                Guard.MustBeBetweenOrEqualTo(_baseColorFactor.Value.X, 0, 1, nameof(_baseColorFactor));
-                Guard.MustBeBetweenOrEqualTo(_baseColorFactor.Value.Y, 0, 1, nameof(_baseColorFactor));
-                Guard.MustBeBetweenOrEqualTo(_baseColorFactor.Value.Z, 0, 1, nameof(_baseColorFactor));
-                Guard.MustBeBetweenOrEqualTo(_baseColorFactor.Value.W, 0, 1, nameof(_baseColorFactor));
-            }
+            var colorParam = new MaterialParameter(MaterialParameter.Key.RGBA, _baseColorFactorDefault, () => Color, v => Color = v);
+            var metallicParam = new MaterialParameter(MaterialParameter.Key.MetallicFactor, (float)_metallicFactorDefault, () => MetallicFactor, v => MetallicFactor = v);
+            var roughnessParam = new MaterialParameter(MaterialParameter.Key.RoughnessFactor, (float)_roughnessFactorDefault, () => RoughnessFactor, v => RoughnessFactor = v);
 
-            if (_metallicFactor.HasValue)
-            {
-                Guard.MustBeBetweenOrEqualTo(_metallicFactor.Value, _metallicFactorMinimum, _metallicFactorMaximum, nameof(_metallicFactor));
-            }
+            yield return new MaterialChannel(material, "BaseColor", _GetBaseTexture, colorParam);
+            yield return new MaterialChannel(material, "MetallicRoughness", _GetMetallicTexture, metallicParam, roughnessParam);
         }
     }
 
@@ -275,6 +221,23 @@ namespace SharpGLTF.Schema2
             return base.GetLogicalChildren().ConcatElements(_diffuseTexture, _specularGlossinessTexture);
         }
 
+        protected override void OnValidateContent(ValidationContext validate)
+        {
+            base.OnValidateContent(validate);
+
+            if (_specularFactor.HasValue)
+            {
+                Guard.MustBeBetweenOrEqualTo(_specularFactor.Value.X, 0, 1, nameof(_specularFactor));
+                Guard.MustBeBetweenOrEqualTo(_specularFactor.Value.Y, 0, 1, nameof(_specularFactor));
+                Guard.MustBeBetweenOrEqualTo(_specularFactor.Value.Z, 0, 1, nameof(_specularFactor));
+            }
+
+            if (_glossinessFactor.HasValue)
+            {
+                Guard.MustBeBetweenOrEqualTo(_glossinessFactor.Value, _glossinessFactorMinimum, _glossinessFactorMaximum, nameof(_glossinessFactor));
+            }
+        }
+
         private TextureInfo _GetDiffuseTexture(bool create)
         {
             if (create && _diffuseTexture == null) _diffuseTexture = new TextureInfo();
@@ -287,61 +250,32 @@ namespace SharpGLTF.Schema2
             return _specularGlossinessTexture;
         }
 
-        public static Vector4 ParameterDefault => new Vector4(_specularFactorDefault, (float)_glossinessFactorDefault);
-
-        public Vector4 Parameter
+        public Vector4 DiffuseFactor
         {
-            get
-            {
-                return new Vector4
-                    (
-                    _specularFactor.AsValue(_specularFactorDefault),
-                    (float)_glossinessFactor.AsValue(_glossinessFactorDefault)
-                    );
-            }
-            set
-            {
-                _specularFactor = new Vector3(value.X, value.Y, value.Z).AsNullable(_specularFactorDefault);
-                _glossinessFactor = ((double)value.W).AsNullable(_glossinessFactorDefault, _glossinessFactorMinimum, _glossinessFactorMaximum);
-            }
+            get => _diffuseFactor.AsValue(_diffuseFactorDefault);
+            set => _diffuseFactor = _diffuseFactor = value.AsNullable(_diffuseFactorDefault);
         }
 
-        public IEnumerable<MaterialChannel> GetChannels(Material material)
+        public Vector3 SpecularFactor
         {
-            yield return new MaterialChannel
-                (
-                material, "Diffuse",
-                _GetDiffuseTexture,
-                _diffuseFactorDefault,
-                () => _diffuseFactor.AsValue(_diffuseFactorDefault),
-                value => _diffuseFactor = value.AsNullable(_diffuseFactorDefault)
-                );
-
-            yield return new MaterialChannel
-                (
-                material, "SpecularGlossiness",
-                _GetGlossinessTexture,
-                ParameterDefault,
-                () => this.Parameter,
-                value => this.Parameter = value
-                );
+            get => _specularFactor.AsValue(_specularFactorDefault);
+            set => _specularFactor = value.AsNullable(_specularFactorDefault);
         }
 
-        protected override void OnValidateContent(ValidationContext validate)
+        public float GlossinessFactor
         {
-            base.OnValidateContent(validate);
+            get => (float)_glossinessFactor.AsValue(_glossinessFactorDefault);
+            set => _glossinessFactor = ((double)value).AsNullable(_glossinessFactorDefault, _glossinessFactorMinimum, _glossinessFactorMaximum);
+        }
 
-            if (_specularFactor.HasValue)
-            {
-                Guard.MustBeBetweenOrEqualTo(_specularFactor.Value.X, 0, 1, nameof(_specularFactor));
-                Guard.MustBeBetweenOrEqualTo(_specularFactor.Value.Y, 0, 1, nameof(_specularFactor));
-                Guard.MustBeBetweenOrEqualTo(_specularFactor.Value.Z, 0, 1, nameof(_specularFactor));
-            }
+        public IEnumerable<MaterialChannel> GetChannels(Material material)
+        {
+            var diffuseParam = new MaterialParameter(MaterialParameter.Key.RGBA, _diffuseFactorDefault, () => DiffuseFactor, v => DiffuseFactor = v);
+            var specularParam = new MaterialParameter(MaterialParameter.Key.SpecularFactor, _specularFactorDefault, () => SpecularFactor, v => SpecularFactor = v);
+            var glossinessParam = new MaterialParameter(MaterialParameter.Key.GlossinessFactor, (float)_glossinessFactorDefault, () => GlossinessFactor, v => GlossinessFactor = v);
 
-            if (_glossinessFactor.HasValue)
-            {
-                Guard.MustBeBetweenOrEqualTo(_glossinessFactor.Value, _glossinessFactorMinimum, _glossinessFactorMaximum, nameof(_glossinessFactor));
-            }
+            yield return new MaterialChannel(material, "Diffuse", _GetDiffuseTexture, diffuseParam);
+            yield return new MaterialChannel(material, "SpecularGlossiness", _GetGlossinessTexture, specularParam, glossinessParam);
         }
     }
 
@@ -363,6 +297,21 @@ namespace SharpGLTF.Schema2
             return base.GetLogicalChildren().ConcatElements(_clearcoatTexture, _clearcoatRoughnessTexture, _clearcoatNormalTexture);
         }
 
+        protected override void OnValidateContent(ValidationContext validate)
+        {
+            base.OnValidateContent(validate);
+
+            if (_clearcoatFactor.HasValue)
+            {
+                Guard.MustBeBetweenOrEqualTo(_clearcoatFactor.Value, _clearcoatFactorMinimum, _clearcoatFactorMaximum, nameof(_clearcoatFactor));
+            }
+
+            if (_clearcoatRoughnessFactor.HasValue)
+            {
+                Guard.MustBeBetweenOrEqualTo(_clearcoatRoughnessFactor.Value, _clearcoatRoughnessFactorMinimum, _clearcoatRoughnessFactorMaximum, nameof(_clearcoatRoughnessFactor));
+            }
+        }
+
         private TextureInfo _GetClearCoatTexture(bool create)
         {
             if (create && _clearcoatTexture == null) _clearcoatTexture = new TextureInfo();
@@ -381,49 +330,30 @@ namespace SharpGLTF.Schema2
             return _clearcoatNormalTexture;
         }
 
-        public IEnumerable<MaterialChannel> GetChannels(Material material)
+        public float ClearCoatFactor
         {
-            yield return new MaterialChannel
-                (
-                material, "ClearCoat",
-                _GetClearCoatTexture,
-                (float)_clearcoatFactorDefault,
-                () => (float)this._clearcoatFactor.AsValue(_clearcoatFactorDefault),
-                value => this._clearcoatFactor = value.AsNullable((float)_clearcoatFactorDefault)
-                );
-
-            yield return new MaterialChannel
-                (
-                material, "ClearCoatRoughness",
-                _GetClearCoatRoughnessTexture,
-                (float)_clearcoatRoughnessFactorDefault,
-                () => (float)this._clearcoatRoughnessFactor.AsValue(_clearcoatRoughnessFactorDefault),
-                value => this._clearcoatRoughnessFactor = value.AsNullable((float)_clearcoatRoughnessFactorDefault)
-                );
-
-            yield return new MaterialChannel
-                (
-                material, "ClearCoatNormal",
-                _GetClearCoatNormalTexture,
-                MaterialNormalTextureInfo.ScaleDefault,
-                () => _GetClearCoatNormalTexture(false)?.Scale ?? MaterialNormalTextureInfo.ScaleDefault,
-                value => _GetClearCoatNormalTexture(true).Scale = value
-                );
+            get => (float)this._clearcoatFactor.AsValue(_clearcoatFactorDefault);
+            set => this._clearcoatFactor = value.AsNullable((float)_clearcoatFactorDefault);
         }
 
-        protected override void OnValidateContent(ValidationContext validate)
+        public float RoughnessFactor
         {
-            base.OnValidateContent(validate);
+            get => (float)this._clearcoatRoughnessFactor.AsValue(_clearcoatRoughnessFactorDefault);
+            set => this._clearcoatRoughnessFactor = value.AsNullable((float)_clearcoatRoughnessFactorDefault);
+        }
 
-            if (_clearcoatFactor.HasValue)
-            {
-                Guard.MustBeBetweenOrEqualTo(_clearcoatFactor.Value, _clearcoatFactorMinimum, _clearcoatFactorMaximum, nameof(_clearcoatFactor));
-            }
+        public IEnumerable<MaterialChannel> GetChannels(Material material)
+        {
+            var clearCoatParam = new MaterialParameter(MaterialParameter.Key.ClearCoatFactor, (float)_clearcoatFactorDefault, () => ClearCoatFactor, v => ClearCoatFactor = v);
+            var roughnessParam = new MaterialParameter(MaterialParameter.Key.RoughnessFactor, (float)_clearcoatRoughnessFactorDefault, () => RoughnessFactor, v => RoughnessFactor = v);
+            var normScaleParam = new MaterialParameter(MaterialParameter.Key.NormalScale,
+                MaterialNormalTextureInfo.ScaleDefault,
+                () => _GetClearCoatNormalTexture(false)?.Scale ?? MaterialNormalTextureInfo.ScaleDefault,
+                v => _GetClearCoatNormalTexture(true).Scale = v);
 
-            if (_clearcoatRoughnessFactor.HasValue)
-            {
-                Guard.MustBeBetweenOrEqualTo(_clearcoatRoughnessFactor.Value, _clearcoatRoughnessFactorMinimum, _clearcoatRoughnessFactorMaximum, nameof(_clearcoatRoughnessFactor));
-            }
+            yield return new MaterialChannel(material, "ClearCoat", _GetClearCoatTexture, clearCoatParam);
+            yield return new MaterialChannel(material, "ClearCoatRoughness", _GetClearCoatRoughnessTexture, roughnessParam);
+            yield return new MaterialChannel(material, "ClearCoatNormal", _GetClearCoatNormalTexture, normScaleParam);
         }
     }
 
@@ -438,24 +368,6 @@ namespace SharpGLTF.Schema2
             return base.GetLogicalChildren().ConcatElements(_transmissionTexture);
         }
 
-        public IEnumerable<MaterialChannel> GetChannels(Material material)
-        {
-            yield return new MaterialChannel
-                (
-                material, "Transmission",
-                _GetTransmissionTexture,
-                (float)_transmissionFactorDefault,
-                () => (float)this._transmissionFactor.AsValue(_transmissionFactorDefault),
-                value => this._transmissionFactor = value.AsNullable((float)_transmissionFactorDefault)
-                );
-        }
-
-        private TextureInfo _GetTransmissionTexture(bool create)
-        {
-            if (create && _transmissionTexture == null) _transmissionTexture = new TextureInfo();
-            return _transmissionTexture;
-        }
-
         protected override void OnValidateContent(ValidationContext validate)
         {
             base.OnValidateContent(validate);
@@ -465,52 +377,37 @@ namespace SharpGLTF.Schema2
                 Guard.MustBeBetweenOrEqualTo(_transmissionFactor.Value, _transmissionFactorMinimum, _transmissionFactorMaximum, nameof(_transmissionFactor));
             }
         }
-    }
-
-    internal sealed partial class MaterialSheen
-    {
-        #pragma warning disable CA1801 // Review unused parameters
-        internal MaterialSheen(Material material) { }
-        #pragma warning restore CA1801 // Review unused parameters
 
-        protected override IEnumerable<ExtraProperties> GetLogicalChildren()
+        public float TransmissionFactor
         {
-            return base.GetLogicalChildren().ConcatElements(_sheenColorTexture, _sheenRoughnessTexture);
+            get => (float)this._transmissionFactor.AsValue(_transmissionFactorDefault);
+            set => this._transmissionFactor = value.AsNullable((float)_transmissionFactorDefault);
         }
 
         public IEnumerable<MaterialChannel> GetChannels(Material material)
         {
-            yield return new MaterialChannel
-                (
-                material, "SheenColor",
-                _GetSheenColorTexture,
-                _sheenColorFactorDefault,
-                () => _sheenColorFactor.AsValue(_sheenColorFactorDefault),
-                value => this._sheenColorFactor = value.AsNullable(_sheenColorFactorDefault)
-                );
-
-            yield return new MaterialChannel
-                (
-                material, "SheenRoughness",
-                _GetSheenRoughnessTexture,
-                _sheenRoughnessFactorDefault,
-                () => _sheenRoughnessFactor.AsValue(_sheenRoughnessFactorDefault),
-                value => this._sheenRoughnessFactor = value.AsNullable(_sheenRoughnessFactorDefault)
-                );
+            var transmissionParam = new MaterialParameter(MaterialParameter.Key.TransmissionFactor, (float)_transmissionFactorDefault, () => TransmissionFactor, v => TransmissionFactor = v);
+
+            yield return new MaterialChannel(material, "Transmission", _GetTransmissionTexture, transmissionParam);
         }
 
-        private TextureInfo _GetSheenColorTexture(bool create)
+        private TextureInfo _GetTransmissionTexture(bool create)
         {
-            if (create && _sheenColorTexture == null) _sheenColorTexture = new TextureInfo();
-            return _sheenColorTexture;
+            if (create && _transmissionTexture == null) _transmissionTexture = new TextureInfo();
+            return _transmissionTexture;
         }
+    }
 
-        private TextureInfo _GetSheenRoughnessTexture(bool create)
+    internal sealed partial class MaterialSheen
+    {
+        #pragma warning disable CA1801 // Review unused parameters
+        internal MaterialSheen(Material material) { }
+        #pragma warning restore CA1801 // Review unused parameters
+
+        protected override IEnumerable<ExtraProperties> GetLogicalChildren()
         {
-            if (create && _sheenRoughnessTexture == null) _sheenRoughnessTexture = new TextureInfo();
-            return _sheenRoughnessTexture;
+            return base.GetLogicalChildren().ConcatElements(_sheenColorTexture, _sheenRoughnessTexture);
         }
-
         protected override void OnValidateContent(ValidationContext validate)
         {
             base.OnValidateContent(validate);
@@ -527,6 +424,39 @@ namespace SharpGLTF.Schema2
                 Guard.MustBeBetweenOrEqualTo(_sheenRoughnessFactor.Value, _sheenRoughnessFactorMinimum, _sheenRoughnessFactorMaximum, nameof(_sheenRoughnessFactor));
             }
         }
+
+        public Vector3 ColorFactor
+        {
+            get => _sheenColorFactor.AsValue(_sheenColorFactorDefault);
+            set => this._sheenColorFactor = value.AsNullable(_sheenColorFactorDefault);
+        }
+
+        public float RoughnessFactor
+        {
+            get => _sheenRoughnessFactor.AsValue(_sheenRoughnessFactorDefault);
+            set => this._sheenRoughnessFactor = value.AsNullable(_sheenRoughnessFactorDefault);
+        }
+
+        public IEnumerable<MaterialChannel> GetChannels(Material material)
+        {
+            var colorParam = new MaterialParameter(MaterialParameter.Key.RGB, _sheenColorFactorDefault, () => ColorFactor, v => ColorFactor = v);
+            var roughnessParam = new MaterialParameter(MaterialParameter.Key.RoughnessFactor, _sheenRoughnessFactorDefault, () => RoughnessFactor, v => RoughnessFactor = v);
+
+            yield return new MaterialChannel(material, "SheenColor", _GetSheenColorTexture, colorParam);
+            yield return new MaterialChannel(material, "SheenRoughness", _GetSheenRoughnessTexture, roughnessParam);
+        }
+
+        private TextureInfo _GetSheenColorTexture(bool create)
+        {
+            if (create && _sheenColorTexture == null) _sheenColorTexture = new TextureInfo();
+            return _sheenColorTexture;
+        }
+
+        private TextureInfo _GetSheenRoughnessTexture(bool create)
+        {
+            if (create && _sheenRoughnessTexture == null) _sheenRoughnessTexture = new TextureInfo();
+            return _sheenRoughnessTexture;
+        }
     }
 
     internal sealed partial class MaterialIOR
@@ -535,14 +465,6 @@ namespace SharpGLTF.Schema2
         internal MaterialIOR(Material material) { }
         #pragma warning restore CA1801 // Review unused parameters
 
-        public static float DefaultIndexOfRefraction => (float)_iorDefault;
-
-        public float IndexOfRefraction
-        {
-            get => (float)(this._ior ?? _iorDefault);
-            set => this._ior = ((double)value).AsNullable(_iorDefault);
-        }
-
         protected override void OnValidateContent(ValidationContext validate)
         {
             base.OnValidateContent(validate);
@@ -550,6 +472,14 @@ namespace SharpGLTF.Schema2
             if (_ior == 0) return; // a value of 0 is allowed by the spec as a special value
             if (_ior < 1) throw new ArgumentOutOfRangeException(nameof(IndexOfRefraction));
         }
+
+        public static float DefaultIndexOfRefraction => (float)_iorDefault;
+
+        public float IndexOfRefraction
+        {
+            get => (float)(this._ior ?? _iorDefault);
+            set => this._ior = ((double)value).AsNullable(_iorDefault);
+        }
     }
 
     internal sealed partial class MaterialSpecular
@@ -563,6 +493,23 @@ namespace SharpGLTF.Schema2
             return base.GetLogicalChildren().ConcatElements(_specularColorTexture, _specularTexture);
         }
 
+        protected override void OnValidateContent(ValidationContext validate)
+        {
+            base.OnValidateContent(validate);
+
+            if (_specularColorFactor.HasValue)
+            {
+                Guard.MustBeBetweenOrEqualTo(_specularColorFactor.Value.X, 0, float.MaxValue, nameof(_specularColorFactor));
+                Guard.MustBeBetweenOrEqualTo(_specularColorFactor.Value.Y, 0, float.MaxValue, nameof(_specularColorFactor));
+                Guard.MustBeBetweenOrEqualTo(_specularColorFactor.Value.Z, 0, float.MaxValue, nameof(_specularColorFactor));
+            }
+
+            if (_specularFactor.HasValue)
+            {
+                Guard.MustBeBetweenOrEqualTo(_specularFactor.Value, _specularFactorMinimum, _specularFactorMaximum, nameof(_specularFactor));
+            }
+        }
+
         private TextureInfo _GetSpecularColorTexture(bool create)
         {
             if (create && _specularColorTexture == null) _specularColorTexture = new TextureInfo();
@@ -581,8 +528,6 @@ namespace SharpGLTF.Schema2
             set => _specularColorFactor = value.AsNullable(_specularColorFactorDefault);
         }
 
-        public static float SpecularFactorDefault => (float)_specularFactorDefault;
-
         public float SpecularFactor
         {
             get => (float)_specularFactor.AsValue(_specularFactorDefault);
@@ -591,56 +536,42 @@ namespace SharpGLTF.Schema2
 
         public IEnumerable<MaterialChannel> GetChannels(Material material)
         {
-            yield return new MaterialChannel
-                (
-                material,
-                "SpecularColor",
-                _GetSpecularColorTexture,
-                _specularColorFactorDefault,
-                () => this.SpecularColor,
-                value => this.SpecularColor = value
-                );
-
-            yield return new MaterialChannel
-                (
-                material,
-                "SpecularFactor",
-                _GetSpecularFactorTexture,
-                SpecularFactorDefault,
-                () => this.SpecularFactor,
-                value => this.SpecularFactor = value
-                );
-        }
+            var colorParam = new MaterialParameter(MaterialParameter.Key.RGB, _specularColorFactorDefault, () => SpecularColor, v => SpecularColor = v);
+            var factorParam = new MaterialParameter(MaterialParameter.Key.SpecularFactor, (float)_specularFactorDefault, () => SpecularFactor, v => SpecularFactor = v);
 
-        protected override void OnValidateContent(ValidationContext validate)
-        {
-            base.OnValidateContent(validate);
-
-            if (_specularColorFactor.HasValue)
-            {
-                Guard.MustBeBetweenOrEqualTo(_specularColorFactor.Value.X, 0, float.MaxValue, nameof(_specularColorFactor));
-                Guard.MustBeBetweenOrEqualTo(_specularColorFactor.Value.Y, 0, float.MaxValue, nameof(_specularColorFactor));
-                Guard.MustBeBetweenOrEqualTo(_specularColorFactor.Value.Z, 0, float.MaxValue, nameof(_specularColorFactor));
-            }
-
-            if (_specularFactor.HasValue)
-            {
-                Guard.MustBeBetweenOrEqualTo(_specularFactor.Value, _specularFactorMinimum, _specularFactorMaximum, nameof(_specularFactor));
-            }
+            yield return new MaterialChannel(material, "SpecularColor", _GetSpecularColorTexture, colorParam);
+            yield return new MaterialChannel(material, "SpecularFactor", _GetSpecularFactorTexture, factorParam);
         }
     }
 
     internal sealed partial class MaterialVolume
     {
-#pragma warning disable CA1801 // Review unused parameters
+        #pragma warning disable CA1801 // Review unused parameters
         internal MaterialVolume(Material material) { }
-#pragma warning restore CA1801 // Review unused parameters
+        #pragma warning restore CA1801 // Review unused parameters
 
         protected override IEnumerable<ExtraProperties> GetLogicalChildren()
         {
             return base.GetLogicalChildren().ConcatElements(_thicknessTexture);
         }
 
+        protected override void OnValidateContent(ValidationContext validate)
+        {
+            base.OnValidateContent(validate);
+
+            if (_attenuationColor.HasValue)
+            {
+                Guard.MustBeBetweenOrEqualTo(_attenuationColor.Value.X, 0, float.MaxValue, nameof(_attenuationColor));
+                Guard.MustBeBetweenOrEqualTo(_attenuationColor.Value.Y, 0, float.MaxValue, nameof(_attenuationColor));
+                Guard.MustBeBetweenOrEqualTo(_attenuationColor.Value.Z, 0, float.MaxValue, nameof(_attenuationColor));
+            }
+
+            if (_thicknessFactor.HasValue)
+            {
+                Guard.MustBeBetweenOrEqualTo(_thicknessFactor.Value, _thicknessFactorMinimum, float.MaxValue, nameof(_thicknessFactor));
+            }
+        }
+
         private TextureInfo _GetThicknessTexture(bool create)
         {
             if (create && _thicknessTexture == null) _thicknessTexture = new TextureInfo();
@@ -661,67 +592,18 @@ namespace SharpGLTF.Schema2
 
         public float AttenuationDistance
         {
-            get => (float)_attenuationDistance;
+            get => (float)_attenuationDistance.AsValue((double)0);
             set => _attenuationDistance = value > _attenuationDistanceExclusiveMinimum ? value : throw new ArgumentOutOfRangeException();
         }
 
-        private static Vector4 _AttenuationDefault => new Vector4(_attenuationColorDefault, float.MaxValue);
-
-        private Vector4 _Attenuation
-        {
-            get
-            {
-                return new Vector4
-                    (
-                    _attenuationColor.AsValue(_attenuationColorDefault),
-                    (float)_attenuationDistance.AsValue(float.MaxValue)
-                    );
-            }
-            set
-            {
-                _attenuationColor = new Vector3(value.X, value.Y, value.Z).AsNullable(_attenuationColorDefault);
-                _attenuationDistance = ((double)value.W).AsNullable(float.MaxValue, _attenuationDistanceExclusiveMinimum, float.PositiveInfinity);
-            }
-        }
-
         public IEnumerable<MaterialChannel> GetChannels(Material material)
         {
-            yield return new MaterialChannel
-                (
-                material,
-                "VolumeThickness",
-                _GetThicknessTexture,
-                (float)_thicknessFactorDefault,
-                () => ThicknessFactor,
-                value => ThicknessFactor = value
-                );
-
-            yield return new MaterialChannel
-                (
-                material,
-                "VolumeAttenuation",
-                null,
-                _AttenuationDefault,
-                () => _Attenuation,
-                value => _Attenuation = value
-                );
-        }
+            var thicknessParam = new MaterialParameter(MaterialParameter.Key.ThicknessFactor, (float)_thicknessFactorDefault, () => ThicknessFactor, v => ThicknessFactor = v);
+            var attColorParam = new MaterialParameter(MaterialParameter.Key.RGB, _attenuationColorDefault, () => AttenuationColor, v => AttenuationColor = v);
+            var attDistParam = new MaterialParameter(MaterialParameter.Key.AttenuationDistance, 0, () => AttenuationDistance, v => AttenuationDistance = v);
 
-        protected override void OnValidateContent(ValidationContext validate)
-        {
-            base.OnValidateContent(validate);
-
-            if (_attenuationColor.HasValue)
-            {
-                Guard.MustBeBetweenOrEqualTo(_attenuationColor.Value.X, 0, float.MaxValue, nameof(_attenuationColor));
-                Guard.MustBeBetweenOrEqualTo(_attenuationColor.Value.Y, 0, float.MaxValue, nameof(_attenuationColor));
-                Guard.MustBeBetweenOrEqualTo(_attenuationColor.Value.Z, 0, float.MaxValue, nameof(_attenuationColor));
-            }
-
-            if (_thicknessFactor.HasValue)
-            {
-                Guard.MustBeBetweenOrEqualTo(_thicknessFactor.Value, _thicknessFactorMinimum, float.MaxValue, nameof(_thicknessFactor));
-            }
+            yield return new MaterialChannel(material, "VolumeThickness", _GetThicknessTexture, thicknessParam);
+            yield return new MaterialChannel(material, "VolumeAttenuation", onCreate => null, attColorParam, attDistParam);
         }
     }
 }

+ 37 - 60
src/SharpGLTF.Toolkit/Materials/ChannelBuilder.cs

@@ -18,46 +18,10 @@ namespace SharpGLTF.Materials
         {
             var txt = Key.ToString();
 
-            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:
-                    case KnownChannel.SpecularFactor:
-                        txt += $" {Parameter.X}"; break;
-
-                    case KnownChannel.Emissive:
-                        txt += $" ({rgb})"; break;
-
-                    case KnownChannel.Diffuse:
-                    case KnownChannel.BaseColor:
-                    case KnownChannel.SpecularColor:
-                        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();
             if (tex?.PrimaryImage != null)
             {
-                if (hasParam) txt += " ×";
+                // if (hasParam) txt += " ×";
                 txt += $" {tex.PrimaryImage.Content.ToDebuggerDisplay()}";
             }
 
@@ -75,7 +39,7 @@ namespace SharpGLTF.Materials
             _Parent = parent;
             _Key = key;
 
-            SetDefaultParameter();
+            _Parameters = MaterialValue.CreateDefaultProperties(key);
         }
 
         #endregion
@@ -88,11 +52,8 @@ namespace SharpGLTF.Materials
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         private readonly KnownChannel _Key;
 
-        /// <summary>
-        /// Gets or sets the <see cref="ChannelBuilder"/> paramenter.
-        /// Its meaning depends on <see cref="Key"/>.
-        /// </summary>
-        public Vector4 Parameter { get; set; }
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly MaterialValue.Collection _Parameters;
 
         public TextureBuilder Texture { get; private set; }
 
@@ -102,7 +63,7 @@ namespace SharpGLTF.Materials
 
             if (x._Key != y._Key) return false;
 
-            if (x.Parameter != y.Parameter) return false;
+            if (!MaterialValue.Collection.AreEqual(x._Parameters, y._Parameters)) return false;
 
             if (!TextureBuilder.AreEqualByContent(x.Texture, y.Texture)) return false;
 
@@ -115,7 +76,7 @@ namespace SharpGLTF.Materials
 
             var h = x._Key.GetHashCode();
 
-            h ^= x.Parameter.GetHashCode();
+            h ^= x._Parameters.GetHashCode();
 
             h ^= TextureBuilder.GetContentHashCode(x.Texture);
 
@@ -131,13 +92,34 @@ namespace SharpGLTF.Materials
         /// </summary>
         public KnownChannel Key => _Key;
 
+        /// <summary>
+        /// Gets or sets the <see cref="ChannelBuilder"/> parameter.
+        /// </summary>
+        /// <remarks>
+        /// Its meaning differs depending on the value of <see cref="Key"/>.
+        /// </remarks>
+        [Obsolete("Use .Parameters[KnownProperty] or .Parameters.CombinedVector")]
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        public Vector4 Parameter
+        {
+            get => _Parameters.CombinedVector;
+            set => _Parameters.CombinedVector = value;
+        }
+
+        /// <summary>
+        /// Gets the collection of parameters of this channel
+        /// </summary>
+        public MaterialValue.Collection Parameters => _Parameters;
+
+        /// <summary>
+        /// Gets an equality comparer that deep compares the internal fields and collections.
+        /// </summary>
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
         public static IEqualityComparer<ChannelBuilder> ContentComparer => _ContentComparer.Default;
 
         #endregion
 
         #region API
-
         public TextureBuilder GetValidTexture()
         {
             if (Texture == null) return null;
@@ -145,9 +127,17 @@ namespace SharpGLTF.Materials
             return Texture;
         }
 
+        public TextureBuilder UseTexture()
+        {
+            if (Texture == null) Texture = new TextureBuilder(this);
+            return Texture;
+        }
+
+        public void RemoveTexture() { Texture = null; }
+
         internal void CopyTo(ChannelBuilder other)
         {
-            other.Parameter = this.Parameter;
+            this._Parameters.CopyTo(other._Parameters);
 
             if (this.Texture == null)
             {
@@ -159,19 +149,6 @@ namespace SharpGLTF.Materials
             }
         }
 
-        public void SetDefaultParameter()
-        {
-            this.Parameter = _GetDefaultParameter(_Key);
-        }
-
-        public TextureBuilder UseTexture()
-        {
-            if (Texture == null) Texture = new TextureBuilder(this);
-            return Texture;
-        }
-
-        public void RemoveTexture() { Texture = null; }
-
         #endregion
 
         #region Nested types

+ 54 - 62
src/SharpGLTF.Toolkit/Materials/MaterialBuilder.cs

@@ -14,7 +14,7 @@ namespace SharpGLTF.Materials
     /// Represents the root object of a material instance structure.
     /// </summary>
     [System.Diagnostics.DebuggerDisplay("{_DebuggerDisplay(),nq}")]
-    public class MaterialBuilder : BaseBuilder
+    public partial class MaterialBuilder : BaseBuilder
     {
         #region debug
 
@@ -40,32 +40,6 @@ namespace SharpGLTF.Materials
         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,
-            KnownChannel.Transmission,
-            KnownChannel.SheenColor,
-            KnownChannel.SheenRoughness,
-        };
-
-        private static readonly KnownChannel[] _SpeGloChannels = new[]
-        {
-            KnownChannel.Diffuse,
-            KnownChannel.SpecularGlossiness,
-            KnownChannel.Normal,
-            KnownChannel.Occlusion,
-            KnownChannel.Emissive,
-        };
-
         #endregion
 
         #region lifecycle
@@ -228,17 +202,6 @@ namespace SharpGLTF.Materials
             }
         }
 
-        private IReadOnlyList<KnownChannel> _GetValidChannels()
-        {
-            switch (ShaderStyle)
-            {
-                case SHADERUNLIT: return _UnlitChannels;
-                case SHADERPBRMETALLICROUGHNESS: return _MetRouChannels;
-                case SHADERPBRSPECULARGLOSSINESS: return _SpeGloChannels;
-                default: throw new NotImplementedException();
-            }
-        }
-
         public ChannelBuilder GetChannel(KnownChannel channelKey)
         {
             return _Channels.FirstOrDefault(item => item.Key == channelKey);
@@ -338,19 +301,19 @@ namespace SharpGLTF.Materials
         public MaterialBuilder WithShader(string shader) { _SetShader(shader); return this; }
 
         /// <summary>
-        /// Sets <see cref="MaterialBuilder.ShaderStyle"/> to use <see cref="SHADERUNLIT"/>.
+        /// Sets <see cref="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"/>.
+        /// Sets <see cref="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"/>.
+        /// Sets <see cref="ShaderStyle"/> to use <see cref="SHADERPBRSPECULARGLOSSINESS"/>.
         /// </summary>
         /// <returns>This <see cref="MaterialBuilder"/>.</returns>
         public MaterialBuilder WithSpecularGlossinessShader() { _SetShader(SHADERPBRSPECULARGLOSSINESS); return this; }
@@ -370,6 +333,7 @@ namespace SharpGLTF.Materials
             return this;
         }
 
+        [Obsolete("Use WithChannelParam(KnownChannel channelKey, KnownProperty propertyName, Object parameter)")]
         public MaterialBuilder WithChannelParam(KnownChannel channelKey, Vector4 parameter)
         {
             this.UseChannel(channelKey).Parameter = parameter;
@@ -377,12 +341,19 @@ namespace SharpGLTF.Materials
             return this;
         }
 
+        [Obsolete("Use WithChannelParam(KnownChannel channelKey, KnownProperty propertyName, Object parameter)")]
         public MaterialBuilder WithChannelParam(string channelKey, Vector4 parameter)
         {
             this.UseChannel(channelKey).Parameter = parameter;
             return this;
         }
 
+        public MaterialBuilder WithChannelParam(KnownChannel channelKey, KnownProperty propertyName, Object parameter)
+        {
+            this.UseChannel(channelKey).Parameters[propertyName] = MaterialValue.CreateFrom(parameter);
+            return this;
+        }
+
         public MaterialBuilder WithChannelImage(KnownChannel channelKey, IMAGEFILE primaryImage)
         {
             if (primaryImage.IsEmpty)
@@ -440,18 +411,21 @@ namespace SharpGLTF.Materials
         public MaterialBuilder WithNormal(IMAGEFILE imageFile, float scale = 1)
         {
             WithChannelImage(KnownChannel.Normal, imageFile);
-            WithChannelParam(KnownChannel.Normal, new Vector4(scale, 0, 0, 0));
+            WithChannelParam(KnownChannel.Normal, KnownProperty.NormalScale, scale);
             return this;
         }
 
         public MaterialBuilder WithOcclusion(IMAGEFILE imageFile, float strength = 1)
         {
             WithChannelImage(KnownChannel.Occlusion, imageFile);
-            WithChannelParam(KnownChannel.Occlusion, new Vector4(strength, 0, 0, 0));
+            WithChannelParam(KnownChannel.Occlusion, KnownProperty.OcclusionStrength, strength);
             return this;
         }
 
-        public MaterialBuilder WithEmissive(Vector3 rgb) { return WithChannelParam(KnownChannel.Emissive, new Vector4(rgb, 1)); }
+        public MaterialBuilder WithEmissive(Vector3 rgb)
+        {
+            return WithChannelParam(KnownChannel.Emissive, KnownProperty.RGB, rgb);
+        }
 
         public MaterialBuilder WithEmissive(IMAGEFILE imageFile, Vector3? rgb = null)
         {
@@ -460,7 +434,10 @@ namespace SharpGLTF.Materials
             return this;
         }
 
-        public MaterialBuilder WithBaseColor(Vector4 rgba) { return WithChannelParam(KnownChannel.BaseColor, rgba); }
+        public MaterialBuilder WithBaseColor(Vector4 rgba)
+        {
+            return WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, rgba);
+        }
 
         public MaterialBuilder WithBaseColor(IMAGEFILE imageFile, Vector4? rgba = null)
         {
@@ -474,11 +451,8 @@ namespace SharpGLTF.Materials
             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;
-
+            if (metallic.HasValue) channel.Parameters[KnownProperty.MetallicFactor] = metallic.Value;
+            if (roughness.HasValue) channel.Parameters[KnownProperty.RoughnessFactor] = roughness.Value;
             return this;
         }
 
@@ -489,7 +463,7 @@ namespace SharpGLTF.Materials
             return this;
         }
 
-        public MaterialBuilder WithDiffuse(Vector4 rgba) { return WithChannelParam(KnownChannel.Diffuse, rgba); }
+        public MaterialBuilder WithDiffuse(Vector4 rgba) { return WithChannelParam(KnownChannel.Diffuse, KnownProperty.RGBA, rgba); }
 
         public MaterialBuilder WithDiffuse(IMAGEFILE imageFile, Vector4? rgba = null)
         {
@@ -504,17 +478,8 @@ namespace SharpGLTF.Materials
 
             var channel = UseChannel(KnownChannel.SpecularGlossiness);
 
-            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;
+            if (specular.HasValue) channel.Parameters[KnownProperty.SpecularFactor] = specular.Value;
+            if (glossiness.HasValue) channel.Parameters[KnownProperty.GlossinessFactor] = glossiness.Value;
 
             return this;
         }
@@ -553,6 +518,33 @@ namespace SharpGLTF.Materials
             return this;
         }
 
+        public MaterialBuilder WithSpecularColor(IMAGEFILE imageFile, Vector3? rgb = null)
+        {
+            WithChannelImage(KnownChannel.SpecularColor, imageFile);
+            if (rgb.HasValue) WithChannelParam(KnownChannel.SpecularColor, new Vector4(rgb.Value, 0));
+            return this;
+        }
+
+        public MaterialBuilder WithSpecularFactor(IMAGEFILE imageFile, float factor)
+        {
+            WithChannelImage(KnownChannel.SpecularFactor, imageFile);
+            WithChannelParam(KnownChannel.SpecularFactor, new Vector4(factor, 0, 0, 0));
+            return this;
+        }
+
+        public MaterialBuilder WithVolumeThickness(IMAGEFILE imageFile, float factor)
+        {
+            WithChannelImage(KnownChannel.VolumeThickness, imageFile);
+            WithChannelParam(KnownChannel.VolumeThickness, new Vector4(factor, 0, 0, 0));
+            return this;
+        }
+
+        public MaterialBuilder WithVolumeAttenuation(Vector3 color, float distance)
+        {
+            WithChannelParam(KnownChannel.VolumeAttenuation, new Vector4(color, distance));
+            return this;
+        }
+
         #endregion
 
         #region nested types

+ 117 - 21
src/SharpGLTF.Toolkit/Materials/MaterialEnums.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Numerics;
 using System.Text;
 
@@ -38,37 +39,132 @@ namespace SharpGLTF.Materials
 
         SpecularColor,
         SpecularFactor,
+
+        VolumeThickness,
+        VolumeAttenuation,
+    }
+
+    /// <summary>
+    /// Enumeration of channel properties used in <see cref="ChannelBuilder.Parameters"/>
+    /// </summary>
+    /// <remarks>
+    /// This enumeration must match <see cref="Schema2.MaterialParameter.Key"/>
+    /// </remarks>
+    public enum KnownProperty
+    {
+        RGB,
+        RGBA,
+
+        NormalScale,
+        OcclusionStrength,
+
+        MetallicFactor,
+        RoughnessFactor,
+        SpecularFactor,
+        GlossinessFactor,
+        ClearCoatFactor,
+        ThicknessFactor,
+        TransmissionFactor,
+        AttenuationDistance,
     }
 
-    partial class ChannelBuilder
+    partial class MaterialBuilder
     {
-        private static Vector4 _GetDefaultParameter(KnownChannel key)
+        private IReadOnlyList<KnownChannel> _GetValidChannels()
         {
-            switch (key)
+            switch (ShaderStyle)
             {
-                case KnownChannel.Emissive: return Vector4.Zero;
-
-                case KnownChannel.Normal:
-                case KnownChannel.ClearCoatNormal:
-                case KnownChannel.Occlusion:
-                case KnownChannel.SpecularFactor:
-                    return Vector4.UnitX;
+                case SHADERUNLIT: return _UnlitChannels;
+                case SHADERPBRMETALLICROUGHNESS: return _MetRouChannels;
+                case SHADERPBRSPECULARGLOSSINESS: return _SpeGloChannels;
+                default: throw new NotImplementedException();
+            }
+        }
 
-                case KnownChannel.BaseColor:
-                case KnownChannel.Diffuse:
-                case KnownChannel.SpecularColor:
-                    return Vector4.One;
+        private static readonly KnownChannel[] _UnlitChannels = new[]
+        {
+            KnownChannel.BaseColor
+        };
 
-                case KnownChannel.MetallicRoughness: return new Vector4(1, 1, 0, 0);
-                case KnownChannel.SpecularGlossiness: return Vector4.One;
+        internal static readonly KnownChannel[] _MetRouChannels = new[]
+        {
+            KnownChannel.Normal,
+            KnownChannel.Occlusion,
+            KnownChannel.Emissive,
+
+            KnownChannel.BaseColor,
+            KnownChannel.MetallicRoughness,
+
+            KnownChannel.ClearCoat,
+            KnownChannel.ClearCoatNormal,
+            KnownChannel.ClearCoatRoughness,
+            KnownChannel.Transmission,
+            KnownChannel.SheenColor,
+            KnownChannel.SheenRoughness,
+            KnownChannel.SpecularColor,
+            KnownChannel.SpecularFactor,
+            KnownChannel.VolumeThickness,
+            KnownChannel.VolumeAttenuation
+        };
+
+        private static readonly KnownChannel[] _SpeGloChannels = new[]
+        {
+            KnownChannel.Normal,
+            KnownChannel.Occlusion,
+            KnownChannel.Emissive,
 
-                case KnownChannel.ClearCoat: return Vector4.Zero;
-                case KnownChannel.ClearCoatRoughness: return Vector4.Zero;
+            KnownChannel.Diffuse,
+            KnownChannel.SpecularGlossiness,
+        };
+    }
 
-                case KnownChannel.Transmission: return Vector4.Zero;
+    partial struct MaterialValue
+    {
+        internal static Collection CreateDefaultProperties(KnownChannel key)
+        {
+            var ppp = _CreateDefaultProperties(key).ToArray();
+            var collection = new Collection(ppp);
+            collection.Reset();
+            return collection;
+        }
 
-                case KnownChannel.SheenColor: return Vector4.Zero;
-                case KnownChannel.SheenRoughness: return Vector4.Zero;
+        private static IEnumerable<_Property> _CreateDefaultProperties(KnownChannel key)
+        {
+            switch (key)
+            {
+                case KnownChannel.Emissive: yield return new _Property(KnownProperty.RGB, Vector3.Zero); break;
+                case KnownChannel.Normal: yield return new _Property(KnownProperty.NormalScale, 1f); break;
+                case KnownChannel.Occlusion: yield return new _Property(KnownProperty.OcclusionStrength, 1f); break;
+
+                case KnownChannel.Diffuse: yield return new _Property(KnownProperty.RGBA, Vector4.One); break;
+                case KnownChannel.SpecularGlossiness:
+                    yield return new _Property(KnownProperty.SpecularFactor, Vector3.One);
+                    yield return new _Property(KnownProperty.GlossinessFactor, 1f);
+                    break;
+
+                case KnownChannel.BaseColor: yield return new _Property(KnownProperty.RGBA, Vector4.One);break;
+                case KnownChannel.MetallicRoughness:
+                    yield return new _Property(KnownProperty.MetallicFactor, 1f);
+                    yield return new _Property(KnownProperty.RoughnessFactor, 1f);
+                    break;
+
+                case KnownChannel.ClearCoat: yield return new _Property(KnownProperty.ClearCoatFactor, 0f); break;
+                case KnownChannel.ClearCoatNormal: yield return new _Property(KnownProperty.NormalScale, 1f); break;
+                case KnownChannel.ClearCoatRoughness: yield return new _Property(KnownProperty.RoughnessFactor, 0f); break;
+
+                case KnownChannel.Transmission: yield return new _Property(KnownProperty.TransmissionFactor, 0f); break;
+
+                case KnownChannel.SheenColor: yield return new _Property(KnownProperty.RGB, Vector3.Zero); break;
+                case KnownChannel.SheenRoughness: yield return new _Property(KnownProperty.RoughnessFactor, 0f); break;
+
+                case KnownChannel.SpecularColor: yield return new _Property(KnownProperty.RGB, Vector3.One); break;
+                case KnownChannel.SpecularFactor: yield return new _Property(KnownProperty.SpecularFactor, 1f); break;
+
+                case KnownChannel.VolumeThickness: yield return new _Property(KnownProperty.ThicknessFactor, 0f); break;
+                case KnownChannel.VolumeAttenuation:
+                    yield return new _Property(KnownProperty.RGB, Vector3.One);
+                    yield return new _Property(KnownProperty.AttenuationDistance, 0f);
+                    break;
 
                 default: throw new NotImplementedException();
             }

+ 438 - 0
src/SharpGLTF.Toolkit/Materials/MaterialValue.cs

@@ -0,0 +1,438 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.Materials
+{
+    [System.Diagnostics.DebuggerDisplay("{ToString(),nq}")]
+    public readonly partial struct MaterialValue : IEquatable<MaterialValue>
+    {
+        #region constructors
+
+        public static implicit operator MaterialValue(Single value) { return new MaterialValue(value); }
+
+        public static implicit operator MaterialValue(Vector2 value) { return new MaterialValue(value.X, value.Y); }
+
+        public static implicit operator MaterialValue(Vector3 value) { return new MaterialValue(value.X, value.Y, value.Z); }
+
+        public static implicit operator MaterialValue(Vector4 value) { return new MaterialValue(value.X, value.Y, value.Z, value.W); }
+
+        public static MaterialValue CreateFrom(Object value)
+        {
+            if (value is Single v1) return v1;
+            if (value is Vector2 v2) return v2;
+            if (value is Vector3 v3) return v3;
+            if (value is Vector4 v4) return v4;
+            throw new ArgumentException(nameof(value));
+        }
+
+        private MaterialValue(float x) { _Length = 1; _X = x; _Y = 0; _Z = 0; _W = 0; }
+        private MaterialValue(float x, float y) { _Length = 2; _X = x; _Y = y; _Z = 0; _W = 0; }
+        private MaterialValue(float x, float y, float z) { _Length = 3; _X = x; _Y = y; _Z = z; _W = 0; }
+        private MaterialValue(float x, float y, float z, float w) { _Length = 4; _X = x; _Y = y; _Z = z; _W = w; }
+
+        #endregion
+
+        #region data
+
+        private readonly int _Length;
+        private readonly float _X;
+        private readonly float _Y;
+        private readonly float _Z;
+        private readonly float _W;
+
+        public override int GetHashCode()
+        {
+            if (_Length == 0) return 0;
+            var h = _X.GetHashCode();
+            if (_Length == 1) return h;
+            h ^= _Y.GetHashCode();
+            if (_Length == 2) return h;
+            h ^= _Z.GetHashCode();
+            if (_Length == 3) return h;
+            h ^= _W.GetHashCode();
+            return h;
+        }
+
+        public override bool Equals(object obj) { return obj is MaterialValue other && Equals(other); }
+
+        public bool Equals(MaterialValue other) { return AreEqual(this, other); }
+
+        public static bool operator ==(in MaterialValue a, in MaterialValue b) => AreEqual(a, b);
+
+        public static bool operator !=(in MaterialValue a, in MaterialValue b) => !AreEqual(a, b);
+
+        public static bool AreEqual(in MaterialValue a, in MaterialValue b)
+        {
+            if (a._Length != b._Length) return false;
+
+            if (a._Length == 0) return true;
+            if (a._X != b._X) return false;
+            if (a._Length == 1) return true;
+            if (a._Y != b._Y) return false;
+            if (a._Length == 2) return true;
+            if (a._Z != b._Z) return false;
+            if (a._Length == 3) return true;
+            if (a._W != b._W) return false;
+            return true;
+        }
+
+        #endregion
+
+        #region properties
+
+        public Type ValueType
+        {
+            get
+            {
+                switch (_Length)
+                {
+                    case 1: return typeof(float);
+                    case 2: return typeof(Vector2);
+                    case 3: return typeof(Vector3);
+                    case 4: return typeof(Vector4);
+                    default: throw new NotImplementedException();
+                }
+            }
+        }
+
+        #endregion
+
+        #region API
+
+        public static explicit operator Single(MaterialValue value)
+        {
+            if (value._Length != 1) throw new InvalidOperationException();
+            return value._X;
+        }
+
+        public static explicit operator Vector2(MaterialValue value)
+        {
+            if (value._Length != 2) throw new InvalidOperationException();
+            return new Vector2(value._X, value._Y);
+        }
+
+        public static explicit operator Vector3(MaterialValue value)
+        {
+            if (value._Length != 3) throw new InvalidOperationException();
+            return new Vector3(value._X, value._Y, value._Z);
+        }
+
+        public static explicit operator Vector4(MaterialValue value)
+        {
+            if (value._Length != 4) throw new InvalidOperationException();
+            return new Vector4(value._X, value._Y, value._Z, value._W);
+        }
+
+        public object ToTypeless()
+        {
+            switch (_Length)
+            {
+                case 1: return (Single)this;
+                case 2: return (Vector2)this;
+                case 3: return (Vector3)this;
+                case 4: return (Vector4)this;
+                default: throw new NotImplementedException();
+            }
+        }
+
+        public override string ToString()
+        {
+            return ToTypeless().ToString();
+        }
+
+        #endregion
+
+        #region nested types
+
+        [System.Diagnostics.DebuggerDisplay("{ToString(),nq}")]
+        internal sealed class _Property : IEquatable<_Property>
+        {
+            #region lifecycle
+            internal _Property(KnownProperty key, float value)
+            {
+                this.Key = key;
+                this._Default = value;
+                this.Value = _Default;
+            }
+
+            internal _Property(KnownProperty key, Vector2 value)
+            {
+                this.Key = key;
+                this._Default = value;
+                this.Value = _Default;
+            }
+
+            internal _Property(KnownProperty key, Vector3 value)
+            {
+                this.Key = key;
+                this._Default = value;
+                this.Value = _Default;
+            }
+
+            internal _Property(KnownProperty key, Vector4 value)
+            {
+                this.Key = key;
+                this._Default = value;
+                this.Value = _Default;
+            }
+
+            #endregion
+
+            #region data
+
+            public KnownProperty Key { get; }
+
+            private readonly MaterialValue _Default;
+            private MaterialValue _Value;
+
+            public override int GetHashCode()
+            {
+                return Key.GetHashCode() ^ _Value.GetHashCode();
+            }
+
+            public bool Equals(_Property other)
+            {
+                return AreEqual(this, other);
+            }
+
+            public static bool AreEqual(_Property a, _Property b)
+            {
+                if (a.Key != b.Key) return false;
+                if (!a._Default.Equals(b._Default)) return false;
+                if (!a._Value.Equals(b._Value)) return false;
+                return true;
+            }
+
+            #endregion
+
+            #region API
+            public string Name => Key.ToString();
+
+            public MaterialValue Value
+            {
+                get => _Value;
+                set
+                {
+                    if (value._Length != this._Default._Length) throw new ArgumentOutOfRangeException(nameof(value));
+                    _Value = value;
+                }
+            }
+
+            public void SetDefault() { _Value = _Default; }
+
+            public override string ToString()
+            {
+                return new KeyValuePair<string, MaterialValue>(Name, Value).ToString();
+            }
+
+            #endregion
+        }
+
+        [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
+        public sealed class Collection : IReadOnlyDictionary<KnownProperty, MaterialValue>
+        {
+            #region debug
+
+            private string _GetDebuggerDisplay()
+            {
+                return string.Join(", ", _Properties.Select(item => item.ToString()));
+            }
+
+            #endregion
+
+            #region lifecycle
+
+            internal Collection(_Property[] properties)
+            {
+                _Properties = properties;
+            }
+
+            #endregion
+
+            #region data
+
+            [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+            private readonly _Property[] _Properties;
+
+            public override int GetHashCode()
+            {
+                int h = 0;
+
+                foreach (var p in _Properties)
+                {
+                    h ^= p.GetHashCode();
+                }
+
+                return h;
+            }
+
+            public static bool AreEqual(Collection x, Collection y)
+            {
+                if (x._Properties.Length != y._Properties.Length) return false;
+
+                for (int i = 0; i < x._Properties.Length; ++i)
+                {
+                    var xp = x._Properties[i];
+                    var yp = y._Properties[i];
+
+                    if (xp.Name != yp.Name) return false;
+                    if (xp.Value != yp.Value) return false;
+                }
+
+                return true;
+            }
+
+            #endregion
+
+            #region properties
+
+            public MaterialValue this[KnownProperty key]
+            {
+                get => _Properties.First(item => item.Key == key).Value;
+                set
+                {
+                    var idx = Array.FindIndex(_Properties, item => item.Key == key);
+                    if (idx < 0) throw new KeyNotFoundException(key.ToString());
+                    _Properties[idx].Value = value;
+                }
+            }
+
+            public MaterialValue this[string keyName]
+            {
+                get
+                {
+                    return Enum.TryParse<KnownProperty>(keyName, out var key)
+                        ? this[key]
+                        : throw new KeyNotFoundException(keyName);
+                }
+
+                set
+                {
+                    if (!Enum.TryParse<KnownProperty>(keyName, out var key)) throw new KeyNotFoundException(keyName);
+                    this[key] = value;
+                }
+            }
+
+            [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+            public IEnumerable<KnownProperty> Keys => _Properties.Select(item => item.Key);
+
+            [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+            public IEnumerable<MaterialValue> Values => _Properties.Select(item => item.Value);
+
+            public int Count => _Properties.Length;
+
+            /// <summary>
+            /// Combines multiple properties into a single Vector4
+            /// (as long as the combined number of floats is 4 or less)
+            /// </summary>
+            [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+            public Vector4 CombinedVector
+            {
+                get
+                {
+                    Span<float> tmp = stackalloc float[4];
+                    int idx = 0;
+
+                    foreach (var p in _Properties)
+                    {
+                        idx += p.Value._CopyTo(tmp.Slice(idx));
+                    }
+
+                    return new Vector4(tmp[0], tmp[1], tmp[2], tmp[3]);
+                }
+                set
+                {
+                    Span<float> tmp = stackalloc float[4];
+                    tmp[0] = value.X;
+                    tmp[1] = value.Y;
+                    tmp[2] = value.Z;
+                    tmp[3] = value.W;
+
+                    int idx = 0;
+
+                    foreach (var p in _Properties)
+                    {
+                        var t = p.Value.ValueType;
+
+                        if (t == typeof(Single)) { p.Value = tmp[idx++]; }
+                        if (t == typeof(Vector2)) { p.Value = new Vector2(tmp[idx + 0], tmp[idx + 1]); idx += 2; }
+                        if (t == typeof(Vector3)) { p.Value = new Vector3(tmp[idx + 0], tmp[idx + 1], tmp[idx + 2]); idx += 3; }
+                        if (t == typeof(Vector4)) { p.Value = new Vector4(tmp[idx + 0], tmp[idx + 1], tmp[idx + 2], tmp[idx + 3]); idx += 4; }
+                    }
+                }
+            }
+
+            #endregion
+
+            #region API
+
+            public bool ContainsKey(KnownProperty key)
+            {
+                return _Properties.Any(item => item.Key == key);
+            }
+
+            public bool TryGetValue(KnownProperty key, out MaterialValue value)
+            {
+                var idx = Array.FindIndex(_Properties, item => item.Key == key);
+
+                if (idx < 0) { value = default; return false; }
+
+                value = _Properties[idx].Value;
+                return true;
+            }
+
+            public IEnumerator<KeyValuePair<KnownProperty, MaterialValue>> GetEnumerator()
+            {
+                return _Properties
+                    .Select(item => new KeyValuePair<KnownProperty, MaterialValue>(item.Key, item.Value))
+                    .GetEnumerator();
+            }
+
+            IEnumerator IEnumerable.GetEnumerator()
+            {
+                return _Properties
+                    .Select(item => new KeyValuePair<KnownProperty, MaterialValue>(item.Key, item.Value))
+                    .GetEnumerator();
+            }
+
+            public void Reset()
+            {
+                foreach (var p in _Properties) p.SetDefault();
+            }
+
+            public void CopyTo(Collection other)
+            {
+                for (int i = 0; i < this._Properties.Length; ++i)
+                {
+                    var src = this._Properties[i];
+                    var dst = other._Properties[i];
+
+                    if (src.Name != dst.Name) throw new ArgumentException(nameof(other));
+
+                    dst.Value = src.Value;
+                }
+            }
+
+            #endregion
+        }
+
+        #endregion
+
+        #region helpers
+
+        internal int _CopyTo(Span<float> dst)
+        {
+            if (_Length > 0) dst[0] = _X;
+            if (_Length > 1) dst[1] = _Y;
+            if (_Length > 2) dst[2] = _Z;
+            if (_Length > 3) dst[3] = _W;
+            return _Length;
+        }
+
+        #endregion
+    }
+}

+ 21 - 8
src/SharpGLTF.Toolkit/Schema2/MaterialExtensions.cs

@@ -262,11 +262,12 @@ namespace SharpGLTF.Schema2
         private static void _CopyMetallicRoughnessTo(Material srcMaterial, MaterialBuilder dstMaterial)
         {
             dstMaterial.WithMetallicRoughnessShader();
-            srcMaterial.CopyChannelsTo(dstMaterial, "BaseColor", "MetallicRoughness");
-            srcMaterial.CopyChannelsTo(dstMaterial, "ClearCoat", "ClearCoatRoughness", "ClearCoatNormal");
-            srcMaterial.CopyChannelsTo(dstMaterial, "Transmission");
-            srcMaterial.CopyChannelsTo(dstMaterial, "SheenColor", "SheenRoughness");
-            srcMaterial.CopyChannelsTo(dstMaterial, "SpecularColor", "SpecularFactor");
+
+            var channels = MaterialBuilder._MetRouChannels
+                .Select(item => item.ToString())
+                .ToArray();
+
+            srcMaterial.CopyChannelsTo(dstMaterial, channels);
         }
 
         private static void _CopyDefaultTo(Material srcMaterial, MaterialBuilder dstMaterial)
@@ -308,7 +309,10 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(srcChannel, nameof(srcChannel));
             Guard.NotNull(dstChannel, nameof(dstChannel));
 
-            dstChannel.Parameter = srcChannel.Parameter;
+            foreach (var srcProp in srcChannel.Parameters)
+            {
+                dstChannel.Parameters[srcProp.Name] = MaterialValue.CreateFrom(srcProp.Value);
+            }
 
             if (srcChannel.Texture == null) return;
             if (dstChannel.Texture == null) dstChannel.UseTexture();
@@ -371,6 +375,10 @@ namespace SharpGLTF.Schema2
                 = srcMaterial.GetChannel("SpecularColor") != null
                 || srcMaterial.GetChannel("SpecularFactor") != null;
 
+            var hasVolume
+                = srcMaterial.GetChannel("VolumeThickness") != null
+                || srcMaterial.GetChannel("VolumeAttenuation") != null;
+
             srcMaterial.CopyChannelsTo(dstMaterial, "Normal", "Occlusion", "Emissive");
 
             MaterialBuilder defMaterial = null;
@@ -389,7 +397,8 @@ namespace SharpGLTF.Schema2
                     hasClearCoat ? "ClearCoat" : null,
                     hasTransmission ? "Transmission" : null,
                     hasSheen ? "Sheen" : null,
-                    hasSpecular ? "Specular" : null);
+                    hasSpecular ? "Specular" : null,
+                    hasVolume ? "Volume" : null);
 
                 defMaterial = srcMaterial;
             }
@@ -409,6 +418,7 @@ namespace SharpGLTF.Schema2
                 defMaterial.CopyChannelsTo(dstMaterial, "Transmission");
                 defMaterial.CopyChannelsTo(dstMaterial, "SheenColor", "SheenRoughness");
                 defMaterial.CopyChannelsTo(dstMaterial, "SpecularColor", "SpecularFactor");
+                defMaterial.CopyChannelsTo(dstMaterial, "VolumeThickness", "VolumeAttenuation");
             }
         }
 
@@ -434,7 +444,10 @@ namespace SharpGLTF.Schema2
             Guard.NotNull(srcChannel, nameof(srcChannel));
             Guard.NotNull(dstChannel, nameof(dstChannel));
 
-            dstChannel.Parameter = srcChannel.Parameter;
+            foreach (var dstProp in dstChannel.Parameters)
+            {
+                dstProp.Value = srcChannel.Parameters[dstProp.Name].ToTypeless();
+            }
 
             var srcTex = srcChannel.GetValidTexture();
             if (srcTex == null) return;

+ 4 - 4
tests/SharpGLTF.Core.Tests/Schema2/Authoring/ExtensionsCreationTests.cs

@@ -102,7 +102,7 @@ namespace SharpGLTF.Schema2.Authoring
                 .WithChannelImage(Materials.KnownChannel.BaseColor, System.IO.Path.Combine(basePath, "WaterBottle_baseColor.png"))
                 .WithChannelImage(Materials.KnownChannel.MetallicRoughness, System.IO.Path.Combine(basePath, "WaterBottle_roughnessMetallic.png"))
                 .WithChannelImage(Materials.KnownChannel.ClearCoat, System.IO.Path.Combine(basePath, "WaterBottle_emissive.png"))
-                .WithChannelParam(Materials.KnownChannel.ClearCoat, new Vector4(0.5f, 0, 0, 0))
+                .WithChannelParam(Materials.KnownChannel.ClearCoat, Materials.KnownProperty.ClearCoatFactor, 0.5f)
                 .WithChannelImage(Materials.KnownChannel.ClearCoatRoughness, System.IO.Path.Combine(basePath, "WaterBottle_roughnessMetallic.png"))
                 .WithChannelImage(Materials.KnownChannel.ClearCoatNormal, System.IO.Path.Combine(basePath, "WaterBottle_normal.png"));
 
@@ -141,7 +141,7 @@ namespace SharpGLTF.Schema2.Authoring
                 .WithChannelImage(Materials.KnownChannel.BaseColor, System.IO.Path.Combine(basePath, "WaterBottle_baseColor.png"))
                 .WithChannelImage(Materials.KnownChannel.MetallicRoughness, System.IO.Path.Combine(basePath, "WaterBottle_roughnessMetallic.png"))
                 .WithChannelImage(Materials.KnownChannel.Transmission, System.IO.Path.Combine(basePath, "WaterBottle_emissive.png"))
-                .WithChannelParam(Materials.KnownChannel.Transmission, new Vector4(0.75f,0,0,0) );                
+                .WithChannelParam(Materials.KnownChannel.Transmission, Materials.KnownProperty.TransmissionFactor, 0.75f);                
 
             var mesh = new Geometry.MeshBuilder<VPOS, VTEX>("mesh1");
             mesh.UsePrimitive(material).AddQuadrangle
@@ -178,9 +178,9 @@ namespace SharpGLTF.Schema2.Authoring
                 .WithChannelImage(Materials.KnownChannel.BaseColor, System.IO.Path.Combine(basePath, "WaterBottle_baseColor.png"))
                 .WithChannelImage(Materials.KnownChannel.MetallicRoughness, System.IO.Path.Combine(basePath, "WaterBottle_roughnessMetallic.png"))
                 .WithChannelImage(Materials.KnownChannel.SheenColor, System.IO.Path.Combine(basePath, "WaterBottle_emissive.png"))
-                .WithChannelParam(Materials.KnownChannel.SheenColor, new Vector4(1,1,1,0))
+                .WithChannelParam(Materials.KnownChannel.SheenColor, Materials.KnownProperty.RGB, Vector3.One)
                 .WithChannelImage(Materials.KnownChannel.SheenRoughness, System.IO.Path.Combine(basePath, "WaterBottle_occlusion.png"))
-                .WithChannelParam(Materials.KnownChannel.SheenRoughness, new Vector4(0.5f, 0, 0, 0));
+                .WithChannelParam(Materials.KnownChannel.SheenRoughness, Materials.KnownProperty.RoughnessFactor, 0.5f);
 
             var mesh = new Geometry.MeshBuilder<VPOS, VTEX>("mesh1");
             mesh.UsePrimitive(material).AddQuadrangle

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

@@ -121,6 +121,25 @@ namespace SharpGLTF.Materials
             Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, srcMaterial.Clone()));
         }
 
+        [Test]
+        public void CreateVolume()
+        {
+            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)
+                .WithMetallicRoughnessShader()
+                    .WithBaseColor(tex1, new Vector4(0.7f, 0, 0f, 0.8f))
+                    .WithMetallicRoughness(tex1, 0.2f, 0.4f)
+
+                .WithVolumeAttenuation(Vector3.One * 0.3f, 0.6f)
+                .WithVolumeThickness(tex1, 0.4f);
+
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, Schema2Roundtrip(srcMaterial)));
+            Assert.IsTrue(MaterialBuilder.AreEqualByContent(srcMaterial, srcMaterial.Clone()));
+        }
+
         [Test]
         public void CreateSpecularGlossiness()
         {