浏览代码

Refactored API for better custom vertex format support.
+docs

Vicente Penades 4 年之前
父节点
当前提交
a97d6a1a91

+ 13 - 12
src/SharpGLTF.Toolkit/Geometry/MeshBuilder.cs

@@ -16,26 +16,27 @@ namespace SharpGLTF.Geometry
     /// <typeparam name="TvG">
     /// The vertex fragment type with Position, Normal and Tangent.<br/>
     /// Valid types are:<br/>
-    /// - <see cref="VertexPosition"/>,<br/>
-    /// - <see cref="VertexPositionNormal"/>,<br/>
-    /// - <see cref="VertexPositionNormalTangent"/>.<br/>
+    /// - <see cref="VertexPosition"/><br/>
+    /// - <see cref="VertexPositionNormal"/><br/>
+    /// - <see cref="VertexPositionNormalTangent"/>
     /// </typeparam>
     /// <typeparam name="TvM">
     /// The vertex fragment type with Colors and Texture Coordinates.<br/>
     /// Valid types are:<br/>
-    /// - <see cref="VertexEmpty"/>,<br/>
-    /// - <see cref="VertexColor1"/>,<br/>
-    /// - <see cref="VertexTexture1"/>,<br/>
-    /// - <see cref="VertexColor1Texture1"/>.<br/>
-    /// - <see cref="VertexColor1Texture2"/>.<br/>
-    /// - <see cref="VertexColor2Texture2"/>.<br/>
+    /// - <see cref="VertexEmpty"/><br/>
+    /// - <see cref="VertexColor1"/><br/>
+    /// - <see cref="VertexTexture1"/><br/>
+    /// - <see cref="VertexColor1Texture1"/><br/>
+    /// - <see cref="VertexColor1Texture2"/><br/>
+    /// - <see cref="VertexColor1Texture1"/><br/>
+    /// - <see cref="VertexColor2Texture2"/>
     /// </typeparam>
     /// <typeparam name="TvS">
     /// The vertex fragment type with Skin Joint Weights.<br/>
     /// Valid types are:<br/>
-    /// - <see cref="VertexEmpty"/>,<br/>
-    /// - <see cref="VertexJoints4"/>,<br/>
-    /// - <see cref="VertexJoints8"/>.<br/>
+    /// - <see cref="VertexEmpty"/><br/>
+    /// - <see cref="VertexJoints4"/><br/>
+    /// - <see cref="VertexJoints8"/>
     /// </typeparam>
     public class MeshBuilder<TMaterial, TvG, TvM, TvS> : BaseBuilder, IMeshBuilder<TMaterial>
         where TvG : struct, IVertexGeometry

+ 54 - 16
src/SharpGLTF.Toolkit/Geometry/PrimitiveBuilder.cs

@@ -16,26 +16,27 @@ namespace SharpGLTF.Geometry
     /// <typeparam name="TvG">
     /// The vertex fragment type with Position, Normal and Tangent.<br/>
     /// Valid types are:<br/>
-    /// - <see cref="VertexPosition"/>,<br/>
-    /// - <see cref="VertexPositionNormal"/>,<br/>
-    /// - <see cref="VertexPositionNormalTangent"/>.<br/>
+    /// - <see cref="VertexPosition"/><br/>
+    /// - <see cref="VertexPositionNormal"/><br/>
+    /// - <see cref="VertexPositionNormalTangent"/><br/>
     /// </typeparam>
     /// <typeparam name="TvM">
     /// The vertex fragment type with Colors and Texture Coordinates.<br/>
     /// Valid types are:<br/>
-    /// - <see cref="VertexEmpty"/>,<br/>
-    /// - <see cref="VertexColor1"/>,<br/>
-    /// - <see cref="VertexTexture1"/>,<br/>
-    /// - <see cref="VertexColor1Texture1"/>.<br/>
-    /// - <see cref="VertexColor1Texture2"/>.<br/>
-    /// - <see cref="VertexColor2Texture2"/>.<br/>
+    /// - <see cref="VertexEmpty"/><br/>
+    /// - <see cref="VertexColor1"/><br/>
+    /// - <see cref="VertexTexture1"/><br/>
+    /// - <see cref="VertexColor1Texture1"/><br/>
+    /// - <see cref="VertexColor1Texture2"/><br/>
+    /// - <see cref="VertexColor1Texture1"/><br/>
+    /// - <see cref="VertexColor2Texture2"/>
     /// </typeparam>
     /// <typeparam name="TvS">
     /// The vertex fragment type with Skin Joint Weights.<br/>
     /// Valid types are:<br/>
-    /// - <see cref="VertexEmpty"/>,<br/>
-    /// - <see cref="VertexJoints4"/>,<br/>
-    /// - <see cref="VertexJoints8"/>.<br/>
+    /// - <see cref="VertexEmpty"/><br/>
+    /// - <see cref="VertexJoints4"/><br/>
+    /// - <see cref="VertexJoints8"/><br/>
     /// </typeparam>
     public abstract class PrimitiveBuilder<TMaterial, TvG, TvM, TvS> : IPrimitiveBuilder, IPrimitiveReader<TMaterial>
         where TvG : struct, IVertexGeometry
@@ -84,8 +85,14 @@ namespace SharpGLTF.Geometry
 
         #region properties
 
+        /// <summary>
+        /// Gets the parent mesh that owns this primitive.
+        /// </summary>
         public MeshBuilder<TMaterial, TvG, TvM, TvS> Mesh => _Mesh;
 
+        /// <summary>
+        /// Gets the <see cref="TMaterial"/> used by this primitive.
+        /// </summary>
         public TMaterial Material => _Material;
 
         /// <summary>
@@ -96,20 +103,38 @@ namespace SharpGLTF.Geometry
         /// </summary>
         public abstract int VerticesPerPrimitive { get; }
 
+        /// <summary>
+        /// Gets the type of the vertex used by this primitive.
+        /// </summary>
         public Type VertexType => typeof(VertexBuilder<TvG, TvM, TvS>);
 
+        /// <summary>
+        /// Gets the list of vertices used by this primitive.
+        /// </summary>
         public IReadOnlyList<VertexBuilder<TvG, TvM, TvS>> Vertices => _Vertices;
 
         IReadOnlyList<IVertexBuilder> IPrimitiveReader<TMaterial>.Vertices => _Vertices;
 
         IReadOnlyList<IPrimitiveMorphTargetReader> IPrimitiveReader<TMaterial>.MorphTargets => _MorphTargets;
 
+        /// <summary>
+        /// Gets the list in indices of the points contained in this primitive.
+        /// </summary>
         public virtual IReadOnlyList<int> Points => Array.Empty<int>();
 
+        /// <summary>
+        /// Gets the list in indices of the lines contained in this primitive.
+        /// </summary>
         public virtual IReadOnlyList<(int A, int B)> Lines => Array.Empty<(int, int)>();
 
+        /// <summary>
+        /// Gets the list in indices of triangles contained in this primitive.
+        /// </summary>
         public virtual IReadOnlyList<(int A, int B, int C)> Triangles => Array.Empty<(int, int, int)>();
 
+        /// <summary>
+        /// Gets the list in indices of the surfaces contained in this primitive.
+        /// </summary>
         public virtual IReadOnlyList<(int A, int B, int C, int? D)> Surfaces => Array.Empty<(int, int, int, int?)>();
 
         #endregion
@@ -179,7 +204,7 @@ namespace SharpGLTF.Geometry
         /// Adds a point.
         /// </summary>
         /// <param name="a">vertex for this point.</param>
