Browse Source

WIP: continuing on validation...

Vicente Penades 6 năm trước cách đây
mục cha
commit
126232e62d

+ 32 - 45
src/SharpGLTF.Core/Schema2/gltf.Accessors.cs

@@ -339,8 +339,8 @@ namespace SharpGLTF.Schema2
         {
         {
             base.OnValidateReferences(result);
             base.OnValidateReferences(result);
 
 
-            result.CheckIsDefined("BufferView", _bufferView);
-            result.CheckReferenceIndex("BufferView", _bufferView, this.LogicalParent.LogicalBufferViews);
+            result.CheckSchemaIsDefined("BufferView", _bufferView);
+            result.CheckArrayIndexAccess("BufferView", _bufferView, this.LogicalParent.LogicalBufferViews);
         }
         }
 
 
         /// <see href="https://github.com/KhronosGroup/glTF-Validator/blob/master/lib/src/base/accessor.dart"/>
         /// <see href="https://github.com/KhronosGroup/glTF-Validator/blob/master/lib/src/base/accessor.dart"/>
@@ -357,23 +357,14 @@ namespace SharpGLTF.Schema2
             {
             {
                 if (this._componentType != EncodingType.UNSIGNED_BYTE && this._componentType != EncodingType.UNSIGNED_SHORT)
                 if (this._componentType != EncodingType.UNSIGNED_BYTE && this._componentType != EncodingType.UNSIGNED_SHORT)
                 {
                 {
-                    result.AddDataError(Validation.ErrorCodes.ACCESSOR_NORMALIZED_INVALID);
+                    result.AddDataError(nameof(Normalized), "Only (u)byte and (u)short accessors can be normalized.");
                 }
                 }
             }
             }
 
 
             if (SourceBufferView.DeviceBufferTarget == BufferMode.ARRAY_BUFFER)
             if (SourceBufferView.DeviceBufferTarget == BufferMode.ARRAY_BUFFER)
             {
             {
                 var len = Encoding.ByteLength() * Dimensions.DimCount();
                 var len = Encoding.ByteLength() * Dimensions.DimCount();
-                result.CheckMultipleOf("Encoding", len, 4);
-            }
-
-            if (SourceBufferView.DeviceBufferTarget == BufferMode.ELEMENT_ARRAY_BUFFER)
-            {
-                if (this.Normalized) result.AddDataError("Normalized", Validation.ErrorCodes.ACCESSOR_NORMALIZED_INVALID);
-                if (this.Dimensions != DimensionType.SCALAR) result.AddDataError("Dimensions", Validation.ErrorCodes.VALUE_MULTIPLE_OF, Encoding.ByteLength() * Dimensions.DimCount(), Encoding.ByteLength());
-                if (this.Encoding != EncodingType.UNSIGNED_BYTE && this.Encoding != EncodingType.UNSIGNED_INT && this.Encoding != EncodingType.UNSIGNED_SHORT) result.AddDataError("Encoding", Validation.ErrorCodes.VALUE_MULTIPLE_OF, Encoding.ByteLength() * Dimensions.DimCount(), Encoding.ByteLength());
-
-                // if this.Encoding != EncodingType.UNSIGNED_BYTE > warning, no longer valid.
+                result.CheckSchemaIsMultipleOf("Encoding", len, 4);
             }
             }
         }
         }
 
 
@@ -424,14 +415,9 @@ namespace SharpGLTF.Schema2
             result = result.GetContext(this);
             result = result.GetContext(this);
 
 
             SourceBufferView.ValidateBufferUsage(result, BufferMode.ELEMENT_ARRAY_BUFFER);
             SourceBufferView.ValidateBufferUsage(result, BufferMode.ELEMENT_ARRAY_BUFFER);
-
-            if (this.Normalized) result.AddDataError(Validation.ErrorCodes.MESH_PRIMITIVE_INDICES_ACCESSOR_INVALID_FORMAT, this.Normalized, false);
-
-            if (Encoding != EncodingType.UNSIGNED_BYTE &&
-                Encoding != EncodingType.UNSIGNED_SHORT &&
-                Encoding != EncodingType.UNSIGNED_INT) result.AddDataError(Validation.ErrorCodes.MESH_PRIMITIVE_INDICES_ACCESSOR_INVALID_FORMAT, this.Encoding, EncodingType.UNSIGNED_BYTE, EncodingType.UNSIGNED_SHORT, EncodingType.UNSIGNED_INT);
-
-            if (Dimensions != DimensionType.SCALAR) result.AddDataError(Validation.ErrorCodes.MESH_PRIMITIVE_INDICES_ACCESSOR_INVALID_FORMAT, this.Dimensions, DimensionType.SCALAR);
+            result.CheckLinkMustBeAnyOf(nameof(Normalized), Normalized, false);
+            result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.UNSIGNED_BYTE, EncodingType.UNSIGNED_SHORT, EncodingType.UNSIGNED_INT);
+            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.SCALAR);
 
 
             uint restart_value = 0xff;
             uint restart_value = 0xff;
             if (this.Encoding == EncodingType.UNSIGNED_SHORT) restart_value = 0xffff;
             if (this.Encoding == EncodingType.UNSIGNED_SHORT) restart_value = 0xffff;
@@ -450,13 +436,15 @@ namespace SharpGLTF.Schema2
             result = result.GetContext(this);
             result = result.GetContext(this);
 
 
             SourceBufferView.ValidateBufferUsage(result, BufferMode.ARRAY_BUFFER);
             SourceBufferView.ValidateBufferUsage(result, BufferMode.ARRAY_BUFFER);
+            result.CheckLinkMustBeAnyOf(nameof(Normalized), Normalized, false);
+            result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.FLOAT);
+            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC3);
 
 
             var positions = this.AsVector3Array();
             var positions = this.AsVector3Array();
 
 
             for (int i = 0; i < positions.Count; ++i)
             for (int i = 0; i < positions.Count; ++i)
             {
             {
-                var pos = positions[i];
-                result.CheckDataIsFinite(i, pos);
+                result.CheckIsFinite(i, positions[i]);
             }
             }
         }
         }
 
 
@@ -465,14 +453,15 @@ namespace SharpGLTF.Schema2
             result = result.GetContext(this);
             result = result.GetContext(this);
 
 
             SourceBufferView.ValidateBufferUsage(result, BufferMode.ARRAY_BUFFER);
             SourceBufferView.ValidateBufferUsage(result, BufferMode.ARRAY_BUFFER);
+            result.CheckLinkMustBeAnyOf(nameof(Normalized), Normalized, false);
+            result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.FLOAT);
+            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC3);
 
 
             var normals = this.AsVector3Array();
             var normals = this.AsVector3Array();
 
 
             for (int i = 0; i < normals.Count; ++i)
             for (int i = 0; i < normals.Count; ++i)
             {
             {
-                var nrm = normals[i];
-                result.CheckDataIsFinite(i, nrm);
-                result.CheckDataIsUnitLength(i, nrm);
+                result.CheckIsUnitLength(i, normals[i]);
             }
             }
         }
         }
 
 
@@ -481,48 +470,46 @@ namespace SharpGLTF.Schema2
             result = result.GetContext(this);
             result = result.GetContext(this);
 
 
             SourceBufferView.ValidateBufferUsage(result, BufferMode.ARRAY_BUFFER);
             SourceBufferView.ValidateBufferUsage(result, BufferMode.ARRAY_BUFFER);
+            result.CheckLinkMustBeAnyOf(nameof(Normalized), Normalized, false);
+            result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.FLOAT);
+            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC3, DimensionType.VEC4);
 
 
             var tangents = this.AsVector4Array();
             var tangents = this.AsVector4Array();
 
 
             for (int i = 0; i < tangents.Count; ++i)
             for (int i = 0; i < tangents.Count; ++i)
             {
             {
-                var tgt = tangents[i];
-
-                result.CheckDataIsFinite(i, tgt);
-                result.CheckDataIsUnitLength(i, new Vector3(tgt.X, tgt.Y, tgt.Z));
-                result.CheckDataIsValidSign(i, tgt.W);
+                result.CheckIsTangent(i, tangents[i]);
             }
             }
         }
         }
 
 
