diff --git a/specs/gltfExtensions/validateGltf.ts b/specs/gltfExtensions/validateGltf.ts index 5ed371bc..67d12a31 100644 --- a/specs/gltfExtensions/validateGltf.ts +++ b/specs/gltfExtensions/validateGltf.ts @@ -4,10 +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 { GltfDataReader } from "../../src/validation/gltf/GltfDataReader"; import { GltfExtensionValidators } from "../../src/validation/gltf/GltfExtensionValidators"; -export async function validateGltf(gltfFileName: string) { +import { IoValidationIssues } from "../../src/issues/IoValidationIssue"; + +/** + * 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); @@ -16,12 +40,24 @@ export async function validateGltf(gltfFileName: string) { ResourceResolvers.createFileResourceResolver(directory); const context = new ValidationContext(directory, resourceResolver); const gltfFileData = await resourceResolver.resolveData(fileName); - if (gltfFileData) { - await GltfExtensionValidators.validateGltfExtensions( + 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, context ); + if (gltfData) { + await GltfExtensionValidators.validateGltfExtensions( + gltfFileName, + gltfData, + context + ); + } } const validationResult = context.getResult(); return validationResult; 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 557cf80a..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"); @@ -126,19 +128,59 @@ 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); + } + + // 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( 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); @@ -150,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( @@ -158,14 +200,11 @@ 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); - } 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. @@ -177,22 +216,46 @@ 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); } // 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; } return true; } + + /** + * Counts and returns the number of issues in the given array that have + * the given severity. + * + * @param issues - The issues + * @param severity - The severity + * @returns The number of issues with the given severity + */ + private static countIssueSeverities( + issues: ValidationIssue[], + severity: ValidationIssueSeverity + ): number { + let count = 0; + for (const issue of issues) { + if (issue.severity === severity) { + count++; + } + } + return count; + } } diff --git a/src/validation/gltf/GltfExtensionIssues.ts b/src/validation/gltf/GltfExtensionIssues.ts new file mode 100644 index 00000000..7bce4041 --- /dev/null +++ b/src/validation/gltf/GltfExtensionIssues.ts @@ -0,0 +1,146 @@ +import { ValidationIssue } from "../ValidationIssue"; + +/** + * 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 { + /** + * 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. + * + * 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 issue - The validation issue + * @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 + */ + static isObsoleteIssueAboutUnusedObject( + issue: ValidationIssue, + topLevelName: string, + usedIndices: number[] + ) { + const message = issue.message; + const path = issue.path; + if (!message.startsWith("This object may be unused.")) { + return false; + } + const prefix = "/" + topLevelName + "/"; + if (!path.startsWith(prefix)) { + return; + } + 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; + } + const isUsed = usedIndices.includes(index); + return isUsed; + } + + /** + * 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 + */ + 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); + } + + /** + * 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. + * + * 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/GltfExtensionIssuesKhrTextureBasisu.ts b/src/validation/gltf/GltfExtensionIssuesKhrTextureBasisu.ts new file mode 100644 index 00000000..59509030 --- /dev/null +++ b/src/validation/gltf/GltfExtensionIssuesKhrTextureBasisu.ts @@ -0,0 +1,139 @@ +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 processCauses( + path: string, + gltfData: GltfData, + causes: ValidationIssue[] + ): Promise { + const gltf = gltfData.gltf; + const usedImageIndices = + GltfExtensionIssuesKhrTextureBasisu.computeUsedImageIndices(gltf); + + const processedCauses: ValidationIssue[] = []; + for (const cause of causes) { + const remove = await GltfExtensionIssuesKhrTextureBasisu.shouldRemove( + usedImageIndices, + 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 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 shouldRemove( + usedImageIndices: 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 (usedImageIndices.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 computeUsedImageIndices(gltf: any): number[] { + const imageIndices = []; + const textures = gltf.textures ?? []; + for (const texture of textures) { + const extensions = texture.extensions ?? {}; + const extension = extensions["KHR_texture_basisu"] ?? {}; + const source = extension.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 67a06f7d..e8f45fad 100644 --- a/src/validation/gltf/GltfExtensionValidators.ts +++ b/src/validation/gltf/GltfExtensionValidators.ts @@ -1,14 +1,67 @@ import { ValidationContext } from "../ValidationContext"; +import { ValidationIssue } from "../ValidationIssue"; +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"; +import { GltfExtensionIssuesKhrTextureBasisu } from "./GltfExtensionIssuesKhrTextureBasisu"; +import { ExtStructuralMetadataIssues } from "./structuralMetadata/ExtStructuralMetadataIssues"; +import { ExtMeshFeaturesIssues } from "./meshFeatures/ExtMeshFeaturesIssues"; + +/** + * An internal type definition for glTF extension validators + */ +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 @@ -16,96 +69,173 @@ import { KhrLightsPunctualValidator } from "./lightsPunctual/KhrLightsPunctualVa * 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; + } + + 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: ExtMeshFeaturesIssues.processCauses, + }); + GltfExtensionValidators.registerValidator("EXT_instance_features", { + validate: ExtInstanceFeaturesValidator.validateGltf, + processCauses: emptyProcessing, + }); + GltfExtensionValidators.registerValidator("EXT_structural_metadata", { + validate: ExtStructuralMetadataValidator.validateGltf, + processCauses: ExtStructuralMetadataIssues.processCauses, + }); + 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: GltfExtensionIssuesKhrTextureBasisu.processCauses, + }); + GltfExtensionValidators.didRegisterValidators = true; + } + /** * 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 { - const gltfData = await GltfDataReader.readGltfData(path, input, context); - if (!gltfData) { - // Issue was already added to context - return false; - } - + GltfExtensionValidators.registerValidators(); 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; - } - - // Validate `EXT_structural_metadata` - const extStructuralMetadataValid = - await ExtStructuralMetadataValidator.validateGltf( - path, - gltfData, - context - ); - if (!extStructuralMetadataValid) { - result = false; + for (const validator of validators) { + const valid = await validator.validate(path, gltfData, context); + if (!valid) { + result = false; + } } + return result; + } - // Validate `NGA_gpm_local` - const ngaGpmLocalValid = await NgaGpmLocalValidator.validateGltf( - path, - gltfData, - context - ); - if (!ngaGpmLocalValid) { - result = false; - } + /** + * 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 modified list of validation issues + */ + static async processCauses( + path: string, + gltfData: GltfData, + allCauses: ValidationIssue[] + ): Promise { + GltfExtensionValidators.registerValidators(); - // Validate `MAXAR_image_ortho` - const maxarImageOrthoValid = await MaxarImageOrthoValidator.validateGltf( - path, - gltfData, - context + let result = allCauses.slice(); + const validators = Object.values( + GltfExtensionValidators.gltfExtensionValidators ); - 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; + for (const validator of validators) { + result = await validator.processCauses(path, gltfData, result); } - return result; } } 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; + } +}