-        /// <returns>The indices of the vertices.</returns>
+        /// <returns>The index of the vertex.</returns>
         public int AddPoint(IVertexBuilder a)
         {
             Guard.NotNull(a, nameof(a));
@@ -190,8 +215,8 @@ namespace SharpGLTF.Geometry
         /// <summary>
         /// Adds a line.
         /// </summary>
-        /// <param name="a">First corner of the line.</param>
-        /// <param name="b">Second corner of the line.</param>
+        /// <param name="a">First end of the line.</param>
+        /// <param name="b">Last end of the line.</param>
         /// <returns>The indices of the vertices, or, in case the line is degenerated, (-1,-1).</returns>
         public (int A, int B) AddLine(IVertexBuilder a, IVertexBuilder b)
         {
@@ -444,14 +469,17 @@ namespace SharpGLTF.Geometry
 
         #region properties
 
+        /// <inheritdoc/>
         public override int VerticesPerPrimitive => 1;
 
+        /// <inheritdoc/>
         public override IReadOnlyList<int> Points => new PointListWrapper<VertexBuilder<TvG, TvM, TvS>>(this.Vertices);
 
         #endregion
 
         #region API
 
+        /// <inheritdoc/>
         public override int AddPoint(VertexBuilder<TvG, TvM, TvS> a)
         {
             if (Mesh.VertexPreprocessor != null)
@@ -462,6 +490,7 @@ namespace SharpGLTF.Geometry
             return UseVertex(a);
         }
 
+        /// <inheritdoc/>
         public override IReadOnlyList<int> GetIndices() { return Array.Empty<int>(); }
 
         #endregion
@@ -524,14 +553,17 @@ namespace SharpGLTF.Geometry
 
         #region properties
 
+        /// <inheritdoc/>
         public override int VerticesPerPrimitive => 2;
 
+        /// <inheritdoc/>
         public override IReadOnlyList<(int A, int B)> Lines => _Indices;
 
         #endregion
 
         #region API
 
+        /// <inheritdoc/>
         public override (int A, int B) AddLine(VertexBuilder<TvG, TvM, TvS> a, VertexBuilder<TvG, TvM, TvS> b)
         {
             if (Mesh.VertexPreprocessor != null)
@@ -553,6 +585,7 @@ namespace SharpGLTF.Geometry
             return (aa, bb);
         }
 
+        /// <inheritdoc/>
         public override IReadOnlyList<int> GetIndices()
         {
             return _Indices
@@ -600,16 +633,20 @@ namespace SharpGLTF.Geometry
 
         #region properties
 
+        /// <inheritdoc/>
         public override int VerticesPerPrimitive => 3;
 
+        /// <inheritdoc/>
         public override IReadOnlyList<(int A, int B, int C)> Triangles => new TriangleList(_TriIndices, _QuadIndices);
 
+        /// <inheritdoc/>
         public override IReadOnlyList<(int A, int B, int C, int? D)> Surfaces => new SurfaceList(_TriIndices, _QuadIndices);
 
         #endregion
 
         #region API
 
+        /// <inheritdoc/>
         public override (int A, int B, int C) AddTriangle(VertexBuilder<TvG, TvM, TvS> a, VertexBuilder<TvG, TvM, TvS> b, VertexBuilder<TvG, TvM, TvS> c)
         {
             if (Mesh.VertexPreprocessor != null)
@@ -622,6 +659,7 @@ namespace SharpGLTF.Geometry
             return _AddTriangle(a, b, c);
         }
 
+        /// <inheritdoc/>
         public override (int A, int B, int C, int D) AddQuadrangle(VertexBuilder<TvG, TvM, TvS> a, VertexBuilder<TvG, TvM, TvS> b, VertexBuilder<TvG, TvM, TvS> c, VertexBuilder<TvG, TvM, TvS> d)
         {
             if (Mesh.VertexPreprocessor != null)
@@ -809,7 +847,7 @@ namespace SharpGLTF.Geometry
     /// Helper class used to calculate Normals and Tangents of missing meshes.
     /// </summary>
     /// <typeparam name="TMaterial">default material</typeparam>
-    class MeshPrimitiveNormalsAndTangents<TMaterial> : VertexNormalsFactory.IMeshPrimitive, VertexTangentsFactory.IMeshPrimitive
+    sealed class MeshPrimitiveNormalsAndTangents<TMaterial> : VertexNormalsFactory.IMeshPrimitive, VertexTangentsFactory.IMeshPrimitive
     {
         #region constructor
 

+ 21 - 20
src/SharpGLTF.Toolkit/Geometry/VertexBuilder.cs

@@ -39,28 +39,29 @@ namespace SharpGLTF.Geometry
     /// Represents an individual vertex object.
     /// </summary>
     /// <typeparam name="TvG">
-    /// The vertex fragment type with Position, Normal and Tangent.
-    /// Valid types are:
-    /// <see cref="VertexPosition"/>,
-    /// <see cref="VertexPositionNormal"/>,
-    /// <see cref="VertexPositionNormalTangent"/>.
+    /// The vertex fragment type with Position, Normal and Tangent.<br/>
+    /// Valid types are:<br/>
+    /// - <see cref="VertexPosition"/><br/>
+    /// - <see cref="VertexPositionNormal"/><br/>
+    /// - <see cref="VertexPositionNormalTangent"/>
     /// </typeparam>
     /// <typeparam name="TvM">
-    /// The vertex fragment type with Colors and Texture Coordinates.
-    /// Valid types are:
-    /// <see cref="VertexEmpty"/>,
-    /// <see cref="VertexColor1"/>,
-    /// <see cref="VertexTexture1"/>,
-    /// <see cref="VertexColor1Texture1"/>.
-    /// <see cref="VertexColor1Texture2"/>.
-    /// <see cref="VertexColor2Texture2"/>.
+    /// The vertex fragment type with Colors and Texture Coordinates.<br/>
+    /// Valid types are:<br/>
+    /// - <see cref="VertexEmpty"/><br/>
+    /// - <see cref="VertexColor1"/><br/>
+    /// - <see cref="VertexTexture1"/><br/>
+    /// - <see cref="VertexColor1Texture1"/><br/>
+    /// - <see cref="VertexColor1Texture2"/><br/>
+    /// - <see cref="VertexColor2Texture1"/><br/>
+    /// - <see cref="VertexColor2Texture2"/>
     /// </typeparam>
     /// <typeparam name="TvS">
     /// The vertex fragment type with Skin Joint Weights.
-    /// Valid types are:
-    /// <see cref="VertexEmpty"/>,
-    /// <see cref="VertexJoints4"/>,
-    /// <see cref="VertexJoints8"/>.
+    /// Valid types are:<br/>
+    /// - <see cref="VertexEmpty"/><br/>
+    /// - <see cref="VertexJoints4"/><br/>
+    /// - <see cref="VertexJoints8"/>
     /// </typeparam>
     [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
     public partial struct VertexBuilder<TvG, TvM, TvS> : IVertexBuilder
@@ -365,9 +366,9 @@ namespace SharpGLTF.Geometry
 
         public void Validate()
         {
-            Geometry.Validate();
-            Material.Validate();
-            Skinning.Validate();
+            VertexUtils.ValidateVertexGeometry(Geometry);
+            VertexUtils.ValidateVertexMaterial(Material);
+            VertexUtils.ValidateVertexSkinning(Skinning);
         }
 
         #pragma warning disable CA1000 // Do not declare static members on generic types

+ 66 - 0
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexCustom.cs

@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF.Geometry.VertexTypes
+{
+    /// <summary>
+    /// Represents the interface that must be implemented by a custom vertex fragment.
+    /// </summary>
+    public interface IVertexCustom : IVertexMaterial
+    {
+        /// <summary>
+        /// Validates the custom attributes of the vertex fragment.<br/>
+        /// Called by <see cref="VertexBuilder{TvG, TvM, TvS}.Validate"/>.
+        /// </summary>
+        void Validate();
+
+        /// <summary>
+        /// Gets a collection of the attribute keys defined in this vertex.
+        /// </summary>
+        /// <example>
+        /// <code>
+        /// private static readonly string[] _CustomNames = { "CustomFloat" };
+        /// public IEnumerable&lt;string&gt; CustomAttributes =&gt; _CustomNames;
+        /// </code>
+        /// </example>
+        IEnumerable<string> CustomAttributes { get; }
+
+        /// <summary>
+        /// Tries to get a custom attribute.
+        /// </summary>
+        /// <param name="attributeName">The attribute name.</param>
+        /// <param name="value">the value if found, or null if not found.</param>
+        /// <returns>true if the value was found. False otherwise.</returns>
+        /// <example>
+        /// <code>
+        /// public bool TryGetCustomAttribute(string attributeName, out object value)
+        /// {
+        ///     if (attributeName != "CustomFloat") { value = null; return false; }
+        ///     value = this.CustomValue; return true;
+        /// }
+        /// </code>
+        /// </example>
+        bool TryGetCustomAttribute(string attributeName, out Object value);
+
+        /// <summary>
+        /// Sets a custom attribute only if <paramref name="attributeName"/> is defined in the vertex.
+        /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
+        /// </summary>
+        /// <param name="attributeName">The attribute name.</param>
+        /// <param name="value">The attribute value.</param>
+        /// <example>
+        /// <code>
+        /// public void SetCustomAttribute(string attributeName, object value)
+        /// {
+        ///     if (attributeName == "CustomFloat" &amp;&amp; value is float f) this.CustomValue = f;
+        /// }
+        /// </code>
+        /// </example>
+        /// <remarks>
+        /// If <paramref name="attributeName"/> is not defined in the custom vertex,<br/>
+        /// the method must not do any action.
+        /// </remarks>
+        void SetCustomAttribute(string attributeName, Object value);
+    }
+}

+ 5 - 2
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexEmpty.cs

@@ -32,10 +32,13 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         #region properties
 
+        /// <inheritdoc/>
         public int MaxBindings => 0;
 
+        /// <inheritdoc/>
         public int MaxColors => 0;
 
+        /// <inheritdoc/>
         public int MaxTextCoords => 0;
 
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
@@ -59,16 +62,16 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         Vector2 IVertexMaterial.GetTexCoord(int index) { throw new ArgumentOutOfRangeException(nameof(index)); }
 
+        /// <inheritdoc/>
         public SparseWeight8 GetWeights() { return default; }
 
+        /// <inheritdoc/>
         public void SetWeights(in SparseWeight8 weights) { throw new NotSupportedException(); }
 
         void IVertexSkinning.SetJointBinding(int index, int joint, float weight) { throw new ArgumentOutOfRangeException(nameof(index)); }
 
         (int, float) IVertexSkinning.GetJointBinding(int index) { throw new ArgumentOutOfRangeException(nameof(index)); }
 
-        public object GetCustomAttribute(string attributeName) { return null; }
-
         #endregion
     }
 }

+ 80 - 10
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexGeometry.cs

@@ -6,22 +6,76 @@ using System.Text;
 
 namespace SharpGLTF.Geometry.VertexTypes
 {
+    /// <summary>
+    /// Represents the interface that must be implemented by a geometry vertex fragment.<br/>
+    /// Implemented by:<br/>
+    /// - <see cref="VertexPosition"/><br/>
+    /// - <see cref="VertexPositionNormal"/><br/>
+    /// - <see cref="VertexPositionNormalTangent"/><br/>
+    /// - <see cref="VertexGeometryDelta"/><br/>
+    /// </summary>
     public interface IVertexGeometry
     {
-        void Validate();
-
+        /// <summary>
+        /// Gets the position of the vertex.
+        /// </summary>
+        /// <returns>A <see cref="Vector3"/> position.</returns>
         Vector3 GetPosition();
+
+        /// <summary>
+        /// Tries to get the normal of the vertex.
+        /// </summary>
+        /// <param name="normal">A <see cref="Vector3"/> normal.</param>
+        /// <returns>True if the normal exists.</returns>
         Boolean TryGetNormal(out Vector3 normal);
+
+        /// <summary>
+        /// Tries to get the tangent of the vertex.
+        /// </summary>
+        /// <param name="tangent">A <see cref="Vector4"/> tangent.</param>
+        /// <returns>True if the tangent exists.</returns>
         Boolean TryGetTangent(out Vector4 tangent);
 
+        /// <summary>
+        /// Sets the position of the vertex.
+        /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
+        /// </summary>
+        /// <param name="position">A <see cref="Vector3"/> position.</param>
         void SetPosition(in Vector3 position);
+
+        /// <summary>
+        /// Sets the normal of the vertex.
+        /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
+        /// </summary>
+        /// <param name="normal">A <see cref="Vector3"/> normal.</param>
         void SetNormal(in Vector3 normal);
+
+        /// <summary>
+        /// Sets the tangent of the vertex.
+        /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
+        /// </summary>
+        /// <param name="tangent">A <see cref="Vector4"/> tangent.</param>
         void SetTangent(in Vector4 tangent);
 
+        /// <summary>
+        /// Applies a transform to the position, the normal and the tangent of this vertex.
+        /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
+        /// </summary>
+        /// <param name="xform">a valid <see cref="Matrix4x4"/> transform.</param>
         void ApplyTransform(in Matrix4x4 xform);
 
+        /// <summary>
+        /// calculates the difference between this vertex and <paramref name="baseValue"/>
+        /// </summary>
+        /// <param name="baseValue">The other vertex.</param>
+        /// <returns>The <see cref="VertexGeometryDelta"/> value to subtract.</returns>
         VertexGeometryDelta Subtract(IVertexGeometry baseValue);
 
+        /// <summary>
+        /// Adds a vertex delta to this value.
+        /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
+        /// </summary>
+        /// <param name="delta">The <see cref="VertexGeometryDelta"/> value to add.</param>
         void Add(in VertexGeometryDelta delta);
     }
 
@@ -87,29 +141,33 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         void IVertexGeometry.SetTangent(in Vector4 tangent) { }
 
+        /// <inheritdoc/>
         public VertexGeometryDelta Subtract(IVertexGeometry baseValue)
         {
             return new VertexGeometryDelta((VertexPosition)baseValue, this);
         }
 
+        /// <inheritdoc/>
         public void Add(in VertexGeometryDelta delta)
         {
             this.Position += delta.PositionDelta;
         }
 
+        /// <inheritdoc/>
         public Vector3 GetPosition() { return this.Position; }
 
+        /// <inheritdoc/>
         public bool TryGetNormal(out Vector3 normal) { normal = default; return false; }
 
+        /// <inheritdoc/>
         public bool TryGetTangent(out Vector4 tangent) { tangent = default; return false; }
 
+        /// <inheritdoc/>
         public void ApplyTransform(in Matrix4x4 xform)
         {
             Position = Vector3.Transform(Position, xform);
         }
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexGeometry(this); }
-
         #endregion
     }
 
@@ -183,31 +241,35 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         void IVertexGeometry.SetTangent(in Vector4 tangent) { }
 
+        /// <inheritdoc/>
         public VertexGeometryDelta Subtract(IVertexGeometry baseValue)
         {
             return new VertexGeometryDelta((VertexPositionNormal)baseValue, this);
         }
 
+        /// <inheritdoc/>
         public void Add(in VertexGeometryDelta delta)
         {
             this.Position += delta.PositionDelta;
             this.Normal += delta.NormalDelta;
         }
 
+        /// <inheritdoc/>
         public Vector3 GetPosition() { return this.Position; }
 
+        /// <inheritdoc/>
         public bool TryGetNormal(out Vector3 normal) { normal = this.Normal; return true; }
 
+        /// <inheritdoc/>
         public bool TryGetTangent(out Vector4 tangent) { tangent = default; return false; }
 
+        /// <inheritdoc/>
         public void ApplyTransform(in Matrix4x4 xform)
         {
             Position = Vector3.Transform(Position, xform);
             Normal = Vector3.Normalize(Vector3.TransformNormal(Normal, xform));
         }
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexGeometry(this); }
-
         #endregion
     }
 
@@ -280,11 +342,13 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         void IVertexGeometry.SetTangent(in Vector4 tangent) { this.Tangent = tangent; }
 
+        /// <inheritdoc/>
         public VertexGeometryDelta Subtract(IVertexGeometry baseValue)
         {
             return new VertexGeometryDelta((VertexPositionNormalTangent)baseValue, this);
         }
 
+        /// <inheritdoc/>
         public void Add(in VertexGeometryDelta delta)
         {
             this.Position += delta.PositionDelta;
@@ -292,12 +356,16 @@ namespace SharpGLTF.Geometry.VertexTypes
             this.Tangent += new Vector4(delta.TangentDelta, 0);
         }
 
+        /// <inheritdoc/>
         public Vector3 GetPosition() { return this.Position; }
 
+        /// <inheritdoc/>
         public bool TryGetNormal(out Vector3 normal) { normal = this.Normal; return true; }
 
+        /// <inheritdoc/>
         public bool TryGetTangent(out Vector4 tangent) { tangent = this.Tangent; return true; }
 
+        /// <inheritdoc/>
         public void ApplyTransform(in Matrix4x4 xform)
         {
             Position = Vector3.Transform(Position, xform);
@@ -308,8 +376,6 @@ namespace SharpGLTF.Geometry.VertexTypes
             Tangent = new Vector4(txyz, Tangent.W);
         }
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexGeometry(this); }
-
         #endregion
     }
 