-        internal void ValidateJoints(Validation.ValidationContext result, int jwset, int jointsCount)
+        internal void ValidateJoints(Validation.ValidationContext result, string attributeName)
         {
         {
             result = result.GetContext(this);
             result = result.GetContext(this);
 
 
             SourceBufferView.ValidateBufferUsage(result, BufferMode.ARRAY_BUFFER);
             SourceBufferView.ValidateBufferUsage(result, BufferMode.ARRAY_BUFFER);
+            result.CheckLinkMustBeAnyOf(nameof(Normalized), Normalized, false);
+            result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.UNSIGNED_BYTE, EncodingType.UNSIGNED_SHORT, EncodingType.FLOAT);
+            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC4);
 
 
             var joints = this.AsVector4Array();
             var joints = this.AsVector4Array();
 
 
             for (int i = 0; i < joints.Count; ++i)
             for (int i = 0; i < joints.Count; ++i)
             {
             {
-                var jjjj = joints[i];
-                result.CheckDataIsFinite(i, jjjj);
-                result.CheckDataIsInRange(i, jjjj, 0, jointsCount-1);
+                result.CheckIsFinite(i, joints[i]);
             }
             }
         }
         }
 
 
         internal void ValidateWeights(Validation.ValidationContext result, int jwset)
         internal void ValidateWeights(Validation.ValidationContext result, int jwset)
         {
         {
             result = result.GetContext(this);
             result = result.GetContext(this);
-
-            SourceBufferView.ValidateBufferUsage(result, BufferMode.ARRAY_BUFFER);
+            result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.UNSIGNED_BYTE, EncodingType.UNSIGNED_SHORT, EncodingType.FLOAT);
+            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.VEC4);
 
 
             var weights = this.AsVector4Array();
             var weights = this.AsVector4Array();
 
 
             for (int i = 0; i < weights.Count; ++i)
             for (int i = 0; i < weights.Count; ++i)
             {
             {
-                var wwww = weights[i];
-                result.CheckDataIsFinite(i, wwww);
-                result.CheckDataIsInRange(i, wwww, 0, 1);
+                result.CheckIsInRange(i, weights[i], 0, 1);
 
 
                 // theoretically, the sum of all the weights should give 1, ASSUMING there's only one weight set.
                 // theoretically, the sum of all the weights should give 1, ASSUMING there's only one weight set.
                 // but in practice, that seems not to be true.
                 // but in practice, that seems not to be true.
@@ -533,16 +520,16 @@ namespace SharpGLTF.Schema2
         {
         {
             result = result.GetContext(this);
             result = result.GetContext(this);
 
 
-            // if (SourceBufferView.DeviceBufferTarget != null)  
+            // SourceBufferView.ValidateBufferUsage(result, null);
+            // result.CheckLinkMustBeAnyOf(nameof(Normalized), Normalized, false);
+            // result.CheckLinkMustBeAnyOf(nameof(Encoding), Encoding, EncodingType.UNSIGNED_BYTE, EncodingType.UNSIGNED_SHORT, EncodingType.FLOAT);
+            result.CheckLinkMustBeAnyOf(nameof(Dimensions), Dimensions, DimensionType.MAT4);
 
 
             var matrices = this.AsMatrix4x4Array();
             var matrices = this.AsMatrix4x4Array();
 
 
             for (int i = 0; i < matrices.Count; ++i)
             for (int i = 0; i < matrices.Count; ++i)
             {
             {
-                var m = matrices[i];
-
-                if (Matrix4x4.Invert(m, out Matrix4x4 r)) continue;
-                result.AddDataError(Validation.ErrorCodes.ACCESSOR_INDECOMPOSABLE_MATRIX, i);
+                result.CheckIsMatrix(i, matrices[i]);
             }
             }
         }
         }
 
 

+ 52 - 0
src/SharpGLTF.Core/Schema2/gltf.Animations.cs

@@ -7,6 +7,7 @@ using System.Numerics;
 using SharpGLTF.Collections;
 using SharpGLTF.Collections;
 using SharpGLTF.Transforms;
 using SharpGLTF.Transforms;
 using SharpGLTF.Animations;
 using SharpGLTF.Animations;
+using SharpGLTF.Validation;
 
 
 namespace SharpGLTF.Schema2
 namespace SharpGLTF.Schema2
 {
 {
@@ -216,6 +217,21 @@ namespace SharpGLTF.Schema2
         }
         }
 
 
         #endregion
         #endregion
+
+        #region Validation
+
+        protected override void OnValidateReferences(ValidationContext result)
+        {
+            base.OnValidateReferences(result);
+
+            result.CheckLinksInCollection("Samplers", _samplers);
+            result.CheckLinksInCollection("Channels", _channels);
+
+            foreach (var s in _samplers) s.ValidateReferences(result);
+            foreach (var c in _channels) c.ValidateReferences(result);
+        }
+
+        #endregion
     }
     }
 
 
     sealed partial class AnimationChannelTarget
     sealed partial class AnimationChannelTarget
@@ -239,6 +255,17 @@ namespace SharpGLTF.Schema2
         internal PropertyPath _NodePath => this._path;
         internal PropertyPath _NodePath => this._path;
 
 
         #endregion
         #endregion
+
+        #region Validation
+
+        protected override void OnValidateReferences(ValidationContext result)
+        {
+            base.OnValidateReferences(result);
+
+            result.CheckArrayIndexAccess("Node", _node, result.Root.LogicalNodes);
+        }
+
+        #endregion
     }
     }
 
 
     sealed partial class AnimationChannel : IChildOf<Animation>
     sealed partial class AnimationChannel : IChildOf<Animation>
@@ -290,6 +317,19 @@ namespace SharpGLTF.Schema2
         public PropertyPath TargetNodePath => this._target?._NodePath ?? PropertyPath.translation;
         public PropertyPath TargetNodePath => this._target?._NodePath ?? PropertyPath.translation;
 
 
         #endregion
         #endregion
+
+        #region Validation
+
+        protected override void OnValidateReferences(ValidationContext result)
+        {
+            base.OnValidateReferences(result);
+
+            result.CheckArrayIndexAccess("Sampler", _sampler, this.LogicalParent._Samplers);
+
+            _target.ValidateReferences(result);
+        }
+
+        #endregion
     }
     }
 
 
     sealed partial class AnimationSampler : IChildOf<Animation>,
     sealed partial class AnimationSampler : IChildOf<Animation>,
@@ -687,6 +727,18 @@ namespace SharpGLTF.Schema2
         }
         }
 
 
         #endregion
         #endregion
+
+        #region validation
+
+        protected override void OnValidateReferences(ValidationContext result)
+        {
+            base.OnValidateReferences(result);
+
+            result.CheckArrayIndexAccess("Input", _input, this.LogicalParent.LogicalParent.LogicalAccessors);
+            result.CheckArrayIndexAccess("Output", _output, this.LogicalParent.LogicalParent.LogicalAccessors);
+        }
+
+        #endregion
     }
     }
 
 
     public interface IAnimationSampler<T>
     public interface IAnimationSampler<T>

+ 21 - 0
src/SharpGLTF.Core/Schema2/gltf.Buffer.cs

@@ -109,6 +109,27 @@ namespace SharpGLTF.Schema2
         }
         }
 
 
         #endregion
         #endregion
