@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
JavaScript
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;