@@ -424,19 +490,25 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         void IVertexGeometry.SetTangent(in Vector4 tangent) { this.TangentDelta = new Vector3(tangent.X, tangent.Y, tangent.Z); }
 
+        /// <inheritdoc/>
         public Vector3 GetPosition() { return this.PositionDelta; }
 
+        /// <inheritdoc/>
         public bool TryGetNormal(out Vector3 normal) { normal = this.NormalDelta; return true; }
 
+        /// <inheritdoc/>
         public bool TryGetTangent(out Vector4 tangent) { tangent = new Vector4(this.TangentDelta, 0); return true; }
 
+        /// <inheritdoc/>
         public void ApplyTransform(in Matrix4x4 xform) { throw new NotSupportedException(); }
 
+        /// <inheritdoc/>
         public VertexGeometryDelta Subtract(IVertexGeometry baseValue)
         {
             return new VertexGeometryDelta((VertexGeometryDelta)baseValue, this);
         }
 
+        /// <inheritdoc/>
         public void Add(in VertexGeometryDelta delta)
         {
             this.PositionDelta += delta.PositionDelta;
@@ -444,8 +516,6 @@ namespace SharpGLTF.Geometry.VertexTypes
             this.TangentDelta += delta.TangentDelta;
         }
 
-        public void Validate() { }
-
         #endregion
     }
 }

+ 174 - 31
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexMaterial.cs

