From e96ed3289c104028c40fbe1d9e5c3848a6483c30 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 30 Aug 2025 16:05:55 +0200 Subject: [PATCH 01/10] Minor generalization of GltfExtensionValidators --- .../gltf/GltfExtensionValidators.ts | 179 +++++++++++------- 1 file changed, 109 insertions(+), 70 deletions(-) diff --git a/src/validation/gltf/GltfExtensionValidators.ts b/src/validation/gltf/GltfExtensionValidators.ts index 67a06f7d..1ac83467 100644 --- a/src/validation/gltf/GltfExtensionValidators.ts +++ b/src/validation/gltf/GltfExtensionValidators.ts @@ -1,21 +1,121 @@ import { ValidationContext } from "../ValidationContext"; +import { GltfDataReader } from "./GltfDataReader"; +import { GltfData } from "./GltfData"; + import { ExtInstanceFeaturesValidator } from "./instanceFeatures/ExtInstanceFeaturesValidator"; import { ExtMeshFeaturesValidator } from "./meshFeatures/ExtMeshFeaturesValidator"; import { ExtStructuralMetadataValidator } from "./structuralMetadata/ExtStructuralMetadataValidator"; import { MaxarNonvisualGeometryValidator } from "./nonvisualGeometry/MaxarNonvisualGeometryValidator"; - -import { GltfDataReader } from "./GltfDataReader"; import { NgaGpmLocalValidator } from "./gpmLocal/NgaGpmLocalValidator"; import { MaxarImageOrthoValidator } from "./imageOrtho/MaxarImageOrthoValidator"; import { KhrLightsPunctualValidator } from "./lightsPunctual/KhrLightsPunctualValidator"; +/** + * An internal type definition for a function that performs the + * validation of a glTF extension in a given GltfData object, + * and adds any issues to a given context. + */ +type GltfExtensionValidator = ( + path: string, + gltfData: GltfData, + context: ValidationContext +) => Promise; + /** * A class that only serves as an entry point for validating * glTF extensions, given the raw glTF input data (either * as embedded glTF, or as binary glTF). */ export class GltfExtensionValidators { + /** + * The mapping from the full name of a glTF extension to the + * `GltfExtensionValidator` for this extension. + */ + private static readonly gltfExtensionValidators: { + [key: string]: GltfExtensionValidator; + } = {}; + + /** + * Whether the 'registerValidators' function was already called. + */ + private static didRegisterValidators = false; + + /** + * Register the given validator as the validator for the extension with + * the given name + * + * @param extensionName - The full glTF extension name + * @param gltfExtensionValidator - The validator + */ + private static registerValidator( + extensionName: string, + gltfExtensionValidator: GltfExtensionValidator + ) { + GltfExtensionValidators.gltfExtensionValidators[extensionName] = + gltfExtensionValidator; + } + + /** + * Returns whether the given name is the name of a glTF extension that + * is "known" by the validator. + * + * This means that there is a dedicated validator for this specific + * extension, and this validator was implemented as part of the + * 3D Tiles Validator. Issues that are caused by the glTF Validator + * not knowing this extension should be filtered out of + * the glTF validation result. + * + * @param extensionName - The full glTF extension name + * @returns Whether the extension is known by the 3D Tiles Validator + */ + static isRegistered(extensionName: string) { + GltfExtensionValidators.registerValidators(); + const names = Object.keys(GltfExtensionValidators.gltfExtensionValidators); + return names.includes(extensionName); + } + + /** + * Registers all known extension validators if they have not + * yet been registered. + */ + private static registerValidators() { + if (GltfExtensionValidators.didRegisterValidators) { + return; + } + + GltfExtensionValidators.registerValidator( + "EXT_mesh_features", + ExtMeshFeaturesValidator.validateGltf + ); + GltfExtensionValidators.registerValidator( + "EXT_instance_features", + ExtInstanceFeaturesValidator.validateGltf + ); + GltfExtensionValidators.registerValidator( + "EXT_structural_metadata", + ExtStructuralMetadataValidator.validateGltf + ); + GltfExtensionValidators.registerValidator( + "NGA_gpm_local", + NgaGpmLocalValidator.validateGltf + ); + GltfExtensionValidators.registerValidator( + "MAXAR_image_ortho", + MaxarImageOrthoValidator.validateGltf + ); + GltfExtensionValidators.registerValidator( + "KHR_lights_punctual", + KhrLightsPunctualValidator.validateGltf + ); + GltfExtensionValidators.registerValidator( + "MAXAR_nonvisual_geometry", + MaxarNonvisualGeometryValidator.validateGltf + ); + + GltfExtensionValidators.didRegisterValidators = true; + } + /** * Ensure that the extensions in the given glTF data are valid. * @@ -36,76 +136,15 @@ export class GltfExtensionValidators { } let result = true; - - // Validate `EXT_mesh_features` - const extMeshFeaturesValid = await ExtMeshFeaturesValidator.validateGltf( - path, - gltfData, - context - ); - if (!extMeshFeaturesValid) { - result = false; - } - - // Validate `EXT_instance_features` - const extInstanceFeatures = await ExtInstanceFeaturesValidator.validateGltf( - path, - gltfData, - context + const validators = Object.values( + GltfExtensionValidators.gltfExtensionValidators ); - if (!extInstanceFeatures) { - result = false; + for (const validator of validators) { + const valid = validator(path, gltfData, context); + if (!valid) { + result = false; + } } - - // Validate `EXT_structural_metadata` - const extStructuralMetadataValid = - await ExtStructuralMetadataValidator.validateGltf( - path, - gltfData, - context - ); - if (!extStructuralMetadataValid) { - result = false; - } - - // Validate `NGA_gpm_local` - const ngaGpmLocalValid = await NgaGpmLocalValidator.validateGltf( - path, - gltfData, - context - ); - if (!ngaGpmLocalValid) { - result = false; - } - - // Validate `MAXAR_image_ortho` - const maxarImageOrthoValid = await MaxarImageOrthoValidator.validateGltf( - path, - gltfData, - context - ); - if (!maxarImageOrthoValid) { - result = false; - } - - // Validate `KHR_lights_punctual` - const khrLightsPunctualValid = - await KhrLightsPunctualValidator.validateGltf(path, gltfData, context); - if (!khrLightsPunctualValid) { - result = false; - } - - // Validate `MAXAR_nonvisual_geometry` - const maxarNonvisualGeometryValid = - await MaxarNonvisualGeometryValidator.validateGltf( - path, - gltfData, - context - ); - if (!maxarNonvisualGeometryValid) { - result = false; - } - return result; } } From f11875f640867f17103f56a1740f74f9baddd66a Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 30 Aug 2025 16:06:07 +0200 Subject: [PATCH 02/10] Draft for glTF validation issue filtering --- src/tileFormats/GltfValidator.ts | 123 ++++++++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 11 deletions(-) diff --git a/src/tileFormats/GltfValidator.ts b/src/tileFormats/GltfValidator.ts index 557cf80a..5ff1cdad 100644 --- a/src/tileFormats/GltfValidator.ts +++ b/src/tileFormats/GltfValidator.ts @@ -126,6 +126,25 @@ export class GltfValidator implements Validator { return false; } + // Convert all messages from the glTF validator into ValidationIssue + // objects that act as the "causes" of the content validation issue + // that may be about to be created + const allCauses: ValidationIssue[] = []; + const gltfMessages = gltfResult.issues?.messages ?? []; + for (const gltfMessage of gltfMessages) { + //console.log(gltfMessage); + const cause = + GltfValidator.createValidationIssueFromGltfMessage(gltfMessage); + allCauses.push(cause); + } + + // XXX Draft for filtering. + // TODO When the filtering removes the only WARNING, then + // the "gltfResult.issues.numWarnings > 0" does no longer + // make sense. The number of errors/warnings/infos have to + // be determined based on the filtered result! + const causes = GltfValidator.filterCauses(allCauses); + // If there are any errors, then summarize ALL issues from the glTF // validation as 'internal issues' in a CONTENT_VALIDATION_ERROR if (gltfResult.issues.numErrors > 0) { @@ -135,10 +154,7 @@ export class GltfValidator implements Validator { path, message ); - for (const gltfMessage of gltfResult.issues.messages) { - //console.log(gltfMessage); - const cause = - GltfValidator.createValidationIssueFromGltfMessage(gltfMessage); + for (const cause of causes) { issue.addCause(cause); } context.addIssue(issue); @@ -158,10 +174,7 @@ export class GltfValidator implements Validator { message ); - for (const gltfMessage of gltfResult.issues.messages) { - //console.log(gltfMessage); - const cause = - GltfValidator.createValidationIssueFromGltfMessage(gltfMessage); + for (const cause of causes) { issue.addCause(cause); } context.addIssue(issue); @@ -177,9 +190,7 @@ export class GltfValidator implements Validator { message ); - for (const gltfMessage of gltfResult.issues.messages) { - const cause = - GltfValidator.createValidationIssueFromGltfMessage(gltfMessage); + for (const cause of causes) { issue.addCause(cause); } context.addIssue(issue); @@ -195,4 +206,94 @@ export class GltfValidator implements Validator { return true; } + + /** + * Tries to extract an extension name from the message that is generated by + * the glTF Validator when it encounters an extension that is not supported. + * + * This makes assumptions about the structure of the message, and that it + * is (verbatim) the message as it is generated by the glTF validator. + * + * @param message - The validation message + * @returns The unsupported extension name, or `undefined` if the message + * does not indicate an unsupported extension + */ + private static extractUnsupportedExtensionName( + message: string + ): string | undefined { + // Example: + // "Cannot validate an extension as it is not supported by the validator: 'EXT_mesh_features'.", + const prefix = + "Cannot validate an extension as it is not supported by the validator: '"; + if (message.startsWith(prefix)) { + const extensionName = message.substring( + prefix.length, + message.length - 2 + ); + return extensionName; + } + return undefined; + } + + /** + * Filter the given list of validation issues based on the knowledge about + * the the glTF extension validations that are implemented as part of the + * 3D Tiles validator. + * + * The given list are the `ValidationIssue` objects that have been created + * from the messages of the full glTF Validator output, using + * `createValidationIssueFromGltfMessage`. + * + * @param causes - The causes + * @returns The filtered causes + */ + private static filterCauses(causes: ValidationIssue[]): ValidationIssue[] { + console.log("Filtering causes"); + const filteredCauses: ValidationIssue[] = causes.filter( + (issue) => !GltfValidator.shouldRemove(issue) + ); + return filteredCauses; + } + + // TODO DRAFT + private static shouldRemove(issue: ValidationIssue): boolean { + const message = issue.message; + + // Messages about unsupported extensions should be removed + // if and only if they are about an extension that was + // registered in the GltfExtensionValidators + const unsupportedExtensionName = + GltfValidator.extractUnsupportedExtensionName(message); + if (unsupportedExtensionName) { + const isRegistered = GltfExtensionValidators.isRegistered( + unsupportedExtensionName + ); + if (isRegistered) { + console.log( + "Filtering out message about " + + unsupportedExtensionName + + " not being supported in glTF Validator" + ); + return true; + } + } + + // TODO XXX DUMMY IMPLEMENTATION + if (unsupportedExtensionName === "KHR_texture_basisu") { + console.warn( + "Filtering out message about KHR_texture_basisu not being supported!" + ); + return true; + } + if (message.startsWith("Invalid value 'image/ktx2'.")) { + console.warn("Filtering out message invalid image media type!"); + return true; + } + if (message === "Image format not recognized.") { + console.warn("Filtering out unrecognized image format!"); + return true; + } + + return false; + } } From d69be9b398f11957517b20f48be90c5ce28841c9 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 30 Aug 2025 16:36:01 +0200 Subject: [PATCH 03/10] Ensure that extension validators are registered --- src/validation/gltf/GltfExtensionValidators.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/validation/gltf/GltfExtensionValidators.ts b/src/validation/gltf/GltfExtensionValidators.ts index 1ac83467..1defc62d 100644 --- a/src/validation/gltf/GltfExtensionValidators.ts +++ b/src/validation/gltf/GltfExtensionValidators.ts @@ -129,6 +129,7 @@ export class GltfExtensionValidators { input: Buffer, context: ValidationContext ): Promise { + GltfExtensionValidators.registerValidators(); const gltfData = await GltfDataReader.readGltfData(path, input, context); if (!gltfData) { // Issue was already added to context From 8724e84d5a59d8411c2d0dfc9b02a3dcd41fdfbb Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 30 Aug 2025 16:40:36 +0200 Subject: [PATCH 04/10] Add missing await --- src/validation/gltf/GltfExtensionValidators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/gltf/GltfExtensionValidators.ts b/src/validation/gltf/GltfExtensionValidators.ts index 1defc62d..7029a6ef 100644 --- a/src/validation/gltf/GltfExtensionValidators.ts +++ b/src/validation/gltf/GltfExtensionValidators.ts @@ -141,7 +141,7 @@ export class GltfExtensionValidators { GltfExtensionValidators.gltfExtensionValidators ); for (const validator of validators) { - const valid = validator(path, gltfData, context); + const valid = await validator(path, gltfData, context); if (!valid) { result = false; } From be3e78f3dcf68554a937cdbcecfb083a6f944c4b Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 17 Oct 2025 20:29:41 +0200 Subject: [PATCH 05/10] Generalized validation issue filtering --- src/issues/ValidationIssues.ts | 15 ++ src/tileFormats/GltfValidator.ts | 150 +++++-------- src/validation/gltf/GltfExtensionIssues.ts | 209 ++++++++++++++++++ .../gltf/GltfExtensionValidators.ts | 179 +++++++++++---- 4 files changed, 412 insertions(+), 141 deletions(-) create mode 100644 src/validation/gltf/GltfExtensionIssues.ts diff --git a/src/issues/ValidationIssues.ts b/src/issues/ValidationIssues.ts index 9df6af98..5ab5ef5c 100644 --- a/src/issues/ValidationIssues.ts +++ b/src/issues/ValidationIssues.ts @@ -21,4 +21,19 @@ export class ValidationIssues { const issue = new ValidationIssue(type, path, message, severity); return issue; } + + /** + * An issue that just summarizes information about the validation + * process or result. + * + * @param path - The path for the `ValidationIssue` + * @param message - The message for the `ValidationIssue` + * @returns The `ValidationIssue` + */ + static VALIDATION_INFO(path: string, message: string) { + const type = "VALIDATION_INFO"; + const severity = ValidationIssueSeverity.INFO; + const issue = new ValidationIssue(type, path, message, severity); + return issue; + } } diff --git a/src/tileFormats/GltfValidator.ts b/src/tileFormats/GltfValidator.ts index 5ff1cdad..56a417d1 100644 --- a/src/tileFormats/GltfValidator.ts +++ b/src/tileFormats/GltfValidator.ts @@ -6,6 +6,8 @@ import { ValidationIssue } from "../validation/ValidationIssue"; import { ContentValidationIssues } from "../issues/ContentValidationIssues"; import { GltfExtensionValidators } from "../validation/gltf/GltfExtensionValidators"; +import { ValidationIssueSeverity } from "../validation/ValidationIssueSeverity"; +import { GltfDataReader } from "../validation/gltf/GltfDataReader"; // eslint-disable-next-line @typescript-eslint/no-var-requires const validator = require("gltf-validator"); @@ -138,16 +140,40 @@ export class GltfValidator implements Validator { allCauses.push(cause); } - // XXX Draft for filtering. - // TODO When the filtering removes the only WARNING, then - // the "gltfResult.issues.numWarnings > 0" does no longer - // make sense. The number of errors/warnings/infos have to - // be determined based on the filtered result! - const causes = GltfValidator.filterCauses(allCauses); + // Read the glTF data + const gltfData = await GltfDataReader.readGltfData(uri, input, context); + if (!gltfData) { + // Issue was already added to context + return false; + } + + // Process the list of causes, possibly filtering out the ones that + // are known to be obsolete due to the validation that is performed + // by validators that are part of the 3D Tiles Validator + const causes = await GltfExtensionValidators.processCauses( + uri, + gltfData, + allCauses + ); + + // The number of errors/warnings/infos is determined based on + // the filtered issues. + const numErrors = GltfValidator.countIssueSeverities( + causes, + ValidationIssueSeverity.ERROR + ); + const numWarnings = GltfValidator.countIssueSeverities( + causes, + ValidationIssueSeverity.WARNING + ); + const numInfos = GltfValidator.countIssueSeverities( + causes, + ValidationIssueSeverity.INFO + ); // If there are any errors, then summarize ALL issues from the glTF // validation as 'internal issues' in a CONTENT_VALIDATION_ERROR - if (gltfResult.issues.numErrors > 0) { + if (numErrors > 0) { const path = uri; const message = `Content ${uri} caused validation errors`; const issue = ContentValidationIssues.CONTENT_VALIDATION_ERROR( @@ -166,7 +192,7 @@ export class GltfValidator implements Validator { // If there are any warnings, then summarize them in a // CONTENT_VALIDATION_WARNING, but still consider the // object to be valid. - if (gltfResult.issues.numWarnings > 0) { + if (numWarnings > 0) { const path = uri; const message = `Content ${uri} caused validation warnings`; const issue = ContentValidationIssues.CONTENT_VALIDATION_WARNING( @@ -178,7 +204,7 @@ export class GltfValidator implements Validator { issue.addCause(cause); } context.addIssue(issue); - } else if (gltfResult.issues.numInfos > 0) { + } else if (numInfos > 0) { // If there are no warnings, but infos, then summarize them in a // CONTENT_VALIDATION_INFO, but still consider the // object to be valid. @@ -197,9 +223,14 @@ export class GltfValidator implements Validator { } // When the glTF itself is considered to be valid, then perform - // the validation of the Cesium glTF metadata extensions + // the validation of the glTF extensions that are implemented + // as part of the 3D Tiles Validator const extensionsValid = - await GltfExtensionValidators.validateGltfExtensions(uri, input, context); + await GltfExtensionValidators.validateGltfExtensions( + uri, + gltfData, + context + ); if (!extensionsValid) { return false; } @@ -208,92 +239,23 @@ export class GltfValidator implements Validator { } /** - * Tries to extract an extension name from the message that is generated by - * the glTF Validator when it encounters an extension that is not supported. - * - * This makes assumptions about the structure of the message, and that it - * is (verbatim) the message as it is generated by the glTF validator. - * - * @param message - The validation message - * @returns The unsupported extension name, or `undefined` if the message - * does not indicate an unsupported extension - */ - private static extractUnsupportedExtensionName( - message: string - ): string | undefined { - // Example: - // "Cannot validate an extension as it is not supported by the validator: 'EXT_mesh_features'.", - const prefix = - "Cannot validate an extension as it is not supported by the validator: '"; - if (message.startsWith(prefix)) { - const extensionName = message.substring( - prefix.length, - message.length - 2 - ); - return extensionName; - } - return undefined; - } - - /** - * Filter the given list of validation issues based on the knowledge about - * the the glTF extension validations that are implemented as part of the - * 3D Tiles validator. - * - * The given list are the `ValidationIssue` objects that have been created - * from the messages of the full glTF Validator output, using - * `createValidationIssueFromGltfMessage`. + * Counts and returns the number of issues in the given array that have + * the given severity. * - * @param causes - The causes - * @returns The filtered causes + * @param issues - The issues + * @param severity - The severity + * @returns The number of issues with the given severity */ - private static filterCauses(causes: ValidationIssue[]): ValidationIssue[] { - console.log("Filtering causes"); - const filteredCauses: ValidationIssue[] = causes.filter( - (issue) => !GltfValidator.shouldRemove(issue) - ); - return filteredCauses; - } - - // TODO DRAFT - private static shouldRemove(issue: ValidationIssue): boolean { - const message = issue.message; - - // Messages about unsupported extensions should be removed - // if and only if they are about an extension that was - // registered in the GltfExtensionValidators - const unsupportedExtensionName = - GltfValidator.extractUnsupportedExtensionName(message); - if (unsupportedExtensionName) { - const isRegistered = GltfExtensionValidators.isRegistered( - unsupportedExtensionName - ); - if (isRegistered) { - console.log( - "Filtering out message about " + - unsupportedExtensionName + - " not being supported in glTF Validator" - ); - return true; + private static countIssueSeverities( + issues: ValidationIssue[], + severity: ValidationIssueSeverity + ): number { + let count = 0; + for (const issue of issues) { + if (issue.severity === severity) { + count++; } } - - // TODO XXX DUMMY IMPLEMENTATION - if (unsupportedExtensionName === "KHR_texture_basisu") { - console.warn( - "Filtering out message about KHR_texture_basisu not being supported!" - ); - return true; - } - if (message.startsWith("Invalid value 'image/ktx2'.")) { - console.warn("Filtering out message invalid image media type!"); - return true; - } - if (message === "Image format not recognized.") { - console.warn("Filtering out unrecognized image format!"); - return true; - } - - return false; + return count; } } diff --git a/src/validation/gltf/GltfExtensionIssues.ts b/src/validation/gltf/GltfExtensionIssues.ts new file mode 100644 index 00000000..693c369d --- /dev/null +++ b/src/validation/gltf/GltfExtensionIssues.ts @@ -0,0 +1,209 @@ +import { ValidationIssue } from "../ValidationIssue"; +import { GltfData } from "./GltfData"; + +import { ValidationIssues } from "../../issues/ValidationIssues"; + +/** + * Functions for implementing filters on lists of validation issues, + * to filter out issues that are obsolete due to glTF validation + * steps that are performed by the 3D Tiles validator. + */ +export class GltfExtensionIssues { + /** + * Process the given list of issues in view of KHR_texture_basisu. + * + * This will omit the issues that are considered obsolete due to + * the lack of support for KHR_texture_basisu validation. If any + * issues have been filtered out, a single, summarizing issue will + * be added to the list, saying that the result was filtered. + * + * @param path - The path for validation issues + * @param gltfData - The GltfData + * @param causes - The causes + * @returns The filtered causes + */ + static async processCausesKhrTextureBasisu( + path: string, + gltfData: GltfData, + causes: ValidationIssue[] + ): Promise { + const gltf = gltfData.gltf; + const khrTextureBasisuImageIndices = + GltfExtensionIssues.computeImageIndicesKhrTextureBasisu(gltf); + + const processedCauses: ValidationIssue[] = []; + for (const cause of causes) { + const remove = await GltfExtensionIssues.shouldRemoveKhrTextureBasisu( + khrTextureBasisuImageIndices, + cause + ); + if (!remove) { + processedCauses.push(cause); + } + } + + if (causes.length > processedCauses.length) { + const numRemoved = causes.length - processedCauses.length; + const message = + "Omitted " + + numRemoved + + " issues that have been " + + "created due to the lack of support of the KHR_texture_basisu " + + "extension in the glTF validator"; + const summaryIssue = ValidationIssues.VALIDATION_INFO(path, message); + processedCauses.push(summaryIssue); + } + return processedCauses; + } + + /** + * Returns whether the given issue is an issue that should be removed + * because it is caused by the lack of support of the KHR_texture_basisu + * extension in the glTF validator. + * + * @param khrTextureBasisuImageIndices - The indices of images that + * are used by the KHR_texture_basisu extension + * @param issue - The validation issue + * @returns Whether the issue should be removed + */ + private static async shouldRemoveKhrTextureBasisu( + khrTextureBasisuImageIndices: number[], + issue: ValidationIssue + ): Promise { + const message = issue.message; + const path = issue.path; + + // Remove the message about KHR_texture_basisu not being supported + const unsupportedExtensionName = + GltfExtensionIssues.extractUnsupportedExtensionName(message); + if (unsupportedExtensionName === "KHR_texture_basisu") { + return true; + } + + // Remove all issues about + // - invalid KTX MIME type + // - unrecognized image format + // - unused object + // for issues that refer to a path starting with "/images/N" + // where N is one of the images that are referred to by + // the KHR_texture_basisu extension in any texture. + if ( + message.startsWith("Invalid value 'image/ktx2'.") || + message.startsWith("Image format not recognized.") || + message.startsWith("This object may be unused.") + ) { + const imageIndex = GltfExtensionIssues.extractIndex("images", path); + if (imageIndex === undefined) { + // This is making some assumptions about the path that + // is generated by the glTF validator. Just print a + // warning for the case that this ever breaks. + console.warn("Could not extract image index from path: " + path); + return false; + } + if (khrTextureBasisuImageIndices.includes(imageIndex)) { + return true; + } + } + return false; + } + + /** + * Compute the indices of all images that appear as any + * gltf.textures[i].extensions["KHR_texture_basisu"].source. + * + * @param gltf - The glTF JSON object + * @returns The image indices + */ + private static computeImageIndicesKhrTextureBasisu(gltf: any): number[] { + const imageIndices = []; + const textures = gltf.textures ?? []; + for (const texture of textures) { + const extensions = texture.extensions ?? {}; + const khrTextureBasisuExtension = extensions["KHR_texture_basisu"] ?? {}; + const source = khrTextureBasisuExtension.source; + if (typeof source === "number") { + imageIndices.push(source); + } + } + return imageIndices; + } + + /** + * Tries to extract the index of the top-level element that is + * indicated by the given JSON path. + * + * The given path is a JSON path that is generated by the glTF validator, + * for example + * "/images/4/mimeType" + * + * This function will omit the prefix, and (if present) the part + * starting with the subsequent slash, and return the resulting + * element ("4" in this example) as a number, if it is in fact + * a number. + * + * If any aspect of this parsing does not succeed, then undefined + * is returned. Yes, this could be solved with a 1.5MB "JSON path" dependency. + * Let's be pragmatic for now. + * + * @param topLevelName - The name of the top-level type (e.g. 'images' + * or 'accessors') + * @param path - The path as generated by the glTF validator + * @returns The index + */ + private static extractIndex( + topLevelName: string, + path: string + ): number | undefined { + const prefix = "/" + topLevelName + "/"; + if (!path.startsWith(prefix)) { + return undefined; + } + let s = path.substring(prefix.length); + const slashIndex = s.indexOf("/"); + if (slashIndex !== -1) { + s = s.substring(0, slashIndex); + } + if (GltfExtensionIssues.isNonnegativeInteger(s)) { + return Number(s); + } + return undefined; + } + + /** + * Returns whether the given string is likely a nonnegative integer. + * + * @param s - The string + * @returns The result + */ + private static isNonnegativeInteger(s: string): boolean { + return /^\d+$/.test(s); + } + + /** + * Tries to extract an extension name from the message that is generated by + * the glTF Validator when it encounters an extension that is not supported. + * + * This makes assumptions about the structure of the message, and that it + * is (verbatim) the message as it is generated by the glTF validator. + * + * @param message - The validation message + * @returns The unsupported extension name, or `undefined` if the message + * does not indicate an unsupported extension + */ + private static extractUnsupportedExtensionName( + message: string + ): string | undefined { + // Example: + // "Cannot validate an extension as it is not supported by the validator: 'EXT_mesh_features'.", + const prefix = + "Cannot validate an extension as it is not supported by the validator: '"; + if (message.startsWith(prefix)) { + const extensionName = message.substring( + prefix.length, + message.length - 2 + ); + return extensionName; + } + return undefined; + } +} diff --git a/src/validation/gltf/GltfExtensionValidators.ts b/src/validation/gltf/GltfExtensionValidators.ts index 7029a6ef..acf09266 100644 --- a/src/validation/gltf/GltfExtensionValidators.ts +++ b/src/validation/gltf/GltfExtensionValidators.ts @@ -1,6 +1,5 @@ import { ValidationContext } from "../ValidationContext"; - -import { GltfDataReader } from "./GltfDataReader"; +import { ValidationIssue } from "../ValidationIssue"; import { GltfData } from "./GltfData"; import { ExtInstanceFeaturesValidator } from "./instanceFeatures/ExtInstanceFeaturesValidator"; @@ -10,17 +9,57 @@ import { MaxarNonvisualGeometryValidator } from "./nonvisualGeometry/MaxarNonvis import { NgaGpmLocalValidator } from "./gpmLocal/NgaGpmLocalValidator"; import { MaxarImageOrthoValidator } from "./imageOrtho/MaxarImageOrthoValidator"; import { KhrLightsPunctualValidator } from "./lightsPunctual/KhrLightsPunctualValidator"; +import { GltfExtensionIssues } from "./GltfExtensionIssues"; /** - * An internal type definition for a function that performs the - * validation of a glTF extension in a given GltfData object, - * and adds any issues to a given context. + * An internal type definition for glTF extension validators */ -type GltfExtensionValidator = ( - path: string, - gltfData: GltfData, - context: ValidationContext -) => Promise; +interface GltfExtensionValidator { + /** + * Performs the validation of a glTF extension in a given GltfData + * object. + * + * This adds any issues to the given context, and returns + * whether the extension was valid. + * + * @param path - The path for validation issues + * @param gltfData - The GltfData object + * @param context - The validation context + * @returns Whether the extension was valid + */ + validate( + path: string, + gltfData: GltfData, + context: ValidationContext + ): Promise; + + /** + * Process the given list of validation issues, based on the knowledge + * that only this validator implementation has. + * + * The given list are the validation issues that have been created + * from the validation issues of the glTF validator (possibly processed + * from other GltfExtensionValidator implementations). + * + * This method can omit some of these issues, if it determines that the + * respective issue is obsolete due to the validation that is performed + * by this instance. For example, all implementations of this interface + * will remove the issue where the message is + * "Cannot validate an extension as it is not supported by the validator:" + * followed by the name of the extension that this validator is + * responsible for. (Note that the method can also add new issues, but + * this is usually supposed to be done in the 'validate' method) + * + * @param path - The path for validation issues + * @param gltfData - The GltfData objects + * @param causes - The validation issues + */ + processCauses( + path: string, + gltfData: GltfData, + causes: ValidationIssue[] + ): Promise; +} /** * A class that only serves as an entry point for validating @@ -84,34 +123,58 @@ export class GltfExtensionValidators { return; } - GltfExtensionValidators.registerValidator( - "EXT_mesh_features", - ExtMeshFeaturesValidator.validateGltf - ); - GltfExtensionValidators.registerValidator( - "EXT_instance_features", - ExtInstanceFeaturesValidator.validateGltf - ); - GltfExtensionValidators.registerValidator( - "EXT_structural_metadata", - ExtStructuralMetadataValidator.validateGltf - ); - GltfExtensionValidators.registerValidator( - "NGA_gpm_local", - NgaGpmLocalValidator.validateGltf - ); - GltfExtensionValidators.registerValidator( - "MAXAR_image_ortho", - MaxarImageOrthoValidator.validateGltf - ); - GltfExtensionValidators.registerValidator( - "KHR_lights_punctual", - KhrLightsPunctualValidator.validateGltf - ); - GltfExtensionValidators.registerValidator( - "MAXAR_nonvisual_geometry", - MaxarNonvisualGeometryValidator.validateGltf - ); + const emptyValidation = async ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + path: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + gltfData: GltfData, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context: ValidationContext + ) => true; + const emptyProcessing = async ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + path: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + gltfData: GltfData, + causes: ValidationIssue[] + ) => causes; + + GltfExtensionValidators.registerValidator("EXT_mesh_features", { + validate: ExtMeshFeaturesValidator.validateGltf, + processCauses: emptyProcessing, + }); + GltfExtensionValidators.registerValidator("EXT_instance_features", { + validate: ExtInstanceFeaturesValidator.validateGltf, + processCauses: emptyProcessing, + }); + GltfExtensionValidators.registerValidator("EXT_structural_metadata", { + validate: ExtStructuralMetadataValidator.validateGltf, + processCauses: emptyProcessing, + }); + GltfExtensionValidators.registerValidator("NGA_gpm_local", { + validate: NgaGpmLocalValidator.validateGltf, + processCauses: emptyProcessing, + }); + GltfExtensionValidators.registerValidator("MAXAR_image_ortho", { + validate: MaxarImageOrthoValidator.validateGltf, + processCauses: emptyProcessing, + }); + GltfExtensionValidators.registerValidator("KHR_lights_punctual", { + validate: KhrLightsPunctualValidator.validateGltf, + processCauses: emptyProcessing, + }); + GltfExtensionValidators.registerValidator("MAXAR_nonvisual_geometry", { + validate: MaxarNonvisualGeometryValidator.validateGltf, + processCauses: emptyProcessing, + }); + + // Register an empty validator for KHR_texture_basisu that only + // filters out the messages about unused images and + // unsupported MIME types. + GltfExtensionValidators.registerValidator("KHR_texture_basisu", { + validate: emptyValidation, + processCauses: GltfExtensionIssues.processCausesKhrTextureBasisu, + }); GltfExtensionValidators.didRegisterValidators = true; } @@ -120,32 +183,54 @@ export class GltfExtensionValidators { * Ensure that the extensions in the given glTF data are valid. * * @param path - The path for `ValidationIssue` instances - * @param input - The raw glTF data + * @param gltfData - The GltfData * @param context - The `ValidationContext` * @returns Whether the object is valid */ static async validateGltfExtensions( path: string, - input: Buffer, + gltfData: GltfData, context: ValidationContext ): Promise { GltfExtensionValidators.registerValidators(); - const gltfData = await GltfDataReader.readGltfData(path, input, context); - if (!gltfData) { - // Issue was already added to context - return false; - } - let result = true; const validators = Object.values( GltfExtensionValidators.gltfExtensionValidators ); for (const validator of validators) { - const valid = await validator(path, gltfData, context); + const valid = await validator.validate(path, gltfData, context); if (!valid) { result = false; } } return result; } + + /** + * Filter the given list of issues, to omit the ones that are obsolete + * due to the validation that is performed by validators that are + * implemented as part of the 3D Tiles validator. + * + * @param path - The path for validation issues + * @param gltfData - The GltfData + * @param allCauses - All validation issues that have been created + * from the issues that are generated by the glTF validator + * @returns A possibly filtered list of validation issues + */ + static async processCauses( + path: string, + gltfData: GltfData, + allCauses: ValidationIssue[] + ): Promise { + GltfExtensionValidators.registerValidators(); + + let result = allCauses.slice(); + const validators = Object.values( + GltfExtensionValidators.gltfExtensionValidators + ); + for (const validator of validators) { + result = await validator.processCauses(path, gltfData, result); + } + return result; + } } From a01752a65918351895e1cc153fb9578807b845b6 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 17 Oct 2025 20:44:38 +0200 Subject: [PATCH 06/10] Update spec utility function for new method signature --- specs/gltfExtensions/validateGltf.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/specs/gltfExtensions/validateGltf.ts b/specs/gltfExtensions/validateGltf.ts index 5ed371bc..9471ede5 100644 --- a/specs/gltfExtensions/validateGltf.ts +++ b/specs/gltfExtensions/validateGltf.ts @@ -6,6 +6,7 @@ import { ResourceResolvers } from "3d-tiles-tools"; import { ValidationContext } from "../../src/validation/ValidationContext"; import { GltfExtensionValidators } from "../../src/validation/gltf/GltfExtensionValidators"; +import { GltfDataReader } from "../../src/validation/gltf/GltfDataReader"; export async function validateGltf(gltfFileName: string) { fs.readFileSync(gltfFileName); @@ -17,11 +18,18 @@ export async function validateGltf(gltfFileName: string) { const context = new ValidationContext(directory, resourceResolver); const gltfFileData = await resourceResolver.resolveData(fileName); if (gltfFileData) { - await GltfExtensionValidators.validateGltfExtensions( + const gltfData = await GltfDataReader.readGltfData( gltfFileName, gltfFileData, context ); + if (gltfData) { + await GltfExtensionValidators.validateGltfExtensions( + gltfFileName, + gltfData, + context + ); + } } const validationResult = context.getResult(); return validationResult; From 6804bf1fb24c391acce7a8761db6c48a77a772d7 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 18 Oct 2025 16:03:05 +0200 Subject: [PATCH 07/10] Comments for spec utility function --- specs/gltfExtensions/validateGltf.ts | 34 +++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/specs/gltfExtensions/validateGltf.ts b/specs/gltfExtensions/validateGltf.ts index 9471ede5..67d12a31 100644 --- a/specs/gltfExtensions/validateGltf.ts +++ b/specs/gltfExtensions/validateGltf.ts @@ -4,11 +4,34 @@ import path from "path"; import { ResourceResolvers } from "3d-tiles-tools"; import { ValidationContext } from "../../src/validation/ValidationContext"; +import { ValidationResult } from "../../src/validation/ValidationResult"; -import { GltfExtensionValidators } from "../../src/validation/gltf/GltfExtensionValidators"; import { GltfDataReader } from "../../src/validation/gltf/GltfDataReader"; +import { GltfExtensionValidators } from "../../src/validation/gltf/GltfExtensionValidators"; + +import { IoValidationIssues } from "../../src/issues/IoValidationIssue"; -export async function validateGltf(gltfFileName: string) { +/** + * An INTERNAL utility function that is only intended to be used in the + * specs: It ONLY performs the glTF validation that is implemented as + * part of the 3D Tiles Validator. + * + * It will NOT perform a full validation with the glTF-Validator. It assumes + * that the part of the glTF file that is checked with the glTF-Validator + * is already valid. + * + * This is intended for the use in the specs, where the validation result + * is checked to contain the validation issues that are generated by the + * 3D Tiles Validator implementation, without the distraction from the + * glTF-Validator issues. + * + * + * @param gltfFileName - The glTF file name + * @returns The validation result + */ +export async function validateGltf( + gltfFileName: string +): Promise { fs.readFileSync(gltfFileName); const directory = path.dirname(gltfFileName); @@ -17,7 +40,12 @@ export async function validateGltf(gltfFileName: string) { ResourceResolvers.createFileResourceResolver(directory); const context = new ValidationContext(directory, resourceResolver); const gltfFileData = await resourceResolver.resolveData(fileName); - if (gltfFileData) { + if (!gltfFileData) { + const message = `Could not read glTF for specs`; + const issue = IoValidationIssues.IO_ERROR(gltfFileName, message); + context.addIssue(issue); + } else { + // If this fails, and issue will be added to the context: const gltfData = await GltfDataReader.readGltfData( gltfFileName, gltfFileData, From f3d517a26293c1d9c7245d29a739f62011183767 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 18 Oct 2025 16:03:53 +0200 Subject: [PATCH 08/10] First pass of filtering out obsolete metadata issues --- src/validation/gltf/GltfExtensionIssues.ts | 163 ++++++----------- .../GltfExtensionIssuesKhrTextureBasisu.ts | 142 ++++++++++++++ .../gltf/GltfExtensionValidators.ts | 21 ++- .../meshFeatures/ExtMeshFeaturesIssues.ts | 173 ++++++++++++++++++ .../ExtStructuralMetadataIssues.ts | 171 +++++++++++++++++ 5 files changed, 552 insertions(+), 118 deletions(-) create mode 100644 src/validation/gltf/GltfExtensionIssuesKhrTextureBasisu.ts create mode 100644 src/validation/gltf/meshFeatures/ExtMeshFeaturesIssues.ts create mode 100644 src/validation/gltf/structuralMetadata/ExtStructuralMetadataIssues.ts diff --git a/src/validation/gltf/GltfExtensionIssues.ts b/src/validation/gltf/GltfExtensionIssues.ts index 693c369d..c3c99bc5 100644 --- a/src/validation/gltf/GltfExtensionIssues.ts +++ b/src/validation/gltf/GltfExtensionIssues.ts @@ -2,6 +2,7 @@ import { ValidationIssue } from "../ValidationIssue"; import { GltfData } from "./GltfData"; import { ValidationIssues } from "../../issues/ValidationIssues"; +import { ValidationIssueSeverity } from "../ValidationIssueSeverity"; /** * Functions for implementing filters on lists of validation issues, @@ -10,122 +11,47 @@ import { ValidationIssues } from "../../issues/ValidationIssues"; */ export class GltfExtensionIssues { /** - * Process the given list of issues in view of KHR_texture_basisu. + * Returns whether the given issue is a message about a potentially + * unused object from the given top-level category, and this object + * is actually used according to the given used indices. * - * This will omit the issues that are considered obsolete due to - * the lack of support for KHR_texture_basisu validation. If any - * issues have been filtered out, a single, summarizing issue will - * be added to the list, saying that the result was filtered. + * For example, when the given issue contains + * - message: "This object may be unused." + * - path: "/textures/1" + * and the given usedIndices does contain '1', then this method + * returns true. * - * @param path - The path for validation issues - * @param gltfData - The GltfData - * @param causes - The causes - * @returns The filtered causes - */ - static async processCausesKhrTextureBasisu( - path: string, - gltfData: GltfData, - causes: ValidationIssue[] - ): Promise { - const gltf = gltfData.gltf; - const khrTextureBasisuImageIndices = - GltfExtensionIssues.computeImageIndicesKhrTextureBasisu(gltf); - - const processedCauses: ValidationIssue[] = []; - for (const cause of causes) { - const remove = await GltfExtensionIssues.shouldRemoveKhrTextureBasisu( - khrTextureBasisuImageIndices, - cause - ); - if (!remove) { - processedCauses.push(cause); - } - } - - if (causes.length > processedCauses.length) { - const numRemoved = causes.length - processedCauses.length; - const message = - "Omitted " + - numRemoved + - " issues that have been " + - "created due to the lack of support of the KHR_texture_basisu " + - "extension in the glTF validator"; - const summaryIssue = ValidationIssues.VALIDATION_INFO(path, message); - processedCauses.push(summaryIssue); - } - return processedCauses; - } - - /** - * Returns whether the given issue is an issue that should be removed - * because it is caused by the lack of support of the KHR_texture_basisu - * extension in the glTF validator. - * - * @param khrTextureBasisuImageIndices - The indices of images that - * are used by the KHR_texture_basisu extension * @param issue - The validation issue - * @returns Whether the issue should be removed + * @param topLevelName - The top level name, e.g. "textures" or "bufferViews" + * @param usedIndices - The indices that are actually used + * @returns Whether the issue is obsolete */ - private static async shouldRemoveKhrTextureBasisu( - khrTextureBasisuImageIndices: number[], - issue: ValidationIssue - ): Promise { + static isObsoleteIssueAboutUnusedObject( + issue: ValidationIssue, + topLevelName: string, + usedIndices: number[] + ) { const message = issue.message; const path = issue.path; - - // Remove the message about KHR_texture_basisu not being supported - const unsupportedExtensionName = - GltfExtensionIssues.extractUnsupportedExtensionName(message); - if (unsupportedExtensionName === "KHR_texture_basisu") { - return true; + if (!message.startsWith("This object may be unused.")) { + return false; } - - // Remove all issues about - // - invalid KTX MIME type - // - unrecognized image format - // - unused object - // for issues that refer to a path starting with "/images/N" - // where N is one of the images that are referred to by - // the KHR_texture_basisu extension in any texture. - if ( - message.startsWith("Invalid value 'image/ktx2'.") || - message.startsWith("Image format not recognized.") || - message.startsWith("This object may be unused.") - ) { - const imageIndex = GltfExtensionIssues.extractIndex("images", path); - if (imageIndex === undefined) { - // This is making some assumptions about the path that - // is generated by the glTF validator. Just print a - // warning for the case that this ever breaks. - console.warn("Could not extract image index from path: " + path); - return false; - } - if (khrTextureBasisuImageIndices.includes(imageIndex)) { - return true; - } + const prefix = "/" + topLevelName + "/"; + if (!path.startsWith(prefix)) { + return; } - return false; - } - - /** - * Compute the indices of all images that appear as any - * gltf.textures[i].extensions["KHR_texture_basisu"].source. - * - * @param gltf - The glTF JSON object - * @returns The image indices - */ - private static computeImageIndicesKhrTextureBasisu(gltf: any): number[] { - const imageIndices = []; - const textures = gltf.textures ?? []; - for (const texture of textures) { - const extensions = texture.extensions ?? {}; - const khrTextureBasisuExtension = extensions["KHR_texture_basisu"] ?? {}; - const source = khrTextureBasisuExtension.source; - if (typeof source === "number") { - imageIndices.push(source); - } + const index = GltfExtensionIssues.extractIndex(topLevelName, path); + if (index === undefined) { + // This is making some assumptions about the path that + // is generated by the glTF validator. Just print a + // warning for the case that this ever breaks. + console.warn( + "Could not extract " + topLevelName + " index from path: " + path + ); + return false; } - return imageIndices; + const isUsed = usedIndices.includes(index); + return isUsed; } /** @@ -150,10 +76,7 @@ export class GltfExtensionIssues { * @param path - The path as generated by the glTF validator * @returns The index */ - private static extractIndex( - topLevelName: string, - path: string - ): number | undefined { + static extractIndex(topLevelName: string, path: string): number | undefined { const prefix = "/" + topLevelName + "/"; if (!path.startsWith(prefix)) { return undefined; @@ -179,6 +102,24 @@ export class GltfExtensionIssues { return /^\d+$/.test(s); } + /** + * Returns whether the given validation issue is an issue that just + * reports that the extension with the given name is not supported + * by the glTF validator. + * + * @param issue - The validation issue + * @param extensionName - The extension name + * @returns The result + */ + static isIssueAboutUnsupportedExtension( + issue: ValidationIssue, + extensionName: string + ): boolean { + const extensionNameInMessage = + GltfExtensionIssues.extractUnsupportedExtensionName(issue.message); + return extensionNameInMessage === extensionName; + } + /** * Tries to extract an extension name from the message that is generated by * the glTF Validator when it encounters an extension that is not supported. diff --git a/src/validation/gltf/GltfExtensionIssuesKhrTextureBasisu.ts b/src/validation/gltf/GltfExtensionIssuesKhrTextureBasisu.ts new file mode 100644 index 00000000..1ec4e3b5 --- /dev/null +++ b/src/validation/gltf/GltfExtensionIssuesKhrTextureBasisu.ts @@ -0,0 +1,142 @@ +import { ValidationIssue } from "../ValidationIssue"; +import { GltfData } from "./GltfData"; + +import { ValidationIssues } from "../../issues/ValidationIssues"; +import { ValidationIssueSeverity } from "../ValidationIssueSeverity"; +import { GltfExtensionIssues } from "./GltfExtensionIssues"; + +/** + * Functions for implementing filters on lists of validation issues, + * related to the KHR_texture_basisu extension + */ +export class GltfExtensionIssuesKhrTextureBasisu { + /** + * Process the given list of issues in view of KHR_texture_basisu. + * + * This will omit the issues that are considered obsolete due to + * the lack of support for KHR_texture_basisu validation. If any + * issues have been filtered out, a single, summarizing issue will + * be added to the list, saying that the result was filtered. + * + * @param path - The path for validation issues + * @param gltfData - The GltfData + * @param causes - The causes + * @returns The filtered causes + */ + static async processCausesKhrTextureBasisu( + path: string, + gltfData: GltfData, + causes: ValidationIssue[] + ): Promise { + const gltf = gltfData.gltf; + const khrTextureBasisuImageIndices = + GltfExtensionIssuesKhrTextureBasisu.computeImageIndicesKhrTextureBasisu( + gltf + ); + + const processedCauses: ValidationIssue[] = []; + for (const cause of causes) { + const remove = + await GltfExtensionIssuesKhrTextureBasisu.shouldRemoveKhrTextureBasisu( + khrTextureBasisuImageIndices, + cause + ); + if (!remove) { + processedCauses.push(cause); + } + } + + if (causes.length > processedCauses.length) { + const numRemoved = causes.length - processedCauses.length; + const message = + "Omitted " + + numRemoved + + " issues that have been " + + "created due to the lack of support of the KHR_texture_basisu " + + "extension in the glTF validator"; + const summaryIssue = ValidationIssues.VALIDATION_INFO(path, message); + processedCauses.push(summaryIssue); + } + return processedCauses; + } + + /** + * Returns whether the given issue is an issue that should be removed + * because it is caused by the lack of support of the KHR_texture_basisu + * extension in the glTF validator. + * + * @param khrTextureBasisuImageIndices - The indices of images that + * are used by the KHR_texture_basisu extension + * @param issue - The validation issue + * @returns Whether the issue should be removed + */ + private static async shouldRemoveKhrTextureBasisu( + khrTextureBasisuImageIndices: number[], + issue: ValidationIssue + ): Promise { + // Never remove errors! + if (issue.severity === ValidationIssueSeverity.ERROR) { + return false; + } + + const message = issue.message; + const path = issue.path; + + // Remove the message about the extension not being supported + const isIssueAboutUnsupportedExtension = + GltfExtensionIssues.isIssueAboutUnsupportedExtension( + issue, + "KHR_texture_basisu" + ); + if (isIssueAboutUnsupportedExtension) { + return true; + } + + // Remove all INFO- and WARNING issues about + // - invalid KTX MIME type + // - unrecognized image format + // - unused object + // for issues that refer to a path starting with "/images/N" + // where N is one of the images that are referred to by + // the KHR_texture_basisu extension in any texture. + if ( + message.startsWith("Invalid value 'image/ktx2'.") || + message.startsWith("Image format not recognized.") || + message.startsWith("This object may be unused.") + ) { + const imageIndex = GltfExtensionIssues.extractIndex("images", path); + if (imageIndex === undefined) { + // This is making some assumptions about the path that + // is generated by the glTF validator. Just print a + // warning for the case that this ever breaks. + console.warn("Could not extract image index from path: " + path); + return false; + } + if (khrTextureBasisuImageIndices.includes(imageIndex)) { + return true; + } + } + return false; + } + + /** + * Compute the indices of all images that appear as any + * gltf.textures[i].extensions["KHR_texture_basisu"].source. + * + * @param gltf - The glTF JSON object + * @returns The image indices + */ + private static computeImageIndicesKhrTextureBasisu(gltf: any): number[] { + const imageIndices = []; + const textures = gltf.textures ?? []; + for (const texture of textures) { + const extensions = texture.extensions ?? {}; + const khrTextureBasisuExtension = extensions["KHR_texture_basisu"] ?? {}; + const source = khrTextureBasisuExtension.source; + if (typeof source === "number") { + imageIndices.push(source); + } + } + return imageIndices; + } +} diff --git a/src/validation/gltf/GltfExtensionValidators.ts b/src/validation/gltf/GltfExtensionValidators.ts index acf09266..35b4d18f 100644 --- a/src/validation/gltf/GltfExtensionValidators.ts +++ b/src/validation/gltf/GltfExtensionValidators.ts @@ -10,6 +10,9 @@ import { NgaGpmLocalValidator } from "./gpmLocal/NgaGpmLocalValidator"; import { MaxarImageOrthoValidator } from "./imageOrtho/MaxarImageOrthoValidator"; import { KhrLightsPunctualValidator } from "./lightsPunctual/KhrLightsPunctualValidator"; import { GltfExtensionIssues } from "./GltfExtensionIssues"; +import { GltfExtensionIssuesKhrTextureBasisu } from "./GltfExtensionIssuesKhrTextureBasisu"; +import { ExtStructuralMetadataIssues } from "./structuralMetadata/ExtStructuralMetadataIssues"; +import { ExtMeshFeaturesIssues } from "./meshFeatures/ExtMeshFeaturesIssues"; /** * An internal type definition for glTF extension validators @@ -141,7 +144,7 @@ export class GltfExtensionValidators { GltfExtensionValidators.registerValidator("EXT_mesh_features", { validate: ExtMeshFeaturesValidator.validateGltf, - processCauses: emptyProcessing, + processCauses: ExtMeshFeaturesIssues.processCauses, }); GltfExtensionValidators.registerValidator("EXT_instance_features", { validate: ExtInstanceFeaturesValidator.validateGltf, @@ -149,7 +152,7 @@ export class GltfExtensionValidators { }); GltfExtensionValidators.registerValidator("EXT_structural_metadata", { validate: ExtStructuralMetadataValidator.validateGltf, - processCauses: emptyProcessing, + processCauses: ExtStructuralMetadataIssues.processCauses, }); GltfExtensionValidators.registerValidator("NGA_gpm_local", { validate: NgaGpmLocalValidator.validateGltf, @@ -173,9 +176,9 @@ export class GltfExtensionValidators { // unsupported MIME types. GltfExtensionValidators.registerValidator("KHR_texture_basisu", { validate: emptyValidation, - processCauses: GltfExtensionIssues.processCausesKhrTextureBasisu, + processCauses: + GltfExtensionIssuesKhrTextureBasisu.processCausesKhrTextureBasisu, }); - GltfExtensionValidators.didRegisterValidators = true; } @@ -207,15 +210,19 @@ export class GltfExtensionValidators { } /** - * Filter the given list of issues, to omit the ones that are obsolete - * due to the validation that is performed by validators that are + * Process the given list of issues with all registered glTF extension + * validators. + * + * This will call 'GltfExtensionValidator.processCauses' for each registered + * validator. This is mainly intended for filtering out the issues that are + * obsolete due to the validation that is performed by validators that are * implemented as part of the 3D Tiles validator. * * @param path - The path for validation issues * @param gltfData - The GltfData * @param allCauses - All validation issues that have been created * from the issues that are generated by the glTF validator - * @returns A possibly filtered list of validation issues + * @returns A possibly modified list of validation issues */ static async processCauses( path: string, diff --git a/src/validation/gltf/meshFeatures/ExtMeshFeaturesIssues.ts b/src/validation/gltf/meshFeatures/ExtMeshFeaturesIssues.ts new file mode 100644 index 00000000..0ae87a11 --- /dev/null +++ b/src/validation/gltf/meshFeatures/ExtMeshFeaturesIssues.ts @@ -0,0 +1,173 @@ +import { GltfData } from "../GltfData"; + +import { ValidationIssue } from "../../ValidationIssue"; +import { ValidationIssueSeverity } from "../../ValidationIssueSeverity"; +import { GltfExtensionIssues } from "../GltfExtensionIssues"; + +/** + * A class for processing the list of issues that are generated by + * the glTF validator, to filter out the issues that are obsolete + * due to the validation from the ExtMeshFeaturesValidator. + * + * @internal + */ +export class ExtMeshFeaturesIssues { + /** + * Process the given list of validation issues, and possibly filer + * out the issues that are obsolete due to the validation that + * is performed by 'validateGltf'. + * + * @param path - The path for validation issues + * @param gltfData - The GltfData objects + * @param causes - The validation issues + */ + static async processCauses( + path: string, + gltfData: GltfData, + causes: ValidationIssue[] + ): Promise { + const usedAccessorIndices = + ExtMeshFeaturesIssues.computeUsedAccessorIndices(gltfData.gltf); + const usedTextureIndices = ExtMeshFeaturesIssues.computeUsedTextureIndices( + gltfData.gltf + ); + + const processedCauses: ValidationIssue[] = []; + for (const cause of causes) { + const remove = await ExtMeshFeaturesIssues.shouldRemove( + usedAccessorIndices, + usedTextureIndices, + cause + ); + if (!remove) { + processedCauses.push(cause); + } + } + return processedCauses; + } + + /** + * Returns whether the given issue is an issue that should be removed + * because it is obsolete due to the validation that is performed by + * 'validateGltf' + * + * @param usedAccessorIndices - The accessor indices that are + * actually used by the extension, via feature ID attributes + * @param usedTextureIndices - The texture indices that are + * actually used by the extension, via feature ID textures + * @param issue - The validation issue + * @returns Whether the issue should be removed + */ + private static async shouldRemove( + usedAccessorIndices: number[], + usedTextureIndices: number[], + issue: ValidationIssue + ): Promise { + // Never remove errors! + if (issue.severity === ValidationIssueSeverity.ERROR) { + return false; + } + + // Remove the message about the extension not being supported + const isIssueAboutUnsupportedExtension = + GltfExtensionIssues.isIssueAboutUnsupportedExtension( + issue, + "EXT_mesh_features" + ); + if (isIssueAboutUnsupportedExtension) { + return true; + } + + // Remove all INFO- and WARNING issues about unused objects + // for buffer views and textures that are actually used + // by the extension + const isObsoleteAboutAccessor = + GltfExtensionIssues.isObsoleteIssueAboutUnusedObject( + issue, + "accessors", + usedAccessorIndices + ); + if (isObsoleteAboutAccessor) { + return true; + } + const isObsoleteAboutTexture = + GltfExtensionIssues.isObsoleteIssueAboutUnusedObject( + issue, + "textures", + usedTextureIndices + ); + if (isObsoleteAboutTexture) { + return true; + } + return false; + } + + /** + * Pragmatically drill into the given glTF object to find all accessor + * indices that are used via feature ID attributes or as texture + * coordinate accessors for feature ID textures. + * + * This will fall back to empty objects and arrays everywhere, and return + * only the indices that are definitely known to be used. + * + * @param gltf - The glTF JSON object + * @returns The accessor indices that are used by the + * EXT_mesh_features extension + */ + private static computeUsedAccessorIndices(gltf: any): number[] { + const accessorIndices: number[] = []; + const meshes = gltf.meshes ?? []; + for (const mesh of meshes) { + const primitives = mesh.primitives ?? []; + for (const primitive of primitives) { + const extensions = primitive.extensions ?? {}; + const extension = extensions["EXT_mesh_features"] ?? {}; + const featureIds = extension.featureIds ?? []; + for (const featureId of featureIds) { + const attributeAccessorIndex = featureId.attribute; + if (typeof attributeAccessorIndex === "number") { + accessorIndices.push(attributeAccessorIndex); + } + const texture = featureId.texture ?? {}; + const texcoordAccessorIndex = texture.texCoord; + if (typeof texcoordAccessorIndex === "number") { + accessorIndices.push(texcoordAccessorIndex); + } + } + } + } + return accessorIndices; + } + + /** + * Pragmatically drill into the given glTF object to find all texture + * indices that are used via feature ID textures + * + * This will fall back to empty objects and arrays everywhere, and return + * only the indices that are definitely known to be used. + * + * @param gltf - The glTF JSON object + * @returns The texture indices that are used by the + * EXT_mesh_features extension + */ + private static computeUsedTextureIndices(gltf: any): number[] { + const textureIndices: number[] = []; + const meshes = gltf.meshes ?? []; + for (const mesh of meshes) { + const primitives = mesh.primitives ?? []; + for (const primitive of primitives) { + const extensions = primitive.extensions ?? {}; + const extension = extensions["EXT_mesh_features"] ?? {}; + const featureIds = extension.featureIds ?? []; + for (const featureId of featureIds) { + const texture = featureId.texture ?? {}; + const textureIndex = texture.index; + if (typeof textureIndex === "number") { + textureIndices.push(textureIndex); + } + } + } + } + return textureIndices; + } +} diff --git a/src/validation/gltf/structuralMetadata/ExtStructuralMetadataIssues.ts b/src/validation/gltf/structuralMetadata/ExtStructuralMetadataIssues.ts new file mode 100644 index 00000000..2adb57cc --- /dev/null +++ b/src/validation/gltf/structuralMetadata/ExtStructuralMetadataIssues.ts @@ -0,0 +1,171 @@ +import { GltfData } from "../GltfData"; + +import { ValidationIssue } from "../../ValidationIssue"; +import { ValidationIssueSeverity } from "../../ValidationIssueSeverity"; +import { GltfExtensionIssues } from "../GltfExtensionIssues"; + +/** + * A class for processing the list of issues that are generated by + * the glTF validator, to filter out the issues that are obsolete + * due to the validation from the ExtStructuralMetadataValidator. + * + * @internal + */ +export class ExtStructuralMetadataIssues { + /** + * Process the given list of validation issues, and possibly filer + * out the issues that are obsolete due to the validation that + * is performed by 'validateGltf'. + * + * @param path - The path for validation issues + * @param gltfData - The GltfData objects + * @param causes - The validation issues + */ + static async processCauses( + path: string, + gltfData: GltfData, + causes: ValidationIssue[] + ): Promise { + const usedBufferViewIndices = + ExtStructuralMetadataIssues.computeUsedBufferViewIndices(gltfData.gltf); + const usedTextureIndices = + ExtStructuralMetadataIssues.computeUsedTextureIndices(gltfData.gltf); + + const processedCauses: ValidationIssue[] = []; + for (const cause of causes) { + const remove = await ExtStructuralMetadataIssues.shouldRemove( + usedBufferViewIndices, + usedTextureIndices, + cause + ); + if (!remove) { + processedCauses.push(cause); + } + } + return processedCauses; + } + + /** + * Returns whether the given issue is an issue that should be removed + * because it is obsolete due to the validation that is performed by + * 'validateGltf' + * + * @param usedBufferViewIndices - The buffer view indices that are + * actually used by the extension, via properties from the + * property tables + * @param usedTextureIndices - The texture indices that are + * actually used by the extension, via properties from the + * property textures + * @param issue - The validation issue + * @returns Whether the issue should be removed + */ + private static async shouldRemove( + usedBufferViewIndices: number[], + usedTextureIndices: number[], + issue: ValidationIssue + ): Promise { + // Never remove errors! + if (issue.severity === ValidationIssueSeverity.ERROR) { + return false; + } + + // Remove the message about the extension not being supported + const isIssueAboutUnsupportedExtension = + GltfExtensionIssues.isIssueAboutUnsupportedExtension( + issue, + "EXT_structural_metadata" + ); + if (isIssueAboutUnsupportedExtension) { + return true; + } + + // Remove all INFO- and WARNING issues about unused objects + // for buffer views and textures that are actually used + // by the extension + const isObsoleteAboutBufferView = + GltfExtensionIssues.isObsoleteIssueAboutUnusedObject( + issue, + "bufferViews", + usedBufferViewIndices + ); + if (isObsoleteAboutBufferView) { + return true; + } + const isObsoleteAboutTexture = + GltfExtensionIssues.isObsoleteIssueAboutUnusedObject( + issue, + "textures", + usedTextureIndices + ); + if (isObsoleteAboutTexture) { + return true; + } + return false; + } + + /** + * Pragmatically drill into the given glTF object to find all bufferView + * indices that are used via properties of property tables. + * + * This will fall back to empty objects and arrays everywhere, and return + * only the indices that are definitely known to be used. + * + * @param gltf - The glTF JSON object + * @returns The buffer view indices that are used by the + * EXT_structural_metadata extension + */ + private static computeUsedBufferViewIndices(gltf: any): number[] { + const bufferViewIndices: number[] = []; + const extensions = gltf.extensions ?? {}; + const extension = extensions["EXT_structural_metadata"] ?? {}; + const propertyTables = extension.propertyTables ?? []; + for (const propertyTable of propertyTables) { + const properties = propertyTable.properties ?? {}; + for (const property of Object.values(properties)) { + const p = property as any; + const valuesBufferViewIndex = p.values; + const stringOffsetsBufferViewIndex = p.stringOffsets; + const arrayOffsetsBufferViewIndex = p.arrayOffsets; + if (typeof valuesBufferViewIndex === "number") { + bufferViewIndices.push(valuesBufferViewIndex); + } + if (typeof stringOffsetsBufferViewIndex === "number") { + bufferViewIndices.push(stringOffsetsBufferViewIndex); + } + if (typeof arrayOffsetsBufferViewIndex === "number") { + bufferViewIndices.push(arrayOffsetsBufferViewIndex); + } + } + } + return bufferViewIndices; + } + + /** + * Pragmatically drill into the given glTF object to find all texture + * indices that are used via properties of property textures. + * + * This will fall back to empty objects and arrays everywhere, and return + * only the indices that are definitely known to be used. + * + * @param gltf - The glTF JSON object + * @returns The texture indices that are used by the + * EXT_structural_metadata extension + */ + private static computeUsedTextureIndices(gltf: any): number[] { + const textureIndices: number[] = []; + const extensions = gltf.extensions ?? {}; + const extension = extensions["EXT_structural_metadata"] ?? {}; + const propertyTextures = extension.propertyTextures ?? []; + for (const propertyTexture of propertyTextures) { + const properties = propertyTexture.properties ?? {}; + for (const property of Object.values(properties)) { + const p = property as any; + const textureIndex = p.index; + if (typeof textureIndex === "number") { + textureIndices.push(textureIndex); + } + } + } + return textureIndices; + } +} From 7f042e7637d2beb9b4d22cc799b2075deec09e3a Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 18 Oct 2025 16:04:27 +0200 Subject: [PATCH 09/10] Remove unused imports --- src/validation/gltf/GltfExtensionIssues.ts | 4 ---- src/validation/gltf/GltfExtensionValidators.ts | 1 - 2 files changed, 5 deletions(-) diff --git a/src/validation/gltf/GltfExtensionIssues.ts b/src/validation/gltf/GltfExtensionIssues.ts index c3c99bc5..7bce4041 100644 --- a/src/validation/gltf/GltfExtensionIssues.ts +++ b/src/validation/gltf/GltfExtensionIssues.ts @@ -1,8 +1,4 @@ import { ValidationIssue } from "../ValidationIssue"; -import { GltfData } from "./GltfData"; - -import { ValidationIssues } from "../../issues/ValidationIssues"; -import { ValidationIssueSeverity } from "../ValidationIssueSeverity"; /** * Functions for implementing filters on lists of validation issues, diff --git a/src/validation/gltf/GltfExtensionValidators.ts b/src/validation/gltf/GltfExtensionValidators.ts index 35b4d18f..6e10ce7e 100644 --- a/src/validation/gltf/GltfExtensionValidators.ts +++ b/src/validation/gltf/GltfExtensionValidators.ts @@ -9,7 +9,6 @@ import { MaxarNonvisualGeometryValidator } from "./nonvisualGeometry/MaxarNonvis import { NgaGpmLocalValidator } from "./gpmLocal/NgaGpmLocalValidator"; import { MaxarImageOrthoValidator } from "./imageOrtho/MaxarImageOrthoValidator"; import { KhrLightsPunctualValidator } from "./lightsPunctual/KhrLightsPunctualValidator"; -import { GltfExtensionIssues } from "./GltfExtensionIssues"; import { GltfExtensionIssuesKhrTextureBasisu } from "./GltfExtensionIssuesKhrTextureBasisu"; import { ExtStructuralMetadataIssues } from "./structuralMetadata/ExtStructuralMetadataIssues"; import { ExtMeshFeaturesIssues } from "./meshFeatures/ExtMeshFeaturesIssues"; From 88483ad2a6d7cab5ce2e38eb9684958e61acb1aa Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 18 Oct 2025 16:36:47 +0200 Subject: [PATCH 10/10] More concise names --- .../GltfExtensionIssuesKhrTextureBasisu.ts | 31 +++++++++---------- .../gltf/GltfExtensionValidators.ts | 3 +- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/validation/gltf/GltfExtensionIssuesKhrTextureBasisu.ts b/src/validation/gltf/GltfExtensionIssuesKhrTextureBasisu.ts index 1ec4e3b5..59509030 100644 --- a/src/validation/gltf/GltfExtensionIssuesKhrTextureBasisu.ts +++ b/src/validation/gltf/GltfExtensionIssuesKhrTextureBasisu.ts @@ -23,24 +23,21 @@ export class GltfExtensionIssuesKhrTextureBasisu { * @param causes - The causes * @returns The filtered causes */ - static async processCausesKhrTextureBasisu( + static async processCauses( path: string, gltfData: GltfData, causes: ValidationIssue[] ): Promise { const gltf = gltfData.gltf; - const khrTextureBasisuImageIndices = - GltfExtensionIssuesKhrTextureBasisu.computeImageIndicesKhrTextureBasisu( - gltf - ); + const usedImageIndices = + GltfExtensionIssuesKhrTextureBasisu.computeUsedImageIndices(gltf); const processedCauses: ValidationIssue[] = []; for (const cause of causes) { - const remove = - await GltfExtensionIssuesKhrTextureBasisu.shouldRemoveKhrTextureBasisu( - khrTextureBasisuImageIndices, - cause - ); + const remove = await GltfExtensionIssuesKhrTextureBasisu.shouldRemove( + usedImageIndices, + cause + ); if (!remove) { processedCauses.push(cause); } @@ -65,13 +62,13 @@ export class GltfExtensionIssuesKhrTextureBasisu { * because it is caused by the lack of support of the KHR_texture_basisu * extension in the glTF validator. * - * @param khrTextureBasisuImageIndices - The indices of images that + * @param usedImageIndices - The indices of images that * are used by the KHR_texture_basisu extension * @param issue - The validation issue * @returns Whether the issue should be removed */ - private static async shouldRemoveKhrTextureBasisu( - khrTextureBasisuImageIndices: number[], + private static async shouldRemove( + usedImageIndices: number[], issue: ValidationIssue ): Promise { // Never remove errors! @@ -112,7 +109,7 @@ export class GltfExtensionIssuesKhrTextureBasisu { console.warn("Could not extract image index from path: " + path); return false; } - if (khrTextureBasisuImageIndices.includes(imageIndex)) { + if (usedImageIndices.includes(imageIndex)) { return true; } } @@ -126,13 +123,13 @@ export class GltfExtensionIssuesKhrTextureBasisu { * @param gltf - The glTF JSON object * @returns The image indices */ - private static computeImageIndicesKhrTextureBasisu(gltf: any): number[] { + private static computeUsedImageIndices(gltf: any): number[] { const imageIndices = []; const textures = gltf.textures ?? []; for (const texture of textures) { const extensions = texture.extensions ?? {}; - const khrTextureBasisuExtension = extensions["KHR_texture_basisu"] ?? {}; - const source = khrTextureBasisuExtension.source; + const extension = extensions["KHR_texture_basisu"] ?? {}; + const source = extension.source; if (typeof source === "number") { imageIndices.push(source); } diff --git a/src/validation/gltf/GltfExtensionValidators.ts b/src/validation/gltf/GltfExtensionValidators.ts index 6e10ce7e..e8f45fad 100644 --- a/src/validation/gltf/GltfExtensionValidators.ts +++ b/src/validation/gltf/GltfExtensionValidators.ts @@ -175,8 +175,7 @@ export class GltfExtensionValidators { // unsupported MIME types. GltfExtensionValidators.registerValidator("KHR_texture_basisu", { validate: emptyValidation, - processCauses: - GltfExtensionIssuesKhrTextureBasisu.processCausesKhrTextureBasisu, + processCauses: GltfExtensionIssuesKhrTextureBasisu.processCauses, }); GltfExtensionValidators.didRegisterValidators = true; }