+
+        #region validation
+
+        protected override void OnValidateReferences(Validation.ValidationContext result)
+        {
+            base.OnValidateReferences(result);
+
+            result.CheckSchemaIsValidURI("Uri", this._uri);
+
+            result.CheckSchemaIsInRange("ByteLength", _byteLength, _byteLengthMinimum, int.MaxValue);
+            result.CheckSchemaIsMultipleOf("ByteLength", _byteLength, 4);
+        }
+
+        protected override void OnValidate(Validation.ValidationContext result)
+        {
+            base.OnValidate(result);
+
+            if (_Content.Length < _byteLength) result.AddDataError("ByteLength", $"Actual data length {_Content.Length} is less than the declared buffer byteLength {_byteLength}.");
+        }
+
+        #endregion
     }
     }
 
 
     public partial class ModelRoot
     public partial class ModelRoot

+ 23 - 4
src/SharpGLTF.Core/Schema2/gltf.BufferView.cs

@@ -75,7 +75,10 @@ namespace SharpGLTF.Schema2
             get
             get
             {
             {
                 var buffer = this.LogicalParent.LogicalBuffers[this._buffer];
                 var buffer = this.LogicalParent.LogicalBuffers[this._buffer];
-                return new BYTES(buffer.Content, this._byteOffset.AsValue(0), this._byteLength);
+                var offset = this._byteOffset.AsValue(_byteOffsetDefault);
+                var length = this._byteLength;
+
+                return new BYTES(buffer.Content, offset, length);
             }
             }
         }
         }
 
 
@@ -132,14 +135,30 @@ namespace SharpGLTF.Schema2
         {
         {
             base.OnValidateReferences(result);
             base.OnValidateReferences(result);
 
 
-            result.CheckReferenceIndex(nameof(Buffer), _buffer, this.LogicalParent.LogicalBuffers);
+            result.CheckArrayIndexAccess(nameof(Buffer), _buffer, this.LogicalParent.LogicalBuffers);
+
+            result.CheckSchemaNonNegative("ByteOffset", _byteOffset);
+
+            result.CheckSchemaIsInRange("ByteLength", _byteLength, _byteLengthMinimum, int.MaxValue);
+
+            // ByteStride must be multiple of 4, between 4 and 252
+            if (_byteStride.HasValue)
+            {
+                result.CheckSchemaIsInRange(nameof(ByteStride), _byteStride.Value, _byteStrideMinimum, _byteStrideMaximum);
+                result.CheckSchemaIsMultipleOf(nameof(ByteStride), _byteStride.Value, 4);
+            }
         }
         }
 
 
         protected override void OnValidate(Validation.ValidationContext result)
         protected override void OnValidate(Validation.ValidationContext result)
         {
         {
             base.OnValidate(result);
             base.OnValidate(result);
 
 
-            result.CheckMultipleOf(nameof(ByteStride), ByteStride, 4);
+            var buffer = this.LogicalParent.LogicalBuffers[this._buffer];
+            var bcontent = buffer.Content;
+
+            result.CheckArrayRangeAccess("ByteOffset", _byteOffset, _byteLength, buffer.Content);
+
+            if (ByteStride > _byteLength) result.AddSemanticError(nameof(ByteStride), $"value ({ByteStride}) is larger than byteLength ({_byteLength}).");
 
 
             // if (this.DeviceBufferTarget.HasValue && this.FindAccessors().Any(item => item.IsSparse)) result.AddError()
             // if (this.DeviceBufferTarget.HasValue && this.FindAccessors().Any(item => item.IsSparse)) result.AddError()
         }
         }
@@ -151,7 +170,7 @@ namespace SharpGLTF.Schema2
             if (!this.DeviceBufferTarget.HasValue) return;
             if (!this.DeviceBufferTarget.HasValue) return;
             if (usingMode == this.DeviceBufferTarget.Value) return;
             if (usingMode == this.DeviceBufferTarget.Value) return;
 
 
-            result.AddSchemaError(nameof(DeviceBufferTarget), Validation.ErrorCodes.BUFFER_VIEW_TARGET_OVERRIDE, this.DeviceBufferTarget.Value, usingMode);
+            result.AddLinkError(nameof(DeviceBufferTarget), $"is set as {this.DeviceBufferTarget.Value}. But an accessor wants to use it as '{usingMode}'.");
         }
         }
 
 
         #endregion
         #endregion

+ 1 - 1
src/SharpGLTF.Core/Schema2/gltf.ExtraProperties.cs

@@ -138,7 +138,7 @@ namespace SharpGLTF.Schema2
 
 
             if (this._extras != null)
             if (this._extras != null)
             {
             {
-                result.CheckJsonSerializable("Extras", this._extras);
+                result.CheckSchemaIsJsonSerializable("Extras", this._extras);
             }
             }
         }
         }
 
 

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

@@ -327,12 +327,9 @@ namespace SharpGLTF.Schema2
         {
         {
             base.OnValidateReferences(result);
             base.OnValidateReferences(result);
 
 
-            if (!String.IsNullOrWhiteSpace(_uri))
-            {
-                if (!Uri.TryCreate(_uri, UriKind.Relative, out Uri uri)) result.AddLinkError(Validation.ErrorCodes.INVALID_URI);
-            }
+            result.CheckSchemaIsValidURI("Uri", this._uri);
 
 
-            result.CheckReferenceIndex("BufferView", _bufferView, this.LogicalParent.LogicalBufferViews);
+            result.CheckArrayIndexAccess("BufferView", _bufferView, this.LogicalParent.LogicalBufferViews);
         }
         }
 
 
         #endregion
         #endregion

+ 1 - 1
src/SharpGLTF.Core/Schema2/gltf.LogicalChildOfRoot.cs

@@ -38,7 +38,7 @@ namespace SharpGLTF.Schema2
         {
         {
             return items.All(item => Object.ReferenceEquals(this.LogicalParent, item.LogicalParent));
             return items.All(item => Object.ReferenceEquals(this.LogicalParent, item.LogicalParent));
         }
         }
-        
+
         #endregion
         #endregion
 
 
         #region API
         #region API

+ 12 - 6
src/SharpGLTF.Core/Schema2/gltf.Mesh.cs

@@ -48,6 +48,8 @@ namespace SharpGLTF.Schema2
 
 
         public IReadOnlyList<Single> MorphWeights => _weights.Count == 0 ? null : _weights.Select(item => (Single)item).ToList();
         public IReadOnlyList<Single> MorphWeights => _weights.Count == 0 ? null : _weights.Select(item => (Single)item).ToList();
 
 
+        public bool AllPrimitivesHaveJoints => Primitives.All(p => p.GetVertexAccessor("JOINTS_0") != null);
+
         #endregion
         #endregion
 
 
         #region API
         #region API
@@ -92,11 +94,20 @@ namespace SharpGLTF.Schema2
 
 
         #region Validation
         #region Validation
 
 
+        protected override void OnValidateReferences(Validation.ValidationContext result)
+        {
+            base.OnValidateReferences(result);
+
+            result.CheckLinksInCollection("Primitives", _primitives);
+
+            foreach (var p in this.Primitives) p.ValidateReferences(result);
+        }
+
         protected override void OnValidate(Validation.ValidationContext result)
         protected override void OnValidate(Validation.ValidationContext result)
         {
         {
             base.OnValidate(result);
             base.OnValidate(result);
 
 
-            foreach (var p in this.Primitives) { p.Validate(result); }
+            foreach (var p in this.Primitives) p.Validate(result);
 
 
             var morphTargetsCount = this.Primitives
             var morphTargetsCount = this.Primitives
                 .Select(item => item.MorphTargetsCount)
                 .Select(item => item.MorphTargetsCount)
@@ -107,11 +118,6 @@ namespace SharpGLTF.Schema2
             if (_weights.Count != 0 && morphTargetsCount.First() != _weights.Count) result.AddSemanticError(Validation.ErrorCodes.MESH_INVALID_WEIGHTS_COUNT, _weights.Count, morphTargetsCount.First());
             if (_weights.Count != 0 && morphTargetsCount.First() != _weights.Count) result.AddSemanticError(Validation.ErrorCodes.MESH_INVALID_WEIGHTS_COUNT, _weights.Count, morphTargetsCount.First());
         }
         }
 
 