@@ -7,21 +7,59 @@ using ENCODING = SharpGLTF.Schema2.EncodingType;
 
 namespace SharpGLTF.Geometry.VertexTypes
 {
+    /// <summary>
+    /// Represents the interface that must be implemented by a material vertex fragment.<br/>
+    /// Implemented by:<br/>
+    /// - <see cref="VertexColor1"/><br/>
+    /// - <see cref="VertexColor2"/><br/>
+    /// - <see cref="VertexTexture1"/><br/>
+    /// - <see cref="VertexTexture2"/><br/>
+    /// - <see cref="VertexColor1Texture1"/><br/>
+    /// - <see cref="VertexColor1Texture2"/><br/>
+    /// - <see cref="VertexColor2Texture1"/><br/>
+    /// - <see cref="VertexColor2Texture2"/><br/>
+    /// </summary>
     public interface IVertexMaterial
     {
+        /// <summary>
+        /// Gets the number of color attributes available in this vertex
+        /// </summary>
         int MaxColors { get; }
 
+        /// <summary>
+        /// Gets the number of texture coordinate attributes available in this vertex
+        /// </summary>
         int MaxTextCoords { get; }
 
-        void Validate();
-
+        /// <summary>
+        /// Gets a color attribute.
+        /// </summary>
+        /// <param name="index">An index from 0 to <see cref="MaxColors"/>.</param>
+        /// <returns>A <see cref="Vector4"/> value in the range of 0 to 1</returns>
         Vector4 GetColor(int index);
+
+        /// <summary>
+        /// Gets a UV texture coordinate attribute.
+        /// </summary>
+        /// <param name="index">An index from 0 to <see cref="MaxTextCoords"/>.</param>
+        /// <returns>A <see cref="Vector2"/> UV texture coordinate.</returns>
         Vector2 GetTexCoord(int index);
 
+        /// <summary>
+        /// Sets a color attribute.
+        /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
+        /// </summary>
+        /// <param name="setIndex">An index from 0 to <see cref="MaxColors"/>.</param>
+        /// <param name="color">A <see cref="Vector4"/> value in the range of 0 to 1</param>
         void SetColor(int setIndex, Vector4 color);
-        void SetTexCoord(int setIndex, Vector2 coord);
 
-        Object GetCustomAttribute(string attributeName);
+        /// <summary>
+        /// Sets a UV texture coordinate attribute.
+        /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
+        /// </summary>
+        /// <param name="setIndex">An index from 0 to <see cref="MaxTextCoords"/>.</param>
+        /// <param name="coord">A <see cref="Vector2"/> UV texture coordinate.</param>
+        void SetTexCoord(int setIndex, Vector2 coord);
     }
 
     /// <summary>
@@ -62,8 +100,10 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("COLOR_0", ENCODING.UNSIGNED_BYTE, true)]
         public Vector4 Color;
 
+        /// <inheritdoc/>
         public int MaxColors => 1;
 
+        /// <inheritdoc/>
         public int MaxTextCoords => 0;
 
         public override bool Equals(object obj) { return obj is VertexColor1 other && AreEqual(this, other); }
@@ -86,21 +126,19 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord) { }
 
-        object IVertexMaterial.GetCustomAttribute(string attributeName) { return null; }
-
+        /// <inheritdoc/>
         public Vector4 GetColor(int index)
         {
             if (index != 0) throw new ArgumentOutOfRangeException(nameof(index));
             return Color;
         }
 
+        /// <inheritdoc/>
         public Vector2 GetTexCoord(int index)
         {
             throw new NotSupportedException();
         }
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexMaterial(this); }
-
         #endregion
     }
 
@@ -147,8 +185,10 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("COLOR_1", ENCODING.UNSIGNED_BYTE, true)]
         public Vector4 Color1;
 
+        /// <inheritdoc/>
         public int MaxColors => 2;
 
+        /// <inheritdoc/>
         public int MaxTextCoords => 0;
 
         public override bool Equals(object obj) { return obj is VertexColor2 other && AreEqual(this, other); }
@@ -175,8 +215,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord) { }
 
-        object IVertexMaterial.GetCustomAttribute(string attributeName) { return null; }
-
+        /// <inheritdoc/>
         public Vector4 GetColor(int index)
         {
             if (index == 0) return Color0;
@@ -184,10 +223,9 @@ namespace SharpGLTF.Geometry.VertexTypes
             throw new ArgumentOutOfRangeException(nameof(index));
         }
 
+        /// <inheritdoc/>
         public Vector2 GetTexCoord(int index) { throw new NotSupportedException(); }
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexMaterial(this); }
-
         #endregion
     }
 
@@ -229,8 +267,10 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("TEXCOORD_0")]
         public Vector2 TexCoord;
 
+        /// <inheritdoc/>
         public int MaxColors => 0;
 
+        /// <inheritdoc/>
         public int MaxTextCoords => 1;
 
         public override bool Equals(object obj) { return obj is VertexTexture1 other && AreEqual(this, other); }
@@ -252,21 +292,19 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord) { if (setIndex == 0) this.TexCoord = coord; }
 
-        object IVertexMaterial.GetCustomAttribute(string attributeName) { return null; }
-
+        /// <inheritdoc/>
         public Vector4 GetColor(int index)
         {
             throw new NotSupportedException();
         }
 
+        /// <inheritdoc/>
         public Vector2 GetTexCoord(int index)
         {
             if (index != 0) throw new ArgumentOutOfRangeException(nameof(index));
             return TexCoord;
         }
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexMaterial(this); }
-
         #endregion
     }
 
@@ -313,8 +351,10 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("TEXCOORD_1")]
         public Vector2 TexCoord1;
 
+        /// <inheritdoc/>
         public int MaxColors => 0;
 
+        /// <inheritdoc/>
         public int MaxTextCoords => 2;
 
         public override bool Equals(object obj) { return obj is VertexTexture2 other && AreEqual(this, other); }
@@ -340,13 +380,13 @@ namespace SharpGLTF.Geometry.VertexTypes
             if (setIndex == 1) this.TexCoord1 = coord;
         }
 
-        object IVertexMaterial.GetCustomAttribute(string attributeName) { return null; }
-
+        /// <inheritdoc/>
         public Vector4 GetColor(int index)
         {
             throw new NotSupportedException();
         }
 
+        /// <inheritdoc/>
         public Vector2 GetTexCoord(int index)
         {
             if (index == 0) return TexCoord0;
@@ -354,8 +394,6 @@ namespace SharpGLTF.Geometry.VertexTypes
             throw new ArgumentOutOfRangeException(nameof(index));
         }
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexMaterial(this); }
-
         #endregion
     }
 
@@ -402,8 +440,10 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("TEXCOORD_0")]
         public Vector2 TexCoord;
 
+        /// <inheritdoc/>
         public int MaxColors => 1;
 
+        /// <inheritdoc/>
         public int MaxTextCoords => 1;
 
         public override bool Equals(object obj) { return obj is VertexColor1Texture1 other && AreEqual(this, other); }
@@ -425,22 +465,20 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord) { if (setIndex == 0) this.TexCoord = coord; }
 
-        object IVertexMaterial.GetCustomAttribute(string attributeName) { return null; }
-
+        /// <inheritdoc/>
         public Vector4 GetColor(int index)
         {
             if (index != 0) throw new ArgumentOutOfRangeException(nameof(index));
             return Color;
         }
 
+        /// <inheritdoc/>
         public Vector2 GetTexCoord(int index)
         {
             if (index != 0) throw new ArgumentOutOfRangeException(nameof(index));
             return TexCoord;
         }
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexMaterial(this); }
-
         #endregion
     }
 
@@ -492,8 +530,10 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("TEXCOORD_1")]
         public Vector2 TexCoord1;
 
+        /// <inheritdoc/>
         public int MaxColors => 1;
 
+        /// <inheritdoc/>
         public int MaxTextCoords => 2;
 
         public override bool Equals(object obj) { return obj is VertexColor1Texture2 other && AreEqual(this, other); }
@@ -519,14 +559,14 @@ namespace SharpGLTF.Geometry.VertexTypes
             if (setIndex == 1) this.TexCoord1 = coord;
         }
 
-        object IVertexMaterial.GetCustomAttribute(string attributeName) { return null; }
-
+        /// <inheritdoc/>
         public Vector4 GetColor(int index)
         {
             if (index != 0) throw new ArgumentOutOfRangeException(nameof(index));
             return Color;
         }
 
+        /// <inheritdoc/>
         public Vector2 GetTexCoord(int index)
         {
             switch (index)
@@ -537,7 +577,110 @@ namespace SharpGLTF.Geometry.VertexTypes
             }
         }
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexMaterial(this); }
+        #endregion
+    }
+
+    /// <summary>
+    /// Defines a Vertex attribute with two material Colors and two Texture Coordinates.
+    /// </summary>
+    [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
+    public struct VertexColor2Texture1 : IVertexMaterial, IEquatable<VertexColor2Texture1>
+    {
+        #region debug
+
+        private string _GetDebuggerDisplay() => $"𝐂₀:{Color0} 𝐂₁:{Color1} 𝐔𝐕:{TexCoord}";
+
+        #endregion
+
+        #region constructors
+
+        public VertexColor2Texture1(Vector4 color0, Vector4 color1, Vector2 tex)
+        {
+            Color0 = color0;
+            Color1 = color1;
+            TexCoord = tex;
+        }
+
+        public VertexColor2Texture1(IVertexMaterial src)
+        {
+            Guard.NotNull(src, nameof(src));
+
+            this.Color0 = src.MaxColors > 0 ? src.GetColor(0) : Vector4.One;
+            this.Color1 = src.MaxColors > 1 ? src.GetColor(1) : Vector4.One;
+            this.TexCoord = src.MaxTextCoords > 0 ? src.GetTexCoord(0) : Vector2.Zero;
+        }
+
+        public static implicit operator VertexColor2Texture1((Vector4 Color0, Vector4 Color1, Vector2 Tex) tuple)
+        {
+            return new VertexColor2Texture1(tuple.Color0, tuple.Color1, tuple.Tex);
+        }
+
+        #endregion
+
+        #region data
+
+        [VertexAttribute("COLOR_0", ENCODING.UNSIGNED_BYTE, true)]
+        public Vector4 Color0;
+
+        [VertexAttribute("COLOR_1", ENCODING.UNSIGNED_BYTE, true)]
+        public Vector4 Color1;
+
+        [VertexAttribute("TEXCOORD_0")]
+        public Vector2 TexCoord;
+
+        /// <inheritdoc/>
+        public int MaxColors => 2;
+
+        /// <inheritdoc/>
+        public int MaxTextCoords => 1;
+
+        public override bool Equals(object obj) { return obj is VertexColor2Texture1 other && AreEqual(this, other); }
+        public bool Equals(VertexColor2Texture1 other) { return AreEqual(this, other); }
+        public static bool operator ==(in VertexColor2Texture1 a, in VertexColor2Texture1 b) { return AreEqual(a, b); }
+        public static bool operator !=(in VertexColor2Texture1 a, in VertexColor2Texture1 b) { return !AreEqual(a, b); }
+
+        public static bool AreEqual(in VertexColor2Texture1 a, in VertexColor2Texture1 b)
+        {
+            return a.Color0 == b.Color0 && a.Color1 == b.Color1 && a.TexCoord == b.TexCoord;
+        }
+
+        public override int GetHashCode() { return Color0.GetHashCode() ^ Color1.GetHashCode() ^ TexCoord.GetHashCode(); }
+
+        #endregion
+
+        #region API
+
+        void IVertexMaterial.SetColor(int setIndex, Vector4 color)
+        {
+            if (setIndex == 0) this.Color0 = color;
+            if (setIndex == 1) this.Color1 = color;
+        }
+
+        void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord)
+        {
+            if (setIndex == 0) this.TexCoord = coord;
+        }
+
+        /// <inheritdoc/>
+        public Vector4 GetColor(int index)
+        {
+            switch (index)
+            {
+                case 0: return this.Color0;
+                case 1: return this.Color1;
+                default: throw new ArgumentOutOfRangeException(nameof(index));
+            }
+        }
+
+        /// <inheritdoc/>
+        public Vector2 GetTexCoord(int index)
+        {
+            switch (index)
+            {
+                case 0: return this.TexCoord;
+                default: throw new ArgumentOutOfRangeException(nameof(index));
+            }
+        }
 
         #endregion
     }
