@gltf-transform/core
Version:
glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.
556 lines (494 loc) • 18.5 kB
text/typescript
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.');
}
}