UNPKG

@gltf-transform/core

Version:

glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.

556 lines (494 loc) 18.5 kB
import { type Nullable, PropertyType, type TypedArray } from '../constants.js'; import type { GLTF } from '../types/gltf.js'; import { MathUtils } from '../utils/index.js'; import type { Buffer } from './buffer.js'; import { ExtensibleProperty, type IExtensibleProperty } from './extensible-property.js'; interface IAccessor extends IExtensibleProperty { array: TypedArray | null; type: GLTF.AccessorType; componentType: GLTF.AccessorComponentType; normalized: boolean; sparse: boolean; buffer: Buffer; } /** * *Accessors store lists of numeric, vector, or matrix elements in a typed array.* * * All large data for {@link Mesh}, {@link Skin}, and {@link Animation} properties is stored in * {@link Accessor}s, organized into one or more {@link Buffer}s. Each accessor provides data in * typed arrays, with two abstractions: * * *Elements* are the logical divisions of the data into useful types: `"SCALAR"`, `"VEC2"`, * `"VEC3"`, `"VEC4"`, `"MAT3"`, or `"MAT4"`. The element type can be determined with the * {@link Accessor.getType getType}() method, and the number of elements in the accessor determine its * {@link Accessor.getCount getCount}(). The number of components in an element — e.g. 9 for `"MAT3"` — are its * {@link Accessor.getElementSize getElementSize}(). See {@link Accessor.Type}. * * *Components* are the numeric values within an element — e.g. `.x` and `.y` for `"VEC2"`. Various * component types are available: `BYTE`, `UNSIGNED_BYTE`, `SHORT`, `UNSIGNED_SHORT`, * `UNSIGNED_INT`, and `FLOAT`. The component type can be determined with the * {@link Accessor.getComponentType getComponentType} method, and the number of bytes in each component determine its * {@link Accessor.getComponentSize getComponentSize}. See {@link Accessor.ComponentType}. * * Usage: * * ```typescript * const accessor = doc.createAccessor('myData') * .setArray(new Float32Array([1,2,3,4,5,6,7,8,9,10,11,12])) * .setType(Accessor.Type.VEC3) * .setBuffer(doc.getRoot().listBuffers()[0]); * * accessor.getCount(); // → 4 * accessor.getElementSize(); // → 3 * accessor.getByteLength(); // → 48 * accessor.getElement(1, []); // → [4, 5, 6] * * accessor.setElement(0, [10, 20, 30]); * ``` * * Data access through the {@link Accessor.getElement getElement} and {@link Accessor.setElement setElement} * methods reads or overwrites the content of the underlying typed array. These methods use * element arrays intended to be compatible with the [gl-matrix](https://github.com/toji/gl-matrix) * library, or with the `toArray`/`fromArray` methods of libraries like three.js and babylon.js. * * Each Accessor must be assigned to a {@link Buffer}, which determines where the accessor's data * is stored in the final file. Assigning Accessors to different Buffers allows the data to be * written to different `.bin` files. * * glTF Transform does not expose many details of sparse, normalized, or interleaved accessors * through its API. It reads files using those techniques, presents a simplified view of the data * for editing, and attempts to write data back out with optimizations. For example, vertex * attributes will typically be interleaved by default, regardless of the input file. * * References: * - [glTF → Accessors](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#accessors) * * @category Properties */ export class Accessor extends ExtensibleProperty<IAccessor> { public declare propertyType: PropertyType.ACCESSOR; /********************************************************************************************** * Constants. */ /** Element type contained by the accessor (SCALAR, VEC2, ...). */ public static Type: Record<string, GLTF.AccessorType> = { /** Scalar, having 1 value per element. */ SCALAR: 'SCALAR', /** 2-component vector, having 2 components per element. */ VEC2: 'VEC2', /** 3-component vector, having 3 components per element. */ VEC3: 'VEC3', /** 4-component vector, having 4 components per element. */ VEC4: 'VEC4', /** 2x2 matrix, having 4 components per element. */ MAT2: 'MAT2', /** 3x3 matrix, having 9 components per element. */ MAT3: 'MAT3', /** 4x3 matrix, having 16 components per element. */ MAT4: 'MAT4', }; /** Data type of the values composing each element in the accessor. */ public static ComponentType: Record<string, GLTF.AccessorComponentType> = { /** * 1-byte signed integer, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int8Array Int8Array}. */ BYTE: 5120, /** * 1-byte unsigned integer, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array Uint8Array}. */ UNSIGNED_BYTE: 5121, /** * 2-byte signed integer, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int16Array Int16Array}. */ SHORT: 5122, /** * 2-byte unsigned integer, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint16Array Uint16Array}. */ UNSIGNED_SHORT: 5123, /** * 4-byte unsigned integer, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint32Array Uint32Array}. */ UNSIGNED_INT: 5125, /** * 4-byte floating point number, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array Float32Array}. */ FLOAT: 5126, }; /********************************************************************************************** * Instance. */ protected init(): void { this.propertyType = PropertyType.ACCESSOR; } protected getDefaults(): Nullable<IAccessor> { return Object.assign(super.getDefaults() as IExtensibleProperty, { array: null, type: Accessor.Type.SCALAR, componentType: Accessor.ComponentType.FLOAT, normalized: false, sparse: false, buffer: null, }); } /********************************************************************************************** * Static. */ /** Returns size of a given element type, in components. */ public static getElementSize(type: GLTF.AccessorType): number { switch (type) { case Accessor.Type.SCALAR: return 1; case Accessor.Type.VEC2: return 2; case Accessor.Type.VEC3: return 3; case Accessor.Type.VEC4: return 4; case Accessor.Type.MAT2: return 4; case Accessor.Type.MAT3: return 9; case Accessor.Type.MAT4: return 16; default: throw new Error('Unexpected type: ' + type); } } /** Returns size of a given component type, in bytes. */ public static getComponentSize(componentType: GLTF.AccessorComponentType): number { switch (componentType) { case Accessor.ComponentType.BYTE: return 1; case Accessor.ComponentType.UNSIGNED_BYTE: return 1; case Accessor.ComponentType.SHORT: return 2; case Accessor.ComponentType.UNSIGNED_SHORT: return 2; case Accessor.ComponentType.UNSIGNED_INT: return 4; case Accessor.ComponentType.FLOAT: return 4; default: throw new Error('Unexpected component type: ' + componentType); } } /********************************************************************************************** * Min/max bounds. */ /** * Minimum value of each component in this attribute. Unlike in a final glTF file, values * returned by this method will reflect the minimum accounting for {@link .normalized} * state. */ public getMinNormalized(target: number[]): number[] { const normalized = this.getNormalized(); const elementSize = this.getElementSize(); const componentType = this.getComponentType(); this.getMin(target); if (normalized) { for (let j = 0; j < elementSize; j++) { target[j] = MathUtils.decodeNormalizedInt(target[j], componentType); } } return target; } /** * Minimum value of each component in this attribute. Values returned by this method do not * reflect normalization: use {@link .getMinNormalized} in that case. */ public getMin(target: number[]): number[] { const array = this.getArray()!; const count = this.getCount(); const elementSize = this.getElementSize(); for (let j = 0; j < elementSize; j++) target[j] = Infinity; for (let i = 0; i < count * elementSize; i += elementSize) { for (let j = 0; j < elementSize; j++) { const value = array[i + j]; if (Number.isFinite(value)) { target[j] = Math.min(target[j], value); } } } return target; } /** * Maximum value of each component in this attribute. Unlike in a final glTF file, values * returned by this method will reflect the minimum accounting for {@link .normalized} * state. */ public getMaxNormalized(target: number[]): number[] { const normalized = this.getNormalized(); const elementSize = this.getElementSize(); const componentType = this.getComponentType(); this.getMax(target); if (normalized) { for (let j = 0; j < elementSize; j++) { target[j] = MathUtils.decodeNormalizedInt(target[j], componentType); } } return target; } /** * Maximum value of each component in this attribute. Values returned by this method do not * reflect normalization: use {@link .getMinNormalized} in that case. */ public getMax(target: number[]): number[] { const array = this.get('array'); const count = this.getCount(); const elementSize = this.getElementSize(); for (let j = 0; j < elementSize; j++) target[j] = -Infinity; for (let i = 0; i < count * elementSize; i += elementSize) { for (let j = 0; j < elementSize; j++) { const value = array![i + j]; if (Number.isFinite(value)) { target[j] = Math.max(target[j], value); } } } return target; } /********************************************************************************************** * Layout. */ /** * Number of elements in the accessor. An array of length 30, containing 10 `VEC3` elements, * will have a count of 10. */ public getCount(): number { const array = this.get('array'); return array ? array.length / this.getElementSize() : 0; } /** Type of element stored in the accessor. `VEC2`, `VEC3`, etc. */ public getType(): GLTF.AccessorType { return this.get('type'); } /** * Sets type of element stored in the accessor. `VEC2`, `VEC3`, etc. Array length must be a * multiple of the component size (`VEC2` = 2, `VEC3` = 3, ...) for the selected type. */ public setType(type: GLTF.AccessorType): Accessor { return this.set('type', type); } /** * Number of components in each element of the accessor. For example, the element size of a * `VEC2` accessor is 2. This value is determined automatically based on array length and * accessor type, specified with {@link Accessor.setType setType()}. */ // biome-ignore lint/suspicious/useAdjacentOverloadSignatures: Static vs. non-static. public getElementSize(): number { return Accessor.getElementSize(this.get('type')); } /** * Size of each component (a value in the raw array), in bytes. For example, the * `componentSize` of data backed by a `float32` array is 4 bytes. */ public getComponentSize(): number { return this.get('array')!.BYTES_PER_ELEMENT; } /** * Component type (float32, uint16, etc.). This value is determined automatically, and can only * be modified by replacing the underlying array. */ public getComponentType(): GLTF.AccessorComponentType { return this.get('componentType'); } /********************************************************************************************** * Normalization. */ /** * Specifies whether integer data values should be normalized (true) to [0, 1] (for unsigned * types) or [-1, 1] (for signed types), or converted directly (false) when they are accessed. * This property is defined only for accessors that contain vertex attributes or animation * output data. */ public getNormalized(): boolean { return this.get('normalized'); } /** * Specifies whether integer data values should be normalized (true) to [0, 1] (for unsigned * types) or [-1, 1] (for signed types), or converted directly (false) when they are accessed. * This property is defined only for accessors that contain vertex attributes or animation * output data. */ public setNormalized(normalized: boolean): this { return this.set('normalized', normalized); } /********************************************************************************************** * Data access. */ /** * Returns the scalar element value at the given index. For * {@link Accessor.getNormalized normalized} integer accessors, values are * decoded and returned in floating-point form. */ public getScalar(index: number): number { const elementSize = this.getElementSize(); const componentType = this.getComponentType(); const array = this.getArray()!; if (this.getNormalized()) { return MathUtils.decodeNormalizedInt(array[index * elementSize], componentType); } return array[index * elementSize]; } /** * Assigns the scalar element value at the given index. For * {@link Accessor.getNormalized normalized} integer accessors, "value" should be * given in floating-point form — it will be integer-encoded before writing * to the underlying array. */ public setScalar(index: number, x: number): this { const elementSize = this.getElementSize(); const componentType = this.getComponentType(); const array = this.getArray()!; if (this.getNormalized()) { array[index * elementSize] = MathUtils.encodeNormalizedInt(x, componentType); } else { array[index * elementSize] = x; } return this; } /** * Returns the vector or matrix element value at the given index. For * {@link Accessor.getNormalized normalized} integer accessors, values are * decoded and returned in floating-point form. * * Example: * * ```javascript * import { add } from 'gl-matrix/add'; * * const element = []; * const offset = [1, 1, 1]; * * for (let i = 0; i < accessor.getCount(); i++) { * accessor.getElement(i, element); * add(element, element, offset); * accessor.setElement(i, element); * } * ``` */ public getElement<T extends number[]>(index: number, target: T): T { const normalized = this.getNormalized(); const elementSize = this.getElementSize(); const componentType = this.getComponentType(); const array = this.getArray()!; for (let i = 0; i < elementSize; i++) { if (normalized) { target[i] = MathUtils.decodeNormalizedInt(array[index * elementSize + i], componentType); } else { target[i] = array[index * elementSize + i]; } } return target; } /** * Assigns the vector or matrix element value at the given index. For * {@link Accessor.getNormalized normalized} integer accessors, "value" should be * given in floating-point form — it will be integer-encoded before writing * to the underlying array. * * Example: * * ```javascript * import { add } from 'gl-matrix/add'; * * const element = []; * const offset = [1, 1, 1]; * * for (let i = 0; i < accessor.getCount(); i++) { * accessor.getElement(i, element); * add(element, element, offset); * accessor.setElement(i, element); * } * ``` */ public setElement(index: number, value: number[]): this { const normalized = this.getNormalized(); const elementSize = this.getElementSize(); const componentType = this.getComponentType(); const array = this.getArray()!; for (let i = 0; i < elementSize; i++) { if (normalized) { array[index * elementSize + i] = MathUtils.encodeNormalizedInt(value[i], componentType); } else { array[index * elementSize + i] = value[i]; } } return this; } /********************************************************************************************** * Raw data storage. */ /** * Specifies whether the accessor should be stored sparsely. When written to a glTF file, sparse * accessors store only values that differ from base values. When loaded in glTF Transform (or most * runtimes) a sparse accessor can be treated like any other accessor. Currently, glTF Transform always * uses zeroes for the base values when writing files. * @experimental */ public getSparse(): boolean { return this.get('sparse'); } /** * Specifies whether the accessor should be stored sparsely. When written to a glTF file, sparse * accessors store only values that differ from base values. When loaded in glTF Transform (or most * runtimes) a sparse accessor can be treated like any other accessor. Currently, glTF Transform always * uses zeroes for the base values when writing files. * @experimental */ public setSparse(sparse: boolean): this { return this.set('sparse', sparse); } /** Returns the {@link Buffer} into which this accessor will be organized. */ public getBuffer(): Buffer | null { return this.getRef('buffer'); } /** Assigns the {@link Buffer} into which this accessor will be organized. */ public setBuffer(buffer: Buffer | null): this { return this.setRef('buffer', buffer); } /** Returns the raw typed array underlying this accessor. */ public getArray(): TypedArray | null { return this.get('array'); } /** Assigns the raw typed array underlying this accessor. */ public setArray(array: TypedArray | null): this { this.set('componentType', array ? arrayToComponentType(array) : Accessor.ComponentType.FLOAT); this.set('array', array); return this; } /** Returns the total bytelength of this accessor, exclusive of padding. */ public getByteLength(): number { const array = this.get('array'); return array ? array.byteLength : 0; } } /************************************************************************************************** * Accessor utilities. */ /** @internal */ function arrayToComponentType(array: TypedArray): GLTF.AccessorComponentType { switch (array.constructor) { case Float32Array: return Accessor.ComponentType.FLOAT; case Uint32Array: return Accessor.ComponentType.UNSIGNED_INT; case Uint16Array: return Accessor.ComponentType.UNSIGNED_SHORT; case Uint8Array: return Accessor.ComponentType.UNSIGNED_BYTE; case Int16Array: return Accessor.ComponentType.SHORT; case Int8Array: return Accessor.ComponentType.BYTE; default: throw new Error('Unknown accessor componentType.'); } }