-        internal void ValidateSkinning(Validation.ValidationContext result, int jointsCount)
-        {
-            foreach (var p in Primitives) p.ValidateSkinning(result, jointsCount);
-        }
-
         #endregion
         #endregion
     }
     }
 
 

+ 63 - 30
src/SharpGLTF.Core/Schema2/gltf.MeshPrimitive.cs

@@ -200,17 +200,17 @@ namespace SharpGLTF.Schema2
 
 
             var root = this.LogicalParent.LogicalParent;
             var root = this.LogicalParent.LogicalParent;
 
 
-            result.CheckReferenceIndex("Material", _material, root.LogicalMaterials);
-            result.CheckReferenceIndex("Indices", _indices, root.LogicalAccessors);
+            result.CheckArrayIndexAccess("Material", _material, root.LogicalMaterials);
+            result.CheckArrayIndexAccess("Indices", _indices, root.LogicalAccessors);
 
 
             foreach (var idx in _attributes.Values)
             foreach (var idx in _attributes.Values)
             {
             {
-                result.CheckReferenceIndex("Attributes", idx, root.LogicalAccessors);
+                result.CheckArrayIndexAccess("Attributes", idx, root.LogicalAccessors);
             }
             }
 
 
             foreach (var idx in _targets.SelectMany(item => item.Values))
             foreach (var idx in _targets.SelectMany(item => item.Values))
             {
             {
-                result.CheckReferenceIndex("Targets", idx, root.LogicalAccessors);
+                result.CheckArrayIndexAccess("Targets", idx, root.LogicalAccessors);
             }
             }
         }
         }
 
 
@@ -275,47 +275,80 @@ namespace SharpGLTF.Schema2
                 }
                 }
             }
             }
 
 
+            _ValidatePositions(result);
+            _ValidateNormals(result);
+            _ValidateTangents(result);
+
+            // find skins using this mesh primitive:
+
+            var skins = this.LogicalParent
+                .LogicalParent
+                .LogicalNodes
+                .Where(item => item.Mesh == this.LogicalParent)
+                .Select(item => item.Skin)
+                .ToList();
+
+            _ValidateJoints(result, skins, "JOINTS_0");
+            _ValidateJoints(result, skins, "JOINTS_1");
+        }
+
+        private void _ValidatePositions(Validation.ValidationContext result)
+        {
             var positions = GetVertexAccessor("POSITION");
             var positions = GetVertexAccessor("POSITION");
-            if (positions != null)
-            {
-                positions.ValidatePositions(result);
-            }
-            else
+            if (positions == null)
             {
             {
                 result.AddSemanticWarning(Validation.WarnCodes.MESH_PRIMITIVE_NO_POSITION);
                 result.AddSemanticWarning(Validation.WarnCodes.MESH_PRIMITIVE_NO_POSITION);
+                return;
             }
             }
 
 
-            GetVertexAccessor("NORMAL")?.ValidateNormals(result);
+            positions.ValidatePositions(result);
+        }
 
 
-            var tangents = GetVertexAccessor("TANGENT");
-            if (tangents != null)
-            {
-                if (GetVertexAccessor("NORMAL") == null) result.AddSemanticWarning(Validation.WarnCodes.MESH_PRIMITIVE_TANGENT_WITHOUT_NORMAL);
-                if (DrawPrimitiveType == PrimitiveType.POINTS) result.AddSemanticWarning(Validation.WarnCodes.MESH_PRIMITIVE_TANGENT_POINTS);
-                tangents.ValidateTangents(result);
-            }
+        private void _ValidateNormals(Validation.ValidationContext result)
+        {
+            var normals = GetVertexAccessor("NORMAL");
+            if (normals == null) return;
 
 
+            normals.ValidateNormals(result);
         }
         }
 
 
-        internal void ValidateSkinning(Validation.ValidationContext result, int jointsCount)
+        private void _ValidateTangents(Validation.ValidationContext result)
         {
         {
-            var j0 = GetVertexAccessor("JOINTS_0");
-            var w0 = GetVertexAccessor("WEIGHTS_0");
-            ValidateSkinning(result, j0, w0, 0, jointsCount);
+            var tangents = GetVertexAccessor("TANGENT");
+            if (tangents == null) return;
+
+            if (GetVertexAccessor("NORMAL") == null) result.AddSemanticWarning(Validation.WarnCodes.MESH_PRIMITIVE_TANGENT_WITHOUT_NORMAL);
+            if (DrawPrimitiveType == PrimitiveType.POINTS) result.AddSemanticWarning(Validation.WarnCodes.MESH_PRIMITIVE_TANGENT_POINTS);
 
 
-            var j1 = GetVertexAccessor("JOINTS_1");
-            var w1 = GetVertexAccessor("WEIGHTS_1");
-            if (j1 != null || w1 != null) ValidateSkinning(result, j1, w1, 1, jointsCount);
+            tangents.ValidateTangents(result);
         }
         }
 
 
-        private void ValidateSkinning(Validation.ValidationContext result, Accessor j, Accessor w, int jwset, int jointsCount)
+        private void _ValidateJoints(Validation.ValidationContext result, IEnumerable<Skin> skins, string attributeName)
         {
         {
-            // if (j == null) result.AddError(this, $"Missing JOINTS_{jwset} vertex attribute");
-            // if (w == null) result.AddError(this, $"Missing WEIGHTS_{jwset} vertex attribute");
-            if (j == null || w == null) return;
+            var joints = GetVertexAccessor(attributeName);
 
 
-            j.ValidateJoints(result, jwset, jointsCount);
-            w.ValidateWeights(result, jwset);
+            if (joints == null) return;
+
+            joints.ValidateJoints(result, attributeName);
+
+            int max = 0;
+            foreach (var jjjj in joints.AsVector4Array())
+            {
+                max = Math.Max(max, (int)jjjj.X);
+                max = Math.Max(max, (int)jjjj.Y);
+                max = Math.Max(max, (int)jjjj.Z);
+                max = Math.Max(max, (int)jjjj.W);
+            }
+
+            foreach (var skin in skins)
+            {
+                if (skin == null) continue;
+                else
+                {
+                    var skinJoints = new int[skin.JointsCount];
+                    result.CheckArrayIndexAccess(attributeName, max, skinJoints);
+                }
+            }
         }
         }
 
 
         #endregion
         #endregion

+ 53 - 27
src/SharpGLTF.Core/Schema2/gltf.Node.cs

@@ -371,56 +371,82 @@ namespace SharpGLTF.Schema2
             // check out of range indices
             // check out of range indices
             foreach (var idx in this._children)
             foreach (var idx in this._children)
             {
             {
-                result.CheckReferenceIndex(nameof(VisualChildren), idx, this.LogicalParent.LogicalNodes);
+                result.CheckArrayIndexAccess(nameof(VisualChildren), idx, this.LogicalParent.LogicalNodes);
             }
             }
 
 