@@ -595,8 +738,10 @@ namespace SharpGLTF.Geometry.VertexTypes
         [VertexAttribute("TEXCOORD_1")]
         public Vector2 TexCoord1;
 
+        /// <inheritdoc/>
         public int MaxColors => 2;
 
+        /// <inheritdoc/>
         public int MaxTextCoords => 2;
 
         public override bool Equals(object obj) { return obj is VertexColor2Texture2 other && AreEqual(this, other); }
@@ -627,8 +772,7 @@ namespace SharpGLTF.Geometry.VertexTypes
             if (setIndex == 1) this.TexCoord1 = coord;
         }
 
-        object IVertexMaterial.GetCustomAttribute(string attributeName) { return null; }
-
+        /// <inheritdoc/>
         public Vector4 GetColor(int index)
         {
             switch (index)
@@ -639,6 +783,7 @@ namespace SharpGLTF.Geometry.VertexTypes
             }
         }
 
+        /// <inheritdoc/>
         public Vector2 GetTexCoord(int index)
         {
             switch (index)
@@ -649,8 +794,6 @@ namespace SharpGLTF.Geometry.VertexTypes
             }
         }
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexMaterial(this); }
-
         #endregion
     }
 }

+ 6 - 6
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexPreprocessors.cs

@@ -79,17 +79,17 @@ namespace SharpGLTF.Geometry.VertexTypes
         public void SetDebugPreprocessors()
         {
             Clear();
-            Append(FragmentPreprocessors.ValidateVertexGeometry);
-            Append(FragmentPreprocessors.ValidateVertexMaterial);
-            Append(FragmentPreprocessors.ValidateVertexSkinning);
+            Append(VertexUtils.ValidateVertexGeometry);
+            Append(VertexUtils.ValidateVertexMaterial);
+            Append(VertexUtils.ValidateVertexSkinning);
         }
 
         public void SetSanitizerPreprocessors()
         {
             Clear();
-            Append(FragmentPreprocessors.SanitizeVertexGeometry);
-            Append(FragmentPreprocessors.SanitizeVertexMaterial);
-            Append(FragmentPreprocessors.SanitizeVertexSkinning);
+            Append(VertexUtils.SanitizeVertexGeometry);
+            Append(VertexUtils.SanitizeVertexMaterial);
+            Append(VertexUtils.SanitizeVertexSkinning);
         }
 
         public bool PreprocessVertex(ref VertexBuilder<TvG, TvM, TvS> vertex)

+ 53 - 6
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexSkinning.cs

@@ -8,23 +8,66 @@ using ENCODING = SharpGLTF.Schema2.EncodingType;
 
 namespace SharpGLTF.Geometry.VertexTypes
 {
+    /// <summary>
+    /// Represents the interface that must be implemented by a skiining vertex fragment.
+    /// Implemented by:<br/>
+    /// - <see cref="VertexJoints4"/><br/>
+    /// - <see cref="VertexJoints8"/><br/>
+    /// </summary>
     public interface IVertexSkinning
     {
+        /// <summary>
+        /// Gets the Number of valid joints supported.<br/>Typical values are 0, 4 or 8.
+        /// </summary>
         int MaxBindings { get; }
 
-        void Validate();
-
+        /// <summary>
+        /// Gets a joint-weight pair.
+        /// </summary>
+        /// <param name="index">An index from 0 to <see cref="MaxBindings"/>.</param>
+        /// <returns>The joint-weight pair.</returns>
         (int Index, float Weight) GetJointBinding(int index);
+
+        /// <summary>
+        /// Sets a joint-weight pair.
+        /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
+        /// </summary>
+        /// <param name="index">An index from 0 to <see cref="MaxBindings"/>.</param>
+        /// <param name="joint">The joint index.</param>
+        /// <param name="weight">The weight of the joint.</param>
         void SetJointBinding(int index, int joint, float weight);
 
+        /// <summary>
+        /// Sets the packed joints-weights.
+        /// <para><b>⚠️ USE ONLY ON UNBOXED VALUES ⚠️</b></para>
+        /// </summary>
+        /// <param name="weights">The packed joints-weights.</param>
         void SetWeights(in SparseWeight8 weights);
 
+        /// <summary>
+        /// Gets the packed joints-weights.
+        /// </summary>
+        /// <returns>The packed joints-weights.</returns>
         SparseWeight8 GetWeights();
 
+        /// <summary>
+        /// Gets the indices of the first 4 joints.
+        /// </summary>
         Vector4 JointsLow { get; }
+
+        /// <summary>
+        /// Gets the indices of the next 4 joints, if supported.
+        /// </summary>
         Vector4 JointsHigh { get; }
 
+        /// <summary>
+        /// Gets the weights of the first 4 joints.
+        /// </summary>
         Vector4 WeightsLow { get; }
+
+        /// <summary>
+        /// Gets the weights of the next 4 joints, if supported.
+        /// </summary>
         Vector4 WeightsHigh { get; }
     }
 
@@ -85,12 +128,13 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         #region API
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexSkinning(this); }
-
+        /// <inheritdoc/>
         public SparseWeight8 GetWeights() { return new SparseWeight8(this.Joints, this.Weights); }
 
+        /// <inheritdoc/>
         public void SetWeights(in SparseWeight8 weights) { this = new VertexJoints4(weights); }
 
+        /// <inheritdoc/>
         public (int, float) GetJointBinding(int index)
         {
             switch (index)
@@ -103,6 +147,7 @@ namespace SharpGLTF.Geometry.VertexTypes
             }
         }
 
+        /// <inheritdoc/>
         public void SetJointBinding(int index, int joint, float weight)
         {
             switch (index)
@@ -191,12 +236,13 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         #region API
 
-        public void Validate() { FragmentPreprocessors.ValidateVertexSkinning(this); }
-
+        /// <inheritdoc/>
         public SparseWeight8 GetWeights() { return new SparseWeight8(this.Joints0, this.Joints1, this.Weights0, this.Weights1); }
 
+        /// <inheritdoc/>
         public void SetWeights(in SparseWeight8 weights) { this = new VertexJoints8(weights); }
 
+        /// <inheritdoc/>
         public (int Index, float Weight) GetJointBinding(int index)
         {
             switch (index)
@@ -213,6 +259,7 @@ namespace SharpGLTF.Geometry.VertexTypes
             }
         }
 
