nanogl-gltf
Version:
365 lines (364 loc) • 15 kB
JavaScript
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;
}
}