UNPKG

nanogl-gltf

Version:
365 lines (364 loc) 15 kB
import Gltf from "../Gltf"; import { ExtensionList } from "../extensions/Registry"; import GltfTypes from "../types/GltfTypes"; import "../extensions/DefaultExtension"; import Assert from "../lib/assert"; import { AbortSignal } from "@azure/abort-controller"; let UID = 0; function getUUID() { return (UID++) + ""; } const defaultMaterialData = { gltftype: GltfTypes.MATERIAL, elementIndex: -1, elementParent: null, uuid: '_default_mat_', name: 'default', }; export class PendingElement { constructor(data, element) { this.data = data; this.promise = element; } } const MAGIC = 0x46546C67; // "glTF" const JSON_MAGIC = 0x4E4F534A; // "JSON" const GLB_HEADER_SIZE = 20; const GL_LINEAR = 9729; /** * This class is used to load a Gltf file and its resources, including extensions processing. */ export default class GltfLoader { /** * @param gltfIO Implementation of IOInterface made for GLTFs * @param url Gltf file path * @param options Options for Gltf loader */ constructor(gltfIO, url, options = {}) { var _a, _b; /** * Map of all created elements from the Gltf data file, by uuid */ this._elements = new Map(); /** * List of elements that are being created, waiting to be resolved and added to the Gltf object */ this._pendingElements = []; /** * Map of all elements of the Gltf data file, ordered by type */ this._byType = new Map(); /** * Map of all properties of the Gltf data file, ordered by type */ this._propertyMaps = new Map(); // loadBuffer = (b: Buffer) => { /** * Load a buffer from an URI, if no URI is provided the _glbData buffer will be returned * @param uri URI to load */ this.loadBufferUri = (uri) => { if (uri === undefined) return Promise.resolve(this._glbData); const resolvedUri = this.gltfIO.resolvePath(uri, this._baseUrl); return this.gltfIO.loadBinaryResource(resolvedUri, this.abortSignal); }; this.gltfIO = gltfIO; this._url = url; this._baseUrl = options.baseurl; this.abortSignal = (_a = options.abortSignal) !== null && _a !== void 0 ? _a : AbortSignal.none; this.defaultTextureFilter = (_b = options.defaultTextureFilter) !== null && _b !== void 0 ? _b : GL_LINEAR; if (this._baseUrl === undefined) { [this._baseUrl, this._url] = gltfIO.resolveBaseDir(this._url); } this.gltf = new Gltf(); this._data = null; this._extensions = new ExtensionList(); Gltf.getExtensionsRegistry().setupExtensions(this, options.extensions); } /** * Load and return the gltf file */ async load() { const buffer = await this.gltfIO.loadBinaryResource(this.gltfIO.resolvePath(this._url, this._baseUrl), this.abortSignal); this.unpack(buffer); await this.parseAll(); return this.gltf; } /** * Parse the buffer then prepare it for loading by adding uuids, types and indexes * If the buffer represents a GLTF file (plain JSON), il will be parsed directly. * If the buffer represents a GLB file (binary), it will be first decoded, then parsed. * @param buffer Buffer to unpack */ unpack(buffer) { const magic = new Uint32Array(buffer, 0, 1)[0]; if (magic === MAGIC) { this.unpackGlb(buffer); } else { const jsonStr = this.gltfIO.decodeUTF8(buffer); this._data = JSON.parse(jsonStr); this.prepareGltfDatas(this._data); } } /** * Decode a buffer representing a binary GLB file, then parse it and prepare it for loading * @param buffer Buffer to unpack */ unpackGlb(buffer) { const [, version, , jsonSize, magic] = new Uint32Array(buffer, 0, 5); // Check that the version is 2 if (version !== 2) throw new Error('Binary glTF version is not 2'); // Check that the scene format is 0, indicating that it is JSON if (magic !== JSON_MAGIC) throw new Error('Binary glTF scene format is not JSON'); const scene = this.gltfIO.decodeUTF8(buffer, GLB_HEADER_SIZE, jsonSize); this._glbData = buffer.slice(GLB_HEADER_SIZE + jsonSize + 8); this._data = JSON.parse(scene); this.prepareGltfDatas(this._data); } /** * Resolve an absolute file path relative to this loader base directory * @param uri URI to resolve */ resolveUri(uri) { return this.gltfIO.resolvePath(uri, this._baseUrl); } /** * If element has no name or extras, give it the ones from the data * @param data Data to add * @param element Element to add data on */ parseCommonGltfProperty(data, element) { if (element.name === undefined) { element.name = data.name; } if (element.extras === undefined) { element.extras = data.extras; } } /** * Create a Gltf element from its raw data, with extension handling if needed. * @param data Element data coming from the .gltf file */ async _createElement(data) { const element = await this._createElementInstance(data); this.parseCommonGltfProperty(data, element); return this._extensionsAccept(data, element); } /** * Create a Gltf element from its raw data, passing it through all the loader's extensions. * If no extension handles this specific element, a basic corresponding element will be created. * @param data Element data coming from the .gltf file */ _createElementInstance(data) { const extensions = this._extensions._list; for (const ext of extensions) { const res = ext.loadElement(data); if (res === undefined) throw new Error("extension should not return undefined"); if (res !== null) return res; } throw new Error("Unhandled type"); } /** * Some extensions may want to modify the element after it has been created, this method will call all the extensions that want to do so. * @param data Element data coming from the .gltf file * @param element Corresponding element already created */ async _extensionsAccept(data, element) { const extensions = this._extensions._list; let res; for (const ext of extensions) { res = ext.acceptElement(data, element); if (res !== null) { element = await res; } } return element; } /** * Load an element from its data, if the element is already loaded it will be returned directly. * @param data Data to load */ _loadElement(data) { let res = this._elements.get(data.uuid); if (res === undefined) { res = this._createElement(data); const pe = new PendingElement(data, res); this._pendingElements.push(pe); this._elements.set(data.uuid, res); } return res; } /** * Provide a default material if needed. Used for Primitives that don't have a material. */ loadDefaultMaterial() { return this._loadElement(defaultMaterialData); } /** * Get the array of elements of a given type. If the array doesn't exist, it will be created. * @param type Type of elements to get */ _getElementHolder(type) { let array = this._byType.get(type); if (array === undefined) { array = []; this._byType.set(type, array); } return array; } /** * Get an element of a given type and index. If the element doesn't exist, it will be created by retrieving its data from the Gltf data file. * @param type Element's type * @param index Element's index */ getElement(type, index) { const holder = this._getElementHolder(type); if (holder[index] !== undefined) return holder[index]; // get existing or create if not exist! const properties = this._propertyMaps.get(type); const property = properties[index]; return this._loadElement(property); } /** * Parse all prepared elements of the Gltf data file, create them and add them to the Gltf object */ async parseAll() { this._extensions.validate(this._data.extensionsUsed, this._data.extensionsRequired); const asset = await this._loadElement(this._data.asset); if (asset.version != '2.0') { console.warn(`Gltf version should be "2.0" found "${asset.version}"`); } await this._loadElements(this._data.scenes); await this._loadElements(this._data.nodes); await this._loadElements(this._data.animations); await this.resolveElements(); } /** * Load multiple elements from their data * @param dataList Array of elements to load */ _loadElements(dataList) { if (dataList !== undefined) { const promises = dataList.map((data) => this._loadElement(data)); return Promise.all(promises).then(); } } /** * Wait for all pending elements creation to complete and register them in Gltf object */ async resolveElements() { while (this._pendingElements.length > 0) { const pelements = this._pendingElements.splice(0, this._pendingElements.length); const elements = await Promise.all(pelements.map((pe) => pe.promise)); for (let i = 0; i < pelements.length; i++) { const element = elements[i]; Assert.isDefined(element.gltftype); this.gltf.addElement(elements[i], pelements[i].data.elementIndex); } } } /** * Parse the Gltf data and prepare it for loading * @param gltfData Gltf data to parse */ prepareGltfDatas(gltfData) { this.prepareGltfRootProperties(gltfData.accessors, GltfTypes.ACCESSOR, null); this.prepareGltfRootProperties(gltfData.animations, GltfTypes.ANIMATION, null); this.prepareGltfRootProperties([gltfData.asset], GltfTypes.ASSET, null); this.prepareGltfRootProperties(gltfData.buffers, GltfTypes.BUFFER, null); this.prepareGltfRootProperties(gltfData.bufferViews, GltfTypes.BUFFERVIEW, null); this.prepareGltfRootProperties(gltfData.cameras, GltfTypes.CAMERA, null); this.prepareGltfRootProperties(gltfData.images, GltfTypes.IMAGE, null); this.prepareGltfRootProperties(gltfData.materials, GltfTypes.MATERIAL, null); this.prepareGltfRootProperties(gltfData.meshes, GltfTypes.MESH, null); this.prepareGltfRootProperties(gltfData.nodes, GltfTypes.NODE, null); this.prepareGltfRootProperties(gltfData.samplers, GltfTypes.SAMPLER, null); this.prepareGltfRootProperties(gltfData.scenes, GltfTypes.SCENE, null); this.prepareGltfRootProperties(gltfData.skins, GltfTypes.SKIN, null); this.prepareGltfRootProperties(gltfData.textures, GltfTypes.TEXTURE, null); if (gltfData.animations !== undefined) { for (const animation of gltfData.animations) { this.prepareGltfProperties(animation.samplers, GltfTypes.ANIMATION_SAMPLER, animation); this.prepareGltfProperties(animation.channels, GltfTypes.ANIMATION_CHANNEL, animation); } } if (gltfData.materials !== undefined) { for (const material of gltfData.materials) { this.prepareGltfProperty(material.normalTexture, GltfTypes.NORMAL_TEXTURE_INFO, -1, material); this.prepareGltfProperty(material.occlusionTexture, GltfTypes.OCCLUSION_TEXTURE_INFO, -1, material); this.prepareGltfProperty(material.emissiveTexture, GltfTypes.TEXTURE_INFO, -1, material); if (material.pbrMetallicRoughness !== undefined) { this.prepareGltfProperty(material.pbrMetallicRoughness.baseColorTexture, GltfTypes.TEXTURE_INFO, -1, material); this.prepareGltfProperty(material.pbrMetallicRoughness.metallicRoughnessTexture, GltfTypes.TEXTURE_INFO, -1, material); } } } if (gltfData.meshes !== undefined) { for (const mesh of gltfData.meshes) { this.prepareGltfProperties(mesh.primitives, GltfTypes.PRIMITIVE, mesh); } } if (gltfData.accessors !== undefined) { for (const accessor of gltfData.accessors) { this.prepareGltfProperty(accessor.sparse, GltfTypes.ACCESSOR_SPARSE, -1, accessor); if (accessor.sparse !== undefined) { this.prepareGltfProperty(accessor.sparse.indices, GltfTypes.ACCESSOR_SPARSE_INDICES, -1, accessor.sparse); this.prepareGltfProperty(accessor.sparse.values, GltfTypes.ACCESSOR_SPARSE_VALUES, -1, accessor.sparse); } } } } /** * Prepare multiple Gltf properties for loading, having the same type and parent * @param elementsData Elements to prepare * @param type Elements' type * @param parent Elements' parent */ prepareGltfProperties(elementsData, type, parent) { if (elementsData === undefined) return; for (let i = 0; i < elementsData.length; i++) { const element = elementsData[i]; this.prepareGltfProperty(element, type, i, parent); } } /** * Prepare multiple Gltf root properties for loading, having the same type and parent. * A root property is a property that doesn't have a parent in the Gltf data. * @param elementsData Elements to prepare * @param type Elements' type * @param parent Elements' parent */ prepareGltfRootProperties(elementsData, type, parent) { if (elementsData === undefined) return; this._propertyMaps.set(type, elementsData); for (let i = 0; i < elementsData.length; i++) { const element = elementsData[i]; this.prepareGltfProperty(element, type, i, parent); } } /** * Prepare Gltf property for loading by adding him a uuid, a type, an index and a parent * @param element Element to prepare * @param type Element's type * @param index Element's index * @param parent Element's parent */ prepareGltfProperty(element, type, index, parent) { if (element === undefined) return; // eslint-disable-next-line @typescript-eslint/no-explicit-any element.gltftype = type; element.uuid = getUUID(); element.elementIndex = index; element.elementParent = parent; } }