@loaders.gl/gltf
Version:
Framework-independent loader for the glTF format
574 lines • 20.7 kB
JavaScript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { getBinaryImageMetadata } from '@loaders.gl/images';
import { padToNBytes, copyToArray } from '@loaders.gl/loader-utils';
import { assert } from "../utils/assert.js";
import { getAccessorTypeFromSize, getComponentTypeFromArray } from "../gltf-utils/gltf-utils.js";
import { getTypedArrayForAccessor as _getTypedArrayForAccessor } from "../gltf-utils/get-typed-array.js";
function makeDefaultGLTFJson() {
return {
asset: {
version: '2.0',
generator: 'loaders.gl'
},
buffers: [],
extensions: {},
extensionsRequired: [],
extensionsUsed: []
};
}
/**
* Class for structured access to GLTF data
*/
export class GLTFScenegraph {
// internal
gltf;
sourceBuffers;
byteLength;
// TODO - why is this not GLTFWithBuffers - what happens to images?
constructor(gltf) {
// Declare locally so
this.gltf = {
json: gltf?.json || makeDefaultGLTFJson(),
buffers: gltf?.buffers || [],
images: gltf?.images || []
};
this.sourceBuffers = [];
this.byteLength = 0;
// Initialize buffers
if (this.gltf.buffers && this.gltf.buffers[0]) {
this.byteLength = this.gltf.buffers[0].byteLength;
this.sourceBuffers = [this.gltf.buffers[0]];
}
}
// Accessors
get json() {
return this.gltf.json;
}
getApplicationData(key) {
// TODO - Data is already unpacked by GLBParser
const data = this.json[key];
return data;
}
getExtraData(key) {
// TODO - Data is already unpacked by GLBParser
const extras = (this.json.extras || {});
return extras[key];
}
hasExtension(extensionName) {
const isUsedExtension = this.getUsedExtensions().find((name) => name === extensionName);
const isRequiredExtension = this.getRequiredExtensions().find((name) => name === extensionName);
return typeof isUsedExtension === 'string' || typeof isRequiredExtension === 'string';
}
getExtension(extensionName) {
const isExtension = this.getUsedExtensions().find((name) => name === extensionName);
const extensions = this.json.extensions || {};
return isExtension ? extensions[extensionName] : null;
}
getRequiredExtension(extensionName) {
const isRequired = this.getRequiredExtensions().find((name) => name === extensionName);
return isRequired ? this.getExtension(extensionName) : null;
}
getRequiredExtensions() {
return this.json.extensionsRequired || [];
}
getUsedExtensions() {
return this.json.extensionsUsed || [];
}
getRemovedExtensions() {
return (this.json.extensionsRemoved || []);
}
getObjectExtension(object, extensionName) {
const extensions = object.extensions || {};
return extensions[extensionName];
}
getScene(index) {
return this.getObject('scenes', index);
}
getNode(index) {
return this.getObject('nodes', index);
}
getSkin(index) {
return this.getObject('skins', index);
}
getMesh(index) {
return this.getObject('meshes', index);
}
getMaterial(index) {
return this.getObject('materials', index);
}
getAccessor(index) {
return this.getObject('accessors', index);
}
// getCamera(index: number): object | null {
// return null; // TODO: fix thi: object as null;
// }
getTexture(index) {
return this.getObject('textures', index);
}
getSampler(index) {
return this.getObject('samplers', index);
}
getImage(index) {
return this.getObject('images', index);
}
getBufferView(index) {
return this.getObject('bufferViews', index);
}
getBuffer(index) {
return this.getObject('buffers', index);
}
getObject(array, index) {
// check if already resolved
if (typeof index === 'object') {
return index;
}
const object = this.json[array] && this.json[array][index];
if (!object) {
throw new Error(`glTF file error: Could not find ${array}[${index}]`); // eslint-disable-line
}
return object;
}
/**
* Accepts buffer view index or buffer view object
* @returns a `Uint8Array`
*/
getTypedArrayForBufferView(bufferView) {
bufferView = this.getBufferView(bufferView);
// @ts-ignore
const bufferIndex = bufferView.buffer;
// Get hold of the arrayBuffer
// const buffer = this.getBuffer(bufferIndex);
const binChunk = this.gltf.buffers[bufferIndex];
assert(binChunk);
// @ts-ignore
const byteOffset = (bufferView.byteOffset || 0) + binChunk.byteOffset;
// @ts-ignore
return new Uint8Array(binChunk.arrayBuffer, byteOffset, bufferView.byteLength);
}
/** Accepts accessor index or accessor object
* @returns a typed array with type that matches the types
*/
getTypedArrayForAccessor(accessor) {
// @ts-ignore
const gltfAccessor = this.getAccessor(accessor);
return _getTypedArrayForAccessor(this.gltf.json, this.gltf.buffers, gltfAccessor);
}
/** accepts accessor index or accessor object
* returns a `Uint8Array`
*/
getTypedArrayForImageData(image) {
// @ts-ignore
image = this.getAccessor(image);
// @ts-ignore
const bufferView = this.getBufferView(image.bufferView);
const buffer = this.getBuffer(bufferView.buffer);
// @ts-ignore
const arrayBuffer = buffer.data;
const byteOffset = bufferView.byteOffset || 0;
return new Uint8Array(arrayBuffer, byteOffset, bufferView.byteLength);
}
// MODIFERS
/**
* Add an extra application-defined key to the top-level data structure
*/
addApplicationData(key, data) {
this.json[key] = data;
return this;
}
/**
* `extras` - Standard GLTF field for storing application specific data
*/
addExtraData(key, data) {
this.json.extras = this.json.extras || {};
this.json.extras[key] = data;
return this;
}
addObjectExtension(object, extensionName, data) {
// @ts-ignore
object.extensions = object.extensions || {};
// TODO - clobber or merge?
// @ts-ignore
object.extensions[extensionName] = data;
this.registerUsedExtension(extensionName);
return this;
}
setObjectExtension(object, extensionName, data) {
const extensions = object.extensions || {};
extensions[extensionName] = data;
// TODO - add to usedExtensions...
}
removeObjectExtension(object, extensionName) {
const extensions = object?.extensions || {};
if (extensions[extensionName]) {
this.json.extensionsRemoved = this.json.extensionsRemoved || [];
const extensionsRemoved = this.json.extensionsRemoved;
if (!extensionsRemoved.includes(extensionName)) {
extensionsRemoved.push(extensionName);
}
}
delete extensions[extensionName];
}
/**
* Add to standard GLTF top level extension object, mark as used
*/
addExtension(extensionName, extensionData = {}) {
assert(extensionData);
this.json.extensions = this.json.extensions || {};
this.json.extensions[extensionName] = extensionData;
this.registerUsedExtension(extensionName);
return extensionData;
}
/**
* Standard GLTF top level extension object, mark as used and required
*/
addRequiredExtension(extensionName, extensionData = {}) {
assert(extensionData);
this.addExtension(extensionName, extensionData);
this.registerRequiredExtension(extensionName);
return extensionData;
}
/**
* Add extensionName to list of used extensions
*/
registerUsedExtension(extensionName) {
this.json.extensionsUsed = this.json.extensionsUsed || [];
if (!this.json.extensionsUsed.find((ext) => ext === extensionName)) {
this.json.extensionsUsed.push(extensionName);
}
}
/**
* Add extensionName to list of required extensions
*/
registerRequiredExtension(extensionName) {
this.registerUsedExtension(extensionName);
this.json.extensionsRequired = this.json.extensionsRequired || [];
if (!this.json.extensionsRequired.find((ext) => ext === extensionName)) {
this.json.extensionsRequired.push(extensionName);
}
}
/**
* Removes an extension from the top-level list
*/
removeExtension(extensionName) {
if (this.json.extensions?.[extensionName]) {
this.json.extensionsRemoved = this.json.extensionsRemoved || [];
const extensionsRemoved = this.json.extensionsRemoved;
if (!extensionsRemoved.includes(extensionName)) {
extensionsRemoved.push(extensionName);
}
}
if (this.json.extensions) {
delete this.json.extensions[extensionName];
}
if (this.json.extensionsRequired) {
this._removeStringFromArray(this.json.extensionsRequired, extensionName);
}
if (this.json.extensionsUsed) {
this._removeStringFromArray(this.json.extensionsUsed, extensionName);
}
}
/**
* Set default scene which is to be displayed at load time
*/
setDefaultScene(sceneIndex) {
this.json.scene = sceneIndex;
}
/**
* @todo: add more properties for scene initialization:
* name`, `extensions`, `extras`
* https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-scene
*/
addScene(scene) {
const { nodeIndices } = scene;
this.json.scenes = this.json.scenes || [];
this.json.scenes.push({ nodes: nodeIndices });
return this.json.scenes.length - 1;
}
/**
* @todo: add more properties for node initialization:
* `name`, `extensions`, `extras`, `camera`, `children`, `skin`, `rotation`, `scale`, `translation`, `weights`
* https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#node
*/
addNode(node) {
const { meshIndex, matrix } = node;
this.json.nodes = this.json.nodes || [];
const nodeData = { mesh: meshIndex };
if (matrix) {
// @ts-ignore
nodeData.matrix = matrix;
}
this.json.nodes.push(nodeData);
return this.json.nodes.length - 1;
}
/** Adds a mesh to the json part */
addMesh(mesh) {
const { attributes, indices, material, mode = 4 } = mesh;
const accessors = this._addAttributes(attributes);
const glTFMesh = {
primitives: [
{
attributes: accessors,
mode
}
]
};
if (indices) {
const indicesAccessor = this._addIndices(indices);
// @ts-ignore
glTFMesh.primitives[0].indices = indicesAccessor;
}
if (Number.isFinite(material)) {
// @ts-ignore
glTFMesh.primitives[0].material = material;
}
this.json.meshes = this.json.meshes || [];
this.json.meshes.push(glTFMesh);
return this.json.meshes.length - 1;
}
addPointCloud(attributes) {
// @ts-ignore
const accessorIndices = this._addAttributes(attributes);
const glTFMesh = {
primitives: [
{
attributes: accessorIndices,
mode: 0 // GL.POINTS
}
]
};
this.json.meshes = this.json.meshes || [];
this.json.meshes.push(glTFMesh);
return this.json.meshes.length - 1;
}
/**
* Adds a binary image. Builds glTF "JSON metadata" and saves buffer reference
* Buffer will be copied into BIN chunk during "pack"
* Currently encodes as glTF image
* @param imageData
* @param mimeType
*/
addImage(imageData, mimeTypeOpt) {
// If image is referencing a bufferView instead of URI, mimeType must be defined:
// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#images
// "a reference to a bufferView; in that case mimeType must be defined."
const metadata = getBinaryImageMetadata(imageData);
const mimeType = mimeTypeOpt || metadata?.mimeType;
const bufferViewIndex = this.addBufferView(imageData);
const glTFImage = {
bufferView: bufferViewIndex,
mimeType
};
this.json.images = this.json.images || [];
this.json.images.push(glTFImage);
return this.json.images.length - 1;
}
/**
* Add one untyped source buffer, create a matching glTF `bufferView`, and return its index
* @param buffer
*/
addBufferView(buffer, bufferIndex = 0, byteOffset = this.byteLength) {
const byteLength = buffer.byteLength;
assert(Number.isFinite(byteLength));
// Add this buffer to the list of buffers to be written to the body.
this.sourceBuffers = this.sourceBuffers || [];
this.sourceBuffers.push(buffer);
const glTFBufferView = {
buffer: bufferIndex,
// Write offset from the start of the binary body
byteOffset,
byteLength
};
// We've now added the contents to the body, so update the total length
// Every sub-chunk needs to be 4-byte align ed
this.byteLength += padToNBytes(byteLength, 4);
// Add a bufferView indicating start and length of this binary sub-chunk
this.json.bufferViews = this.json.bufferViews || [];
this.json.bufferViews.push(glTFBufferView);
return this.json.bufferViews.length - 1;
}
/**
* Adds an accessor to a bufferView
* @param bufferViewIndex
* @param accessor
*/
addAccessor(bufferViewIndex, accessor) {
const glTFAccessor = {
bufferView: bufferViewIndex,
// @ts-ignore
type: getAccessorTypeFromSize(accessor.size),
// @ts-ignore
componentType: accessor.componentType,
// @ts-ignore
count: accessor.count,
// @ts-ignore
max: accessor.max,
// @ts-ignore
min: accessor.min
};
this.json.accessors = this.json.accessors || [];
this.json.accessors.push(glTFAccessor);
return this.json.accessors.length - 1;
}
/**
* Add a binary buffer. Builds glTF "JSON metadata" and saves buffer reference
* Buffer will be copied into BIN chunk during "pack"
* Currently encodes buffers as glTF accessors, but this could be optimized
* @param sourceBuffer
* @param accessor
*/
addBinaryBuffer(sourceBuffer, accessor = { size: 3 }) {
const bufferViewIndex = this.addBufferView(sourceBuffer);
// @ts-ignore
let minMax = { min: accessor.min, max: accessor.max };
if (!minMax.min || !minMax.max) {
// @ts-ignore
minMax = this._getAccessorMinMax(sourceBuffer, accessor.size);
}
const accessorDefaults = {
// @ts-ignore
size: accessor.size,
componentType: getComponentTypeFromArray(sourceBuffer),
// @ts-ignore
count: Math.round(sourceBuffer.length / accessor.size),
min: minMax.min,
max: minMax.max
};
return this.addAccessor(bufferViewIndex, Object.assign(accessorDefaults, accessor));
}
/**
* Adds a texture to the json part
* @todo: add more properties for texture initialization
* `sampler`, `name`, `extensions`, `extras`
* https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#texture
*/
addTexture(texture) {
const { imageIndex } = texture;
const glTFTexture = {
source: imageIndex
};
this.json.textures = this.json.textures || [];
this.json.textures.push(glTFTexture);
return this.json.textures.length - 1;
}
/** Adds a material to the json part */
addMaterial(pbrMaterialInfo) {
this.json.materials = this.json.materials || [];
this.json.materials.push(pbrMaterialInfo);
return this.json.materials.length - 1;
}
/** Pack the binary chunk */
createBinaryChunk() {
// Allocate total array
const totalByteLength = this.byteLength;
const arrayBuffer = new ArrayBuffer(totalByteLength);
const targetArray = new Uint8Array(arrayBuffer);
// Copy each array into
let dstByteOffset = 0;
for (const sourceBuffer of this.sourceBuffers || []) {
dstByteOffset = copyToArray(sourceBuffer, targetArray, dstByteOffset);
}
// Update the glTF BIN CHUNK byte length
if (this.json?.buffers?.[0]) {
this.json.buffers[0].byteLength = totalByteLength;
}
else {
this.json.buffers = [{ byteLength: totalByteLength }];
}
// Save generated arrayBuffer
this.gltf.binary = arrayBuffer;
// Put arrayBuffer to sourceBuffers for possible additional writing data in the chunk
this.sourceBuffers = [arrayBuffer];
this.gltf.buffers = [{ arrayBuffer, byteOffset: 0, byteLength: arrayBuffer.byteLength }];
}
// PRIVATE
_removeStringFromArray(array, string) {
let found = true;
while (found) {
const index = array.indexOf(string);
if (index > -1) {
array.splice(index, 1);
}
else {
found = false;
}
}
}
/**
* Add attributes to buffers and create `attributes` object which is part of `mesh`
*/
_addAttributes(attributes = {}) {
const result = {};
for (const attributeKey in attributes) {
const attributeData = attributes[attributeKey];
const attrName = this._getGltfAttributeName(attributeKey);
const accessor = this.addBinaryBuffer(attributeData.value, attributeData);
result[attrName] = accessor;
}
return result;
}
/**
* Add indices to buffers
*/
_addIndices(indices) {
return this.addBinaryBuffer(indices, { size: 1 });
}
/**
* Deduce gltf specific attribue name from input attribute name
*/
_getGltfAttributeName(attributeName) {
switch (attributeName.toLowerCase()) {
case 'position':
case 'positions':
case 'vertices':
return 'POSITION';
case 'normal':
case 'normals':
return 'NORMAL';
case 'color':
case 'colors':
return 'COLOR_0';
case 'texcoord':
case 'texcoords':
return 'TEXCOORD_0';
default:
return attributeName;
}
}
/**
* Calculate `min` and `max` arrays of accessor according to spec:
* https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-accessor
*/
_getAccessorMinMax(buffer, size) {
const result = { min: null, max: null };
if (buffer.length < size) {
return result;
}
// @ts-ignore
result.min = [];
// @ts-ignore
result.max = [];
const initValues = buffer.subarray(0, size);
for (const value of initValues) {
// @ts-ignore
result.min.push(value);
// @ts-ignore
result.max.push(value);
}
for (let index = size; index < buffer.length; index += size) {
for (let componentIndex = 0; componentIndex < size; componentIndex++) {
// @ts-ignore
result.min[0 + componentIndex] = Math.min(
// @ts-ignore
result.min[0 + componentIndex], buffer[index + componentIndex]);
// @ts-ignore
result.max[0 + componentIndex] = Math.max(
// @ts-ignore
result.max[0 + componentIndex], buffer[index + componentIndex]);
}
}
return result;
}
}
//# sourceMappingURL=gltf-scenegraph.js.map