From 448d224392c15f999a25d10fb37d37b722fb987b Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 11 Oct 2023 19:16:44 +0200 Subject: [PATCH] First draft of instance features validation --- .../ExtInstanceFeaturesValidator.ts | 316 ++++++++++ .../ExtMeshFeaturesValidator.ts | 579 +++--------------- .../FeatureIdAccessorValidator.ts | 155 +++++ .../gltfExtensions/FeatureIdValidator.ts | 235 +++++++ .../gltfExtensions/GltfExtensionValidators.ts | 11 + .../PropertyTableDefinitionValidator.ts | 117 ++++ 6 files changed, 907 insertions(+), 506 deletions(-) create mode 100644 src/validation/gltfExtensions/ExtInstanceFeaturesValidator.ts create mode 100644 src/validation/gltfExtensions/FeatureIdAccessorValidator.ts create mode 100644 src/validation/gltfExtensions/FeatureIdValidator.ts create mode 100644 src/validation/gltfExtensions/PropertyTableDefinitionValidator.ts diff --git a/src/validation/gltfExtensions/ExtInstanceFeaturesValidator.ts b/src/validation/gltfExtensions/ExtInstanceFeaturesValidator.ts new file mode 100644 index 00000000..bfb9ee7e --- /dev/null +++ b/src/validation/gltfExtensions/ExtInstanceFeaturesValidator.ts @@ -0,0 +1,316 @@ +import { defined } from "3d-tiles-tools"; + +import { ValidationContext } from "../ValidationContext"; +import { BasicValidator } from "../BasicValidator"; +import { ValidatedElement } from "../ValidatedElement"; + +import { GltfData } from "./GltfData"; +import { FeatureIdValidator } from "./FeatureIdValidator"; +import { PropertyTableDefinitionValidator } from "./PropertyTableDefinitionValidator"; +import { FeatureIdAccessorValidator } from "./FeatureIdAccessorValidator"; + +import { GltfExtensionValidationIssues } from "../../issues/GltfExtensionValidationIssues"; +import { StructureValidationIssues } from "../../issues/StructureValidationIssues"; + +/** + * A class for validating the `EXT_instance_features` extension in + * glTF assets. + * + * This class assumes that the structure of the glTF asset itself + * has already been validated (e.g. with the glTF Validator). + * + * @internal + */ +export class ExtInstanceFeaturesValidator { + /** + * Performs the validation to ensure that the `EXT_instance_features` + * extensions in the given glTF are valid + * + * @param path - The path for validation issues + * @param gltfData - The glTF data, containing the parsed JSON and the + * (optional) binary buffer + * @param context - The `ValidationContext` that any issues will be added to + * @returns Whether the object was valid + */ + static async validateGltf( + path: string, + gltfData: GltfData, + context: ValidationContext + ): Promise { + const gltf = gltfData.gltf; + + // Dig into the (untyped) JSON representation of the + // glTF, to find the nodes that carry the + // EXT_instance_features extension + const nodes = gltf.nodes; + if (!nodes) { + return true; + } + if (!Array.isArray(nodes)) { + return true; + } + + let result = true; + for (let n = 0; n < nodes.length; n++) { + const node = nodes[n]; + const nodePath = path + `/nodes[${n}]`; + const extensions = node.extensions; + if (!extensions) { + continue; + } + const instanceFeatures = extensions["EXT_instance_features"]; + if (defined(instanceFeatures)) { + const meshGpuInstancing = extensions["EXT_mesh_gpu_instancing"]; + if (!defined(meshGpuInstancing)) { + const message = + `The node contains an 'EXT_instance_features' extension ` + + `object, but no 'EXT_mesh_gpu_instancing' extension object`; + const issue = GltfExtensionValidationIssues.INVALID_GLTF_STRUCTURE( + nodePath, + message + ); + context.addIssue(issue); + result = false; + } else { + const objectIsValid = + await ExtInstanceFeaturesValidator.validateExtInstanceFeatures( + nodePath, + instanceFeatures, + meshGpuInstancing, + gltfData, + context + ); + if (!objectIsValid) { + result = false; + } + } + } + } + return result; + } + + /** + * Validate the given EXT_instance_features extension object that was + * found in the given node + * + * This assumes that the given object has already been validated + * to the extent that is checked by the `FeatureIdValidator`, + * with the `validateCommonFeatureId` method. + * + * @param path - The path for validation issues + * @param instanceFeatures - The EXT_instance_features extension object + * @param meshGpuInstancing - The EXT_mesh_gpu_instancing extension object + * @param gltfData - The glTF data + * @param context - The `ValidationContext` that any issues will be added to + * @returns Whether the object was valid + */ + private static async validateExtInstanceFeatures( + path: string, + instanceFeatures: any, + meshGpuInstancing: any, + gltfData: GltfData, + context: ValidationContext + ): Promise { + // Make sure that the given value is an object + if ( + !BasicValidator.validateObject( + path, + "instanceFeatures", + instanceFeatures, + context + ) + ) { + return false; + } + + let result = true; + + // Validate the featureIds + const featureIds = instanceFeatures.featureIds; + const featureIdsPath = path + "/featureIds"; + if (defined(featureIds)) { + // The featureIds MUST be an array of at least 1 objects + if ( + !BasicValidator.validateArray( + featureIdsPath, + "featureIds", + featureIds, + 1, + undefined, + "object", + context + ) + ) { + result = false; + } else { + // Validate each featureId + for (let i = 0; i < featureIds.length; i++) { + const featureId = featureIds[i]; + const featureIdPath = featureIdsPath + "/" + i; + + const commonFeatureIdValid = + FeatureIdValidator.validateCommonFeatureId( + featureIdPath, + featureId, + context + ); + if (!commonFeatureIdValid) { + result = false; + } else { + const featureIdValid = + await ExtInstanceFeaturesValidator.validateInstanceFeaturesFeatureId( + featureIdPath, + featureId, + meshGpuInstancing, + gltfData, + context + ); + if (!featureIdValid) { + result = false; + } + } + } + } + } + return result; + } + + /** + * Validate the given feature ID object that was found in the + * `featureIds` array of an EXT_instance_features extension object + * + * @param path - The path for validation issues + * @param featureId - The feature ID + * @param meshGpuInstancing - The `EXT_mesh_gpu_instancing` extension object + * that contains the attribute definitions + * @param gltfData - The glTF data + * @param context - The `ValidationContext` that any issues will be added to + * @returns Whether the object was valid + */ + private static async validateInstanceFeaturesFeatureId( + path: string, + featureId: any, + meshGpuInstancing: any, + gltfData: GltfData, + context: ValidationContext + ): Promise { + // Validate the propertyTable + const propertyTable = featureId.propertyTable; + const propertyTablePath = path + "/propertyTable"; + const propertyTableState = + PropertyTableDefinitionValidator.validatePropertyTableDefinition( + propertyTablePath, + propertyTable, + gltfData, + context + ); + + let result = true; + + const featureCount = featureId.featureCount; + const nullFeatureId = featureId.nullFeatureId; + + // Validate the attribute + const attribute = featureId.attribute; + const attributePath = path + "/attribute"; + if (defined(attribute)) { + const attributeValid = + ExtInstanceFeaturesValidator.validateFeatureIdAttribute( + attributePath, + attribute, + featureCount, + meshGpuInstancing, + gltfData, + propertyTableState, + nullFeatureId, + context + ); + if (!attributeValid) { + result = false; + } + } + return result; + } + + /** + * Validate the given feature ID `attribute` value that was found in + * a feature ID definition + * + * @param path - The path for validation issues + * @param attribute - The attribute (i.e. the supposed number that + * will be used for the `_FEATURE_ID_${attribute}` attribute name) + * @param featureCount - The `featureCount` value from the feature ID definition + * @param meshGpuInstancing - The `EXT_mesh_gpu_instancing` extension object + * that contains the attribute definitions + * @param gltfData - The glTF data + * @param propertyTableState - The validation state of the property table + * definition (i.e. the index into the property tables array) + * @param nullFeatureId - The `nullFeatureId` of the `featureId` object + * @param context - The `ValidationContext` that any issues will be added to + * @returns Whether the object was valid + */ + private static validateFeatureIdAttribute( + path: string, + attribute: any, + featureCount: number, + meshGpuInstancing: any, + gltfData: GltfData, + propertyTableState: ValidatedElement<{ count: number }>, + nullFeatureId: number | undefined, + context: ValidationContext + ): boolean { + // Validate the attribute + // The attribute MUST be an integer of at least 0 + if ( + !BasicValidator.validateIntegerRange( + path, + "attribute", + attribute, + 0, + true, + undefined, + false, + context + ) + ) { + return false; + } + + let result = true; + + // For a given attribute value, the attribute with the + // name `_FEATURE_ID_${attribute}` must appear as an + // attribute in the `EXT_mesh_gpu_instancing` attributes + const featureIdAttributeName = `_FEATURE_ID_${attribute}`; + const primitiveAttributes = meshGpuInstancing.attributes || {}; + const featureIdAccessorIndex = primitiveAttributes[featureIdAttributeName]; + if (featureIdAccessorIndex === undefined) { + const message = + `The feature ID defines the attribute ${attribute}, ` + + `but the attribute ${featureIdAttributeName} was not ` + + `found in the 'EXT_mesh_gpu_instancing' attributes`; + const issue = StructureValidationIssues.IDENTIFIER_NOT_FOUND( + path, + message + ); + context.addIssue(issue); + result = false; + } else { + const accessorValid = + FeatureIdAccessorValidator.validateFeatureIdAccessor( + path, + featureIdAccessorIndex, + featureCount, + gltfData, + propertyTableState, + nullFeatureId, + context + ); + if (!accessorValid) { + result = false; + } + } + + return result; + } +} diff --git a/src/validation/gltfExtensions/ExtMeshFeaturesValidator.ts b/src/validation/gltfExtensions/ExtMeshFeaturesValidator.ts index cd1c38fd..09d3c9e8 100644 --- a/src/validation/gltfExtensions/ExtMeshFeaturesValidator.ts +++ b/src/validation/gltfExtensions/ExtMeshFeaturesValidator.ts @@ -3,17 +3,17 @@ import { defaultValue } from "3d-tiles-tools"; import { ValidationContext } from "../ValidationContext"; import { BasicValidator } from "../BasicValidator"; +import { ValidatedElement } from "../ValidatedElement"; import { GltfData } from "./GltfData"; import { ImageDataReader } from "./ImageDataReader"; -import { Accessors } from "./Accessors"; import { SamplerValidator } from "./SamplerValidator"; import { TextureValidator } from "./TextureValidator"; +import { FeatureIdValidator } from "./FeatureIdValidator"; +import { FeatureIdAccessorValidator } from "./FeatureIdAccessorValidator"; +import { PropertyTableDefinitionValidator } from "./PropertyTableDefinitionValidator"; -import { GltfExtensionValidationIssues } from "../../issues/GltfExtensionValidationIssues"; import { StructureValidationIssues } from "../../issues/StructureValidationIssues"; -import { ValidationIssues } from "../../issues/ValidationIssues"; -import { JsonValidationIssues } from "../../issues/JsonValidationIssues"; /** * A class for validating the `EXT_mesh_features` extension in @@ -70,21 +70,18 @@ export class ExtMeshFeaturesValidator { if (!extensions) { continue; } - const extensionNames = Object.keys(extensions); - for (const extensionName of extensionNames) { - if (extensionName === "EXT_mesh_features") { - const extensionObject = extensions[extensionName]; - const objectIsValid = - await ExtMeshFeaturesValidator.validateExtMeshFeatures( - path, - extensionObject, - primitive, - gltfData, - context - ); - if (!objectIsValid) { - result = false; - } + const meshFeatures = extensions["EXT_mesh_features"]; + if (defined(meshFeatures)) { + const objectIsValid = + await ExtMeshFeaturesValidator.validateExtMeshFeatures( + path, + meshFeatures, + primitive, + gltfData, + context + ); + if (!objectIsValid) { + result = false; } } } @@ -146,16 +143,27 @@ export class ExtMeshFeaturesValidator { for (let i = 0; i < featureIds.length; i++) { const featureId = featureIds[i]; const featureIdPath = featureIdsPath + "/" + i; - const featureIdValid = - await ExtMeshFeaturesValidator.validateFeatureId( + + const commonFeatureIdValid = + FeatureIdValidator.validateCommonFeatureId( featureIdPath, featureId, - meshPrimitive, - gltfData, context ); - if (!featureIdValid) { + if (!commonFeatureIdValid) { result = false; + } else { + const featureIdValid = + await ExtMeshFeaturesValidator.validateMeshFeaturesFeatureId( + featureIdPath, + featureId, + meshPrimitive, + gltfData, + context + ); + if (!featureIdValid) { + result = false; + } } } } @@ -165,7 +173,11 @@ export class ExtMeshFeaturesValidator { /** * Validate the given feature ID object that was found in the - * `featureIds` array of an EXT_mesh_features extension object + * `featureIds` array of an EXT_mesh_features extension object. + * + * This assumes that the given object has already been validated + * to the extent that is checked by the `FeatureIdValidator`, + * with the `validateCommonFeatureId` method. * * @param path - The path for validation issues * @param featureId - The feature ID @@ -174,112 +186,28 @@ export class ExtMeshFeaturesValidator { * @param context - The `ValidationContext` that any issues will be added to * @returns Whether the object was valid */ - private static async validateFeatureId( + private static async validateMeshFeaturesFeatureId( path: string, featureId: any, meshPrimitive: any, gltfData: GltfData, context: ValidationContext ): Promise { - // Make sure that the given value is an object - if (!BasicValidator.validateObject(path, "featureId", featureId, context)) { - return false; - } + // Validate the propertyTable + const propertyTable = featureId.propertyTable; + const propertyTablePath = path + "/propertyTable"; + const propertyTableState = + PropertyTableDefinitionValidator.validatePropertyTableDefinition( + propertyTablePath, + propertyTable, + gltfData, + context + ); let result = true; - // Validate the nullFeatureId - // The nullFeatureId MUST be an integer of at least 0 - const nullFeatureId = featureId.nullFeatureId; - const nullFeatureIdPath = path + "/nullFeatureId"; - if (defined(nullFeatureId)) { - if ( - !BasicValidator.validateIntegerRange( - nullFeatureIdPath, - "nullFeatureId", - nullFeatureId, - 0, - true, - undefined, - false, - context - ) - ) { - result = false; - } - } - - // Validate the label - // The label MUST be a string - // The label MUST match the ID regex - const label = featureId.label; - const labelPath = path + "/label"; - if (defined(label)) { - if (!BasicValidator.validateString(labelPath, "label", label, context)) { - result = false; - } else { - if ( - !BasicValidator.validateIdentifierString( - labelPath, - "label", - label, - context - ) - ) { - result = false; - } - } - } - - // Validate the featureCount - // The featureCount MUST be defined - // The featureCount MUST be an integer of at least 1 const featureCount = featureId.featureCount; - const featureCountPath = path + "/featureCount"; - if ( - !BasicValidator.validateIntegerRange( - featureCountPath, - "featureCount", - featureCount, - 1, - true, - undefined, - false, - context - ) - ) { - result = false; - - // The remaining validation will require a valid featureCount. - // If the featureCount was not valid, then bail out early: - return result; - } - - // Validate the propertyTable - // If the property table is present and valid, then - // its `count` will be stored as `propertyTableCount` - let usesPropertyTable = false; - let propertyTableCount: number | undefined = undefined; - const propertyTable = featureId.propertyTable; - const propertyTablePath = path + "/propertyTable"; - if (defined(propertyTable)) { - usesPropertyTable = true; - const propertyTableValid = - ExtMeshFeaturesValidator.validateFeatureIdPropertyTable( - propertyTablePath, - propertyTable, - gltfData, - context - ); - if (!propertyTableValid) { - result = false; - } else { - propertyTableCount = ExtMeshFeaturesValidator.obtainPropertyTableCount( - propertyTable, - gltfData - ); - } - } + const nullFeatureId = featureId.nullFeatureId; // Validate the attribute const attribute = featureId.attribute; @@ -292,8 +220,7 @@ export class ExtMeshFeaturesValidator { featureCount, meshPrimitive, gltfData, - usesPropertyTable, - propertyTableCount, + propertyTableState, nullFeatureId, context ); @@ -313,8 +240,7 @@ export class ExtMeshFeaturesValidator { featureCount, meshPrimitive, gltfData, - usesPropertyTable, - propertyTableCount, + propertyTableState, nullFeatureId, context ); @@ -336,10 +262,8 @@ export class ExtMeshFeaturesValidator { * @param featureCount - The `featureCount` value from the feature ID definition * @param meshPrimitive - The mesh primitive that contains the extension * @param gltfData - The glTF data - * @param usesPropertyTable - Whether the feature ID refers to a property table - * @param propertyTableCount - The `count` of the property table that the - * feature ID refers to. This is `undefined` if the feature ID does not use - * a property table, or the property table reference was not valid. + * @param propertyTableState - The validation state of the property table + * definition (i.e. the index into the property tables array) * @param nullFeatureId - The `nullFeatureId` of the `featureId` object * @param context - The `ValidationContext` that any issues will be added to * @returns Whether the object was valid @@ -350,8 +274,7 @@ export class ExtMeshFeaturesValidator { featureCount: number, meshPrimitive: any, gltfData: GltfData, - usesPropertyTable: boolean, - propertyTableCount: number | undefined, + propertyTableState: ValidatedElement<{ count: number }>, nullFeatureId: number | undefined, context: ValidationContext ): boolean { @@ -392,16 +315,16 @@ export class ExtMeshFeaturesValidator { context.addIssue(issue); result = false; } else { - const accessorValid = ExtMeshFeaturesValidator.validateFeatureIdAccessor( - path, - featureIdAccessorIndex, - featureCount, - gltfData, - usesPropertyTable, - propertyTableCount, - nullFeatureId, - context - ); + const accessorValid = + FeatureIdAccessorValidator.validateFeatureIdAccessor( + path, + featureIdAccessorIndex, + featureCount, + gltfData, + propertyTableState, + nullFeatureId, + context + ); if (!accessorValid) { result = false; } @@ -410,153 +333,6 @@ export class ExtMeshFeaturesValidator { return result; } - /** - * Validate the given feature ID attribute accessor index that - * was found in the mesh primitive attributes for the - * `_FEATURE_ID_${attribute}` attribute. - * - * @param path - The path for validation issues - * @param accessorIndex - The accessor index - * @param featureCount - The `featureCount` value from the feature ID definition - * @param gltfData - The glTF data - * @param usesPropertyTable - Whether the feature ID refers to a property table - * @param propertyTableCount - The `count` of the property table that the - * feature ID refers to. This is `undefined` if the feature ID does not use - * a property table, or the property table reference was not valid. - * @param nullFeatureId - The `nullFeatureId` of the `featureId` object - * @param context - The `ValidationContext` that any issues will be added to - * @returns Whether the object was valid - */ - private static validateFeatureIdAccessor( - path: string, - accessorIndex: number, - featureCount: number, - gltfData: GltfData, - usesPropertyTable: boolean, - propertyTableCount: number | undefined, - nullFeatureId: number | undefined, - context: ValidationContext - ): boolean { - // The validity of the accessor index and the accessor - // have already been checked by the glTF-Validator - const gltf = gltfData.gltf; - const accessors = gltf.accessors || []; - const accessor = accessors[accessorIndex]; - - let result = true; - - // The accessor type must be "SCALAR" - if (accessor.type !== "SCALAR") { - const message = - `The feature ID attribute accessor must have the type 'SCALAR', ` + - `but has the type ${accessor.type}`; - const issue = GltfExtensionValidationIssues.INVALID_GLTF_STRUCTURE( - path, - message - ); - context.addIssue(issue); - result = false; - } - - // The accessor must not be normalized - if (accessor.normalized === true) { - const message = `The feature ID attribute accessor may not be normalized`; - const issue = GltfExtensionValidationIssues.INVALID_GLTF_STRUCTURE( - path, - message - ); - context.addIssue(issue); - result = false; - } - - // Only if the structures have been valid until now, - // validate the actual data of the accessor - if (result && gltfData.gltfDocument) { - const dataValid = ExtMeshFeaturesValidator.validateFeatureIdAttributeData( - path, - accessorIndex, - featureCount, - gltfData, - usesPropertyTable, - propertyTableCount, - nullFeatureId, - context - ); - if (!dataValid) { - result = false; - } - } - return result; - } - - /** - * Validate the data of the given feature ID atribute. - * - * This assumes that the glTF data is valid as determined by the - * glTF Validator, **AND** as determined by the validation of - * the JSON part of the extension. So this method should only - * be called when no issues have been detected that may prevent - * the validation of the accessor values. If this is called - * with a `gltfData` object where the `gltfDocument` is - * `undefined`, then an `INTERNAL_ERROR` will be caused. - * - * @param path - The path for validation issues - * @param accessorIndex - The feature ID attribute accessor index - * @param featureCount - The `featureCount` value from the feature ID definition - * @param gltfData - The glTF data - * @param usesPropertyTable - Whether the feature ID refers to a property table - * @param propertyTableCount - The `count` of the property table that the - * feature ID refers to. This is `undefined` if the feature ID does not use - * a property table, or the property table reference was not valid. - * @param nullFeatureId - The `nullFeatureId` of the `featureId` object - * @param context - The `ValidationContext` that any issues will be added to - * @returns Whether the object was valid - */ - private static validateFeatureIdAttributeData( - path: string, - accessorIndex: number, - featureCount: number, - gltfData: GltfData, - usesPropertyTable: boolean, - propertyTableCount: number | undefined, - nullFeatureId: number | undefined, - context: ValidationContext - ): boolean { - const accessorValues = Accessors.readScalarAccessorValues( - accessorIndex, - gltfData - ); - if (!accessorValues) { - // This should only happen for invalid glTF assets (e.g. ones that - // use wrong accessor component types), or when the gltfDocument - // could not be read due to another structual error that should - // be detected by the extension validation. - const message = `Could not read data for feature ID attribute accessor`; - const issue = ValidationIssues.INTERNAL_ERROR(path, message); - context.addIssue(issue); - return false; - } - - // Validate the set of feature ID values - const featureIdSet = new Set(accessorValues); - if ( - !ExtMeshFeaturesValidator.validateFeatureIdSet( - path, - "attribute", - featureIdSet, - featureCount, - usesPropertyTable, - propertyTableCount, - nullFeatureId, - context - ) - ) { - return false; - } - - return true; - } - /** * Validate the given feature ID `texture` value that was found in * a feature ID definition @@ -566,10 +342,8 @@ export class ExtMeshFeaturesValidator { * @param featureCount - The `featureCount` value from the feature ID definition * @param meshPrimitive - The mesh primitive that contains the extension * @param gltfData - The glTF data - * @param usesPropertyTable - Whether the feature ID refers to a property table - * @param propertyTableCount - The `count` of the property table that the - * feature ID refers to. This is `undefined` if the feature ID does not use - * a property table, or the property table reference was not valid. + * @param propertyTableState - The validation state of the property table + * definition (i.e. the index into the property tables array) * @param nullFeatureId - The `nullFeatureId` of the `featureId` object * @param context - The `ValidationContext` that any issues will be added to * @returns Whether the object was valid @@ -580,8 +354,7 @@ export class ExtMeshFeaturesValidator { featureCount: number, meshPrimitive: any, gltfData: GltfData, - usesPropertyTable: boolean, - propertyTableCount: number | undefined, + propertyTableState: ValidatedElement<{ count: number }>, nullFeatureId: number | undefined, context: ValidationContext ): Promise { @@ -667,8 +440,7 @@ export class ExtMeshFeaturesValidator { featureIdTexture, featureCount, gltfData, - usesPropertyTable, - propertyTableCount, + propertyTableState, nullFeatureId, context ); @@ -691,10 +463,8 @@ export class ExtMeshFeaturesValidator { * @param featureIdTexture - The feature ID texture * @param featureCount - The `featureCount` value from the feature ID definition * @param gltfData - The glTF data - * @param usesPropertyTable - Whether the feature ID refers to a property table - * @param propertyTableCount - The `count` of the property table that the - * feature ID refers to. This is `undefined` if the feature ID does not use - * a property table, or the property table reference was not valid. + * @param propertyTableState - The validation state of the property table + * definition (i.e. the index into the property tables array) * @param nullFeatureId - The `nullFeatureId` of the `featureId` object * @param context - The `ValidationContext` that any issues will be added to * @returns Whether the object was valid @@ -704,8 +474,7 @@ export class ExtMeshFeaturesValidator { featureIdTexture: any, featureCount: number, gltfData: GltfData, - usesPropertyTable: boolean, - propertyTableCount: number | undefined, + propertyTableState: ValidatedElement<{ count: number }>, nullFeatureId: number | undefined, context: ValidationContext ) { @@ -759,13 +528,12 @@ export class ExtMeshFeaturesValidator { // Validate the set of feature ID values if ( - !ExtMeshFeaturesValidator.validateFeatureIdSet( + !FeatureIdValidator.validateFeatureIdSet( path, "texture", featureIdSet, featureCount, - usesPropertyTable, - propertyTableCount, + propertyTableState, nullFeatureId, context ) @@ -775,205 +543,4 @@ export class ExtMeshFeaturesValidator { return true; } - - /** - * Validate the given set of feature ID values that have either - * been found in an feature ID texture or in a feature ID attribute. - * - * This will check the validity of the 'featureCount' for the - * given set of features, depending on the presence of the - * 'nullFeatureId', and whether the feature IDs are valid - * indices into a property table (if the property table count - * was given) - * - * @param path - The path for validation issues - * @param sourceName - The source, 'texture' or 'attribute' - * @param featureIdSet - The feature ID set. Note that This set - * might be modifified by this method! - * @param featureCount - The `featureCount` value from the feature ID definition - * @param usesPropertyTable - Whether the feature ID refers to a property table - * @param propertyTableCount - The `count` of the property table that the - * feature ID refers to. This is `undefined` if the feature ID does not use - * a property table, or the property table reference was not valid. - * @param nullFeatureId - The `nullFeatureId` of the `featureId` object - * @param context - The `ValidationContext` that any issues will be added to - * @returns Whether the object was valid - */ - private static validateFeatureIdSet( - path: string, - sourceName: string, - featureIdSet: Set, - featureCount: number, - usesPropertyTable: boolean, - propertyTableCount: number | undefined, - nullFeatureId: number | undefined, - context: ValidationContext - ) { - // Make sure that the actual number of different values that appear - // in the source (excluding the nullFeatureId, if it was defined) - // is not larger than the `featureCount` - if (defined(nullFeatureId)) { - featureIdSet.delete(nullFeatureId); - if (featureIdSet.size > featureCount) { - const message = - `The featureID ${sourceName} contains ${featureIdSet.size} different values ` + - `(excluding the nullFeatureId value), but the featureCount was ${featureCount}`; - const issue = GltfExtensionValidationIssues.FEATURE_COUNT_MISMATCH( - path, - message - ); - context.addIssue(issue); - return false; - } - } else { - if (featureIdSet.size > featureCount) { - const message = - `The feature ID ${sourceName} contains ${featureIdSet.size} different values ` + - `but the featureCount was ${featureCount}`; - const issue = GltfExtensionValidationIssues.FEATURE_COUNT_MISMATCH( - path, - message - ); - context.addIssue(issue); - return false; - } - } - - // If the feature ID refers to a property table, then make - // sure that it only contains feature ID values that are in - // the range [0, propertyTable.count) - if (usesPropertyTable && propertyTableCount !== undefined) { - const featureIdValues = [...featureIdSet]; - const maximumFeatureId = Math.max(...featureIdValues); - const minimumFeatureId = Math.min(...featureIdValues); - if (minimumFeatureId < 0 || maximumFeatureId >= propertyTableCount) { - const message = - `The feature ID refers to a property table with ${propertyTableCount} ` + - `rows, so the feature IDs must be in the range ` + - `[0,${propertyTableCount - 1}], but the feature ID ${sourceName} ` + - `contains values in [${minimumFeatureId},${maximumFeatureId}]`; - const issue = JsonValidationIssues.VALUE_NOT_IN_RANGE(path, message); - context.addIssue(issue); - return false; - } - } - return true; - } - - /** - * Validate the given feature ID `propertyTable` value that was found in - * a feature ID definition. - * - * This will check whether the `propertyTable` refers to an exising - * property table in the `EXT_structural_metadata` extension object, - * and this property table has a valid `count`. - * - * It will NOT check the validity of the property table itself. This - * will be done by the `EXT_structural_metadata` validator. - * - * @param path - The path for validation issues - * @param propertyTableIndex - The value that was found as the `propertyTable` - * in the definition, indicating the index into the property tables array - * @param gltfData - The glTF data - * @param context - The `ValidationContext` that any issues will be added to - * @returns Whether the object was valid - */ - private static validateFeatureIdPropertyTable( - path: string, - propertyTableIndex: number, - gltfData: GltfData, - context: ValidationContext - ): boolean { - const gltf = gltfData.gltf; - const extensions = gltf.extensions || {}; - const structuralMetadata = extensions["EXT_structural_metadata"]; - - if (!structuralMetadata) { - const message = - `The feature ID refers to a property table with index ` + - `${propertyTableIndex}, but the glTF did not contain an ` + - `'EXT_structural_metadata' extension object`; - const issue = GltfExtensionValidationIssues.INVALID_GLTF_STRUCTURE( - path, - message - ); - context.addIssue(issue); - return false; - } - const propertyTables = structuralMetadata.propertyTables; - if (!propertyTables) { - const message = - `The feature ID refers to a property table with index ` + - `${propertyTableIndex}, but the 'EXT_structural_metadata' ` + - `extension object did not define property tables`; - const issue = GltfExtensionValidationIssues.INVALID_GLTF_STRUCTURE( - path, - message - ); - context.addIssue(issue); - return false; - } - - // Validate the propertyTable(Index) - // The propertyTable MUST be an integer in [0,numPropertyTables) - const numPropertyTables = defaultValue(propertyTables.length, 0); - if ( - !BasicValidator.validateIntegerRange( - path, - "propertyTable", - propertyTableIndex, - 0, - true, - numPropertyTables, - false, - context - ) - ) { - return false; - } - - const propertyTable = propertyTables[propertyTableIndex]; - if (!propertyTable) { - // An issue will be added to the validation context by - // the `EXT_structural_metadata` validation - return false; - } - const count = propertyTable.count; - if (count === undefined) { - // An issue will be added to the validation context by - // the `EXT_structural_metadata` validation - return false; - } - return true; - } - - /** - * Obtain the `count` of the property table that is referred to - * with the given index. - * - * This assumes that the validity of this index has already been - * checked with `validateFeatureIdPropertyTable`. If any - * element that leads to the `count` is invalid or not defined, - * then `undefined` will be returned. - * - * @param propertyTableIndex - The value that was found as the `propertyTable` - * in the definition, indicating the index into the property tables array - * @param gltfData - The glTF data - * @returns The `count` of the property table - */ - private static obtainPropertyTableCount( - propertyTableIndex: number, - gltfData: GltfData - ): number | undefined { - const gltf = gltfData.gltf; - const extensions = gltf.extensions || {}; - const structuralMetadata = extensions["EXT_structural_metadata"] || {}; - const propertyTables = structuralMetadata.propertyTables; - if (!propertyTables || propertyTableIndex >= propertyTables.length) { - return undefined; - } - const propertyTable = propertyTables[propertyTableIndex]; - const count = propertyTable.count; - return count; - } } diff --git a/src/validation/gltfExtensions/FeatureIdAccessorValidator.ts b/src/validation/gltfExtensions/FeatureIdAccessorValidator.ts new file mode 100644 index 00000000..e3e11e6d --- /dev/null +++ b/src/validation/gltfExtensions/FeatureIdAccessorValidator.ts @@ -0,0 +1,155 @@ +import { ValidationContext } from "../ValidationContext"; +import { ValidatedElement } from "../ValidatedElement"; + +import { GltfData } from "./GltfData"; +import { Accessors } from "./Accessors"; +import { FeatureIdValidator } from "./FeatureIdValidator"; + +import { GltfExtensionValidationIssues } from "../../issues/GltfExtensionValidationIssues"; +import { ValidationIssues } from "../../issues/ValidationIssues"; + +/** + * Methods related to the validation of accessors that store + * feature IDs, in the context of the `EXT_mesh_features` and + * `EXT_instance_features` extensions. + */ +export class FeatureIdAccessorValidator { + /** + * Validate the given feature ID attribute accessor index that + * was found in the mesh primitive attributes for the + * `_FEATURE_ID_${attribute}` attribute. + * + * @param path - The path for validation issues + * @param accessorIndex - The accessor index + * @param featureCount - The `featureCount` value from the feature ID definition + * @param gltfData - The glTF data + * @param propertyTableState - The validation state of the property table + * definition (i.e. the index into the property tables array) + * @param nullFeatureId - The `nullFeatureId` of the `featureId` object + * @param context - The `ValidationContext` that any issues will be added to + * @returns Whether the object was valid + */ + static validateFeatureIdAccessor( + path: string, + accessorIndex: number, + featureCount: number, + gltfData: GltfData, + propertyTableState: ValidatedElement<{ count: number }>, + nullFeatureId: number | undefined, + context: ValidationContext + ): boolean { + // The validity of the accessor index and the accessor + // have already been checked by the glTF-Validator + const gltf = gltfData.gltf; + const accessors = gltf.accessors || []; + const accessor = accessors[accessorIndex]; + + let result = true; + + // The accessor type must be "SCALAR" + if (accessor.type !== "SCALAR") { + const message = + `The feature ID attribute accessor must have the type 'SCALAR', ` + + `but has the type ${accessor.type}`; + const issue = GltfExtensionValidationIssues.INVALID_GLTF_STRUCTURE( + path, + message + ); + context.addIssue(issue); + result = false; + } + + // The accessor must not be normalized + if (accessor.normalized === true) { + const message = `The feature ID attribute accessor may not be normalized`; + const issue = GltfExtensionValidationIssues.INVALID_GLTF_STRUCTURE( + path, + message + ); + context.addIssue(issue); + result = false; + } + + // Only if the structures have been valid until now, + // validate the actual data of the accessor + if (result && gltfData.gltfDocument) { + const dataValid = FeatureIdAccessorValidator.validateFeatureIdAcessorData( + path, + accessorIndex, + featureCount, + gltfData, + propertyTableState, + nullFeatureId, + context + ); + if (!dataValid) { + result = false; + } + } + return result; + } + + /** + * Validate the data of the given feature ID atribute. + * + * This assumes that the glTF data is valid as determined by the + * glTF Validator, **AND** as determined by the validation of + * the JSON part of the extension. So this method should only + * be called when no issues have been detected that may prevent + * the validation of the accessor values. If this is called + * with a `gltfData` object where the `gltfDocument` is + * `undefined`, then an `INTERNAL_ERROR` will be caused. + * + * @param path - The path for validation issues + * @param accessorIndex - The feature ID attribute accessor index + * @param featureCount - The `featureCount` value from the feature ID definition + * @param gltfData - The glTF data + * @param propertyTableState - The validation state of the property table + * definition (i.e. the index into the property tables array) + * @param nullFeatureId - The `nullFeatureId` of the `featureId` object + * @param context - The `ValidationContext` that any issues will be added to + * @returns Whether the object was valid + */ + private static validateFeatureIdAcessorData( + path: string, + accessorIndex: number, + featureCount: number, + gltfData: GltfData, + propertyTableState: ValidatedElement<{ count: number }>, + nullFeatureId: number | undefined, + context: ValidationContext + ): boolean { + const accessorValues = Accessors.readScalarAccessorValues( + accessorIndex, + gltfData + ); + if (!accessorValues) { + // This should only happen for invalid glTF assets (e.g. ones that + // use wrong accessor component types), or when the gltfDocument + // could not be read due to another structual error that should + // be detected by the extension validation. + const message = `Could not read data for feature ID attribute accessor`; + const issue = ValidationIssues.INTERNAL_ERROR(path, message); + context.addIssue(issue); + return false; + } + + // Validate the set of feature ID values + const featureIdSet = new Set(accessorValues); + if ( + !FeatureIdValidator.validateFeatureIdSet( + path, + "attribute", + featureIdSet, + featureCount, + propertyTableState, + nullFeatureId, + context + ) + ) { + return false; + } + + return true; + } +} diff --git a/src/validation/gltfExtensions/FeatureIdValidator.ts b/src/validation/gltfExtensions/FeatureIdValidator.ts new file mode 100644 index 00000000..5f4f6f95 --- /dev/null +++ b/src/validation/gltfExtensions/FeatureIdValidator.ts @@ -0,0 +1,235 @@ +import { defined } from "3d-tiles-tools"; + +import { ValidationContext } from "../ValidationContext"; +import { ValidatedElement } from "../ValidatedElement"; +import { BasicValidator } from "../BasicValidator"; + +import { GltfData } from "./GltfData"; + +import { GltfExtensionValidationIssues } from "../../issues/GltfExtensionValidationIssues"; +import { JsonValidationIssues } from "../../issues/JsonValidationIssues"; + +/** + * A class for validation functionality related to feature IDs, as they + * appear in the `EXT_mesh_features` and `EXT_instance_features` + * extensions. + * + * @internal + */ +export class FeatureIdValidator { + /** + * Validate the common elements of a feature ID object. + * + * This refers to `featureId` objects that are found in the + * `EXT_mesh_features` and `EXT_instance_features` extension + * objects. + * + * It ensures that... + * - the value being an object + * - the nullFeatureId (if present) being valid + * - the label (if present) being valid + * - the featureCount being present and valid + * + * It does NOT validate the `texture` or `attribute` properties + * that may be found in the object, depending on whether it is + * part of the `EXT_mesh_features` or `EXT_instance_features` + * extension object. + * + * @param path - The path for validation issues + * @param featureId - The feature ID + * @param context - The `ValidationContext` that any issues will be added to + * @returns Whether the object was valid + */ + static validateCommonFeatureId( + path: string, + featureId: any, + context: ValidationContext + ): boolean { + // Make sure that the given value is an object + if (!BasicValidator.validateObject(path, "featureId", featureId, context)) { + return false; + } + + let result = true; + + // Validate the nullFeatureId + // The nullFeatureId MUST be an integer of at least 0 + const nullFeatureId = featureId.nullFeatureId; + const nullFeatureIdPath = path + "/nullFeatureId"; + if (defined(nullFeatureId)) { + if ( + !BasicValidator.validateIntegerRange( + nullFeatureIdPath, + "nullFeatureId", + nullFeatureId, + 0, + true, + undefined, + false, + context + ) + ) { + result = false; + } + } + + // Validate the label + // The label MUST be a string + // The label MUST match the ID regex + const label = featureId.label; + const labelPath = path + "/label"; + if (defined(label)) { + if (!BasicValidator.validateString(labelPath, "label", label, context)) { + result = false; + } else { + if ( + !BasicValidator.validateIdentifierString( + labelPath, + "label", + label, + context + ) + ) { + result = false; + } + } + } + + // Validate the featureCount + // The featureCount MUST be defined + // The featureCount MUST be an integer of at least 1 + const featureCount = featureId.featureCount; + const featureCountPath = path + "/featureCount"; + if ( + !BasicValidator.validateIntegerRange( + featureCountPath, + "featureCount", + featureCount, + 1, + true, + undefined, + false, + context + ) + ) { + result = false; + } + + return result; + } + + /** + * Validate the given set of feature ID values that have either + * been found in an feature ID texture or in a feature ID attribute. + * + * This will check the validity of the 'featureCount' for the + * given set of features, depending on the presence of the + * 'nullFeatureId', and whether the feature IDs are valid + * indices into a property table (if the property table count + * was given) + * + * @param path - The path for validation issues + * @param sourceName - The source, 'texture' or 'attribute' + * @param featureIdSet - The feature ID set. Note that This set + * might be modifified by this method! + * @param featureCount - The `featureCount` value from the feature ID definition + * @param propertyTableState - The validation state of the property table + * definition (i.e. the index into the property tables array) + * @param nullFeatureId - The `nullFeatureId` of the `featureId` object + * @param context - The `ValidationContext` that any issues will be added to + * @returns Whether the object was valid + */ + static validateFeatureIdSet( + path: string, + sourceName: string, + featureIdSet: Set, + featureCount: number, + propertyTableState: ValidatedElement<{ count: number }>, + nullFeatureId: number | undefined, + context: ValidationContext + ) { + // Make sure that the actual number of different values that appear + // in the source (excluding the nullFeatureId, if it was defined) + // is not larger than the `featureCount` + if (defined(nullFeatureId)) { + featureIdSet.delete(nullFeatureId); + if (featureIdSet.size > featureCount) { + const message = + `The featureID ${sourceName} contains ${featureIdSet.size} different values ` + + `(excluding the nullFeatureId value), but the featureCount was ${featureCount}`; + const issue = GltfExtensionValidationIssues.FEATURE_COUNT_MISMATCH( + path, + message + ); + context.addIssue(issue); + return false; + } + } else { + if (featureIdSet.size > featureCount) { + const message = + `The feature ID ${sourceName} contains ${featureIdSet.size} different values ` + + `but the featureCount was ${featureCount}`; + const issue = GltfExtensionValidationIssues.FEATURE_COUNT_MISMATCH( + path, + message + ); + context.addIssue(issue); + return false; + } + } + + // If the feature ID refers to a property table, then make + // sure that it only contains feature ID values that are in + // the range [0, propertyTable.count) + if ( + propertyTableState.wasPresent && + propertyTableState.validatedElement !== undefined + ) { + const propertyTableCount = propertyTableState.validatedElement.count; + const featureIdValues = [...featureIdSet]; + const maximumFeatureId = Math.max(...featureIdValues); + const minimumFeatureId = Math.min(...featureIdValues); + if (minimumFeatureId < 0 || maximumFeatureId >= propertyTableCount) { + const message = + `The feature ID refers to a property table with ${propertyTableCount} ` + + `rows, so the feature IDs must be in the range ` + + `[0,${propertyTableCount - 1}], but the feature ID ${sourceName} ` + + `contains values in [${minimumFeatureId},${maximumFeatureId}]`; + const issue = JsonValidationIssues.VALUE_NOT_IN_RANGE(path, message); + context.addIssue(issue); + return false; + } + } + return true; + } + + /** + * Obtain the `count` of the property table that is referred to + * with the given index. + * + * This assumes that the validity of this index has already been + * checked with `validateFeatureIdPropertyTable`. If any + * element that leads to the `count` is invalid or not defined, + * then `undefined` will be returned. + * + * @param propertyTableIndex - The value that was found as the `propertyTable` + * in the definition, indicating the index into the property tables array + * @param gltfData - The glTF data + * @returns The `count` of the property table + */ + static obtainPropertyTableCount( + propertyTableIndex: number, + gltfData: GltfData + ): number | undefined { + const gltf = gltfData.gltf; + const extensions = gltf.extensions || {}; + const structuralMetadata = extensions["EXT_structural_metadata"] || {}; + const propertyTables = structuralMetadata.propertyTables; + if (!propertyTables || propertyTableIndex >= propertyTables.length) { + return undefined; + } + const propertyTable = propertyTables[propertyTableIndex]; + const count = propertyTable.count; + return count; + } +} diff --git a/src/validation/gltfExtensions/GltfExtensionValidators.ts b/src/validation/gltfExtensions/GltfExtensionValidators.ts index 83e2ec19..ab15113d 100644 --- a/src/validation/gltfExtensions/GltfExtensionValidators.ts +++ b/src/validation/gltfExtensions/GltfExtensionValidators.ts @@ -1,4 +1,5 @@ import { ValidationContext } from "../ValidationContext"; +import { ExtInstanceFeaturesValidator } from "./ExtInstanceFeaturesValidator"; import { ExtMeshFeaturesValidator } from "./ExtMeshFeaturesValidator"; import { ExtStructuralMetadataValidator } from "./ExtStructuralMetadataValidator"; @@ -41,6 +42,16 @@ export class GltfExtensionValidators { result = false; } + // Validate `EXT_instance_features` + const extInstanceFeatures = await ExtInstanceFeaturesValidator.validateGltf( + path, + gltfData, + context + ); + if (!extInstanceFeatures) { + result = false; + } + // Validate `EXT_structural_metadata` const extStructuralMetadataValid = await ExtStructuralMetadataValidator.validateGltf( diff --git a/src/validation/gltfExtensions/PropertyTableDefinitionValidator.ts b/src/validation/gltfExtensions/PropertyTableDefinitionValidator.ts new file mode 100644 index 00000000..7f12bebf --- /dev/null +++ b/src/validation/gltfExtensions/PropertyTableDefinitionValidator.ts @@ -0,0 +1,117 @@ +import { defaultValue, defined } from "3d-tiles-tools"; +import { GltfExtensionValidationIssues } from "../../issues/GltfExtensionValidationIssues"; +import { ValidatedElement } from "../ValidatedElement"; +import { ValidationContext } from "../ValidationContext"; +import { GltfData } from "./GltfData"; +import { BasicValidator } from "../BasicValidator"; + +/** + * A class for the validation of a single property table that + * is referred to by a feature ID definition. + * + */ +export class PropertyTableDefinitionValidator { + /** + * Validate the given feature ID `propertyTable` value that was found in + * a feature ID definition. + * + * The returned object will contain two properties: + * - `wasPresent`: Whether a propertyTable (index) was given + * - `validatedElement`: The validated property table object, only + * insofar that it contains a defined `count` value + * + * This will check whether the `propertyTable` refers to an exising + * property table in the `EXT_structural_metadata` extension object, + * and this property table has a valid `count`. + * + * It will NOT check the validity of the property table itself. This + * will be done by the `EXT_structural_metadata` validator. + * + * @param path - The path for validation issues + * @param propertyTableIndex - The value that was found as the `propertyTable` + * in the definition, indicating the index into the property tables array + * @param gltfData - The glTF data + * @param context - The `ValidationContext` that any issues will be added to + * @returns Whether the state summarizing the definition + */ + static validatePropertyTableDefinition( + path: string, + propertyTableIndex: number | undefined, + gltfData: GltfData, + context: ValidationContext + ): ValidatedElement<{ count: number }> { + // Return immediately when there are no property table + const propertyTableState: ValidatedElement<{ count: number }> = { + wasPresent: false, + validatedElement: undefined, + }; + if (!defined(propertyTableIndex)) { + return propertyTableState; + } + + propertyTableState.wasPresent = true; + + const gltf = gltfData.gltf; + const extensions = gltf.extensions || {}; + const structuralMetadata = extensions["EXT_structural_metadata"]; + + if (!structuralMetadata) { + const message = + `The feature ID refers to a property table with index ` + + `${propertyTableIndex}, but the glTF did not contain an ` + + `'EXT_structural_metadata' extension object`; + const issue = GltfExtensionValidationIssues.INVALID_GLTF_STRUCTURE( + path, + message + ); + context.addIssue(issue); + return propertyTableState; + } + const propertyTables = structuralMetadata.propertyTables; + if (!propertyTables) { + const message = + `The feature ID refers to a property table with index ` + + `${propertyTableIndex}, but the 'EXT_structural_metadata' ` + + `extension object did not define property tables`; + const issue = GltfExtensionValidationIssues.INVALID_GLTF_STRUCTURE( + path, + message + ); + context.addIssue(issue); + return propertyTableState; + } + + // Validate the propertyTable(Index) + // The propertyTable MUST be an integer in [0,numPropertyTables) + const numPropertyTables = defaultValue(propertyTables.length, 0); + if ( + !BasicValidator.validateIntegerRange( + path, + "propertyTable", + propertyTableIndex, + 0, + true, + numPropertyTables, + false, + context + ) + ) { + return propertyTableState; + } + + const propertyTable = propertyTables[propertyTableIndex]; + if (!propertyTable) { + // An issue will be added to the validation context by + // the `EXT_structural_metadata` validation + return propertyTableState; + } + const count = propertyTable.count; + if (count === undefined) { + // An issue will be added to the validation context by + // the `EXT_structural_metadata` validation + return propertyTableState; + } + propertyTableState.validatedElement = propertyTable; + return propertyTableState; + } +}