-            result.CheckReferenceIndex(nameof(Mesh), _mesh, this.LogicalParent.LogicalMeshes);
-            result.CheckReferenceIndex(nameof(Skin), _skin, this.LogicalParent.LogicalSkins);
-            result.CheckReferenceIndex(nameof(Camera), _camera, this.LogicalParent.LogicalCameras);
+            result.CheckArrayIndexAccess(nameof(Mesh), _mesh, this.LogicalParent.LogicalMeshes);
+            result.CheckArrayIndexAccess(nameof(Skin), _skin, this.LogicalParent.LogicalSkins);
+            result.CheckArrayIndexAccess(nameof(Camera), _camera, this.LogicalParent.LogicalCameras);
         }
         }
 
 
         protected override void OnValidate(Validation.ValidationContext result)
         protected override void OnValidate(Validation.ValidationContext result)
         {
         {
             base.OnValidate(result);
             base.OnValidate(result);
 
 
-            var thisIndex = this.LogicalIndex;
+            _ValidateHierarchy(result);
+            _ValidateTransforms(result);
+            _ValidateMeshAndSkin(result, Mesh, Skin);
+        }
 
 
-            // check duplicated indices
-            if (this._children.Distinct().Count() != this._children.Count) result.AddSchemaError(nameof(VisualChildren), Validation.ErrorCodes.DUPLICATE_ELEMENTS);
+        private void _ValidateHierarchy(Validation.ValidationContext result)
+        {
+            var allNodes = this.LogicalParent.LogicalNodes;
 
 
-            // check self references
-            if (this._children.Contains(thisIndex)) result.AddLinkError(Validation.ErrorCodes.NODE_LOOP);
+            var thisIndex = this.LogicalIndex;
+
+            var pidx = thisIndex;
 
 
-            // check circular references
-            var p = this;
             while (true)
             while (true)
             {
             {
-                p = p.VisualParent;
-                if (p == null) break;
-                if (p.LogicalIndex == thisIndex)
+                result = result.GetContext(result.Root.LogicalNodes[pidx]);
+
+                // every node must have 0 or 1 parents.
+
+                var allParents = allNodes
+                    .Where(n => n._HasVisualChild(pidx))
+                    .ToList();
+
+                if (allParents.Count == 0) break; // we're already root
+
+                if (allParents.Count > 1)
                 {
                 {
-                    result.AddLinkError(Validation.ErrorCodes.NODE_LOOP);
+                    var parents = string.Join(" ", allParents);
+
+                    result.AddLinkError($"is child of nodes {parents}. A node can only have one parent.");
                     break;
                     break;
                 }
                 }
-            }
 
 
-            if (_skin.HasValue)
-            {
-                if (!_mesh.HasValue)
-                {
-                    result.AddLinkError(Validation.ErrorCodes.NODE_SKIN_WITH_NON_SKINNED_MESH);
-                }
-                else
+                if (allParents[0].LogicalIndex == pidx)
                 {
                 {
-                    this.Mesh.ValidateSkinning(result, this.Skin.JointsCount);
+                    result.AddLinkError("is a part of a node loop.");
+                    break;
                 }
                 }
+
+                pidx = allParents[0].LogicalIndex;
             }
             }
+        }
 
 
-            // TODO:
+        private void _ValidateTransforms(Validation.ValidationContext result)
+        {
+            result.CheckIsFinite("Scale", _scale);
+            result.CheckIsFinite("Rotation", _rotation);
+            result.CheckIsFinite("Translation", _translation);
+            result.CheckIsMatrix("Matrix", _matrix);
+        }
 
 
-            // check Transforms (out or range, NaN, etc)
+        private static void _ValidateMeshAndSkin(Validation.ValidationContext result, Mesh mesh, Skin skin)
+        {
+            if (mesh == null && skin == null) return;
+
+            if (mesh != null)
+            {
+                if (skin == null && mesh.AllPrimitivesHaveJoints) result.AddLinkWarning(Validation.WarnCodes.NODE_SKINNED_MESH_WITHOUT_SKIN);
+            }
 
 
-            // check morph weights
+            if (skin != null)
+            {
+                if (mesh == null || !mesh.AllPrimitivesHaveJoints) result.AddLinkError(Validation.ErrorCodes.NODE_SKIN_WITH_NON_SKINNED_MESH);
+            }
         }
         }
 
 
         #endregion
         #endregion

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

@@ -146,9 +146,9 @@ namespace SharpGLTF.Schema2
 
 
         protected override void OnValidateReferences(Validation.ValidationContext result)
         protected override void OnValidateReferences(Validation.ValidationContext result)
         {
         {
-            base.OnValidateReferences(result);
+            if (Asset == null) result.AddSchemaError(nameof(Asset), "is missing");
 
 
-            result.CheckReferenceIndex(nameof(DefaultScene), _scene, this.LogicalScenes);
+            result.CheckArrayIndexAccess(nameof(DefaultScene), _scene, this.LogicalScenes);
 
 
             foreach (var b in _bufferViews) b.ValidateReferences(result);
             foreach (var b in _bufferViews) b.ValidateReferences(result);
 
 
@@ -162,14 +162,15 @@ namespace SharpGLTF.Schema2
             foreach (var s in _scenes) s.ValidateReferences(result);
             foreach (var s in _scenes) s.ValidateReferences(result);
             foreach (var n in _nodes) n.ValidateReferences(result);
             foreach (var n in _nodes) n.ValidateReferences(result);
             foreach (var a in _animations) a.ValidateReferences(result);
             foreach (var a in _animations) a.ValidateReferences(result);
+
+            base.OnValidateReferences(result);
         }
         }
 
 
         protected override void OnValidate(Validation.ValidationContext result)
         protected override void OnValidate(Validation.ValidationContext result)
         {
         {
             // 1st check version number
             // 1st check version number
 
 
-            if (Asset == null) result.AddSchemaError("Asset","Missing");
-            else Asset.Validate(result);
+            Asset.Validate(result);
 
 
             if (result.Result.HasErrors) return;
             if (result.Result.HasErrors) return;
 
 

+ 1 - 1
src/SharpGLTF.Core/Schema2/gltf.Scene.cs

@@ -68,7 +68,7 @@ namespace SharpGLTF.Schema2
             // check out of range indices
             // check out of range indices
             foreach (var idx in this._nodes)
             foreach (var idx in this._nodes)
             {
             {
-                result.CheckReferenceIndex( nameof(VisualChildren), idx, this.LogicalParent.LogicalNodes);
+                result.CheckArrayIndexAccess( nameof(VisualChildren), idx, this.LogicalParent.LogicalNodes);
             }
             }
 
 
             // check duplicated indices
             // check duplicated indices

+ 2 - 2
src/SharpGLTF.Core/Schema2/gltf.Serialization.Read.cs

@@ -211,7 +211,7 @@ namespace SharpGLTF.Schema2
                 }
                 }
 
 
                 // reference checking is mandatory and cannot be skipped
                 // reference checking is mandatory and cannot be skipped
-                var result = new Validation.ValidationResult();
+                var result = new Validation.ValidationResult(root);
                 root.ValidateReferences(result.GetContext(root));
                 root.ValidateReferences(result.GetContext(root));
                 var ex = result.Errors.FirstOrDefault();
                 var ex = result.Errors.FirstOrDefault();
                 if (ex != null) throw ex;
                 if (ex != null) throw ex;