+        /// <inheritdoc/>
         public void SetJointBinding(int index, int joint, float weight)
         {
             switch (index)

+ 11 - 166
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexUtils.cs → src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexUtils.Accessors.cs

@@ -4,173 +4,14 @@ using System.Linq;
 using System.Numerics;
 
 using SharpGLTF.Memory;
+
 using DIMENSIONS = SharpGLTF.Schema2.DimensionType;
 using ENCODING = SharpGLTF.Schema2.EncodingType;
 
 namespace SharpGLTF.Geometry.VertexTypes
 {
-    static class VertexUtils
+    static partial class VertexUtils
     {
-        #region vertex builder
-
-        public static Type GetVertexGeometryType(params string[] vertexAttributes)
-        {
-            var t = typeof(VertexPosition);
-            if (vertexAttributes.Contains("NORMAL")) t = typeof(VertexPositionNormal);
-            if (vertexAttributes.Contains("TANGENT")) t = typeof(VertexPositionNormalTangent);
-            return t;
-        }
-
-        public static Type GetVertexMaterialType(params string[] vertexAttributes)
-        {
-            var colors = vertexAttributes.Contains("COLOR_0") ? 1 : 0;
-            colors = vertexAttributes.Contains("COLOR_1") ? 2 : colors;
-            colors = vertexAttributes.Contains("COLOR_2") ? 3 : colors;
-            colors = vertexAttributes.Contains("COLOR_3") ? 4 : colors;
-
-            var uvcoords = vertexAttributes.Contains("TEXCOORD_0") ? 1 : 0;
-            uvcoords = vertexAttributes.Contains("TEXCOORD_1") ? 2 : uvcoords;
-            uvcoords = vertexAttributes.Contains("TEXCOORD_2") ? 3 : uvcoords;
-            uvcoords = vertexAttributes.Contains("TEXCOORD_3") ? 4 : uvcoords;
-
-            return GetVertexMaterialType(colors, uvcoords);
-        }
-
-        public static Type GetVertexMaterialType(int colors, int uvcoords)
-        {
-            if (colors == 0)
-            {
-                if (uvcoords == 0) return typeof(VertexEmpty);
-                if (uvcoords == 1) return typeof(VertexTexture1);
-                if (uvcoords >= 2) return typeof(VertexTexture2);
-            }
-
-            if (colors == 1)
-            {
-                if (uvcoords == 0) return typeof(VertexColor1);
-                if (uvcoords == 1) return typeof(VertexColor1Texture1);
-                if (uvcoords >= 2) return typeof(VertexColor1Texture2);
-            }
-
-            if (colors >= 2)
-            {
-                if (uvcoords == 0) return typeof(VertexColor2);
-                if (uvcoords == 1) return typeof(VertexColor2Texture2);
-                if (uvcoords >= 2) return typeof(VertexColor2Texture2);
-            }
-
-            return typeof(VertexEmpty);
-        }
-
-        public static Type GetVertexSkinningType(params string[] vertexAttributes)
-        {
-            var joints = vertexAttributes.Contains("JOINTS_0") && vertexAttributes.Contains("WEIGHTS_0") ? 4 : 0;
-            joints = vertexAttributes.Contains("JOINTS_1") && vertexAttributes.Contains("WEIGHTS_1") ? 8 : joints;
-
-            if (joints == 4) return typeof(VertexJoints4);
-            if (joints == 8) return typeof(VertexJoints8);
-
-            return typeof(VertexEmpty);
-        }
-
-        public static Type GetVertexBuilderType(params string[] vertexAttributes)
-        {
-            var tvg = GetVertexGeometryType(vertexAttributes);
-            var tvm = GetVertexMaterialType(vertexAttributes);
-            var tvs = GetVertexSkinningType(vertexAttributes);
-
-            var vtype = typeof(VertexBuilder<,,>);
-
-            return vtype.MakeGenericType(tvg, tvm, tvs);
-        }
-
-        public static Type GetVertexBuilderType(bool hasNormals, bool hasTangents, int numCols, int numUV, int numJoints)
-        {
-            var tvg = typeof(VertexPosition);
-            if (hasNormals) tvg = typeof(VertexPositionNormal);
-            if (hasTangents) tvg = typeof(VertexPositionNormalTangent);
-
-            var tvm = GetVertexMaterialType(numCols, numUV);
-
-            var tvs = typeof(VertexEmpty);
-            if (numJoints == 4) tvs = typeof(VertexJoints4);
-            if (numJoints >= 8) tvs = typeof(VertexJoints8);
-
-            var vtype = typeof(VertexBuilder<,,>);
-
-            return vtype.MakeGenericType(tvg, tvm, tvs);
-        }
-
-        public static TvP ConvertToGeometry<TvP>(this IVertexGeometry src)
-            where TvP : struct, IVertexGeometry
-        {
-            if (src is TvP srcTyped) return srcTyped;
-
-            var dst = default(TvP);
-
-            dst.SetPosition(src.GetPosition());
-            if (src.TryGetNormal(out Vector3 nrm)) dst.SetNormal(nrm);
-            if (src.TryGetTangent(out Vector4 tgt)) dst.SetTangent(tgt);
-
-            return dst;
-        }
-
-        public static TvM ConvertToMaterial<TvM>(this IVertexMaterial src)
-            where TvM : struct, IVertexMaterial
-        {
-            if (src is TvM srcTyped) return srcTyped;
-
-            var dst = default(TvM);
-
-            int i = 0;
-
-            while (i < Math.Min(src.MaxColors, dst.MaxColors))
-            {
-                dst.SetColor(i, src.GetColor(i));
-                ++i;
-            }
-
-            while (i < dst.MaxColors)
-            {
-                dst.SetColor(i, Vector4.One);
-                ++i;
-            }
-
-            i = 0;
-
-            while (i < Math.Min(src.MaxTextCoords, dst.MaxTextCoords))
-            {
-                dst.SetTexCoord(i, src.GetTexCoord(i));
-                ++i;
-            }
-
-            while (i < dst.MaxColors)
-            {
-                dst.SetTexCoord(i, Vector2.Zero);
-                ++i;
-            }
-
-            return dst;
-        }
-
-        public static TvS ConvertToSkinning<TvS>(this IVertexSkinning src)
-            where TvS : struct, IVertexSkinning
-        {
-            if (src is TvS srcTyped) return srcTyped;
-
-            var sparse = src.MaxBindings > 0 ? src.GetWeights() : default;
-
-            var dst = default(TvS);
-
-            if (dst.MaxBindings > 0) dst.SetWeights(sparse);
-
-            return dst;
-        }
-
-        #endregion
-
-        #region memory buffers API
-
         public static MemoryAccessor CreateVertexMemoryAccessor<TVertex>(this IReadOnlyList<TVertex> vertices, string attributeName, PackedEncoding vertexEncoding)
             where TVertex : IVertexBuilder
         {
@@ -348,7 +189,13 @@ namespace SharpGLTF.Geometry.VertexTypes
             if (attributeName == "WEIGHTS_0") return v => v.GetSkinning().WeightsLow;
             if (attributeName == "WEIGHTS_1") return v => v.GetSkinning().WeightsHigh;
 
-            return v => v.GetMaterial().GetCustomAttribute(attributeName);
+            return v => _GetVertexBuilderCustomAttributeFunc(v.GetMaterial(), attributeName);
+        }
+
+        private static object _GetVertexBuilderCustomAttributeFunc(IVertexMaterial vertex, string attributeName)
+        {
+            if (!(vertex is IVertexCustom customVertex)) return null;
+            return customVertex.TryGetCustomAttribute(attributeName, out Object value) ? value : null;
         }
 
         private static TColumn[] _GetColumn<TVertex, TColumn>(this IReadOnlyList<TVertex> vertices, Converter<IVertexBuilder, Object> func)
@@ -358,14 +205,12 @@ namespace SharpGLTF.Geometry.VertexTypes
 
             for (int i = 0; i < dst.Length; ++i)
             {
-                var v = vertices[i];
+                var v = func(vertices[i]);
 
-                dst[i] = (TColumn)func(v);
+                dst[i] = v == null ? default : (TColumn)v;
             }
 
             return dst;
         }
-
-        #endregion
     }
 }

+ 181 - 0
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexUtils.Builder.cs

