@loaders.gl/gltf
Version:
Framework-independent loader for the glTF format
182 lines • 7.31 kB
JavaScript
import { parseJSON, sliceArrayBuffer, parseFromContext } from '@loaders.gl/loader-utils';
import { ImageLoader } from '@loaders.gl/images';
import { BasisLoader } from '@loaders.gl/textures';
import { assert } from "../utils/assert.js";
import { isGLB, parseGLBSync } from "./parse-glb.js";
import { resolveUrl } from "../gltf-utils/resolve-url.js";
import { getTypedArrayForBufferView } from "../gltf-utils/get-typed-array.js";
import { preprocessExtensions, decodeExtensions } from "../api/gltf-extensions.js";
import { normalizeGLTFV1 } from "../api/normalize-gltf-v1.js";
/** Check if an array buffer appears to contain GLTF data */
export function isGLTF(arrayBuffer, options) {
const byteOffset = 0;
return isGLB(arrayBuffer, byteOffset, options);
}
export async function parseGLTF(gltf, arrayBufferOrString, byteOffset = 0, options, context) {
parseGLTFContainerSync(gltf, arrayBufferOrString, byteOffset, options);
normalizeGLTFV1(gltf, { normalize: options?.gltf?.normalize });
preprocessExtensions(gltf, options, context);
// Load linked buffers asynchronously and decodes base64 buffers in parallel
if (options?.gltf?.loadBuffers && gltf.json.buffers) {
await loadBuffers(gltf, options, context);
}
// loadImages and decodeExtensions should not be running in parallel, because
// decodeExtensions uses data from images taken during the loadImages call.
if (options?.gltf?.loadImages) {
await loadImages(gltf, options, context);
}
await decodeExtensions(gltf, options, context);
return gltf;
}
/**
*
* @param gltf
* @param data - can be ArrayBuffer (GLB), ArrayBuffer (Binary JSON), String (JSON), or Object (parsed JSON)
* @param byteOffset
* @param options
*/
function parseGLTFContainerSync(gltf, data, byteOffset, options) {
// Initialize gltf container
if (options.core?.baseUrl) {
gltf.baseUri = options.core?.baseUrl;
}
// If data is binary and starting with magic bytes, assume binary JSON text, convert to string
if (data instanceof ArrayBuffer && !isGLB(data, byteOffset, options.glb)) {
const textDecoder = new TextDecoder();
data = textDecoder.decode(data);
}
if (typeof data === 'string') {
// If string, try to parse as JSON
gltf.json = parseJSON(data);
}
else if (data instanceof ArrayBuffer) {
// If still ArrayBuffer, parse as GLB container
const glb = {};
byteOffset = parseGLBSync(glb, data, byteOffset, options.glb);
assert(glb.type === 'glTF', `Invalid GLB magic string ${glb.type}`);
gltf._glb = glb;
gltf.json = glb.json;
}
else {
assert(false, 'GLTF: must be ArrayBuffer or string');
}
// Populate buffers
// Create an external buffers array to hold binary data
const buffers = gltf.json.buffers || [];
gltf.buffers = new Array(buffers.length).fill(null);
// Populates JSON and some bin chunk info
if (gltf._glb && gltf._glb.header.hasBinChunk) {
const { binChunks } = gltf._glb;
gltf.buffers[0] = {
arrayBuffer: binChunks[0].arrayBuffer,
byteOffset: binChunks[0].byteOffset,
byteLength: binChunks[0].byteLength
};
// TODO - this modifies JSON and is a post processing thing
// gltf.json.buffers[0].data = gltf.buffers[0].arrayBuffer;
// gltf.json.buffers[0].byteOffset = gltf.buffers[0].byteOffset;
}
// Populate images
const images = gltf.json.images || [];
gltf.images = new Array(images.length).fill({});
}
/** Asynchronously fetch and parse buffers, store in buffers array outside of json
* TODO - traverse gltf and determine which buffers are actually needed
*/
async function loadBuffers(gltf, options, context) {
// TODO
const buffers = gltf.json.buffers || [];
for (let i = 0; i < buffers.length; ++i) {
const buffer = buffers[i];
if (buffer.uri) {
const { fetch } = context;
assert(fetch);
const uri = resolveUrl(buffer.uri, options, context);
const response = await context?.fetch?.(uri);
const arrayBuffer = await response?.arrayBuffer?.();
gltf.buffers[i] = {
arrayBuffer,
byteOffset: 0,
byteLength: arrayBuffer.byteLength
};
delete buffer.uri;
}
else if (gltf.buffers[i] === null) {
gltf.buffers[i] = {
arrayBuffer: new ArrayBuffer(buffer.byteLength),
byteOffset: 0,
byteLength: buffer.byteLength
};
}
}
}
/**
* Loads all images
* TODO - traverse gltf and determine which images are actually needed
* @param gltf
* @param options
* @param context
* @returns
*/
async function loadImages(gltf, options, context) {
const imageIndices = getReferencesImageIndices(gltf);
const images = gltf.json.images || [];
const promises = [];
for (const imageIndex of imageIndices) {
promises.push(loadImage(gltf, images[imageIndex], imageIndex, options, context));
}
return await Promise.all(promises);
}
/** Make sure we only load images that are actually referenced by textures */
function getReferencesImageIndices(gltf) {
const imageIndices = new Set();
const textures = gltf.json.textures || [];
for (const texture of textures) {
if (texture.source !== undefined) {
imageIndices.add(texture.source);
}
}
return Array.from(imageIndices).sort();
}
/** Asynchronously fetches and parses one image, store in images array outside of json */
async function loadImage(gltf, image, index, options, context) {
let arrayBuffer;
if (image.uri && !image.hasOwnProperty('bufferView')) {
const uri = resolveUrl(image.uri, options, context);
const { fetch } = context;
const response = await fetch(uri);
arrayBuffer = await response.arrayBuffer();
image.bufferView = {
data: arrayBuffer
};
}
if (Number.isFinite(image.bufferView)) {
const array = getTypedArrayForBufferView(gltf.json, gltf.buffers, image.bufferView);
arrayBuffer = sliceArrayBuffer(array.buffer, array.byteOffset, array.byteLength);
}
assert(arrayBuffer, 'glTF image has no data');
const strictOptions = options;
const gltfOptions = {
...strictOptions,
core: { ...strictOptions?.core, mimeType: image.mimeType }
};
// Call `parse`
let parsedImage = (await parseFromContext(arrayBuffer, [ImageLoader, BasisLoader], gltfOptions, context));
if (parsedImage && parsedImage[0]) {
parsedImage = {
compressed: true,
// @ts-expect-error
mipmaps: false,
width: parsedImage[0].width,
height: parsedImage[0].height,
data: parsedImage[0]
};
}
// TODO making sure ImageLoader is overridable by using array of loaders
// const parsedImage = await parse(arrayBuffer, [ImageLoader]);
// Store the loaded image
gltf.images = gltf.images || [];
// @ts-expect-error TODO - sort out image typing asap
gltf.images[index] = parsedImage;
}
//# sourceMappingURL=parse-gltf.js.map