///
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: ArrayBufferView;
}
export interface IGLTFLoader {
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 ISceneLoaderPluginAsync {
public static CreateGLTFLoaderV1: (parent: GLTFFileLoader) => IGLTFLoader;
public static CreateGLTFLoaderV2: (parent: GLTFFileLoader) => IGLTFLoader;
// Common options
public onParsed: (data: IGLTFLoaderData) => void;
// V1 options
public static HomogeneousCoordinates: boolean = false;
public static IncrementalLoading: boolean = true;
// V2 options
public coordinateSystemMode: GLTFLoaderCoordinateSystemMode = GLTFLoaderCoordinateSystemMode.AUTO;
public onTextureLoaded: (texture: BaseTexture) => void;
public onMaterialLoaded: (material: Material) => void;
/**
* Let the user decides if he needs to process the material (like precompilation) before affecting it to meshes
*/
public onBeforeMaterialReadyAsync: (material: Material, targetMesh: AbstractMesh, isLOD: boolean, callback: () => void) => void;
/**
* Raised when the visible components (geometry, materials, textures, etc.) are first ready to be rendered.
* For assets with LODs, raised when the first LOD is complete.
* For assets without LODs, raised when the model is complete just before onComplete.
*/
public onReady: () => 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 onReady.
*/
public onComplete: () => void;
public name = "gltf";
public extensions: ISceneLoaderPluginExtensions = {
".gltf": { isBinary: false },
".glb": { isBinary: true }
};
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 {
var loaderData = GLTFFileLoader._parse(data, onError);
if (!loaderData) {
return;
}
if (this.onParsed) {
this.onParsed(loaderData);
}
var loader = this._getLoader(loaderData, onError);
if (!loader) {
return;
}
loader.importMeshAsync(meshesNames, scene, loaderData, rootUrl, onSuccess, onProgress, onError);
}
public loadAsync(scene: Scene, data: string | ArrayBuffer, rootUrl: string, onSuccess: () => void, onProgress: (event: ProgressEvent) => void, onError: (message: string) => void): void {
var loaderData = GLTFFileLoader._parse(data, onError);
if (!loaderData) {
return;
}
if (this.onParsed) {
this.onParsed(loaderData);
}
var loader = this._getLoader(loaderData, onError);
if (!loader) {
return;
}
return loader.loadAsync(scene, loaderData, rootUrl, onSuccess, onProgress, onError);
}
public canDirectLoad(data: string): boolean {
return ((data.indexOf("scene") !== -1) && (data.indexOf("node") !== -1));
}
private static _parse(data: string | ArrayBuffer, onError: (message: string) => void): IGLTFLoaderData {
if (data instanceof ArrayBuffer) {
return GLTFFileLoader._parseBinary(data, onError);
}
try {
return {
json: JSON.parse(data),
bin: null
};
}
catch (e) {
onError(e.message);
return null;
}
}
private _getLoader(loaderData: IGLTFLoaderData, onError: (message: string) => void): IGLTFLoader {
const loaderVersion = { major: 2, minor: 0 };
var asset = (loaderData.json).asset || {};
var version = GLTFFileLoader._parseVersion(asset.version);
if (!version) {
onError("Invalid version: " + asset.version);
return null;
}
if (asset.minVersion !== undefined) {
var minVersion = GLTFFileLoader._parseVersion(asset.minVersion);
if (!minVersion) {
onError("Invalid minimum version: " + asset.minVersion);
return null;
}
if (GLTFFileLoader._compareVersion(minVersion, loaderVersion) > 0) {
onError("Incompatible minimum version: " + asset.minVersion);
return null;
}
}
var createLoaders = {
1: GLTFFileLoader.CreateGLTFLoaderV1,
2: GLTFFileLoader.CreateGLTFLoaderV2
};
var createLoader = createLoaders[version.major];
if (!createLoader) {
onError("Unsupported version: " + asset.version);
return null;
}
return createLoader(this);
}
private static _parseBinary(data: ArrayBuffer, onError: (message: string) => void): IGLTFLoaderData {
const Binary = {
Magic: 0x46546C67
};
var binaryReader = new BinaryReader(data);
var magic = binaryReader.readUint32();
if (magic !== Binary.Magic) {
onError("Unexpected magic: " + magic);
return null;
}
var version = binaryReader.readUint32();
switch (version) {
case 1: return GLTFFileLoader._parseV1(binaryReader, onError);
case 2: return GLTFFileLoader._parseV2(binaryReader, onError);
}
onError("Unsupported version: " + version);
return null;
}
private static _parseV1(binaryReader: BinaryReader, onError: (message: string) => void): IGLTFLoaderData {
const ContentFormat = {
JSON: 0
};
var length = binaryReader.readUint32();
if (length != binaryReader.getLength()) {
onError("Length in header does not match actual data length: " + length + " != " + binaryReader.getLength());
return null;
}
var contentLength = binaryReader.readUint32();
var contentFormat = binaryReader.readUint32();
var content: Object;
switch (contentFormat) {
case ContentFormat.JSON:
content = JSON.parse(GLTFFileLoader._decodeBufferToText(binaryReader.readUint8Array(contentLength)));
break;
default:
onError("Unexpected content format: " + contentFormat);
return null;
}
var bytesRemaining = binaryReader.getLength() - binaryReader.getPosition();
var body = binaryReader.readUint8Array(bytesRemaining);
return {
json: content,
bin: body
};
}
private static _parseV2(binaryReader: BinaryReader, onError: (message: string) => void): IGLTFLoaderData {
const ChunkFormat = {
JSON: 0x4E4F534A,
BIN: 0x004E4942
};
var length = binaryReader.readUint32();
if (length !== binaryReader.getLength()) {
onError("Length in header does not match actual data length: " + length + " != " + binaryReader.getLength());
return null;
}
// JSON chunk
var chunkLength = binaryReader.readUint32();
var chunkFormat = binaryReader.readUint32();
if (chunkFormat !== ChunkFormat.JSON) {
onError("First chunk format is not JSON");
return null;
}
var json = JSON.parse(GLTFFileLoader._decodeBufferToText(binaryReader.readUint8Array(chunkLength)));
// Look for BIN chunk
var bin: Uint8Array = null;
while (binaryReader.getPosition() < binaryReader.getLength()) {
chunkLength = binaryReader.readUint32();
chunkFormat = binaryReader.readUint32();
switch (chunkFormat) {
case ChunkFormat.JSON:
onError("Unexpected JSON chunk");
return null;
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): { major: number, minor: number } {
if (!version) {
return null;
}
var parts = version.split(".");
if (parts.length != 2) {
return null;
}
var major = +parts[0];
if (isNaN(major)) {
return null;
}
var minor = +parts[1];
if (isNaN(minor)) {
return null;
}
return {
major: major,
minor: minor
};
}
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(view: ArrayBufferView): string {
var result = "";
var length = view.byteLength;
for (var i = 0; i < length; ++i) {
result += String.fromCharCode(view[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 {
var value = this._dataView.getUint32(this._byteOffset, true);
this._byteOffset += 4;
return value;
}
public readUint8Array(length: number): Uint8Array {
var 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());
}
}