@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+using SharpGLTF.Memory;
+using DIMENSIONS = SharpGLTF.Schema2.DimensionType;
+using ENCODING = SharpGLTF.Schema2.EncodingType;
+
+namespace SharpGLTF.Geometry.VertexTypes
+{
+    static partial class VertexUtils
+    {
+        public static Type GetVertexGeometryType(params string[] vertexAttributes)
+        {
+            var t = typeof(VertexPosition);
+            if (vertexAttributes.Contains("NORMAL")) t = typeof(VertexPositionNormal);
+            if (vertexAttributes.Contains("TANGENT")) t = typeof(VertexPositionNormalTangent);
+            return t;
+        }
+
+        public static Type GetVertexMaterialType(params string[] vertexAttributes)
+        {
+            var colors = vertexAttributes.Contains("COLOR_0") ? 1 : 0;
+            colors = vertexAttributes.Contains("COLOR_1") ? 2 : colors;
+            colors = vertexAttributes.Contains("COLOR_2") ? 3 : colors;
+            colors = vertexAttributes.Contains("COLOR_3") ? 4 : colors;
+
+            var uvcoords = vertexAttributes.Contains("TEXCOORD_0") ? 1 : 0;
+            uvcoords = vertexAttributes.Contains("TEXCOORD_1") ? 2 : uvcoords;
+            uvcoords = vertexAttributes.Contains("TEXCOORD_2") ? 3 : uvcoords;
+            uvcoords = vertexAttributes.Contains("TEXCOORD_3") ? 4 : uvcoords;
+
+            return GetVertexMaterialType(colors, uvcoords);
+        }
+
+        public static Type GetVertexMaterialType(int colors, int uvcoords)
+        {
+            if (colors == 0)
+            {
+                if (uvcoords == 0) return typeof(VertexEmpty);
+                if (uvcoords == 1) return typeof(VertexTexture1);
+                if (uvcoords >= 2) return typeof(VertexTexture2);
+            }
+
+            if (colors == 1)
+            {
+                if (uvcoords == 0) return typeof(VertexColor1);
+                if (uvcoords == 1) return typeof(VertexColor1Texture1);
+                if (uvcoords >= 2) return typeof(VertexColor1Texture2);
+            }
+
+            if (colors >= 2)
+            {
+                if (uvcoords == 0) return typeof(VertexColor2);
+                if (uvcoords == 1) return typeof(VertexColor2Texture1);
+                if (uvcoords >= 2) return typeof(VertexColor2Texture2);
+            }
+
+            return typeof(VertexEmpty);
+        }
+
+        public static Type GetVertexSkinningType(params string[] vertexAttributes)
+        {
+            var joints = vertexAttributes.Contains("JOINTS_0") && vertexAttributes.Contains("WEIGHTS_0") ? 4 : 0;
+            joints = vertexAttributes.Contains("JOINTS_1") && vertexAttributes.Contains("WEIGHTS_1") ? 8 : joints;
+
+            if (joints == 4) return typeof(VertexJoints4);
+            if (joints == 8) return typeof(VertexJoints8);
+
+            return typeof(VertexEmpty);
+        }
+
+        public static Type GetVertexBuilderType(params string[] vertexAttributes)
+        {
+            var tvg = GetVertexGeometryType(vertexAttributes);
+            var tvm = GetVertexMaterialType(vertexAttributes);
+            var tvs = GetVertexSkinningType(vertexAttributes);
+
+            var vtype = typeof(VertexBuilder<,,>);
+
+            return vtype.MakeGenericType(tvg, tvm, tvs);
+        }
+
+        public static Type GetVertexBuilderType(bool hasNormals, bool hasTangents, int numCols, int numUV, int numJoints)
+        {
+            var tvg = typeof(VertexPosition);
+            if (hasNormals) tvg = typeof(VertexPositionNormal);
+            if (hasTangents) tvg = typeof(VertexPositionNormalTangent);
+
+            var tvm = GetVertexMaterialType(numCols, numUV);
+
+            var tvs = typeof(VertexEmpty);
+            if (numJoints == 4) tvs = typeof(VertexJoints4);
+            if (numJoints >= 8) tvs = typeof(VertexJoints8);
+
+            var vtype = typeof(VertexBuilder<,,>);
+
+            return vtype.MakeGenericType(tvg, tvm, tvs);
+        }
+
+        public static TvP ConvertToGeometry<TvP>(this IVertexGeometry src)
+            where TvP : struct, IVertexGeometry
+        {
+            if (src is TvP srcTyped) return srcTyped;
+
+            var dst = default(TvP);
+
+            dst.SetPosition(src.GetPosition());
+            if (src.TryGetNormal(out Vector3 nrm)) dst.SetNormal(nrm);
+            if (src.TryGetTangent(out Vector4 tgt)) dst.SetTangent(tgt);
+
+            return dst;
+        }
+
+        public static TvM ConvertToMaterial<TvM>(this IVertexMaterial src)
+            where TvM : struct, IVertexMaterial
+        {
+            if (src is TvM srcTyped) return srcTyped;
+
+            var dst = default(TvM);
+
+            int i = 0;
+
+            while (i < Math.Min(src.MaxColors, dst.MaxColors))
+            {
+                dst.SetColor(i, src.GetColor(i));
+                ++i;
+            }
+
+            while (i < dst.MaxColors)
+            {
+                dst.SetColor(i, Vector4.One);
+                ++i;
+            }
+
+            i = 0;
+
+            while (i < Math.Min(src.MaxTextCoords, dst.MaxTextCoords))
+            {
+                dst.SetTexCoord(i, src.GetTexCoord(i));
+                ++i;
+            }
+
+            while (i < dst.MaxTextCoords)
+            {
+                dst.SetTexCoord(i, Vector2.Zero);
+                ++i;
+            }
+
+            if (src is IVertexCustom srcx && dst is IVertexCustom dstx)
+            {
+                foreach (var key in dstx.CustomAttributes)
+                {
+                    if (srcx.TryGetCustomAttribute(key, out object val))
+                    {
+                        dstx.SetCustomAttribute(key, val);
+                    }
+                }
+
+                dst = (TvM)dstx; // unbox;
+            }
+
+            return dst;
+        }
+
+        public static TvS ConvertToSkinning<TvS>(this IVertexSkinning src)
+            where TvS : struct, IVertexSkinning
+        {
+            if (src is TvS srcTyped) return srcTyped;
+
+            var sparse = src.MaxBindings > 0 ? src.GetWeights() : default;
+
+            var dst = default(TvS);
+
+            if (dst.MaxBindings > 0) dst.SetWeights(sparse);
+
+            return dst;
+        }
+    }
+}

+ 3 - 1
src/SharpGLTF.Toolkit/Geometry/VertexTypes/FragmentPreprocessors.cs → src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexUtils.Validation.cs

@@ -8,7 +8,7 @@ namespace SharpGLTF.Geometry.VertexTypes
     /// <summary>
     /// Defines a set of vertex fragment preprocessors to be used with <see cref="VertexPreprocessor{TvG, TvM, TvS}"/>
     /// </summary>
-    static class FragmentPreprocessors
+    static partial class VertexUtils
     {
         /// <summary>
         /// validates a vertex geometry, throwing exceptions if found invalid
@@ -77,6 +77,8 @@ namespace SharpGLTF.Geometry.VertexTypes
                 Guard.IsTrue(t._IsFinite(), $"TexCoord{i}", "Values are not finite.");
             }
 
+            if (vertex is IVertexCustom custom) custom.Validate();
+
             return vertex;
         }
 

+ 0 - 90
tests/SharpGLTF.Toolkit.Tests/Geometry/CustomVertices.cs

@@ -1,90 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Numerics;
-using System.Text;
-
-using SharpGLTF.Geometry.VertexTypes;
-
-namespace SharpGLTF.Geometry
-{
-    [System.Diagnostics.DebuggerDisplay("𝐂:{Color} 𝐔𝐕:{TexCoord} _CUSTOM_1:{CustomId}")]
-    public struct VertexColor1Texture1Custom1 : IVertexMaterial
-    {
-        #region constructors
-
-        public VertexColor1Texture1Custom1(Vector4 color, Vector2 tex, Single customId)
-        {
-            Color = color;
-            TexCoord = tex;
-            CustomId = customId;
-        }
-
-        public VertexColor1Texture1Custom1(IVertexMaterial src)
-        {
-            this.Color = src.MaxColors > 0 ? src.GetColor(0) : Vector4.One;
-            this.TexCoord = src.MaxTextCoords > 0 ? src.GetTexCoord(0) : Vector2.Zero;
-
-            if (src is VertexColor1Texture1Custom1 custom)
-            {
-                this.CustomId = custom.CustomId;
-            }
-            else
-            {
-                this.CustomId = 0;
-            }
-        }
-
-        public static implicit operator VertexColor1Texture1Custom1((Vector4 color, Vector2 tex, Single customId) tuple)
-        {
-            return new VertexColor1Texture1Custom1(tuple.color, tuple.tex, tuple.customId);
-        }
-
-        #endregion
-
-        #region data
-
-        public const string CUSTOMATTRIBUTENAME = "_CUSTOM_1";
-
-        [VertexAttribute(CUSTOMATTRIBUTENAME, Schema2.EncodingType.FLOAT, false)]
-        public Single CustomId;
-
-        [VertexAttribute("COLOR_0", Schema2.EncodingType.UNSIGNED_BYTE, true)]
-        public Vector4 Color;
-
-        [VertexAttribute("TEXCOORD_0")]
-        public Vector2 TexCoord;
-
-        public int MaxColors => 1;
-
-        public int MaxTextCoords => 1;
-
-        #endregion
-
-        #region API
-
-        void IVertexMaterial.SetColor(int setIndex, Vector4 color) { if (setIndex == 0) this.Color = color; }
-
-        void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord) { if (setIndex == 0) this.TexCoord = coord; }
-
-        public Vector4 GetColor(int index)
-        {
-            if (index != 0) throw new ArgumentOutOfRangeException(nameof(index));
-            return Color;
-        }
-
-        public Vector2 GetTexCoord(int index)
-        {
-            if (index != 0) throw new ArgumentOutOfRangeException(nameof(index));
-            return TexCoord;
-        }
-
-        public void Validate() { FragmentPreprocessors.ValidateVertexMaterial(this); }
-
-        public object GetCustomAttribute(string attributeName)
-        {
-            return attributeName == CUSTOMATTRIBUTENAME ? (Object)CustomId : null;
-        }
-
-        #endregion
-    }
-}

+ 4 - 4
tests/SharpGLTF.Toolkit.Tests/Geometry/MeshBuilderTests.cs

@@ -252,9 +252,9 @@ namespace SharpGLTF.Geometry
 
             prim.AddTriangle
                 (
-                (Vector3.UnitX, (Vector4.One, Vector2.Zero, 1)),
-                (Vector3.UnitY, (Vector4.One, Vector2.Zero, 2)),
-                (Vector3.UnitZ, (Vector4.One, Vector2.Zero, 3))
+                (Vector3.UnitX, (Vector4.One, Vector2.Zero, 0.1f)),
+                (Vector3.UnitY, (Vector4.One, Vector2.Zero, 0.2f)),
+                (Vector3.UnitZ, (Vector4.One, Vector2.Zero, 0.3f))
                 );
 
             var dstScene = new Schema2.ModelRoot();
