UNPKG

@cesium/engine

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

1,282 lines (1,187 loc) 45.6 kB
import Check from "../../Core/Check.js"; import defined from "../../Core/defined.js"; import Cartesian2 from "../../Core/Cartesian2.js"; import Cartesian3 from "../../Core/Cartesian3.js"; import Cartesian4 from "../../Core/Cartesian4.js"; import DeveloperError from "../../Core/DeveloperError.js"; import ComponentDatatype from "../../Core/ComponentDatatype.js"; import AttributeCompression from "../../Core/AttributeCompression.js"; import IndexDatatype from "../../Core/IndexDatatype.js"; import PrimitiveType from "../../Core/PrimitiveType.js"; import Matrix4 from "../../Core/Matrix4.js"; import Quaternion from "../../Core/Quaternion.js"; import Transforms from "../../Core/Transforms.js"; import AttributeType from "../AttributeType.js"; import InstanceAttributeSemantic from "../InstanceAttributeSemantic.js"; import VertexAttributeSemantic from "../VertexAttributeSemantic.js"; import ModelUtility from "./ModelUtility.js"; /** * A class for reading the data from a <code>ModelComponents.Attribute</code>. * * NOTE: Much of the functionality here already exists, scattered in many places. * In most cases, the functionality is tailored for "one case" (like only handling * positions, or only normals, or not considering quantization, or not handling * interleaved buffers, ...). In many cases, the functionality is tailored for an * 'accessor' (and often, the functions also expect the 'gltf' to be given). * Most of what is done here (and in the existing functions) is pretty low-level * and generic, though: The functions could often be fed with some (count, type, * componentType), and there could be convenience functions that EITHER take these * values from an 'accessor' OR from an 'attribute'. The tl;dr: Large parts of * this could be "nicer", or "more generic", and "better" along all dimensions * of this term. Just give me time... * * NOTE: The fact that all this has to operate on TypedArray is unfortunate. * Most of the subsequent processing could operate on some abstraction of * that. The fact that that TypedArrays can be read/written as "bulk", and * then offer access that is "as efficient as it can be" could be a * justification, as part of the performance-genericity trade-off * * NOTE: All this does not properly handle MATn types. There should be SOME * abstraction for element- and component-wise access of the data. See * https://github.com/javagl/JglTF/blob/84ce6d019fec3b75b6af1649bbe834005b2c620f/jgltf-model/src/main/java/de/javagl/jgltf/model/AbstractAccessorData.java#L149 * * @private */ class ModelReader { /** * Reads the data of the given atttribute into a typed array. * * This will read the data into a compact, flat array with the data * type corresponding to the data type of the attribute. * * If the attribute is contained in an interleaved buffer, marked as * 'normalized', quantized, or oct-encoded, then it will be deinterleaved, * normalization will be applied, it will be dequantized and oct-decoded * as necessary. * * The result will be THE actual attribute data. * * @param {Attribute} attribute The attribute * @returns {TypedArray} The attribute data */ static readAttributeAsTypedArray(attribute) { //>>includeStart('debug', pragmas.debug); Check.defined("attribute", attribute); //>>includeEnd('debug'); // Obtain a compact (non-interleaved) typed array that contains // the components. const compactTypedArray = ModelReader.readAttributeAsRawCompactTypedArray(attribute); // If the attribute is not normalized and the data is not quantized // and not normalized, then this can be returned directly const normalized = attribute.normalized; const quantization = attribute.quantization; if (!defined(quantization) && !normalized) { return compactTypedArray; } const elementType = attribute.type; const elementCount = attribute.count; // If the attribute is normalized, normalize the data from // the typed array let normalizedTypedArray = compactTypedArray; if (normalized) { // Note that although this is called "dequantize", it does // not really "dequantize" based on the quantization. It only // performs the conversion from the (normalized) integer // component types into floating point. normalizedTypedArray = AttributeCompression.dequantize( compactTypedArray, attribute.componentDatatype, elementType, elementCount, ); } if (!defined(quantization)) { return normalizedTypedArray; } // Now, this one actually DOES dequantize... const dequantizedTypedArray = ModelReader.dequantize( normalizedTypedArray, elementCount, elementType, quantization, ); return dequantizedTypedArray; } /** * Read the data of the given attribute into a compact typed array. * * If the attribute is stored as interleaved data, then the result * will be the deinterleaved data. If the data is quantized or * normalized, then the resulting data will be the "raw" data, * without applying normalization or dequantization. * * @param {ModelComponents.Attribute} attribute The attribute * @returns {TypedArray} The raw attribute data */ static readAttributeAsRawCompactTypedArray(attribute) { //>>includeStart('debug', pragmas.debug); Check.defined("attribute", attribute); //>>includeEnd('debug'); const elementType = attribute.type; const elementCount = attribute.count; const componentsPerElement = AttributeType.getNumberOfComponents(elementType); const totalComponentCount = elementCount * componentsPerElement; // If the data is quantized, use the quantized component type let componentType = attribute.componentDatatype; const quantization = attribute.quantization; if (defined(quantization)) { componentType = quantization.componentDatatype; } const buffer = attribute.buffer; // If the byte stride is the default (i.e. the total element size), // then just fetch the whole buffer data into a typed array of the // desired target type, and return it const byteOffset = attribute.byteOffset; const byteStride = attribute.byteStride; const bytesPerComponent = ComponentDatatype.getSizeInBytes(componentType); const defaultByteStride = componentsPerElement * bytesPerComponent; const isDefaultStride = !defined(byteStride) || byteStride === defaultByteStride; const hasTypedArray = defined(attribute.typedArray); // If in-memory — return as-is // Important: typedArray is already tightly-packed on creation (See: ModelComponents.typedArray and GltfLoader.getPackedTypedArray) // byteOffset and byteStride should thus be ignored if (hasTypedArray) { return attribute.typedArray; } // Non-interleaved — copy from typedArray or read from GPU if (isDefaultStride) { const typedArray = ComponentDatatype.createTypedArray( componentType, totalComponentCount, ); buffer.getBufferData(typedArray, byteOffset); return typedArray; } // Fetch the whole buffer in its raw form, to pick out the // interleaved values. // Note: When ALL attributes have to be fetched from an // interleaved buffer, then this getBufferData call will // be performed multiple times. It would be preferable to // have ONE "TypedArray[] getThemFrom(buffer)" call that // returns all of the (interleaved) attributes at once, // but this requires abstractions that we don't have. // Read back from GPU if not available in memory const fullTypedArray = new Uint8Array(buffer.sizeInBytes); buffer.getBufferData(fullTypedArray); // Read the components of each element, and write them into // a typed array in a compact form const compactTypedArray = ComponentDatatype.createTypedArray( componentType, totalComponentCount, ); const elementByteStride = byteStride ?? defaultByteStride; const dataView = new DataView( fullTypedArray.buffer, fullTypedArray.byteOffset, fullTypedArray.byteLength, ); const components = new Array(componentsPerElement); const componentsReader = ModelReader.createComponentsReader(componentType); for (let i = 0; i < elementCount; ++i) { const elementByteOffset = byteOffset + i * elementByteStride; componentsReader( dataView, elementByteOffset, componentsPerElement, components, ); for (let j = 0; j < componentsPerElement; ++j) { compactTypedArray[i * componentsPerElement + j] = components[j]; } } return compactTypedArray; } /** * Dequantize the data from the given input array, based on the given * quantization information, and return the result. * * This assumes that normalization has already been applied. This means that * when the <code>quantization.normalized</code> flag is <code>true</code>, * then the input is assumed to contain floating point values in the range * [-1, 1]. * * @param {TypedArray} quantizedTypedArray The quantized typed array * @param {number} elementCount The number of elements * @param {AttributeType} elementType The element type * @param {ModelComponents.Quantization} quantization The quantization * @returns {TypedArray} The result * @throws DeveloperError When the element type is not SCALAR, VEC2, * VEC3, or VEC4 */ static dequantize( quantizedTypedArray, elementCount, elementType, quantization, ) { //>>includeStart('debug', pragmas.debug); Check.defined("quantizedTypedArray", quantizedTypedArray); Check.typeOf.number.greaterThanOrEquals("elementCount", elementCount, 0); Check.defined("elementType", elementType); Check.defined("quantization", quantization); //>>includeEnd('debug'); if (quantization.octEncoded) { const dequantizedTypedArray = ModelReader.octDecode( quantizedTypedArray, elementCount, quantization.normalizationRange, undefined, ); if (quantization.octEncodedZXY) { ModelReader.convertZxyToXyz( dequantizedTypedArray, dequantizedTypedArray, ); } return dequantizedTypedArray; } // These could be generalized, if the offset/stepSize were not // CartesianX objects, but arrays... const stepSize = quantization.quantizedVolumeStepSize; const offset = quantization.quantizedVolumeOffset; if (elementType === AttributeType.SCALAR) { return ModelReader.dequantize1D( quantizedTypedArray, elementCount, stepSize, offset, undefined, ); } if (elementType === AttributeType.VEC2) { return ModelReader.dequantize2D( quantizedTypedArray, elementCount, stepSize, offset, undefined, ); } if (elementType === AttributeType.VEC3) { return ModelReader.dequantize3D( quantizedTypedArray, elementCount, stepSize, offset, undefined, ); } if (elementType === AttributeType.VEC4) { return ModelReader.dequantize4D( quantizedTypedArray, elementCount, stepSize, offset, undefined, ); } throw new DeveloperError( `Element type for dequantization must be SCALAR, VEC2, VEC3, or VEC4, but is ${elementType}`, ); } /** * Decode oct-encoded normals from the given input, and write the * result into the given output, allocating and returning a new * array if the result was undefined. * * This will apply the <code>AttributeCompression.octDecodeInRange</code> * function to each three components of the input. * * @param {TypedArray} quantizedTypedArray The input * @param {number} elementCount The number of elements * @param {number} normalizationRange The normalization range * @param {TypedArray} [dequantizedTypedArray] The result * @returns {TypedArray} The result */ static octDecode( quantizedTypedArray, elementCount, normalizationRange, dequantizedTypedArray, ) { //>>includeStart('debug', pragmas.debug); Check.defined("quantizedTypedArray", quantizedTypedArray); Check.typeOf.number.greaterThanOrEquals("elementCount", elementCount, 0); Check.typeOf.number.greaterThan( "normalizationRange", normalizationRange, 0, ); //>>includeEnd('debug'); if (!defined(dequantizedTypedArray)) { dequantizedTypedArray = new Float32Array(quantizedTypedArray.length); } const c = new Cartesian3(); for (let i = 0; i < elementCount; i++) { Cartesian3.unpack(quantizedTypedArray, i * 3, c); AttributeCompression.octDecodeInRange(c.x, c.y, normalizationRange, c); Cartesian3.pack(c, dequantizedTypedArray, i * 3); } return dequantizedTypedArray; } /** * Swizzle all three consecutive elements in the given input array * from (z, x, y) to (x, y, z), and write the result into the * given output array, creating a new array if the given output * array was undefined. * * @param {TypedArray} input The input * @param {number} elementCount The number of elements * @param {TypedArray} [output] The result * @returns {TypedArray} The result */ static convertZxyToXyz(input, elementCount, output) { //>>includeStart('debug', pragmas.debug); Check.defined("input", input); Check.typeOf.number.greaterThanOrEquals("elementCount", elementCount, 0); //>>includeEnd('debug'); if (!defined(output)) { output = new Float32Array(input.length); } let offset = 0; for (let i = 0; i < elementCount; i++, offset += 3) { const z = input[offset + 0]; const x = input[offset + 1]; const y = input[offset + 2]; output[offset + 0] = x; output[offset + 1] = y; output[offset + 2] = z; } return output; } /** * Dequantize the given quantized array, based on the given quantization * information, and write the result into the given output array, creating * the output array if it was undefined. * * This will simply fill the output array with * <code>output[i] = input[i] * stepSize + offset</code> * * @param {TypedArray} quantizedTypedArray The quantized array * @param {number} elementCount The number of elements * @param {number} stepSize The quantization step size * @param {number} offset The quantization offset * @param {TypedArray} [dequantizedTypedArray] The result * @returns {TypedArray} The result */ static dequantize1D( quantizedTypedArray, elementCount, stepSize, offset, dequantizedTypedArray, ) { //>>includeStart('debug', pragmas.debug); Check.defined("quantizedTypedArray", quantizedTypedArray); Check.typeOf.number.greaterThanOrEquals("elementCount", elementCount, 0); Check.defined("stepSize", stepSize); Check.defined("offset", offset); //>>includeEnd('debug'); if (!defined(dequantizedTypedArray)) { dequantizedTypedArray = new Float32Array(quantizedTypedArray.length); } for (let i = 0; i < elementCount; i++) { const q = quantizedTypedArray[i]; const d = q * stepSize + offset; dequantizedTypedArray[i] = d; } return dequantizedTypedArray; } /** * Dequantize the given quantized array, based on the given quantization * information, and write the result into the given output array, creating * the output array if it was undefined. * * This will simply fill the output array with * <code>output[i] = input[i] * stepSize + offset</code> * when interpreting the input and output as arrays of Cartesian2. * * @param {TypedArray} quantizedTypedArray The quantized array * @param {number} elementCount The number of elements * @param {Cartesian2} stepSize The quantization step size * @param {Cartesian2} offset The quantization offset * @param {TypedArray} [dequantizedTypedArray] The result * @returns {TypedArray} The result */ static dequantize2D( quantizedTypedArray, elementCount, stepSize, offset, dequantizedTypedArray, ) { //>>includeStart('debug', pragmas.debug); Check.defined("quantizedTypedArray", quantizedTypedArray); Check.typeOf.number.greaterThanOrEquals("elementCount", elementCount, 0); Check.defined("stepSize", stepSize); Check.defined("offset", offset); //>>includeEnd('debug'); if (!defined(dequantizedTypedArray)) { dequantizedTypedArray = new Float32Array(quantizedTypedArray.length); } const c = new Cartesian2(); for (let i = 0; i < elementCount; i++) { Cartesian2.unpack(quantizedTypedArray, i * 2, c); Cartesian2.multiplyComponents(c, stepSize, c); Cartesian2.add(c, offset, c); Cartesian2.pack(c, dequantizedTypedArray, i * 2); } return dequantizedTypedArray; } /** * Dequantize the given quantized array, based on the given quantization * information, and write the result into the given output array, creating * the output array if it was undefined. * * This will simply fill the output array with * <code>output[i] = input[i] * stepSize + offset</code> * when interpreting the input and output as arrays of Cartesian3. * * @param {TypedArray} quantizedTypedArray The quantized array * @param {number} elementCount The number of elements * @param {Cartesian3} stepSize The quantization step size * @param {Cartesian3} offset The quantization offset * @param {TypedArray} [dequantizedTypedArray] The result * @returns {TypedArray} The result */ static dequantize3D( quantizedTypedArray, elementCount, stepSize, offset, dequantizedTypedArray, ) { //>>includeStart('debug', pragmas.debug); Check.defined("quantizedTypedArray", quantizedTypedArray); Check.typeOf.number.greaterThanOrEquals("elementCount", elementCount, 0); Check.defined("stepSize", stepSize); Check.defined("offset", offset); //>>includeEnd('debug'); if (!defined(dequantizedTypedArray)) { dequantizedTypedArray = new Float32Array(quantizedTypedArray.length); } const c = new Cartesian3(); for (let i = 0; i < elementCount; i++) { Cartesian3.unpack(quantizedTypedArray, i * 3, c); Cartesian3.multiplyComponents(c, stepSize, c); Cartesian3.add(c, offset, c); Cartesian3.pack(c, dequantizedTypedArray, i * 3); } return dequantizedTypedArray; } /** * Dequantize the given quantized array, based on the given quantization * information, and write the result into the given output array, creating * the output array if it was undefined. * * This will simply fill the output array with * <code>output[i] = input[i] * stepSize + offset</code> * when interpreting the input and output as arrays of Cartesian4. * * @param {TypedArray} quantizedTypedArray The quantized array * @param {number} elementCount The number of elements * @param {Cartesian4} stepSize The quantization step size * @param {Cartesian4} offset The quantization offset * @param {TypedArray} [dequantizedTypedArray] The result * @returns {TypedArray} The result */ static dequantize4D( quantizedTypedArray, elementCount, stepSize, offset, dequantizedTypedArray, ) { //>>includeStart('debug', pragmas.debug); Check.defined("quantizedTypedArray", quantizedTypedArray); Check.typeOf.number.greaterThanOrEquals("elementCount", elementCount, 0); Check.defined("stepSize", stepSize); Check.defined("offset", offset); //>>includeEnd('debug'); if (!defined(dequantizedTypedArray)) { dequantizedTypedArray = new Float32Array(quantizedTypedArray.length); } const c = new Cartesian4(); for (let i = 0; i < elementCount; i++) { Cartesian4.unpack(quantizedTypedArray, i * 4, c); Cartesian4.multiplyComponents(c, stepSize, c); Cartesian4.add(c, offset, c); Cartesian4.pack(c, dequantizedTypedArray, i * 4); } return dequantizedTypedArray; } /** * Reads and returns a value with the given type * at the given byte offset from the data view, in little-endian * order * @callback ComponentsReaderCallback * @param {DataView} dataView Typed data view into a binary buffer * @param {number} byteOffset The offset, in bytes, from the start of the view to read the data from * @param {number} numberOfComponents The number of components to read * @param {number[]} result The array in which to read the result */ /** * Creates a function that reads the specified number of components with * the given type from the given data view, in little-endian * order, and writes them into a given result array. * * @param {ComponentDatatype} componentType The component type * @returns {ComponentsReaderCallback} The reader */ static createComponentsReader(componentType) { const componentReader = ModelReader.createComponentReader(componentType); const sizeInBytes = ComponentDatatype.getSizeInBytes(componentType); return function (dataView, byteOffset, numberOfComponents, result) { let offset = byteOffset; for (let i = 0; i < numberOfComponents; ++i) { result[i] = componentReader(dataView, offset); offset += sizeInBytes; } }; } /** * Reads and returns a value with the given type * at the given byte offset from the data view, in little-endian * order * @callback ComponentReaderCallback * @param {DataView} dataView Typed data view into a binary buffer * @param {number} byteOffset The offset, in bytes, from the start of the view to read the data from * @returns {number|BigInt} The value read from the dataView */ /** * Creates a function that reads and returns a value with the given type * at the given byte offset from the data view, in little-endian * order * @param {ComponentDatatype} componentType The component type * @returns {ComponentReaderCallback} The reader */ static createComponentReader(componentType) { switch (componentType) { case ComponentDatatype.BYTE: return function (dataView, byteOffset) { return dataView.getInt8(byteOffset); }; case ComponentDatatype.UNSIGNED_BYTE: return function (dataView, byteOffset) { return dataView.getUint8(byteOffset); }; case ComponentDatatype.SHORT: return function (dataView, byteOffset) { return dataView.getInt16(byteOffset, true); }; case ComponentDatatype.UNSIGNED_SHORT: return function (dataView, byteOffset) { return dataView.getUint16(byteOffset, true); }; case ComponentDatatype.INT: return function (dataView, byteOffset) { return dataView.getInt32(byteOffset, true); }; case ComponentDatatype.UNSIGNED_INT: return function (dataView, byteOffset) { return dataView.getUint32(byteOffset, true); }; case ComponentDatatype.FLOAT: return function (dataView, byteOffset) { return dataView.getFloat32(byteOffset, true); }; case ComponentDatatype.DOUBLE: return function (dataView, byteOffset) { return dataView.getFloat64(byteOffset, true); }; } throw new DeveloperError( `The componentType must be a valid ComponentDatatype, but is ${componentType}`, ); } /** * Transform the elements of the given array with the given 4x4 matrix, * interpreting each 3 consecutive elements as a 3D point, and write * the result into the given result array, creating the result array * if it was undefined. * * @param {TypedArray} input The input array * @param {Matrix4} matrix The matrix * @param {TypedArray} [result] The result * @returns {TypedArray} The result */ static transform3D(input, matrix, result) { //>>includeStart('debug', pragmas.debug); Check.defined("input", input); Check.defined("matrix", matrix); //>>includeEnd('debug'); if (!defined(result)) { result = new Float32Array(input.length); } const c = new Cartesian3(); const elementCount = input.length / 3; for (let i = 0; i < elementCount; i++) { Cartesian3.unpack(input, i * 3, c); Matrix4.multiplyByPoint(matrix, c, c); Cartesian3.pack(c, result, i * 3); } return result; } /** * Reads feature IDs from an implicit range feature ID set into a typed array. * * Generates values using <code>offset + Math.floor(i / repeat)</code> for * each vertex. If <code>repeat</code> is undefined, all values are set to * <code>offset</code>. * * @param {FeatureIdImplicitRange} featureIdSet The implicit range feature ID set. * @param {object} attributeOwner An object with an <code>attributes</code> array * (a primitive or an instances object) * @returns {Float32Array} The generated feature ID values. */ static readImplicitRangeAsTypedArray(featureIdSet, attributeOwner) { const count = attributeOwner.attributes[0]?.count ?? 0; const offset = featureIdSet.offset; const repeat = featureIdSet.repeat; const typedArray = new Float32Array(count); if (defined(repeat)) { for (let i = 0; i < count; i++) { typedArray[i] = offset + Math.floor(i / repeat); } } else { typedArray.fill(offset); } return typedArray; } /** * Read the indices values from the given primitive indices, and * return them as a typed array. * * If the given object already has a <code>typedArray/code> property, then it * is assumed that this contains the proper indices, and they are returned. * * Otherwise, this reads the data from the <code>buffer</code> of the given * primitive indices object, into a typed array with a type that matches the * <code>indexDataType</code>, and returns it. * * Clients may not modify the returned typed array. * * @param {ModelComponents.Indices} primitiveIndices The primitive indices * @returns {TypedArray} The indices values * @throws {DeveloperError} If the <code>indexDataType</code> of the given * object is neither <code>UNSIGNED_BYTE</code>, nor <code>UNSIGNED_SHORT</code>, * nor <code>UNSIGNED_INT</code> */ static readIndicesAsTypedArray(primitiveIndices) { const existingIndices = primitiveIndices.typedArray; if (defined(existingIndices)) { return existingIndices; } const indicesBuffer = primitiveIndices.buffer; const indicesCount = primitiveIndices.count; const indexDatatype = primitiveIndices.indexDatatype; const indices = ModelReader.createIndexTypedArray( indexDatatype, indicesCount, ); indicesBuffer.getBufferData(indices); return indices; } /** * Read the indices values from the given primitive indices object, and return * them as a typed array of triangle vertex indices. * * If the given primitive type is <code>TRIANGLES</code>, then the indices * values will be read from the given object, and returned. * * If the primitive type is <code>TRIANGLE_STRIP</code> or <code>TRIANGLE_FAN</code>, * then the original indices values, will be read, converted into triangle indices * (i.e. their equivalent <code>TRIANGLES</code> representation), and the result * will be returned. * * The type of the returned array will match the <code>indexDataType</code> * of the given object. * * Clients may not modify the returned typed array. * * @param {ModelComponents.Indices} primitiveIndices The primitive indices * @returns {TypedArray} The indices, converted to triangle indices if necessary * @throws {DeveloperError} If the <code>indexDataType</code> of the given * object is neither <code>UNSIGNED_BYTE</code>, nor <code>UNSIGNED_SHORT</code>, * nor <code>UNSIGNED_INT</code>, or the given <code>primitiveType</code> * is neither <code>TRIANGLES</code>, nor <code>TRIANGLE_STRIP</code>, * nor <code>TRIANGLE_FAN</code> */ static readIndicesAsTriangleIndicesTypedArray( primitiveIndices, primitiveType, ) { const originalIndices = ModelReader.readIndicesAsTypedArray(primitiveIndices); if (primitiveType === PrimitiveType.TRIANGLES) { return originalIndices; } if (primitiveType === PrimitiveType.TRIANGLE_STRIP) { const triangleIndices = ModelReader.convertTriangleStripToTriangleIndices(originalIndices); return triangleIndices; } if (primitiveType === PrimitiveType.TRIANGLE_FAN) { const triangleIndices = ModelReader.convertTriangleFanToTriangleIndices(originalIndices); return triangleIndices; } throw new DeveloperError( `The primitiveType must be TRIANGLES (${PrimitiveType.TRIANGLES}, ` + `TRIANGLE_STRIP (${PrimitiveType.TRIANGLE_STRIP}, or ` + `TRIANGLE_FAN (${PrimitiveType.TRIANGLE_FAN}, but is ${primitiveType}`, ); } /** * Converts the given indices from a <code>TRIANGLE_STRIP</code> representation * into a <code>TRIANGLES</code> representation, and returns the result. * * The type of the result will be the same as the type of the input array. * * @param {TypedArray} indices The input indices * @returns {TypedArray} The resulting triangle indices */ static convertTriangleStripToTriangleIndices(indices) { const triangleIndices = indices.constructor((indices.length - 2) * 3); for (let i = 0; i < indices.length - 2; i++) { if (i % 2 === 1) { triangleIndices[i * 3 + 0] = indices[i + 0]; triangleIndices[i * 3 + 1] = indices[i + 2]; triangleIndices[i * 3 + 2] = indices[i + 1]; } else { triangleIndices[i * 3 + 0] = indices[i + 0]; triangleIndices[i * 3 + 1] = indices[i + 1]; triangleIndices[i * 3 + 2] = indices[i + 2]; } } return triangleIndices; } /** * Converts the given indices from a <code>TRIANGLE_FAN</code> representation * into a <code>TRIANGLES</code> representation, and returns the result. * * The type of the result will be the same as the type of the input array. * * @param {TypedArray} indices The input indices * @returns {TypedArray} The resulting triangle indices */ static convertTriangleFanToTriangleIndices(indices) { const triangleIndices = indices.constructor((indices.length - 2) * 3); for (let i = 0; i < indices.length - 2; i++) { triangleIndices[i * 3 + 0] = indices[i + 0]; triangleIndices[i * 3 + 1] = indices[i + 1]; triangleIndices[i * 3 + 2] = indices[i + 2]; } return triangleIndices; } /** * Create a typed array with a type that matches the given index data type, * and the given size. * * @param {number} indexDatatype The <code>IndexDataType</code> * @param {number} size The size of the array that will be created * @returns {TypedArray} The typed array * @throws {DeveloperError} If the <code>indexDataType</code> is neither * <code>UNSIGNED_BYTE</code>, nor <code>UNSIGNED_SHORT</code>, * nor <code>UNSIGNED_INT</code>, or the size is negative. */ static createIndexTypedArray(indexDatatype, size) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number.greaterThanOrEquals("size", size, 0); //>>includeEnd('debug'); switch (indexDatatype) { case IndexDatatype.UNSIGNED_BYTE: return new Uint8Array(size); case IndexDatatype.UNSIGNED_SHORT: return new Uint16Array(size); case IndexDatatype.UNSIGNED_INT: return new Uint32Array(size); } throw new DeveloperError( `The indexDatatype must be UNSIGNED_BYTE (${IndexDatatype.UNSIGNED_BYTE}, ` + `UNSIGNED_SHORT (${IndexDatatype.UNSIGNED_SHORT}, or ` + `UNSIGNED_INT (${IndexDatatype.UNSIGNED_INT}, but is ${indexDatatype}`, ); } /** * Per-instance data combining a transform matrix with an optional feature ID. * * @typedef {object} ModelReader.Instance * @property {Matrix4} transform The instance transform matrix. * @property {number} [featureId] The per-instance feature ID, or undefined. * * @private */ /** * A callback invoked by {@link ModelReader.forEachPrimitive} for each * runtime primitive in the model. * * @callback ModelReader.ForEachPrimitiveCallback * @param {object} runtimePrimitive The runtime primitive wrapper. * @param {object} primitive The underlying model primitive (runtimePrimitive.primitive). * @param {ModelReader.Instance[]} instances Per-instance data (transforms and optional feature IDs). * @param {Matrix4} computedModelMatrix The computed model matrix for the node. * * @private */ /** * Iterates over every primitive in a model's scene graph, computing * node transforms and instance transforms once per node and invoking * a callback for each runtime primitive. * <p> * When a map projection is provided, the computed model matrix is * projected to 2D via {@link Transforms.basisTo2D}. * </p> * * @param {Model} model The model whose scene graph to traverse. * @param {object} [options] Object with the following properties: * @param {MapProjection} [options.mapProjection] The map projection for 2D mode. When defined, the computed model matrix is projected to 2D. * @param {string} [options.instanceFeatureIdLabel] The label used to select which instance feature ID set to read. When defined, per-instance feature IDs are fetched. When undefined, feature IDs are not fetched. * @param {ModelReader.ForEachPrimitiveCallback} callback The function invoked for each primitive. */ static forEachPrimitive(model, options, callback) { const mapProjection = options?.mapProjection; const instanceFeatureIdLabel = options?.instanceFeatureIdLabel; const sceneGraph = model.sceneGraph; if (!defined(sceneGraph)) { return; } const scratchNodeTransforms = { nodeComputedTransform: new Matrix4(), modelMatrix: new Matrix4(), computedModelMatrix: new Matrix4(), }; const nodes = sceneGraph._runtimeNodes; const nodesLength = nodes.length; for (let n = 0; n < nodesLength; n++) { const runtimeNode = nodes[n]; const nodeTransforms = ModelReader.computeNodeTransforms( runtimeNode, sceneGraph, model, scratchNodeTransforms, ); let computedModelMatrix = nodeTransforms.computedModelMatrix; if (defined(mapProjection)) { computedModelMatrix = Transforms.basisTo2D( mapProjection, computedModelMatrix, computedModelMatrix, ); } const instanceTransforms = ModelReader.computeInstanceTransforms( runtimeNode, computedModelMatrix, nodeTransforms.nodeComputedTransform, nodeTransforms.modelMatrix, ); const instanceFeatureIds = defined(instanceFeatureIdLabel) ? ModelReader.computeInstanceFeatureIds( runtimeNode, instanceFeatureIdLabel, ) : undefined; const instances = []; for (let i = 0; i < instanceTransforms.length; i++) { instances.push({ transform: instanceTransforms[i], featureId: defined(instanceFeatureIds) ? instanceFeatureIds[i] : undefined, }); } const primitivesLength = runtimeNode.runtimePrimitives.length; for (let p = 0; p < primitivesLength; p++) { const runtimePrimitive = runtimeNode.runtimePrimitives[p]; callback( runtimePrimitive, runtimePrimitive.primitive, instances, computedModelMatrix, ); } } } /** * Computes the model matrix for a runtime node, accounting for instancing * and world-space transforms. * * @param {object} runtimeNode The runtime node. * @param {ModelSceneGraph} sceneGraph The model scene graph. * @param {Model} model The model. * @param {object} result An object with scratch matrices: { nodeComputedTransform: Matrix4, modelMatrix: Matrix4, computedModelMatrix: Matrix4 }. * @returns {object} The result parameter, populated with the computed transforms. * */ static computeNodeTransforms(runtimeNode, sceneGraph, model, result) { const node = runtimeNode.node; let nodeComputedTransform = Matrix4.clone( runtimeNode.computedTransform, result.nodeComputedTransform, ); let modelMatrix = Matrix4.clone( sceneGraph.computedModelMatrix, result.modelMatrix, ); const instances = node.instances; if (defined(instances)) { if (instances.transformInWorldSpace) { // Replicate the multiplication order in LegacyInstancingStageVS. modelMatrix = Matrix4.multiplyTransformation( model.modelMatrix, sceneGraph.components.transform, modelMatrix, ); nodeComputedTransform = Matrix4.multiplyTransformation( sceneGraph.axisCorrectionMatrix, runtimeNode.computedTransform, nodeComputedTransform, ); } } const computedModelMatrix = Matrix4.multiplyTransformation( modelMatrix, nodeComputedTransform, result.computedModelMatrix, ); result.computedModelMatrix = computedModelMatrix; result.nodeComputedTransform = nodeComputedTransform; result.modelMatrix = modelMatrix; return result; } /** * Builds an array of instance transforms for a node. * If the node is not instanced, returns an array containing only the * computedModelMatrix. * * @param {object} runtimeNode The runtime node. * @param {Matrix4} computedModelMatrix The computed model matrix. * @param {Matrix4} nodeComputedTransform The node computed transform. * @param {Matrix4} modelMatrix The model matrix. * @returns {Matrix4[]} */ static computeInstanceTransforms( runtimeNode, computedModelMatrix, nodeComputedTransform, modelMatrix, ) { const transforms = []; const node = runtimeNode.node; const instances = node.instances; if (defined(instances)) { const transformsCount = instances.attributes[0].count; const instanceComponentDatatype = instances.attributes[0].componentDatatype; const transformElements = 12; let transformsTypedArray = runtimeNode.transformsTypedArray; if (!defined(transformsTypedArray)) { const instanceTransformsBuffer = runtimeNode.instancingTransformsBuffer; if (defined(instanceTransformsBuffer)) { transformsTypedArray = ComponentDatatype.createTypedArray( instanceComponentDatatype, transformsCount * transformElements, ); instanceTransformsBuffer.getBufferData(transformsTypedArray); } } if (defined(transformsTypedArray)) { ModelReader.computeInstanceTransformsFromTypedArray( transformsTypedArray, transformsCount, transforms, ); } else { ModelReader.computeInstanceTransformsFromAttributes( instances, transformsCount, transforms, ); } for (let i = 0; i < transforms.length; i++) { const transform = transforms[i]; if (instances.transformInWorldSpace) { Matrix4.multiplyTransformation( transform, nodeComputedTransform, transform, ); Matrix4.multiplyTransformation(modelMatrix, transform, transform); } else { Matrix4.multiplyTransformation( computedModelMatrix, transform, transform, ); } } } if (transforms.length === 0) { transforms.push(computedModelMatrix); } return transforms; } /** * Builds an array of per-instance feature IDs for a node. * If the node is not instanced or has no matching feature ID set, * returns <code>undefined</code>. * * @param {object} runtimeNode The runtime node. * @param {string} instanceFeatureIdLabel The label used to select the feature ID set. * @returns {number[]|undefined} The per-instance feature IDs, or undefined. */ static computeInstanceFeatureIds(runtimeNode, instanceFeatureIdLabel) { const node = runtimeNode.node; const instances = node.instances; if (!defined(instances)) { return undefined; } const featureIdSet = ModelUtility.getFeatureIdsByLabel( instances.featureIds, instanceFeatureIdLabel, ); if (!defined(featureIdSet)) { return undefined; } let typedArray; // Case: FeatureIdAttribute if ("setIndex" in featureIdSet) { const attribute = ModelUtility.getAttributeBySemantic( instances, VertexAttributeSemantic.FEATURE_ID, featureIdSet.setIndex, ); if (defined(attribute)) { typedArray = ModelReader.readAttributeAsTypedArray(attribute); } } // Case: FeatureIdImplicitRange if ("offset" in featureIdSet) { typedArray = ModelReader.readImplicitRangeAsTypedArray( featureIdSet, instances, ); } if (!defined(typedArray)) { return undefined; } const featureIds = new Array(typedArray.length); for (let i = 0; i < typedArray.length; i++) { featureIds[i] = typedArray[i]; } return featureIds; } /** * Builds instance transforms from a packed typed array where each instance * is stored as 12 floats (3 rows of 4 columns, row-major). * * @param {TypedArray} transformsTypedArray The packed transforms array. * @param {number} count The number of instances. * @param {Matrix4[]} transforms The output array to push transforms into. */ static computeInstanceTransformsFromTypedArray( transformsTypedArray, count, transforms, ) { const transformElements = 12; for (let i = 0; i < count; i++) { const index = i * transformElements; const transform = new Matrix4( transformsTypedArray[index], transformsTypedArray[index + 1], transformsTypedArray[index + 2], transformsTypedArray[index + 3], transformsTypedArray[index + 4], transformsTypedArray[index + 5], transformsTypedArray[index + 6], transformsTypedArray[index + 7], transformsTypedArray[index + 8], transformsTypedArray[index + 9], transformsTypedArray[index + 10], transformsTypedArray[index + 11], 0, 0, 0, 1, ); transforms.push(transform); } } /** * Builds instance transforms from individual TRANSLATION, ROTATION, and SCALE * attributes when no packed transformsTypedArray is available. * * @param {object} instances The instances object. * @param {number} count The number of instances. * @param {Matrix4[]} transforms The output array to push transforms into. */ static computeInstanceTransformsFromAttributes(instances, count, transforms) { const translationAttribute = ModelUtility.getAttributeBySemantic( instances, InstanceAttributeSemantic.TRANSLATION, ); const rotationAttribute = ModelUtility.getAttributeBySemantic( instances, InstanceAttributeSemantic.ROTATION, ); const scaleAttribute = ModelUtility.getAttributeBySemantic( instances, InstanceAttributeSemantic.SCALE, ); const hasTranslation = defined(translationAttribute); const hasRotation = defined(rotationAttribute); const hasScale = defined(scaleAttribute); if (!hasTranslation && !hasRotation && !hasScale) { return; } let translationTypedArray, rotationTypedArray, scaleTypedArray; if (hasTranslation) { translationTypedArray = ModelReader.readAttributeAsRawCompactTypedArray(translationAttribute); } if (hasRotation) { rotationTypedArray = ModelReader.readAttributeAsRawCompactTypedArray(rotationAttribute); } if (hasScale) { scaleTypedArray = ModelReader.readAttributeAsRawCompactTypedArray(scaleAttribute); } for (let i = 0; i < count; i++) { const translation = hasTranslation ? new Cartesian3( translationTypedArray[i * 3], translationTypedArray[i * 3 + 1], translationTypedArray[i * 3 + 2], ) : Cartesian3.ZERO; const rotation = hasRotation ? new Quaternion( rotationTypedArray[i * 4], rotationTypedArray[i * 4 + 1], rotationTypedArray[i * 4 + 2], rotationTypedArray[i * 4 + 3], ) : Quaternion.IDENTITY; const scale = hasScale ? new Cartesian3( scaleTypedArray[i * 3], scaleTypedArray[i * 3 + 1], scaleTypedArray[i * 3 + 2], ) : Cartesian3.ONE; const transform = Matrix4.fromTranslationQuaternionRotationScale( translation, rotation, scale, new Matrix4(), ); transforms.push(transform); } } } export default ModelReader;