@loaders.gl/gltf
Version:
Framework-independent loader for the glTF format
255 lines (212 loc) • 7.94 kB
text/typescript
/* eslint-disable camelcase, max-statements, no-restricted-globals */
import type {LoaderContext, StrictLoaderOptions} from '@loaders.gl/loader-utils';
import type {GLTFLoaderOptions} from '../../gltf-loader';
import type {GLTFWithBuffers} from '../types/gltf-types';
import type {GLB} from '../types/glb-types';
import type {ParseGLBOptions} from './parse-glb';
import type {ImageType, TextureLevel} from '@loaders.gl/schema';
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';
import {isGLB, parseGLBSync} from './parse-glb';
import {resolveUrl} from '../gltf-utils/resolve-url';
import {getTypedArrayForBufferView} from '../gltf-utils/get-typed-array';
import {preprocessExtensions, decodeExtensions} from '../api/gltf-extensions';
import {normalizeGLTFV1} from '../api/normalize-gltf-v1';
/** */
export type ParseGLTFOptions = ParseGLBOptions & {
normalize?: boolean;
loadImages?: boolean;
loadBuffers?: boolean;
decompressMeshes?: boolean;
excludeExtensions?: string[];
/** @deprecated not supported in v4. `postProcessGLTF()` must be called by the application */
postProcess?: never;
};
/** Check if an array buffer appears to contain GLTF data */
export function isGLTF(arrayBuffer: ArrayBuffer, options?: ParseGLTFOptions): boolean {
const byteOffset = 0;
return isGLB(arrayBuffer, byteOffset, options);
}
export async function parseGLTF(
gltf: GLTFWithBuffers,
arrayBufferOrString,
byteOffset = 0,
options: GLTFLoaderOptions,
context: LoaderContext
): Promise<GLTFWithBuffers> {
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: GLTFLoaderOptions) {
// 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: GLB = {} as 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: GLTFWithBuffers, options, context: LoaderContext) {
// 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: GLTFWithBuffers, options, context: LoaderContext) {
const imageIndices = getReferencesImageIndices(gltf);
const images = gltf.json.images || [];
const promises: Promise<any>[] = [];
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: GLTFWithBuffers): number[] {
const imageIndices = new Set<number>();
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: GLTFWithBuffers,
image,
index: number,
options,
context: LoaderContext
) {
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}
} satisfies StrictLoaderOptions;
// Call `parse`
let parsedImage = (await parseFromContext(
arrayBuffer,
[ImageLoader, BasisLoader],
gltfOptions,
context
)) as ImageType | TextureLevel[][];
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;
}