@@ -263,7 +263,7 @@ namespace SharpGLTF.Geometry
 
             var batchId = dstMesh.Primitives[0].GetVertexAccessor(VertexColor1Texture1Custom1.CUSTOMATTRIBUTENAME).AsScalarArray();
 
-            CollectionAssert.AreEqual(new float[] { 1, 2, 3 }, batchId);
+            CollectionAssert.AreEqual(new float[] { 0.1f, 0.2f, 0.3f }, batchId);
         }
 
         [Test]

+ 31 - 0
tests/SharpGLTF.Toolkit.Tests/Geometry/VertexTypes/CustomVertexTests.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+
+using NUnit.Framework;
+
+namespace SharpGLTF.Geometry.VertexTypes
+{
+    [Category("Toolkit")]
+    public class CustomVertexTests
+    {
+        [Test]
+        public void CreateCustomVertexTest()
+        {
+            var v2 = new VertexCustom2(0.3f, Vector4.One);
+            var v1 = new VertexColor1Texture1Custom1(v2);
+            
+            Assert.AreEqual(0.3f, v1.CustomId);
+        }
+
+        [Test]
+        public void TransferContentTest()
+        {
+            var v1 = new VertexColor1Texture1Custom1(Vector4.One, Vector2.One, 0.3f);
+            var v2 = v1.ConvertToMaterial<VertexCustom2>();
+            Assert.AreEqual(0.3f, v2.CustomId0);
+        }
+
+    }
+}

+ 204 - 0
tests/SharpGLTF.Toolkit.Tests/Geometry/VertexTypes/CustomVertices.cs

@@ -0,0 +1,204 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+
+namespace SharpGLTF.Geometry.VertexTypes
+{
+    [System.Diagnostics.DebuggerDisplay("𝐂:{Color} 𝐔𝐕:{TexCoord} {CustomId}")]
+    public struct VertexColor1Texture1Custom1 : IVertexCustom
+    {
+        #region constructors
+
+        public static implicit operator VertexColor1Texture1Custom1((Vector4 color, Vector2 tex, Single customId) tuple)
+        {
+            return new VertexColor1Texture1Custom1(tuple.color, tuple.tex, tuple.customId);
+        }
+
+        public VertexColor1Texture1Custom1(Vector4 color, Vector2 tex, Single customId)
+        {
+            Color = color;
+            TexCoord = tex;
+            CustomId = customId;
+        }
+
+        public VertexColor1Texture1Custom1(IVertexMaterial src)
+        {
+            this.Color = src.MaxColors > 0 ? src.GetColor(0) : Vector4.One;
+            this.TexCoord = src.MaxTextCoords > 0 ? src.GetTexCoord(0) : Vector2.Zero;
+
+            this.CustomId = 0;
+
+            if (src is VertexColor1Texture1Custom1 custom)
+            {
+                this.CustomId = custom.CustomId;
+            }
+            else if (src is IVertexCustom otherx)
+            {
+                if (otherx.TryGetCustomAttribute(CUSTOMATTRIBUTENAME, out object attr0) && attr0 is float c0) this.CustomId = c0;                
+            }
+        }        
+
+        #endregion
+
+        #region data
+
+        public const string CUSTOMATTRIBUTENAME = "_CUSTOM_0";
+
+        [VertexAttribute(CUSTOMATTRIBUTENAME, Schema2.EncodingType.FLOAT, false)]
+        public Single CustomId;
+
+        [VertexAttribute("COLOR_0", Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        public Vector4 Color;
+
+        [VertexAttribute("TEXCOORD_0")]
+        public Vector2 TexCoord;
+
+        public int MaxColors => 1;
+
+        public int MaxTextCoords => 1;
+
+        private static readonly string[] _CustomNames = { CUSTOMATTRIBUTENAME };
+        public IEnumerable<string> CustomAttributes => _CustomNames;
+
+        #endregion
+
+        #region API
+
+        void IVertexMaterial.SetColor(int setIndex, Vector4 color) { if (setIndex == 0) this.Color = color; }
+
+        void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord) { if (setIndex == 0) this.TexCoord = coord; }
+
+        public Vector4 GetColor(int index)
+        {
+            if (index != 0) throw new ArgumentOutOfRangeException(nameof(index));
+            return Color;
+        }
+
+        public Vector2 GetTexCoord(int index)
+        {
+            if (index != 0) throw new ArgumentOutOfRangeException(nameof(index));
+            return TexCoord;
+        }
+
+        public void Validate()
+        {
+            if (CustomId < 0) throw new ArgumentOutOfRangeException(nameof(CustomId));
+            if (CustomId > 1) throw new ArgumentOutOfRangeException(nameof(CustomId));
+        }
+
+        public bool TryGetCustomAttribute(string attribute, out object value)
+        {
+            if (attribute != CUSTOMATTRIBUTENAME) { value = null; return false; }
+            value = CustomId; return true;
+        }
+
+        public void SetCustomAttribute(string attributeName, object value)
+        {
+            if (attributeName == CUSTOMATTRIBUTENAME && value is Single valueSingle) CustomId = valueSingle;
+        }        
+
+        #endregion
+    }
+
+    [System.Diagnostics.DebuggerDisplay("{CustomId0} {CustomId1}")]
+    public struct VertexCustom2 : IVertexCustom
+    {
+        #region constructors
+
+        public static implicit operator VertexCustom2((Single val, Vector4 vec) tuple)
+        {
+            return new VertexCustom2(tuple.val, tuple.vec);
+        }
+
+        public VertexCustom2(Single val, Vector4 vec)
+        {
+            CustomId0 = val;
+            CustomId1 = vec;
+        }
+
+        public VertexCustom2(IVertexMaterial src)
+        {
+            this.CustomId0 = 0;
+            this.CustomId1 = Vector4.Zero;
+
+            if (src is VertexCustom2 other)
+            {
+                this.CustomId0 = other.CustomId0;
+                this.CustomId1 = other.CustomId1;
+            }
+            else if (src is IVertexCustom otherx)
+            {
+                if (otherx.TryGetCustomAttribute(CUSTOMATTRIBUTENAME0, out object attr0) && attr0 is float c0) this.CustomId0 = c0;
+                if (otherx.TryGetCustomAttribute(CUSTOMATTRIBUTENAME0, out object attr1) && attr1 is Vector4 c1) this.CustomId1 = c1;
+            }            
+        }        
+
+        #endregion
+
+        #region data
+
+        public const string CUSTOMATTRIBUTENAME0 = "_CUSTOM_0";
+        public const string CUSTOMATTRIBUTENAME1 = "_CUSTOM_1";
+
+        [VertexAttribute(CUSTOMATTRIBUTENAME0, Schema2.EncodingType.FLOAT, false)]
+        public Single CustomId0;
+
+        [VertexAttribute(CUSTOMATTRIBUTENAME1, Schema2.EncodingType.UNSIGNED_BYTE, true)]
+        public Vector4 CustomId1;        
+
+        public int MaxColors => 0;
+
+        public int MaxTextCoords => 0;
+
+        public IEnumerable<string> CustomAttributes
+        {
+            get
+            {
+                yield return CUSTOMATTRIBUTENAME0;
+                yield return CUSTOMATTRIBUTENAME1;
+            }
+        }
+
+        #endregion
+
+        #region API
+
+        void IVertexMaterial.SetColor(int setIndex, Vector4 color) { }
+
+        void IVertexMaterial.SetTexCoord(int setIndex, Vector2 coord) { }
+
+        public Vector4 GetColor(int index) { throw new ArgumentOutOfRangeException(nameof(index)); }
+
+        public Vector2 GetTexCoord(int index) { throw new ArgumentOutOfRangeException(nameof(index)); }
+
+        public void Validate()
+        {
+            if (CustomId0 < 0) throw new ArgumentOutOfRangeException(nameof(CustomId0));
+            if (CustomId0 > 1) throw new ArgumentOutOfRangeException(nameof(CustomId0));
+        }
+
+        public bool TryGetCustomAttribute(string attributeName, out object value)
+        {
+            switch (attributeName)
+            {
+                case CUSTOMATTRIBUTENAME0: value = CustomId0; return true;
+                case CUSTOMATTRIBUTENAME1: value = CustomId1; return true;
+            }
+
+            value = null;
+            return false;
+        }
+
+        public void SetCustomAttribute(string attributeName, object value)
+        {
+            switch (attributeName)
+            {
+                case CUSTOMATTRIBUTENAME0: if (value is Single c0) CustomId0 = c0; break;
+                case CUSTOMATTRIBUTENAME1: if (value is Vector4 c1) CustomId1 = c1; break;
+            }
+        }
+
+        #endregion
+    }
+}

+ 1 - 2
tests/SharpGLTF.Toolkit.Tests/Geometry/VertexTypes/VertexSkinningTests.cs

@@ -5,8 +5,7 @@ using System.Text;
 using NUnit.Framework;
 
 namespace SharpGLTF.Geometry.VertexTypes
-{
-    [TestFixture]
+{    
     [Category("Toolkit")]
     public class VertexSkinningTests
     {