|
@@ -1,3 +1,4 @@
|
|
|
+import * as GLTF2 from "babylonjs-gltf2interface";
|
|
|
import { Nullable } from "babylonjs/types";
|
|
|
import { Observable, Observer } from "babylonjs/Misc/observable";
|
|
|
import { Tools } from "babylonjs/Misc/tools";
|
|
@@ -11,8 +12,10 @@ import { AbstractMesh } from "babylonjs/Meshes/abstractMesh";
|
|
|
import { SceneLoader, ISceneLoaderPluginFactory, ISceneLoaderPlugin, ISceneLoaderPluginAsync, SceneLoaderProgressEvent, ISceneLoaderPluginExtensions } from "babylonjs/Loading/sceneLoader";
|
|
|
import { AssetContainer } from "babylonjs/assetContainer";
|
|
|
import { Scene, IDisposable } from "babylonjs/scene";
|
|
|
-
|
|
|
-import * as GLTF2 from "babylonjs-gltf2interface";
|
|
|
+import { WebRequest } from "babylonjs/Misc/webRequest";
|
|
|
+import { IFileRequest } from "babylonjs/Misc/fileRequest";
|
|
|
+import { Logger } from 'babylonjs/Misc/logger';
|
|
|
+import { DataReader, IDataBuffer } from './dataReader';
|
|
|
|
|
|
/**
|
|
|
* glTF validator object
|
|
@@ -59,14 +62,14 @@ export enum GLTFLoaderAnimationStartMode {
|
|
|
*/
|
|
|
export interface IGLTFLoaderData {
|
|
|
/**
|
|
|
- * Object that represents the glTF JSON.
|
|
|
+ * The object that represents the glTF JSON.
|
|
|
*/
|
|
|
json: Object;
|
|
|
|
|
|
/**
|
|
|
* The BIN chunk of a binary glTF.
|
|
|
*/
|
|
|
- bin: Nullable<ArrayBufferView>;
|
|
|
+ bin: Nullable<IDataBuffer>;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -198,6 +201,13 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
|
|
|
public transparencyAsCoverage = false;
|
|
|
|
|
|
/**
|
|
|
+ * Defines if the loader should use range requests when load binary glTF files from HTTP.
|
|
|
+ * Enabling will disable offline support and glTF validator.
|
|
|
+ * Defaults to false.
|
|
|
+ */
|
|
|
+ public useRangeRequests = false;
|
|
|
+
|
|
|
+ /**
|
|
|
* Function called before loading a url referenced by the asset.
|
|
|
*/
|
|
|
public preprocessUrlAsync = (url: string) => Promise.resolve(url);
|
|
@@ -454,6 +464,98 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * The callback called when loading from a url.
|
|
|
+ * @param scene scene loading this url
|
|
|
+ * @param url url to load
|
|
|
+ * @param onSuccess callback called when the file successfully loads
|
|
|
+ * @param onProgress callback called while file is loading (if the server supports this mode)
|
|
|
+ * @param useArrayBuffer defines a boolean indicating that date must be returned as ArrayBuffer
|
|
|
+ * @param onError callback called when the file fails to load
|
|
|
+ * @returns a file request object
|
|
|
+ */
|
|
|
+ public requestFile(scene: Scene, url: string, onSuccess: (data: any, request?: WebRequest) => void, onProgress?: (ev: ProgressEvent) => void, useArrayBuffer?: boolean, onError?: (error: any) => void): IFileRequest {
|
|
|
+ if (useArrayBuffer) {
|
|
|
+ if (this.useRangeRequests) {
|
|
|
+ if (this.validate) {
|
|
|
+ Logger.Warn("glTF validation is not supported when range requests are enabled");
|
|
|
+ }
|
|
|
+
|
|
|
+ let firstWebRequest: WebRequest | undefined;
|
|
|
+ const fileRequests = new Array<IFileRequest>();
|
|
|
+ const aggregatedFileRequest: IFileRequest = {
|
|
|
+ abort: () => fileRequests.forEach((fileRequest) => fileRequest.abort()),
|
|
|
+ onCompleteObservable: new Observable<IFileRequest>()
|
|
|
+ };
|
|
|
+
|
|
|
+ const dataBuffer = {
|
|
|
+ readAsync: (byteOffset: number, byteLength: number) => {
|
|
|
+ return new Promise<ArrayBufferView>((resolve, reject) => {
|
|
|
+ fileRequests.push(scene._requestFile(url, (data, webRequest) => {
|
|
|
+ firstWebRequest = firstWebRequest || webRequest;
|
|
|
+ dataBuffer.byteLength = Number(webRequest!.getResponseHeader("Content-Range")!.split("/")[1]);
|
|
|
+ resolve(new Uint8Array(data as ArrayBuffer));
|
|
|
+ }, onProgress, true, true, (error) => {
|
|
|
+ reject(error);
|
|
|
+ }, (webRequest) => {
|
|
|
+ webRequest.setRequestHeader("Range", `bytes=${byteOffset}-${byteOffset + byteLength - 1}`);
|
|
|
+ }));
|
|
|
+ });
|
|
|
+ },
|
|
|
+ byteLength: 0
|
|
|
+ };
|
|
|
+
|
|
|
+ this._unpackBinaryAsync(new DataReader(dataBuffer)).then((loaderData) => {
|
|
|
+ aggregatedFileRequest.onCompleteObservable.notifyObservers(aggregatedFileRequest);
|
|
|
+ onSuccess(loaderData, firstWebRequest);
|
|
|
+ }, onError);
|
|
|
+
|
|
|
+ return aggregatedFileRequest;
|
|
|
+ }
|
|
|
+
|
|
|
+ return scene._requestFile(url, (data, request) => {
|
|
|
+ const arrayBuffer = data as ArrayBuffer;
|
|
|
+ this._unpackBinaryAsync(new DataReader({
|
|
|
+ readAsync: (byteOffset, byteLength) => Promise.resolve(new Uint8Array(arrayBuffer, byteOffset, byteLength)),
|
|
|
+ byteLength: arrayBuffer.byteLength
|
|
|
+ })).then((loaderData) => {
|
|
|
+ onSuccess(loaderData, request);
|
|
|
+ }, onError);
|
|
|
+ }, onProgress, true, true, onError);
|
|
|
+ }
|
|
|
+
|
|
|
+ return scene._requestFile(url, (data, response) => {
|
|
|
+ this._validateAsync(scene, data, Tools.GetFolderPath(url), Tools.GetFilename(url));
|
|
|
+ onSuccess({ json: this._parseJson(data as string) }, response);
|
|
|
+ }, onProgress, true, false, onError);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The callback called when loading from a file object.
|
|
|
+ * @param scene scene loading this file
|
|
|
+ * @param file defines the file to load
|
|
|
+ * @param onSuccess defines the callback to call when data is loaded
|
|
|
+ * @param onProgress defines the callback to call during loading process
|
|
|
+ * @param useArrayBuffer defines a boolean indicating that data must be returned as an ArrayBuffer
|
|
|
+ * @param onError defines the callback to call when an error occurs
|
|
|
+ * @returns a file request object
|
|
|
+ */
|
|
|
+ public readFile(scene: Scene, file: File, onSuccess: (data: any) => void, onProgress?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean, onError?: (error: any) => void): IFileRequest {
|
|
|
+ return scene._readFile(file, (data) => {
|
|
|
+ this._validateAsync(scene, data, "file:", file.name);
|
|
|
+ if (useArrayBuffer) {
|
|
|
+ const arrayBuffer = data as ArrayBuffer;
|
|
|
+ this._unpackBinaryAsync(new DataReader({
|
|
|
+ readAsync: (byteOffset, byteLength) => Promise.resolve(new Uint8Array(arrayBuffer, byteOffset, byteLength)),
|
|
|
+ byteLength: arrayBuffer.byteLength
|
|
|
+ })).then(onSuccess, onError);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ onSuccess({ json: this._parseJson(data as string) });
|
|
|
+ }
|
|
|
+ }, onProgress, useArrayBuffer, onError);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* Imports one or more meshes from the loaded glTF data and adds them to the scene
|
|
|
* @param meshesNames a string or array of strings of the mesh names that should be loaded from the file
|
|
|
* @param scene the scene the meshes should be added to
|
|
@@ -464,11 +566,12 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
|
|
|
* @returns a promise containg the loaded meshes, particles, skeletons and animations
|
|
|
*/
|
|
|
public importMeshAsync(meshesNames: any, scene: Scene, data: any, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<{ meshes: AbstractMesh[], particleSystems: IParticleSystem[], skeletons: Skeleton[], animationGroups: AnimationGroup[] }> {
|
|
|
- return this._parseAsync(scene, data, rootUrl, fileName).then((loaderData) => {
|
|
|
- this._log(`Loading ${fileName || ""}`);
|
|
|
- this._loader = this._getLoader(loaderData);
|
|
|
- return this._loader.importMeshAsync(meshesNames, scene, loaderData, rootUrl, onProgress, fileName);
|
|
|
- });
|
|
|
+ this.onParsedObservable.notifyObservers(data);
|
|
|
+ this.onParsedObservable.clear();
|
|
|
+
|
|
|
+ this._log(`Loading ${fileName || ""}`);
|
|
|
+ this._loader = this._getLoader(data);
|
|
|
+ return this._loader.importMeshAsync(meshesNames, scene, data, rootUrl, onProgress, fileName);
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -480,12 +583,13 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
|
|
|
* @param fileName Defines the name of the file to load
|
|
|
* @returns a promise which completes when objects have been loaded to the scene
|
|
|
*/
|
|
|
- public loadAsync(scene: Scene, data: string | ArrayBuffer, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<void> {
|
|
|
- return this._parseAsync(scene, data, rootUrl, fileName).then((loaderData) => {
|
|
|
- this._log(`Loading ${fileName || ""}`);
|
|
|
- this._loader = this._getLoader(loaderData);
|
|
|
- return this._loader.loadAsync(scene, loaderData, rootUrl, onProgress, fileName);
|
|
|
- });
|
|
|
+ public loadAsync(scene: Scene, data: any, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<void> {
|
|
|
+ this.onParsedObservable.notifyObservers(data);
|
|
|
+ this.onParsedObservable.clear();
|
|
|
+
|
|
|
+ this._log(`Loading ${fileName || ""}`);
|
|
|
+ this._loader = this._getLoader(data);
|
|
|
+ return this._loader.loadAsync(scene, data, rootUrl, onProgress, fileName);
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -497,48 +601,60 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
|
|
|
* @param fileName Defines the name of the file to load
|
|
|
* @returns The loaded asset container
|
|
|
*/
|
|
|
- public loadAssetContainerAsync(scene: Scene, data: string | ArrayBuffer, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<AssetContainer> {
|
|
|
- return this._parseAsync(scene, data, rootUrl, fileName).then((loaderData) => {
|
|
|
- this._log(`Loading ${fileName || ""}`);
|
|
|
- this._loader = this._getLoader(loaderData);
|
|
|
+ public loadAssetContainerAsync(scene: Scene, data: any, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<AssetContainer> {
|
|
|
+ this._log(`Loading ${fileName || ""}`);
|
|
|
+ this._loader = this._getLoader(data);
|
|
|
|
|
|
- // Get materials/textures when loading to add to container
|
|
|
- let materials: Array<Material> = [];
|
|
|
- this.onMaterialLoadedObservable.add((material) => {
|
|
|
- materials.push(material);
|
|
|
- });
|
|
|
- let textures: Array<BaseTexture> = [];
|
|
|
- this.onTextureLoadedObservable.add((texture) => {
|
|
|
- textures.push(texture);
|
|
|
- });
|
|
|
+ // Get materials/textures when loading to add to container
|
|
|
+ const materials: Array<Material> = [];
|
|
|
+ this.onMaterialLoadedObservable.add((material) => {
|
|
|
+ materials.push(material);
|
|
|
+ });
|
|
|
+ const textures: Array<BaseTexture> = [];
|
|
|
+ this.onTextureLoadedObservable.add((texture) => {
|
|
|
+ textures.push(texture);
|
|
|
+ });
|
|
|
|
|
|
- return this._loader.importMeshAsync(null, scene, loaderData, rootUrl, onProgress, fileName).then((result) => {
|
|
|
- const container = new AssetContainer(scene);
|
|
|
- Array.prototype.push.apply(container.meshes, result.meshes);
|
|
|
- Array.prototype.push.apply(container.particleSystems, result.particleSystems);
|
|
|
- Array.prototype.push.apply(container.skeletons, result.skeletons);
|
|
|
- Array.prototype.push.apply(container.animationGroups, result.animationGroups);
|
|
|
- Array.prototype.push.apply(container.materials, materials);
|
|
|
- Array.prototype.push.apply(container.textures, textures);
|
|
|
- container.removeAllFromScene();
|
|
|
- return container;
|
|
|
- });
|
|
|
+ return this._loader.importMeshAsync(null, scene, data, rootUrl, onProgress, fileName).then((result) => {
|
|
|
+ const container = new AssetContainer(scene);
|
|
|
+ Array.prototype.push.apply(container.meshes, result.meshes);
|
|
|
+ Array.prototype.push.apply(container.particleSystems, result.particleSystems);
|
|
|
+ Array.prototype.push.apply(container.skeletons, result.skeletons);
|
|
|
+ Array.prototype.push.apply(container.animationGroups, result.animationGroups);
|
|
|
+ Array.prototype.push.apply(container.materials, materials);
|
|
|
+ Array.prototype.push.apply(container.textures, textures);
|
|
|
+ container.removeAllFromScene();
|
|
|
+ return container;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * If the data string can be loaded directly.
|
|
|
- * @param data string contianing the file data
|
|
|
+ * The callback that returns true if the data can be directly loaded.
|
|
|
+ * @param data string containing the file data
|
|
|
* @returns if the data can be loaded directly
|
|
|
*/
|
|
|
public canDirectLoad(data: string): boolean {
|
|
|
- return ((data.indexOf("scene") !== -1) && (data.indexOf("node") !== -1));
|
|
|
+ return data.indexOf("asset") !== -1 && data.indexOf("version") !== -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The callback that returns the data to pass to the plugin if the data can be directly loaded.
|
|
|
+ * @param scene scene loading this data
|
|
|
+ * @param data string containing the data
|
|
|
+ * @returns data to pass to the plugin
|
|
|
+ */
|
|
|
+ public directLoad(scene: Scene, data: string): any {
|
|
|
+ this._validateAsync(scene, data);
|
|
|
+ return { json: this._parseJson(data) };
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Rewrites a url by combining a root url and response url.
|
|
|
+ * The callback that allows custom handling of the root url based on the response url.
|
|
|
+ * @param rootUrl the original root url
|
|
|
+ * @param responseURL the response url if available
|
|
|
+ * @returns the new root url
|
|
|
*/
|
|
|
- public rewriteRootURL: (rootUrl: string, responseURL?: string) => string;
|
|
|
+ public rewriteRootURL?(rootUrl: string, responseURL?: string): string;
|
|
|
|
|
|
/**
|
|
|
* Instantiates a glTF file loader plugin.
|
|
@@ -570,30 +686,7 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private _parseAsync(scene: Scene, data: string | ArrayBuffer, rootUrl: string, fileName?: string): Promise<IGLTFLoaderData> {
|
|
|
- return Promise.resolve().then(() => {
|
|
|
- return this._validateAsync(scene, data, rootUrl, fileName).then(() => {
|
|
|
- const unpacked = (data instanceof ArrayBuffer) ? this._unpackBinary(data) : { json: data, bin: null };
|
|
|
-
|
|
|
- this._startPerformanceCounter("Parse JSON");
|
|
|
- this._log(`JSON length: ${unpacked.json.length}`);
|
|
|
-
|
|
|
- const loaderData: IGLTFLoaderData = {
|
|
|
- json: JSON.parse(unpacked.json),
|
|
|
- bin: unpacked.bin
|
|
|
- };
|
|
|
-
|
|
|
- this._endPerformanceCounter("Parse JSON");
|
|
|
-
|
|
|
- this.onParsedObservable.notifyObservers(loaderData);
|
|
|
- this.onParsedObservable.clear();
|
|
|
-
|
|
|
- return loaderData;
|
|
|
- });
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- private _validateAsync(scene: Scene, data: string | ArrayBuffer, rootUrl: string, fileName?: string): Promise<void> {
|
|
|
+ private _validateAsync(scene: Scene, data: string | ArrayBuffer, rootUrl = "", fileName?: string): Promise<void> {
|
|
|
if (!this.validate || typeof GLTFValidator === "undefined") {
|
|
|
return Promise.resolve();
|
|
|
}
|
|
@@ -603,13 +696,13 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
|
|
|
const options: GLTF2.IGLTFValidationOptions = {
|
|
|
externalResourceFunction: (uri) => {
|
|
|
return this.preprocessUrlAsync(rootUrl + uri)
|
|
|
- .then((url) => scene._loadFileAsync(url, true, true))
|
|
|
+ .then((url) => scene._loadFileAsync(url, undefined, true, true))
|
|
|
.then((data) => new Uint8Array(data as ArrayBuffer));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- if (fileName && fileName.substr(0, 5) !== "data:") {
|
|
|
- options.uri = (rootUrl === "file:" ? fileName : `${rootUrl}${fileName}`);
|
|
|
+ if (fileName) {
|
|
|
+ options.uri = (rootUrl === "file:" ? fileName : rootUrl + fileName);
|
|
|
}
|
|
|
|
|
|
const promise = (data instanceof ArrayBuffer)
|
|
@@ -663,123 +756,143 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
|
|
|
return createLoader(this);
|
|
|
}
|
|
|
|
|
|
- private _unpackBinary(data: ArrayBuffer): { json: string, bin: Nullable<ArrayBufferView> } {
|
|
|
- this._startPerformanceCounter("Unpack binary");
|
|
|
- this._log(`Binary length: ${data.byteLength}`);
|
|
|
-
|
|
|
- const Binary = {
|
|
|
- Magic: 0x46546C67
|
|
|
- };
|
|
|
+ private _parseJson(json: string): Object {
|
|
|
+ this._startPerformanceCounter("Parse JSON");
|
|
|
+ this._log(`JSON length: ${json.length}`);
|
|
|
+ const parsed = JSON.parse(json);
|
|
|
+ this._endPerformanceCounter("Parse JSON");
|
|
|
+ return parsed;
|
|
|
+ }
|
|
|
|
|
|
- const binaryReader = new BinaryReader(data);
|
|
|
+ private _unpackBinaryAsync(dataReader: DataReader): Promise<IGLTFLoaderData> {
|
|
|
+ this._startPerformanceCounter("Unpack Binary");
|
|
|
|
|
|
- const magic = binaryReader.readUint32();
|
|
|
- if (magic !== Binary.Magic) {
|
|
|
- throw new Error("Unexpected magic: " + magic);
|
|
|
- }
|
|
|
+ // Read magic + version + length + json length + json format
|
|
|
+ return dataReader.loadAsync(20).then(() => {
|
|
|
+ const Binary = {
|
|
|
+ Magic: 0x46546C67
|
|
|
+ };
|
|
|
|
|
|
- const version = binaryReader.readUint32();
|
|
|
+ const magic = dataReader.readUint32();
|
|
|
+ if (magic !== Binary.Magic) {
|
|
|
+ throw new Error("Unexpected magic: " + magic);
|
|
|
+ }
|
|
|
|
|
|
- if (this.loggingEnabled) {
|
|
|
- this._log(`Binary version: ${version}`);
|
|
|
- }
|
|
|
+ const version = dataReader.readUint32();
|
|
|
|
|
|
- let unpacked: { json: string, bin: Nullable<ArrayBufferView> };
|
|
|
- switch (version) {
|
|
|
- case 1: {
|
|
|
- unpacked = this._unpackBinaryV1(binaryReader);
|
|
|
- break;
|
|
|
+ if (this.loggingEnabled) {
|
|
|
+ this._log(`Binary version: ${version}`);
|
|
|
}
|
|
|
- case 2: {
|
|
|
- unpacked = this._unpackBinaryV2(binaryReader);
|
|
|
- break;
|
|
|
+
|
|
|
+ const length = dataReader.readUint32();
|
|
|
+ if (length !== dataReader.buffer.byteLength) {
|
|
|
+ throw new Error(`Length in header does not match actual data length: ${length} != ${dataReader.buffer.byteLength}`);
|
|
|
}
|
|
|
- default: {
|
|
|
- throw new Error("Unsupported version: " + version);
|
|
|
+
|
|
|
+ let unpacked: Promise<IGLTFLoaderData>;
|
|
|
+ switch (version) {
|
|
|
+ case 1: {
|
|
|
+ unpacked = this._unpackBinaryV1Async(dataReader);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 2: {
|
|
|
+ unpacked = this._unpackBinaryV2Async(dataReader);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default: {
|
|
|
+ throw new Error("Unsupported version: " + version);
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- this._endPerformanceCounter("Unpack binary");
|
|
|
- return unpacked;
|
|
|
+ this._endPerformanceCounter("Unpack Binary");
|
|
|
+
|
|
|
+ return unpacked;
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- private _unpackBinaryV1(binaryReader: BinaryReader): { json: string, bin: Nullable<ArrayBufferView> } {
|
|
|
+ private _unpackBinaryV1Async(dataReader: DataReader): Promise<IGLTFLoaderData> {
|
|
|
const ContentFormat = {
|
|
|
JSON: 0
|
|
|
};
|
|
|
|
|
|
- const length = binaryReader.readUint32();
|
|
|
- if (length != binaryReader.getLength()) {
|
|
|
- throw new Error("Length in header does not match actual data length: " + length + " != " + binaryReader.getLength());
|
|
|
+ const contentLength = dataReader.readUint32();
|
|
|
+ const contentFormat = dataReader.readUint32();
|
|
|
+
|
|
|
+ if (contentFormat !== ContentFormat.JSON) {
|
|
|
+ throw new Error(`Unexpected content format: ${contentFormat}`);
|
|
|
}
|
|
|
|
|
|
- const contentLength = binaryReader.readUint32();
|
|
|
- const contentFormat = binaryReader.readUint32();
|
|
|
+ const bodyLength = dataReader.buffer.byteLength - dataReader.byteOffset;
|
|
|
|
|
|
- let content: string;
|
|
|
- switch (contentFormat) {
|
|
|
- case ContentFormat.JSON: {
|
|
|
- content = GLTFFileLoader._decodeBufferToText(binaryReader.readUint8Array(contentLength));
|
|
|
- break;
|
|
|
- }
|
|
|
- default: {
|
|
|
- throw new Error("Unexpected content format: " + contentFormat);
|
|
|
- }
|
|
|
+ const data: IGLTFLoaderData = { json: this._parseJson(dataReader.readString(contentLength)), bin: null };
|
|
|
+ if (bodyLength !== 0) {
|
|
|
+ const startByteOffset = dataReader.byteOffset;
|
|
|
+ data.bin = {
|
|
|
+ readAsync: (byteOffset, byteLength) => dataReader.buffer.readAsync(startByteOffset + byteOffset, byteLength),
|
|
|
+ byteLength: bodyLength
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- const bytesRemaining = binaryReader.getLength() - binaryReader.getPosition();
|
|
|
- const body = binaryReader.readUint8Array(bytesRemaining);
|
|
|
-
|
|
|
- return {
|
|
|
- json: content,
|
|
|
- bin: body
|
|
|
- };
|
|
|
+ return Promise.resolve(data);
|
|
|
}
|
|
|
|
|
|
- private _unpackBinaryV2(binaryReader: BinaryReader): { json: string, bin: Nullable<ArrayBufferView> } {
|
|
|
+ private _unpackBinaryV2Async(dataReader: DataReader): Promise<IGLTFLoaderData> {
|
|
|
const ChunkFormat = {
|
|
|
JSON: 0x4E4F534A,
|
|
|
BIN: 0x004E4942
|
|
|
};
|
|
|
|
|
|
- const length = binaryReader.readUint32();
|
|
|
- if (length !== binaryReader.getLength()) {
|
|
|
- throw new Error("Length in header does not match actual data length: " + length + " != " + binaryReader.getLength());
|
|
|
- }
|
|
|
-
|
|
|
- // JSON chunk
|
|
|
- const chunkLength = binaryReader.readUint32();
|
|
|
- const chunkFormat = binaryReader.readUint32();
|
|
|
+ // Read the JSON chunk header.
|
|
|
+ const chunkLength = dataReader.readUint32();
|
|
|
+ const chunkFormat = dataReader.readUint32();
|
|
|
if (chunkFormat !== ChunkFormat.JSON) {
|
|
|
throw new Error("First chunk format is not JSON");
|
|
|
}
|
|
|
- const json = GLTFFileLoader._decodeBufferToText(binaryReader.readUint8Array(chunkLength));
|
|
|
-
|
|
|
- // Look for BIN chunk
|
|
|
- let bin: Nullable<Uint8Array> = null;
|
|
|
- while (binaryReader.getPosition() < binaryReader.getLength()) {
|
|
|
- const chunkLength = binaryReader.readUint32();
|
|
|
- const chunkFormat = binaryReader.readUint32();
|
|
|
- switch (chunkFormat) {
|
|
|
- case ChunkFormat.JSON: {
|
|
|
- throw new Error("Unexpected JSON chunk");
|
|
|
- }
|
|
|
- case ChunkFormat.BIN: {
|
|
|
- bin = binaryReader.readUint8Array(chunkLength);
|
|
|
- break;
|
|
|
+
|
|
|
+ // Bail if there are no other chunks.
|
|
|
+ if (dataReader.byteOffset + chunkLength === dataReader.buffer.byteLength) {
|
|
|
+ return dataReader.loadAsync(chunkLength).then(() => {
|
|
|
+ return { json: this._parseJson(dataReader.readString(chunkLength)), bin: null };
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Read the JSON chunk and the length and type of the next chunk.
|
|
|
+ return dataReader.loadAsync(chunkLength + 8).then(() => {
|
|
|
+ const data: IGLTFLoaderData = { json: this._parseJson(dataReader.readString(chunkLength)), bin: null };
|
|
|
+
|
|
|
+ const readAsync = (): Promise<IGLTFLoaderData> => {
|
|
|
+ const chunkLength = dataReader.readUint32();
|
|
|
+ const chunkFormat = dataReader.readUint32();
|
|
|
+
|
|
|
+ switch (chunkFormat) {
|
|
|
+ case ChunkFormat.JSON: {
|
|
|
+ throw new Error("Unexpected JSON chunk");
|
|
|
+ }
|
|
|
+ case ChunkFormat.BIN: {
|
|
|
+ const startByteOffset = dataReader.byteOffset;
|
|
|
+ data.bin = {
|
|
|
+ readAsync: (byteOffset, byteLength) => dataReader.buffer.readAsync(startByteOffset + byteOffset, byteLength),
|
|
|
+ byteLength: chunkLength
|
|
|
+ };
|
|
|
+ dataReader.skipBytes(chunkLength);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default: {
|
|
|
+ // ignore unrecognized chunkFormat
|
|
|
+ dataReader.skipBytes(chunkLength);
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
- default: {
|
|
|
- // ignore unrecognized chunkFormat
|
|
|
- binaryReader.skipBytes(chunkLength);
|
|
|
- break;
|
|
|
+
|
|
|
+ if (dataReader.byteOffset !== dataReader.buffer.byteLength) {
|
|
|
+ return dataReader.loadAsync(8).then(readAsync);
|
|
|
}
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- return {
|
|
|
- json: json,
|
|
|
- bin: bin
|
|
|
- };
|
|
|
+ return Promise.resolve(data);
|
|
|
+ };
|
|
|
+
|
|
|
+ return readAsync();
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
private static _parseVersion(version: string): Nullable<{ major: number, minor: number }> {
|
|
@@ -809,21 +922,6 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
- private static _decodeBufferToText(buffer: Uint8Array): string {
|
|
|
- if (typeof TextDecoder !== "undefined") {
|
|
|
- return new TextDecoder().decode(buffer);
|
|
|
- }
|
|
|
-
|
|
|
- let result = "";
|
|
|
- const length = buffer.byteLength;
|
|
|
-
|
|
|
- for (let i = 0; i < length; i++) {
|
|
|
- result += String.fromCharCode(buffer[i]);
|
|
|
- }
|
|
|
-
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
private static readonly _logSpaces = " ";
|
|
|
private _logIndentLevel = 0;
|
|
|
private _loggingEnabled = false;
|
|
@@ -844,7 +942,7 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
|
|
|
|
|
|
private _logEnabled(message: string): void {
|
|
|
const spaces = GLTFFileLoader._logSpaces.substr(0, this._logIndentLevel * 2);
|
|
|
- Tools.Log(`${spaces}${message}`);
|
|
|
+ Logger.Log(`${spaces}${message}`);
|
|
|
}
|
|
|
|
|
|
private _logDisabled(message: string): void {
|
|
@@ -873,42 +971,6 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-class BinaryReader {
|
|
|
- private _arrayBuffer: ArrayBuffer;
|
|
|
- private _dataView: DataView;
|
|
|
- private _byteOffset: number;
|
|
|
-
|
|
|
- constructor(arrayBuffer: ArrayBuffer) {
|
|
|
- this._arrayBuffer = arrayBuffer;
|
|
|
- this._dataView = new DataView(arrayBuffer);
|
|
|
- this._byteOffset = 0;
|
|
|
- }
|
|
|
-
|
|
|
- public getPosition(): number {
|
|
|
- return this._byteOffset;
|
|
|
- }
|
|
|
-
|
|
|
- public getLength(): number {
|
|
|
- return this._arrayBuffer.byteLength;
|
|
|
- }
|
|
|
-
|
|
|
- public readUint32(): number {
|
|
|
- const value = this._dataView.getUint32(this._byteOffset, true);
|
|
|
- this._byteOffset += 4;
|
|
|
- return value;
|
|
|
- }
|
|
|
-
|
|
|
- public readUint8Array(length: number): Uint8Array {
|
|
|
- const value = new Uint8Array(this._arrayBuffer, this._byteOffset, length);
|
|
|
- this._byteOffset += length;
|
|
|
- return value;
|
|
|
- }
|
|
|
-
|
|
|
- public skipBytes(length: number): void {
|
|
|
- this._byteOffset += length;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
if (SceneLoader) {
|
|
|
SceneLoader.RegisterPlugin(new GLTFFileLoader());
|
|
|
}
|