@@ -228,7 +228,7 @@ namespace SharpGLTF.Schema2
 
 
                 if (!settings.SkipValidation)
                 if (!settings.SkipValidation)
                 {
                 {
-                    result = new Validation.ValidationResult();
+                    result = new Validation.ValidationResult(root);
                     root.Validate(result.GetContext(root));
                     root.Validate(result.GetContext(root));
                     ex = result.Errors.FirstOrDefault();
                     ex = result.Errors.FirstOrDefault();
                     if (ex != null) throw ex;
                     if (ex != null) throw ex;

+ 5 - 10
src/SharpGLTF.Core/Schema2/gltf.Skin.cs

@@ -242,9 +242,9 @@ namespace SharpGLTF.Schema2
         {
         {
             base.OnValidateReferences(result);
             base.OnValidateReferences(result);
 
 
-            result.CheckReferenceIndex("Skeleton", _skeleton, this.LogicalParent.LogicalNodes);
+            result.CheckArrayIndexAccess("Skeleton", _skeleton, this.LogicalParent.LogicalNodes);
 
 
-            result.CheckReferenceIndex("InverseBindMatrices", _inverseBindMatrices, this.LogicalParent.LogicalAccessors);
+            result.CheckArrayIndexAccess("InverseBindMatrices", _inverseBindMatrices, this.LogicalParent.LogicalAccessors);
 
 
             if (_joints.Count < _jointsMinItems)
             if (_joints.Count < _jointsMinItems)
             {
             {
@@ -257,7 +257,7 @@ namespace SharpGLTF.Schema2
             {
             {
                 var jidx = _joints[i];
                 var jidx = _joints[i];
 
 
-                result.CheckReferenceIndex("Joints", _joints[i], this.LogicalParent.LogicalNodes);
+                result.CheckArrayIndexAccess("Joints", _joints[i], this.LogicalParent.LogicalNodes);
             }
             }
         }
         }
 
 
@@ -269,14 +269,9 @@ namespace SharpGLTF.Schema2
 
 
             if (ibxAccessor != null)
             if (ibxAccessor != null)
             {
             {
-                if (_joints.Count != ibxAccessor.Count) result.AddLinkError(Validation.ErrorCodes.INVALID_IBM_ACCESSOR_COUNT, _joints.Count, ibxAccessor.Count);
+                if (_joints.Count != ibxAccessor.Count) result.AddLinkError("InverseBindMatrices", $"has {ibxAccessor.Count} matrices. But expected {_joints.Count}.");
 
 
-                var isValidIBM = true;
-                isValidIBM &= ibxAccessor.Dimensions == DimensionType.MAT4;
-                isValidIBM &= ibxAccessor.SourceBufferView.DeviceBufferTarget == null;
-
-                if (!isValidIBM) result.AddLinkError(Validation.ErrorCodes.SKIN_IBM_INVALID_FORMAT, ibxAccessor.Dimensions);
-                else ibxAccessor.ValidateMatrices(result);
+                ibxAccessor.ValidateMatrices(result);
             }
             }
 
 
             if (_skeleton.HasValue)
             if (_skeleton.HasValue)

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

@@ -156,8 +156,8 @@ namespace SharpGLTF.Schema2
         {
         {
             base.OnValidateReferences(result);
             base.OnValidateReferences(result);
 
 
-            result.CheckReferenceIndex("Source", _source, this.LogicalParent.LogicalImages);
-            result.CheckReferenceIndex("Sampler", _sampler, this.LogicalParent.LogicalTextureSamplers);
+            result.CheckArrayIndexAccess("Source", _source, this.LogicalParent.LogicalImages);
+            result.CheckArrayIndexAccess("Sampler", _sampler, this.LogicalParent.LogicalTextureSamplers);
         }
         }
 
 
         #endregion
         #endregion

+ 1 - 1
src/SharpGLTF.Core/Validation/ErrorCodes.cs

@@ -99,7 +99,7 @@ namespace SharpGLTF.Validation
         public const string ACCESSOR_SPARSE_COUNT_OUT_OF_RANGE = "Sparse accessor overrides more elements ({0}) than the base accessor contains({1}).";
         public const string ACCESSOR_SPARSE_COUNT_OUT_OF_RANGE = "Sparse accessor overrides more elements ({0}) than the base accessor contains({1}).";
         public const string BUFFER_DATA_URI_MIME_TYPE_INVALID = "Buffer's Data URI MIME-Type must be 'application/octet-stream' or 'application/gltf-buffer'. Found '{0}' instead.";
         public const string BUFFER_DATA_URI_MIME_TYPE_INVALID = "Buffer's Data URI MIME-Type must be 'application/octet-stream' or 'application/gltf-buffer'. Found '{0}' instead.";
         public const string BUFFER_VIEW_INVALID_BYTE_STRIDE = "Only buffer views with raw vertex data can have byteStride.";
         public const string BUFFER_VIEW_INVALID_BYTE_STRIDE = "Only buffer views with raw vertex data can have byteStride.";
-        public const string BUFFER_VIEW_TOO_BIG_BYTE_STRIDE = "Buffer view's byteStride ({0}) is smaller than byteLength ({1}).";
+        public const string BUFFER_VIEW_TOO_BIG_BYTE_STRIDE = "Buffer view's byteStride ({0}) is larger than byteLength ({1}).";
         public const string CAMERA_ZFAR_LEQUAL_ZNEAR = "zfar must be greater than znear.";
         public const string CAMERA_ZFAR_LEQUAL_ZNEAR = "zfar must be greater than znear.";
         public const string INVALID_GL_VALUE = "Invalid value {0} for GL type '{1}'.";
         public const string INVALID_GL_VALUE = "Invalid value {0} for GL type '{1}'.";
         public const string KHR_LIGHTS_PUNCTUAL_LIGHT_SPOT_ANGLES = "outerConeAngle ({1}) is less than or equal to innerConeAngle({0}).";
         public const string KHR_LIGHTS_PUNCTUAL_LIGHT_SPOT_ANGLES = "outerConeAngle ({1}) is less than or equal to innerConeAngle({0}).";

+ 274 - 77
src/SharpGLTF.Core/Validation/ValidationContext.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using System.Text;
 
 
 using TARGET = SharpGLTF.IO.JsonSerializable;
 using TARGET = SharpGLTF.IO.JsonSerializable;
@@ -31,6 +32,8 @@ namespace SharpGLTF.Validation
 
 
         #region properties
         #region properties
 
 
+        public Schema2.ModelRoot Root => _Result.Root;
+
         public ValidationResult Result => _Result;
         public ValidationResult Result => _Result;
 
 
         #endregion
         #endregion
@@ -39,186 +42,378 @@ namespace SharpGLTF.Validation
 
 
         public ValidationContext GetContext(TARGET target) { return _Result.GetContext(target); }
         public ValidationContext GetContext(TARGET target) { return _Result.GetContext(target); }
 
 
-        #endregion
+        public void AddSchemaError(ValueLocation location, string message) { AddSchemaError(location.ToString(_Target, message)); }
 
 
-        #region schema errors
+        public void AddLinkError(ValueLocation location, string message) { AddLinkError(location.ToString(_Target, message)); }
 
 
-        public bool CheckIsDefined<T>(string property, T? value)
-            where T : struct
-        {
-            if (value.HasValue) return true;
+        public void AddLinkWarning(String format, params object[] args) { AddLinkWarning(String.Format(format, args)); }
 
 
-            AddSchemaError(property, ErrorCodes.UNDEFINED_PROPERTY, property);
+        public void AddDataError(ValueLocation location, string message) { AddDataError(location.ToString(_Target, message)); }
 
 
-            return false;
-        }
+        public void AddSemanticWarning(String format, params object[] args) { AddSemanticWarning(String.Format(format, args)); }
 
 
-        public void CheckIndex(string property, int? index, int maxExclusive)
+        public void AddSemanticError(String format, params object[] args) { AddSemanticError(String.Format(format, args)); }
+
+        public void AddLinkWarning(string message)
         {
         {
-            if (!index.HasValue) return;
+            var ex = new LinkException(_Target, message);
 
 
-            if (index.Value < 0) AddSchemaError(property, ErrorCodes.INVALID_INDEX, index);
-            if (index.Value >= maxExclusive) AddSchemaError(property, ErrorCodes.VALUE_NOT_IN_RANGE, index);
+            _Result.AddWarning(ex);
         }
         }
 
 
-        public void CheckMultipleOf(string property,  int value, int multiple)
+        public void AddLinkError(string message)
         {
         {
-            if ((value % multiple) == 0) return;
+            var ex = new LinkException(_Target, message);
 
 
-            AddSchemaError(property, ErrorCodes.VALUE_MULTIPLE_OF, value, multiple);
+            _Result.AddError(ex);
         }
         }
 
 
-        public void CheckJsonSerializable(string property, Object value)
+        public void AddSchemaError(string message)
         {
         {
-            if (IO.JsonUtils.IsSerializable(value)) return;
+            var ex = new SchemaException(_Target, message);
 
 
-            AddSchemaError(property, ErrorCodes.INVALID_JSON, string.Empty);
+            _Result.AddError(ex);
         }
         }
 
 
-        public void AddSchemaError(string property, String format, params object[] args)
+        public void AddDataError(string message)
         {
         {
-            var message = property + " " + String.Format(format, args);
+            var ex = new DataException(_Target, message);
+            _Result.AddError(ex);
+        }
 
 
-            var ex = new SchemaException(_Target, message);
+        public void AddSemanticError(String message)
+        {
+            var ex = new SemanticException(_Target, message);
 
 
             _Result.AddError(ex);
             _Result.AddError(ex);
         }
         }
 
 
+        public void AddSemanticWarning(String message)
+        {
+            var ex = new SemanticException(_Target, message);
+
+            _Result.AddWarning(ex);
+        }
+
         #endregion
         #endregion
 
 
-        #region semantic errors
+        #region schema errors
 
 
-        public void AddSemanticError(String message)
+        public bool CheckSchemaIsDefined<T>(ValueLocation location, T? value)
+            where T : struct
         {
         {
-            var ex = new SemanticException(_Target, message);
+            if (value.HasValue) return true;
 
 
-            _Result.AddError(ex);
+            AddSchemaError(location, "must be defined.");
+
+            return false;
         }
         }
 
 
-        public void AddSemanticWarning(String format, params object[] args)
+        public bool CheckSchemaNonNegative(ValueLocation location, int? value)
         {
         {
-            var message = String.Format(format, args);
+            if ((value ?? 0) >= 0) return true;
+            AddSchemaError(location, "must be a non-negative integer.");
+            return false;
+        }
 
 
-            var ex = new SemanticException(_Target, message);
+        public void CheckSchemaIsInRange<T>(ValueLocation location, T value, T minInclusive, T maxInclusive)
+            where T : IComparable<T>
+        {
+            if (value.CompareTo(minInclusive) == -1) AddSchemaError(location, $"is below minimum {minInclusive} value: {value}");
+            if (value.CompareTo(maxInclusive) == +1) AddSchemaError(location, $"is above maximum {maxInclusive} value: {value}");
+        }
 
 
-            _Result.AddWarning(ex);
+        public void CheckSchemaIsMultipleOf(ValueLocation location,  int value, int multiple)
+        {
+            if ((value % multiple) == 0) return;
+
+            AddSchemaError(location, $"Value {value} is not a multiple of {multiple}.");
         }
         }
 
 
-        public void AddSemanticError(String format, params object[] args)
+        public void CheckSchemaIsJsonSerializable(ValueLocation location, Object value)
         {
         {
-            var message = String.Format(format, args);
+            if (IO.JsonUtils.IsSerializable(value)) return;
 
 
-            var ex = new SemanticException(_Target, message);
+            AddSchemaError(location, "Invalid JSON data.");
+        }
 
 
-            _Result.AddError(ex);
+        public void CheckSchemaIsValidURI(ValueLocation location, string uri)
+        {
+            if (string.IsNullOrEmpty(uri)) return;
+
+            if (uri.StartsWith("data:"))
+            {
+                // check decoding
+                return;
+            }
+
+            if (Uri.TryCreate(uri, UriKind.Relative, out Uri xuri)) return;
+
+            AddSchemaError(location, $"Invalid URI '{uri}'.");
         }
         }
 
 
+        #endregion
+
+        #region semantic errors
+
+        
+
         #endregion
         #endregion
 
 
         #region data errors
         #region data errors
 
 
-        public void CheckVertexIndex(int index, UInt32 vertexIndex, UInt32 vertexCount, UInt32 vertexRestart)
+        public void CheckVertexIndex(ValueLocation location, UInt32 vertexIndex, UInt32 vertexCount, UInt32 vertexRestart)
         {
         {
-            if (vertexIndex == vertexRestart) AddDataError(ErrorCodes.ACCESSOR_INDEX_PRIMITIVE_RESTART, index, vertexIndex);
-            else if (vertexIndex >= vertexCount) AddDataError(ErrorCodes.ACCESSOR_INDEX_OOB, index, vertexIndex, vertexCount);
+            if (vertexIndex == vertexRestart)
+            {
+                AddDataError(location, $"is a primitive restart value ({vertexIndex})");
+                return;
+            }
+
+            if (vertexIndex >= vertexCount)
+            {
+                AddDataError(location, $"has a value ({vertexIndex}) that exceeds number of available vertices ({vertexCount})");
+                return;
+            }
         }
         }
 
 
-        public void CheckDataIsFinite(int index, System.Numerics.Vector3 v)
+        public bool CheckIsFinite(ValueLocation location, System.Numerics.Vector2? value)
         {
         {
-            if (v._IsFinite()) return;
-
-            AddDataError(ErrorCodes.ACCESSOR_INVALID_FLOAT, index);
+            if (!value.HasValue) return true;
+            if (value.Value._IsFinite()) return true;
+            AddDataError(location, $"is NaN or Infinity.");
+            return false;
         }
         }
 
 
-        public void CheckDataIsFinite(int index, System.Numerics.Vector4 v)
+        public bool CheckIsFinite(ValueLocation location, System.Numerics.Vector3? value)
         {
         {
-            if (v._IsFinite()) return;
-
-            AddDataError(ErrorCodes.ACCESSOR_INVALID_FLOAT, index);
+            if (!value.HasValue) return true;
+            if (value.Value._IsFinite()) return true;
+            AddDataError(location, "is NaN or Infinity.");
+            return false;
         }
         }
 
 
-        public void CheckDataIsUnitLength(int index, System.Numerics.Vector3 v)
+        public bool CheckIsFinite(ValueLocation location, System.Numerics.Vector4? value)
         {
         {
-            if (v.IsValidNormal()) return;
+            if (!value.HasValue) return true;
+            if (value.Value._IsFinite()) return true;
+            AddDataError(location, "is NaN or Infinity.");
+            return false;
+        }
 
 
-            AddDataError(ErrorCodes.ACCESSOR_NON_UNIT, index, v.Length());
+        public bool CheckIsFinite(ValueLocation location, System.Numerics.Quaternion? value)
+        {
+            if (!value.HasValue) return true;
+            if (value.Value._IsFinite()) return true;
+            AddDataError(location, "is NaN or Infinity.");
+            return false;
         }
         }
 
 
-        public void CheckDataIsInRange(int index, System.Numerics.Vector4 v, float minInclusive, float maxInclusive)
+        public void CheckIsUnitLength(ValueLocation location, System.Numerics.Vector3? value)
         {
         {
-            CheckDataIsInRange(index, v.X, minInclusive, maxInclusive);
-            CheckDataIsInRange(index, v.Y, minInclusive, maxInclusive);
-            CheckDataIsInRange(index, v.Z, minInclusive, maxInclusive);
-            CheckDataIsInRange(index, v.W, minInclusive, maxInclusive);
+            if (!value.HasValue) return;
+            if (!CheckIsFinite(location, value)) return;
+            if (value.Value.IsValidNormal()) return;
+            AddDataError(location, $"is not of unit length: {value.Value.Length()}.");
         }
         }
 
 
-        public void CheckDataIsInRange(int index, float v, float minInclusive, float maxInclusive)
+        public void CheckIsTangent(ValueLocation location, System.Numerics.Vector4 tangent)
         {
         {
-            if (v < minInclusive) AddDataError(ErrorCodes.ACCESSOR_ELEMENT_OUT_OF_MIN_BOUND, index, v);
-            if (v > maxInclusive) AddDataError(ErrorCodes.ACCESSOR_ELEMENT_OUT_OF_MAX_BOUND, index, v);
+            CheckIsUnitLength(location, new System.Numerics.Vector3(tangent.X, tangent.Y, tangent.Z));
+
+            if (tangent.W == 1 || tangent.W == -1) return;
+
+            AddDataError(location, $"has invalid value: {tangent.W}. Must be 1.0 or -1.0.");
         }
         }
 
 
-        public void CheckDataIsValidSign(int index, float w)
+        public void CheckIsInRange(ValueLocation location, System.Numerics.Vector4 v, float minInclusive, float maxInclusive)
         {
         {
-            if (w == 1 || w == -1) return;
+            CheckIsInRange(location, v.X, minInclusive, maxInclusive);
+            CheckIsInRange(location, v.Y, minInclusive, maxInclusive);
+            CheckIsInRange(location, v.Z, minInclusive, maxInclusive);
+            CheckIsInRange(location, v.W, minInclusive, maxInclusive);
+        }
 
 
-            AddDataError(ErrorCodes.ACCESSOR_INVALID_SIGN, index, w);
+        public void CheckIsInRange(ValueLocation location, float value, float minInclusive, float maxInclusive)
+        {
+            if (value < minInclusive) AddDataError(location, $"is below minimum {minInclusive} value: {value}");
+            if (value > maxInclusive) AddDataError(location, $"is above maximum {maxInclusive} value: {value}");
         }
         }
 
 
-        public void AddDataError(String format, params object[] args)
+        public bool CheckIsMatrix(ValueLocation location, System.Numerics.Matrix4x4? matrix)
         {
         {
-            var message = String.Format(format, args);
+            if (matrix == null) return true;
 
 
-            var ex = new DataException(_Target, message);
+            if (!matrix.Value._IsFinite())
+            {
+                AddDataError(location, "is NaN or Infinity.");
+                return false;
+            }
 
 
-            _Result.AddError(ex);
+            if (!System.Numerics.Matrix4x4.Decompose(matrix.Value, out System.Numerics.Vector3 s, out System.Numerics.Quaternion r, out System.Numerics.Vector3 t))
+            {
+                AddDataError(location, "is not decomposable to TRS.");
+                return false;
+            }
+
+            return true;
         }
         }
 
 
         #endregion
         #endregion
 
 
         #region link errors
         #region link errors
 
 
-        public bool CheckReferenceIndex<T>(string property, int? index, IReadOnlyList<T> collection)
+        public bool CheckArrayIndexAccess<T>(ValueLocation location, int? index, IReadOnlyList<T> array)
+        {
+            return CheckArrayRangeAccess(location, index, 1, array);
+        }
+
+        public bool CheckArrayRangeAccess<T>(ValueLocation location, int? offset, int length, IReadOnlyList<T> array)
+        {
+            if (!offset.HasValue) return true;
+
+            if (!CheckSchemaNonNegative(location, offset)) return false;
+
+            if (length <= 0)
+            {
+                AddSchemaError(location, "Invalid length");
+                return false;
+            }
+
+            if (array == null)
+            {
+                AddLinkError(location, $"Index {offset} exceeds the number of available items (null).");
+                return false;
+            }
+
+            if (offset > array.Count - length)
+            {
+                if (length == 1) AddLinkError(location, $"Index {offset} exceeds the number of available items ({array.Count}).");
+                else AddLinkError(location, $"Index {offset}+{length} exceeds the number of available items ({array.Count}).");
+                return false;
+            }
+
+            return true;
+        }
+
+        public bool CheckLinkMustBeAnyOf<T>(ValueLocation location, T value, params T[] values)
         {
         {
-            if (!index.HasValue) return true;
-            if (index.Value >= 0 && index.Value < collection.Count) return true;
+            if (values.Contains(value)) return true;
 
 
-            AddLinkError(ErrorCodes.UNRESOLVED_REFERENCE, property);
+            var validValues = string.Join(" ", values);
+
+            AddLinkError(location, $"value {value} is invalid. Must be one of {validValues}");
 
 
             return false;
             return false;
         }
         }
 
 
+        public bool CheckLinksInCollection<T>(ValueLocation location, IEnumerable<T> collection)
+            where T : class
+        {
+            int idx = 0;
+
+            if (collection == null)
+            {
+                AddLinkError(location, "Is NULL.");
+                return false;
+            }
+
+            var uniqueInstances = new HashSet<T>();
+
+            foreach (var v in collection)
+            {
+                if (v == null)
+                {
+                    AddLinkError((location, idx), "Is NULL.");
+                    return false;
+                }
+                else if (uniqueInstances.Contains(v))
+                {
+                    AddSchemaError((location, idx), "Is duplicated.");
+                    return false;
+                }
+
+                uniqueInstances.Add(v);
+
+                ++idx;
+            }
+
+            return true;
+        }
+
         public void UnsupportedExtensionError(String message)
         public void UnsupportedExtensionError(String message)
         {
         {
             AddLinkError(message);
             AddLinkError(message);
         }
         }
 
 
-        public void AddLinkError(String format, params object[] args)
+        #endregion
+    }
+
+    public struct ValueLocation
+    {
+        public static implicit operator ValueLocation(int index) { return new ValueLocation(string.Empty, index); }
+
+        public static implicit operator ValueLocation(string name) { return new ValueLocation(name); }
+
+        public static implicit operator ValueLocation((string, int) tuple) { return new ValueLocation(tuple.Item1, tuple.Item2); }
+
+        public static implicit operator String(ValueLocation location) { return location.ToString(); }
+
+        private ValueLocation(string name, int idx1 = -1)
         {
         {
-            var message = String.Format(format, args);
+            _Name = name;
+            _Index = idx1;
+        }
 
 
-            var ex = new LinkException(_Target, message);
+        private readonly string _Name;
+        private readonly int _Index;
 
 
-            _Result.AddError(ex);
+        public override string ToString()
+        {
+            if (_Index >= 0) return $"{_Name}[{_Index}]";
+            return _Name;
         }
         }
 
 
-        public void AddLinkWarning(String format, params object[] args)
+        public string ToString(TARGET target, string message)
         {
         {
-            var message = String.Format(format, args);
+            return ToString(target) + " " + message;
+        }
 
 
-            var ex = new LinkException(_Target, message);
+        public string ToString(TARGET target)
+        {
+            if (target == null) return this.ToString();
 
 
-            _Result.AddWarning(ex);
-        }
+            var name = target.GetType().Name;
 
 
-        #endregion
+            var pinfo = target.GetType().GetProperty("LogicalIndex");
+
+            if (pinfo != null)
+            {
+                var idx = pinfo.GetValue(target);
+
+                name += $"[{idx}]";
+            }
+
+            return name + this.ToString();
+        }
     }
     }
 
 
     [System.Diagnostics.DebuggerStepThrough]
     [System.Diagnostics.DebuggerStepThrough]
     public sealed class ValidationResult
     public sealed class ValidationResult
     {
     {
+        #region lifecycle
+
+        public ValidationResult(Schema2.ModelRoot root)
+        {
+            _Root = root;
+        }
+
+        #endregion
+
         #region data
         #region data
 
 
+        private readonly Schema2.ModelRoot _Root;
+
         private readonly List<Exception> _Errors = new List<Exception>();
         private readonly List<Exception> _Errors = new List<Exception>();
         private readonly List<Exception> _Warnings = new List<Exception>();
         private readonly List<Exception> _Warnings = new List<Exception>();
 
 
@@ -226,6 +421,8 @@ namespace SharpGLTF.Validation
 
 
         #region properties
         #region properties
 
 
+        public Schema2.ModelRoot Root => _Root;
+
         public IEnumerable<Exception> Errors => _Errors;
         public IEnumerable<Exception> Errors => _Errors;
 
 
         public bool HasErrors => _Errors.Count > 0;
         public bool HasErrors => _Errors.Count > 0;