/// module BABYLON { export enum GLTFLoaderCoordinateSystemMode { // Automatically convert the glTF right-handed data to the appropriate system based on the current coordinate system mode of the scene (scene.useRightHandedSystem). // NOTE: When scene.useRightHandedSystem is false, an additional transform will be added to the root to transform the data from right-handed to left-handed. AUTO, // The glTF right-handed data is not transformed in any form and is loaded directly. PASS_THROUGH, // Sets the useRightHandedSystem flag on the scene. FORCE_RIGHT_HANDED, } export interface IGLTFLoaderData { json: Object; bin: Nullable; } export interface IGLTFLoader extends IDisposable { importMeshAsync: (meshesNames: any, scene: Scene, data: IGLTFLoaderData, rootUrl: string, onSuccess: (meshes: AbstractMesh[], particleSystems: ParticleSystem[], skeletons: Skeleton[]) => void, onProgress: (event: ProgressEvent) => void, onError: (message: string) => void) => void; loadAsync: (scene: Scene, data: IGLTFLoaderData, rootUrl: string, onSuccess: () => void, onProgress: (event: ProgressEvent) => void, onError: (message: string) => void) => void; } export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISceneLoaderPluginFactory { public static CreateGLTFLoaderV1: (parent: GLTFFileLoader) => IGLTFLoader; public static CreateGLTFLoaderV2: (parent: GLTFFileLoader) => IGLTFLoader; // Common options public onParsed: (data: IGLTFLoaderData) => void; // V1 options public static HomogeneousCoordinates = false; public static IncrementalLoading = true; // V2 options public coordinateSystemMode = GLTFLoaderCoordinateSystemMode.AUTO; public compileMaterials = false; public compileShadowGenerators = false; public useClipPlane = false; public onMeshLoaded: (mesh: AbstractMesh) => void; public onTextureLoaded: (texture: BaseTexture) => void; public onMaterialLoaded: (material: Material) => void; /** * Raised when the asset is completely loaded, just before the loader is disposed. * For assets with LODs, raised when all of the LODs are complete. * For assets without LODs, raised when the model is complete just after onSuccess. */ public onComplete: () => void; private _loader: IGLTFLoader; public name = "gltf"; public extensions: ISceneLoaderPluginExtensions = { ".gltf": { isBinary: false }, ".glb": { isBinary: true } }; public dispose(): void { if (this._loader) { this._loader.dispose(); } } public importMeshAsync(meshesNames: any, scene: Scene, data: any, rootUrl: string, onSuccess: (meshes: AbstractMesh[], particleSystems: ParticleSystem[], skeletons: Skeleton[]) => void, onProgress: (event: ProgressEvent) => void, onError: (message: string) => void): void { try { const loaderData = GLTFFileLoader._parse(data); if (this.onParsed) { this.onParsed(loaderData); } this._loader = this._getLoader(loaderData); this._loader.importMeshAsync(meshesNames, scene, loaderData, rootUrl, onSuccess, onProgress, onError); } catch (e) { onError(e.message); } } public loadAsync(scene: Scene, data: string | ArrayBuffer, rootUrl: string, onSuccess: () => void, onProgress: (event: ProgressEvent) => void, onError: (message: string) => void): void { try { const loaderData = GLTFFileLoader._parse(data); if (this.onParsed) { this.onParsed(loaderData); } this._loader = this._getLoader(loaderData); this._loader.loadAsync(scene, loaderData, rootUrl, onSuccess, onProgress, onError); } catch (e) { onError(e.message); } } public canDirectLoad(data: string): boolean { return ((data.indexOf("scene") !== -1) && (data.indexOf("node") !== -1)); } public rewriteRootURL: (rootUrl: string, responseURL?: string) => string; public createPlugin(): ISceneLoaderPlugin | ISceneLoaderPluginAsync { return new GLTFFileLoader(); } private static _parse(data: string | ArrayBuffer): IGLTFLoaderData { if (data instanceof ArrayBuffer) { return GLTFFileLoader._parseBinary(data); } return { json: JSON.parse(data), bin: null }; } private _getLoader(loaderData: IGLTFLoaderData): IGLTFLoader { const loaderVersion = { major: 2, minor: 0 }; const asset = (loaderData.json).asset || {}; const version = GLTFFileLoader._parseVersion(asset.version); if (!version) { throw new Error("Invalid version: " + asset.version); } if (asset.minVersion !== undefined) { const minVersion = GLTFFileLoader._parseVersion(asset.minVersion); if (!minVersion) { throw new Error("Invalid minimum version: " + asset.minVersion); } if (GLTFFileLoader._compareVersion(minVersion, loaderVersion) > 0) { throw new Error("Incompatible minimum version: " + asset.minVersion); } } const createLoaders: { [key: number]: (parent: GLTFFileLoader) => IGLTFLoader } = { 1: GLTFFileLoader.CreateGLTFLoaderV1, 2: GLTFFileLoader.CreateGLTFLoaderV2 }; const createLoader = createLoaders[version.major]; if (!createLoader) { throw new Error("Unsupported version: " + asset.version); } return createLoader(this); } private static _parseBinary(data: ArrayBuffer): IGLTFLoaderData { const Binary = { Magic: 0x46546C67 }; const binaryReader = new BinaryReader(data); const magic = binaryReader.readUint32(); if (magic !== Binary.Magic) { throw new Error("Unexpected magic: " + magic); } const version = binaryReader.readUint32(); switch (version) { case 1: return GLTFFileLoader._parseV1(binaryReader); case 2: return GLTFFileLoader._parseV2(binaryReader); } throw new Error("Unsupported version: " + version); } private static _parseV1(binaryReader: BinaryReader): 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 = binaryReader.readUint32(); const contentFormat = binaryReader.readUint32(); let content: Object; switch (contentFormat) { case ContentFormat.JSON: { content = JSON.parse(GLTFFileLoader._decodeBufferToText(binaryReader.readUint8Array(contentLength))); break; } default: { throw new Error("Unexpected content format: " + contentFormat); } } const bytesRemaining = binaryReader.getLength() - binaryReader.getPosition(); const body = binaryReader.readUint8Array(bytesRemaining); return { json: content, bin: body }; } private static _parseV2(binaryReader: BinaryReader): 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(); if (chunkFormat !== ChunkFormat.JSON) { throw new Error("First chunk format is not JSON"); } const json = JSON.parse(GLTFFileLoader._decodeBufferToText(binaryReader.readUint8Array(chunkLength))); // Look for BIN chunk let bin: Nullable = 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; } default: { // ignore unrecognized chunkFormat binaryReader.skipBytes(chunkLength); break; } } } return { json: json, bin: bin }; } private static _parseVersion(version: string): Nullable<{ major: number, minor: number }> { const match = (version + "").match(/^(\d+)\.(\d+)$/); if (!match) { return null; } return { major: parseInt(match[1]), minor: parseInt(match[2]) }; } private static _compareVersion(a: { major: number, minor: number }, b: { major: number, minor: number }) { if (a.major > b.major) return 1; if (a.major < b.major) return -1; if (a.minor > b.minor) return 1; if (a.minor < b.minor) return -1; return 0; } private static _decodeBufferToText(buffer: Uint8Array): string { let result = ""; const length = buffer.byteLength; for (let i = 0; i < length; i++) { result += String.fromCharCode(buffer[i]); } return result; } } 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 (BABYLON.SceneLoader) { BABYLON.SceneLoader.RegisterPlugin(new GLTFFileLoader()); } }