import Cartesian3 from "../../Core/Cartesian3.js"; import defined from "../../Core/defined.js"; import Matrix4 from "../../Core/Matrix4.js"; import Quaternion from "../../Core/Quaternion.js"; import RuntimeError from "../../Core/RuntimeError.js"; import Axis from "../Axis.js"; import AttributeType from "../AttributeType.js"; import VertexAttributeSemantic from "../VertexAttributeSemantic.js"; import CullFace from "../CullFace.js"; import PrimitiveType from "../../Core/PrimitiveType.js"; import Matrix3 from "../../Core/Matrix3.js"; /** * Utility functions for {@link Model}. * * @private */ function ModelUtility() {} /** * Create a function for reporting when a model fails to load * * @param {string} type The type of object to report about * @param {string} path The URI of the model file * @param {Error} [error] The error which caused the failure * @returns {RuntimeError} An error for the failed model * * @private */ ModelUtility.getError = function (type, path, error) { let message = `Failed to load ${type}: ${path}`; if (defined(error) && defined(error.message)) { message += `\n${error.message}`; } const runtimeError = new RuntimeError(message); if (defined(error)) { // the original call stack is often more useful than the new error's stack, // so add the information here runtimeError.stack = `Original stack:\n${error.stack}\nHandler stack:\n${runtimeError.stack}`; } return runtimeError; }; /** * Get a transformation matrix from a node in the model. * * @param {ModelComponents.Node} node The node components * @returns {Matrix4} The computed transformation matrix. If no transformation matrix or parameters are specified, this will be the identity matrix. * * @private */ ModelUtility.getNodeTransform = function (node) { if (defined(node.matrix)) { return node.matrix; } return Matrix4.fromTranslationQuaternionRotationScale( defined(node.translation) ? node.translation : Cartesian3.ZERO, defined(node.rotation) ? node.rotation : Quaternion.IDENTITY, defined(node.scale) ? node.scale : Cartesian3.ONE ); }; /** * Find an attribute by semantic such as POSITION or TANGENT. * * @param {ModelComponents.Primitive|ModelComponents.Instances} object The primitive components or instances object * @param {VertexAttributeSemantic|InstanceAttributeSemantic} semantic The semantic to search for * @param {number} [setIndex] The set index of the semantic. May be undefined for some semantics (POSITION, NORMAL, TRANSLATION, ROTATION, for example) * @return {ModelComponents.Attribute} The selected attribute, or undefined if not found. * * @private */ ModelUtility.getAttributeBySemantic = function (object, semantic, setIndex) { const attributes = object.attributes; const attributesLength = attributes.length; for (let i = 0; i < attributesLength; ++i) { const attribute = attributes[i]; const matchesSetIndex = defined(setIndex) ? attribute.setIndex === setIndex : true; if (attribute.semantic === semantic && matchesSetIndex) { return attribute; } } return undefined; }; /** * Similar to getAttributeBySemantic, but search using the name field only, * as custom attributes do not have a semantic. * * @param {ModelComponents.Primitive|ModelComponents.Instances} object The primitive components or instances object * @param {string} name The name of the attribute as it appears in the model file. * @return {ModelComponents.Attribute} The selected attribute, or undefined if not found. * * @private */ ModelUtility.getAttributeByName = function (object, name) { const attributes = object.attributes; const attributesLength = attributes.length; for (let i = 0; i < attributesLength; ++i) { const attribute = attributes[i]; if (attribute.name === name) { return attribute; } } return undefined; }; /** * Find a feature ID from an array with label or positionalLabel matching the * given label * @param {ModelComponents.FeatureIdAttribute[]|ModelComponents.FeatureIdImplicitRange[]|ModelComponents.FeatureIdTexture[]} featureIds * @param {string} label the label to search for * @returns {ModelComponents.FeatureIdAttribute|ModelComponents.FeatureIdImplicitRange|ModelComponents.FeatureIdTexture} The feature ID set if found, otherwise undefined * * @private */ ModelUtility.getFeatureIdsByLabel = function (featureIds, label) { for (let i = 0; i < featureIds.length; i++) { const featureIdSet = featureIds[i]; if ( featureIdSet.positionalLabel === label || featureIdSet.label === label ) { return featureIdSet; } } return undefined; }; ModelUtility.hasQuantizedAttributes = function (attributes) { if (!defined(attributes)) { return false; } for (let i = 0; i < attributes.length; i++) { const attribute = attributes[i]; if (defined(attribute.quantization)) { return true; } } return false; }; /** * @param {ModelComponents.Attribute} attribute * * @private */ ModelUtility.getAttributeInfo = function (attribute) { const semantic = attribute.semantic; const setIndex = attribute.setIndex; let variableName; let hasSemantic = false; if (defined(semantic)) { variableName = VertexAttributeSemantic.getVariableName(semantic, setIndex); hasSemantic = true; } else { variableName = attribute.name; // According to the glTF 2.0 spec, custom attributes must be prepended with // an underscore. variableName = variableName.replace(/^_/, ""); variableName = variableName.toLowerCase(); } const isVertexColor = /^color_\d+$/.test(variableName); const attributeType = attribute.type; let glslType = AttributeType.getGlslType(attributeType); // color_n can be either a vec3 or a vec4. But in GLSL we can always use // attribute vec4 since GLSL promotes vec3 attribute data to vec4 with // the .a channel set to 1.0. if (isVertexColor) { glslType = "vec4"; } const isQuantized = defined(attribute.quantization); let quantizedGlslType; if (isQuantized) { // The quantized color_n attribute also is promoted to a vec4 in the shader quantizedGlslType = isVertexColor ? "vec4" : AttributeType.getGlslType(attribute.quantization.type); } return { attribute: attribute, isQuantized: isQuantized, variableName: variableName, hasSemantic: hasSemantic, glslType: glslType, quantizedGlslType: quantizedGlslType, }; }; const cartesianMaxScratch = new Cartesian3(); const cartesianMinScratch = new Cartesian3(); /** * Get the minimum and maximum values for a primitive's POSITION attribute. * This is used to compute the bounding sphere of the primitive, as well as * the bounding sphere of the whole model. * * @param {ModelComponents.Primitive} primitive The primitive components. * @param {Cartesian3} [instancingTranslationMin] The component-wise minimum value of the instancing translation attribute. * @param {Cartesian3} [instancingTranslationMax] The component-wise maximum value of the instancing translation attribute. * * @returns {object} An object containing the minimum and maximum position values. * * @private */ ModelUtility.getPositionMinMax = function ( primitive, instancingTranslationMin, instancingTranslationMax ) { const positionGltfAttribute = ModelUtility.getAttributeBySemantic( primitive, "POSITION" ); let positionMax = positionGltfAttribute.max; let positionMin = positionGltfAttribute.min; if (defined(instancingTranslationMax) && defined(instancingTranslationMin)) { positionMin = Cartesian3.add( positionMin, instancingTranslationMin, cartesianMinScratch ); positionMax = Cartesian3.add( positionMax, instancingTranslationMax, cartesianMaxScratch ); } return { min: positionMin, max: positionMax, }; }; /** * Model matrices in a model file (e.g. glTF) are typically in a different * coordinate system, such as with y-up instead of z-up in 3D Tiles. * This function returns a matrix that will correct this such that z is up, * and x is forward. * * @param {Axis} upAxis The original up direction * @param {Axis} forwardAxis The original forward direction * @param {Matrix4} result The matrix in which to store the result. * @returns {Matrix4} The axis correction matrix * * @private */ ModelUtility.getAxisCorrectionMatrix = function (upAxis, forwardAxis, result) { result = Matrix4.clone(Matrix4.IDENTITY, result); if (upAxis === Axis.Y) { result = Matrix4.clone(Axis.Y_UP_TO_Z_UP, result); } else if (upAxis === Axis.X) { result = Matrix4.clone(Axis.X_UP_TO_Z_UP, result); } if (forwardAxis === Axis.Z) { // glTF 2.0 has a Z-forward convention that must be adapted here to X-forward. result = Matrix4.multiplyTransformation(result, Axis.Z_UP_TO_X_UP, result); } return result; }; const scratchMatrix3 = new Matrix3(); /** * Get the cull face to use in the command's render state. *

* From the glTF spec section 3.7.4: * When a mesh primitive uses any triangle-based topology (i.e., triangles, * triangle strip, or triangle fan), the determinant of the node’s global * transform defines the winding order of that primitive. If the determinant * is a positive value, the winding order triangle faces is counterclockwise; * in the opposite case, the winding order is clockwise. *

* * @param {Matrix4} modelMatrix The model matrix * @param {PrimitiveType} primitiveType The primitive type * @returns {CullFace} The cull face * * @private */ ModelUtility.getCullFace = function (modelMatrix, primitiveType) { if (!PrimitiveType.isTriangles(primitiveType)) { return CullFace.BACK; } const matrix3 = Matrix4.getMatrix3(modelMatrix, scratchMatrix3); return Matrix3.determinant(matrix3) < 0.0 ? CullFace.FRONT : CullFace.BACK; }; /** * Sanitize the identifier to be used in a GLSL shader. The identifier * is sanitized as follows: * - Replace all sequences of non-alphanumeric characters with a single `_`. * - If the gl_ prefix is present, remove it. The prefix is reserved in GLSL. * - If the identifier starts with a digit, prefix it with an underscore. * * @example * // Returns "customProperty" * ModelUtility.sanitizeGlslIdentifier("gl_customProperty"); * * @example * // Returns "_1234" * ModelUtility.sanitizeGlslIdentifier("1234"); * * @param {string} identifier The original identifier. * * @returns {string} The sanitized version of the identifier. */ ModelUtility.sanitizeGlslIdentifier = function (identifier) { // Remove non-alphanumeric characters and replace with a single underscore. // This regex is designed so that the result won't have multiple underscores // in a row. let sanitizedIdentifier = identifier.replaceAll(/[^A-Za-z0-9]+/g, "_"); // Remove the gl_ prefix if present. sanitizedIdentifier = sanitizedIdentifier.replace(/^gl_/, ""); // Add an underscore if first character is a digit. if (/^\d/.test(sanitizedIdentifier)) { sanitizedIdentifier = `_${sanitizedIdentifier}`; } return sanitizedIdentifier; }; ModelUtility.supportedExtensions = { AGI_articulations: true, CESIUM_primitive_outline: true, CESIUM_RTC: true, EXT_feature_metadata: true, EXT_instance_features: true, EXT_mesh_features: true, EXT_mesh_gpu_instancing: true, EXT_meshopt_compression: true, EXT_structural_metadata: true, EXT_texture_webp: true, KHR_blend: true, KHR_draco_mesh_compression: true, KHR_techniques_webgl: true, KHR_materials_common: true, KHR_materials_pbrSpecularGlossiness: true, KHR_materials_unlit: true, KHR_mesh_quantization: true, KHR_texture_basisu: true, KHR_texture_transform: true, WEB3D_quantized_attributes: true, }; /** * Checks whether or not the extensions required by the glTF are * supported. If an unsupported extension is found, this throws * a {@link RuntimeError} with the extension name. * * @param {string[]} extensionsRequired The extensionsRequired array in the glTF. * * @exception {RuntimeError} Unsupported glTF Extension */ ModelUtility.checkSupportedExtensions = function (extensionsRequired) { const length = extensionsRequired.length; for (let i = 0; i < length; i++) { const extension = extensionsRequired[i]; if (!ModelUtility.supportedExtensions[extension]) { throw new RuntimeError(`Unsupported glTF Extension: ${extension}`); } } }; export default ModelUtility;