All files / engine/Source/Scene/Model ModelUtility.js

99.04% Statements 104/105
96.42% Branches 54/56
92.3% Functions 12/13
99% Lines 99/100

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396                                                          1x 6x 6x 6x     6x 6x     6x     6x                     1x 1960x 1620x     340x                                 1x 7024x 7024x 7024x 11999x 11999x     11999x 6720x       304x                         1x 95x 95x 95x 386x 386x 95x                               1x 4004x 3360x 3360x       3345x       659x     1x 1801x 2x     1799x 4988x 4988x 10x     1789x               1x 5263x 5263x     5263x 5263x 5077x 5077x   186x     186x 186x     5263x 5263x 5263x         5263x 90x     5263x   5263x   63x         5263x                   1x 1x                             1x         1782x         1782x 1782x   1782x 83x         83x             1782x                                     1x 1547x   1547x 1474x 73x 1x     1547x   266x     1547x     1x                                     1x 3255x 171x     3084x 3084x                                           1x       168x   168x   168x 3x     168x     1x                                                                                 1x 100x 100x 122x 122x 2x            
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}`;
  Eif (defined(error) && defined(error.message)) {
    message += `\n${error.message}`;
  }
 
  const runtimeError = new RuntimeError(message);
  Eif (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 <code>undefined</code>
 *
 * @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.
 * <p>
 * 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.
 * </p>
 *
 * @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_implicit_cylinder_region: true,
  EXT_implicit_ellipsoid_region: true,
  EXT_instance_features: true,
  EXT_mesh_features: true,
  EXT_mesh_gpu_instancing: true,
  EXT_meshopt_compression: true,
  EXT_primitive_voxels: true,
  EXT_structural_metadata: true,
  EXT_texture_webp: true,
  KHR_blend: true,
  KHR_draco_mesh_compression: true,
  KHR_implicit_shapes: true,
  KHR_materials_common: true,
  KHR_materials_pbrSpecularGlossiness: true,
  KHR_materials_specular: true,
  KHR_materials_anisotropy: true,
  KHR_materials_clearcoat: true,
  KHR_materials_unlit: true,
  KHR_mesh_quantization: true,
  KHR_techniques_webgl: true,
  KHR_texture_basisu: true,
  KHR_texture_transform: true,
  KHR_gaussian_splatting: true,
  KHR_gaussian_splatting_compression_spz_2: 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;