Browse Source

Loader, scene, engine, tools dispose/abort fixes

Gary Hsu 7 years ago
parent
commit
513df3fb77

+ 15 - 3
loaders/src/glTF/1.0/babylon.glTFLoader.ts

@@ -1557,9 +1557,21 @@ module BABYLON.GLTF1 {
             GLTFLoader.Extensions[extension.name] = extension;
         }
 
-        public dispose(): void {
-            // do nothing
-        }
+        // #region Stubs for IGLTFLoader interface
+        public coordinateSystemMode = GLTFLoaderCoordinateSystemMode.AUTO;
+        public animationStartMode = GLTFLoaderAnimationStartMode.FIRST;
+        public compileMaterials = false;
+        public useClipPlane = false;
+        public compileShadowGenerators = false;
+
+        public onDisposeObservable = new Observable<IGLTFLoader>();
+        public onMeshLoadedObservable = new Observable<AbstractMesh>();
+        public onTextureLoadedObservable = new Observable<BaseTexture>();
+        public onMaterialLoadedObservable = new Observable<Material>();
+        public onCompleteObservable = new Observable<IGLTFLoader>();
+
+        public dispose(): void {}
+        // #endregion
 
         public importMeshAsync(meshesNames: any, scene: Scene, data: IGLTFLoaderData, rootUrl: string, onSuccess: (meshes: AbstractMesh[], particleSystems: ParticleSystem[], skeletons: Skeleton[]) => void, onProgress: (event: SceneLoaderProgressEvent) => void, onError: (message: string) => void): boolean {
             scene.useRightHandedSystem = true;

+ 58 - 52
loaders/src/glTF/2.0/babylon.glTFLoader.ts

@@ -33,7 +33,7 @@ module BABYLON.GLTF2 {
         readonly BYTES_PER_ELEMENT: number;
     }
 
-    interface GLTFLoaderRequest extends XMLHttpRequest {
+    interface IGLTFLoaderFileRequest extends IFileRequest {
         _lengthComputable?: boolean;
         _loaded?: number;
         _total?: number;
@@ -44,7 +44,6 @@ module BABYLON.GLTF2 {
         public _babylonScene: Scene;
 
         private _disposed = false;
-        private _parent: GLTFFileLoader;
         private _rootUrl: string;
         private _defaultMaterial: PBRMaterial;
         private _defaultSampler = {} as IGLTFSampler;
@@ -53,7 +52,7 @@ module BABYLON.GLTF2 {
         private _progressCallback?: (event: SceneLoaderProgressEvent) => void;
         private _errorCallback?: (message: string, exception?: any) => void;
         private _renderReady = false;
-        private _requests = new Array<GLTFLoaderRequest>();
+        private _requests = new Array<IGLTFLoaderFileRequest>();
 
         private _renderReadyObservable = new Observable<GLTFLoader>();
 
@@ -79,9 +78,17 @@ module BABYLON.GLTF2 {
             GLTFLoaderExtension._Extensions.push(extension);
         }
 
-        public constructor(parent: GLTFFileLoader) {
-            this._parent = parent;
-        }
+        public coordinateSystemMode = GLTFLoaderCoordinateSystemMode.AUTO;
+        public animationStartMode = GLTFLoaderAnimationStartMode.FIRST;
+        public compileMaterials = false;
+        public useClipPlane = false;
+        public compileShadowGenerators = false;
+
+        public onDisposeObservable = new Observable<IGLTFLoader>();
+        public onMeshLoadedObservable = new Observable<AbstractMesh>();
+        public onTextureLoadedObservable = new Observable<BaseTexture>();
+        public onMaterialLoadedObservable = new Observable<Material>();
+        public onCompleteObservable = new Observable<IGLTFLoader>();
 
         public dispose(): void {
             if (this._disposed) {
@@ -90,21 +97,10 @@ module BABYLON.GLTF2 {
 
             this._disposed = true;
 
-            // Abort requests that are not complete
-            for (const request of this._requests) {
-                if (request.readyState !== (XMLHttpRequest.DONE || 4)) {
-                    request.abort();
-                }
-            }
+            this._abortRequests();
+            this._releaseResources();
 
-            // Revoke object urls created during load
-            if (this._gltf.textures) {
-                for (const texture of this._gltf.textures) {
-                    if (texture.url) {
-                        URL.revokeObjectURL(texture.url);
-                    }
-                }
-            }
+            this.onDisposeObservable.notifyObservers(this);
         }
 
         public importMeshAsync(meshesNames: any, scene: Scene, data: IGLTFLoaderData, rootUrl: string, onSuccess?: (meshes: AbstractMesh[], particleSystems: ParticleSystem[], skeletons: Skeleton[]) => void, onProgress?: (event: SceneLoaderProgressEvent) => void, onError?: (message: string, exception?: any) => void): void {
@@ -179,11 +175,10 @@ module BABYLON.GLTF2 {
         }
 
         private _onComplete(): void {
-            if (this._parent.onComplete) {
-                this._parent.onComplete();
-            }
+            this._abortRequests();
+            this._releaseResources();
 
-            this.dispose();
+            this.onCompleteObservable.notifyObservers(this);
         }
 
         private _loadData(data: IGLTFLoaderData): void {
@@ -258,7 +253,7 @@ module BABYLON.GLTF2 {
                 return;
             }
 
-            switch (this._parent.animationStartMode) {
+            switch (this.animationStartMode) {
                 case GLTFLoaderAnimationStartMode.NONE: {
                     // do nothing
                     break;
@@ -279,7 +274,7 @@ module BABYLON.GLTF2 {
                     break;
                 }
                 default: {
-                    Tools.Error("Invalid animation start mode " + this._parent.animationStartMode);
+                    Tools.Error("Invalid animation start mode " + this.animationStartMode);
                     return;
                 }
             }
@@ -297,7 +292,7 @@ module BABYLON.GLTF2 {
         private _loadScene(context: string, scene: IGLTFScene, nodeNames: any): void {
             this._rootNode = { babylonMesh: new Mesh("__root__", this._babylonScene) } as IGLTFNode;
 
-            switch (this._parent.coordinateSystemMode) {
+            switch (this.coordinateSystemMode) {
                 case GLTFLoaderCoordinateSystemMode.AUTO: {
                     if (!this._babylonScene.useRightHandedSystem) {
                         this._rootNode.rotation = [0, 1, 0, 0];
@@ -311,14 +306,12 @@ module BABYLON.GLTF2 {
                     break;
                 }
                 default: {
-                    Tools.Error("Invalid coordinate system mode " + this._parent.coordinateSystemMode);
+                    Tools.Error("Invalid coordinate system mode " + this.coordinateSystemMode);
                     return;
                 }
             }
 
-            if (this._parent.onMeshLoaded) {
-                this._parent.onMeshLoaded(this._rootNode.babylonMesh);
-            }
+            this.onMeshLoadedObservable.notifyObservers(this._rootNode.babylonMesh);
 
             let nodeIndices = scene.nodes;
 
@@ -415,9 +408,7 @@ module BABYLON.GLTF2 {
                 }
             }
 
-            if (this._parent.onMeshLoaded) {
-                this._parent.onMeshLoaded(node.babylonMesh);
-            }
+            this.onMeshLoadedObservable.notifyObservers(node.babylonMesh);
         }
 
         private _loadMesh(context: string, node: IGLTFNode, mesh: IGLTFMesh): void {
@@ -466,8 +457,8 @@ module BABYLON.GLTF2 {
                     }
 
                     this._loadMaterial("#/materials/" + material.index, material, (babylonMaterial, isNew) => {
-                        if (isNew && this._parent.onMaterialLoaded) {
-                            this._parent.onMaterialLoaded(babylonMaterial);
+                        if (isNew) {
+                            this.onMaterialLoadedObservable.notifyObservers(babylonMaterial);
                         }
                         node.babylonMesh.material = babylonMaterial;
                     });
@@ -490,8 +481,8 @@ module BABYLON.GLTF2 {
                         }
 
                         this._loadMaterial("#/materials/" + material.index, material, (babylonMaterial, isNew) => {
-                            if (isNew && this._parent.onMaterialLoaded) {
-                                this._parent.onMaterialLoaded(babylonMaterial);
+                            if (isNew) {
+                                this.onMaterialLoadedObservable.notifyObservers(babylonMaterial);
                             }
 
                             subMaterials[index] = babylonMaterial;
@@ -1559,10 +1550,7 @@ module BABYLON.GLTF2 {
             babylonTexture.wrapV = sampler.wrapV;
             babylonTexture.name = texture.name || "texture" + texture.index;
 
-            if (this._parent.onTextureLoaded) {
-                this._parent.onTextureLoaded(babylonTexture);
-            }
-
+            this.onTextureLoadedObservable.notifyObservers(babylonTexture);
             return babylonTexture;
         }
 
@@ -1618,13 +1606,12 @@ module BABYLON.GLTF2 {
                 this._tryCatchOnError(() => {
                     throw new LoadFileError(context + ": Failed to load '" + uri + "'" + (request ? ": " + request.status + " " + request.statusText : ""), request);
                 });
-            }, (oldRequest, newRequest) => {
-                this._requests.splice(this._requests.indexOf(oldRequest), 1, newRequest);
-            }) as GLTFLoaderRequest;
+            }) as IGLTFLoaderFileRequest;
 
-            if (request) {
-                this._requests.push(request);
-            }
+            this._requests.push(request);
+            request.onCompleteObservable.add(() => {
+                this._requests.splice(this._requests.indexOf(request), 1);
+            });
         }
 
         public _tryCatchOnError(handler: () => void): void {
@@ -1728,7 +1715,7 @@ module BABYLON.GLTF2 {
         }
 
         private _compileMaterialAsync(babylonMaterial: Material, babylonMesh: AbstractMesh, onSuccess: () => void): void {
-            if (this._parent.useClipPlane) {
+            if (this.useClipPlane) {
                 babylonMaterial.forceCompilation(babylonMesh, () => {
                     babylonMaterial.forceCompilation(babylonMesh, () => {
                         this._tryCatchOnError(onSuccess);
@@ -1743,7 +1730,7 @@ module BABYLON.GLTF2 {
         }
 
         private _compileMaterialsAsync(onSuccess: () => void): void {
-            if (!this._parent.compileMaterials || !this._gltf.materials) {
+            if (!this.compileMaterials || !this._gltf.materials) {
                 onSuccess();
                 return;
             }
@@ -1792,7 +1779,7 @@ module BABYLON.GLTF2 {
         }
 
         private _compileShadowGeneratorsAsync(onSuccess: () => void): void {
-            if (!this._parent.compileShadowGenerators) {
+            if (!this.compileShadowGenerators) {
                 onSuccess();
                 return;
             }
@@ -1823,7 +1810,26 @@ module BABYLON.GLTF2 {
                 }
             }
         }
+
+        private _abortRequests(): void {
+            for (const request of this._requests) {
+                request.abort();
+            }
+
+            this._requests.length = 0;
+        }
+
+        private _releaseResources(): void {
+            if (this._gltf.textures) {
+                for (const texture of this._gltf.textures) {
+                    if (texture.url) {
+                        URL.revokeObjectURL(texture.url);
+                        texture.url = undefined;
+                    }
+                }
+            }
+        }
     }
 
-    GLTFFileLoader.CreateGLTFLoaderV2 = parent => new GLTFLoader(parent);
+    GLTFFileLoader.CreateGLTFLoaderV2 = () => new GLTFLoader();
 }

+ 108 - 28
loaders/src/glTF/babylon.glTFFileLoader.ts

@@ -36,13 +36,25 @@ module BABYLON {
     }
 
     export interface IGLTFLoader extends IDisposable {
+        coordinateSystemMode: GLTFLoaderCoordinateSystemMode;
+        animationStartMode: GLTFLoaderAnimationStartMode;
+        compileMaterials: boolean;
+        useClipPlane: boolean;
+        compileShadowGenerators: boolean;
+
+        onDisposeObservable: Observable<IGLTFLoader>;
+        onMeshLoadedObservable: Observable<AbstractMesh>;
+        onTextureLoadedObservable: Observable<BaseTexture>;
+        onMaterialLoadedObservable: Observable<Material>;
+        onCompleteObservable: Observable<IGLTFLoader>;
+
         importMeshAsync: (meshesNames: any, scene: Scene, data: IGLTFLoaderData, rootUrl: string, onSuccess?: (meshes: AbstractMesh[], particleSystems: ParticleSystem[], skeletons: Skeleton[]) => void, onProgress?: (event: SceneLoaderProgressEvent) => void, onError?: (message: string, exception?: any) => void) => void;
         loadAsync: (scene: Scene, data: IGLTFLoaderData, rootUrl: string, onSuccess?: () => void, onProgress?: (event: SceneLoaderProgressEvent) => void, onError?: (message: string, exception?: any) => void) => void;
     }
 
     export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISceneLoaderPluginFactory {
-        public static CreateGLTFLoaderV1: (parent: GLTFFileLoader) => IGLTFLoader;
-        public static CreateGLTFLoaderV2: (parent: GLTFFileLoader) => IGLTFLoader;
+        public static CreateGLTFLoaderV1: () => IGLTFLoader;
+        public static CreateGLTFLoaderV2: () => IGLTFLoader;
 
         // #region Common options
 
@@ -51,7 +63,15 @@ module BABYLON {
          * The data.json property stores the glTF JSON.
          * The data.bin property stores the BIN chunk from a glTF binary or null if the input is not a glTF binary.
          */
-        public onParsed: (data: IGLTFLoaderData) => void;
+        public onParsedObservable = new Observable<IGLTFLoaderData>();
+
+        private _onParsedObserver: Nullable<Observer<IGLTFLoaderData>>;
+        public set onParsed(callback: (loaderData: IGLTFLoaderData) => void) {
+            if (this._onParsedObserver) {
+                this.onParsedObservable.remove(this._onParsedObserver);
+            }
+            this._onParsedObserver = this.onParsedObservable.add(callback);
+        }
 
         // #endregion
 
@@ -93,28 +113,73 @@ module BABYLON {
         /**
          * Raised when the loader creates a mesh after parsing the glTF properties of the mesh.
          */
-        public onMeshLoaded: (mesh: AbstractMesh) => void;
+        public onMeshLoadedObservable = new Observable<AbstractMesh>();
+
+        private _onMeshLoadedObserver: Nullable<Observer<AbstractMesh>>;
+        public set onMeshLoaded(callback: (mesh: AbstractMesh) => void) {
+            if (this._onMeshLoadedObserver) {
+                this.onMeshLoadedObservable.remove(this._onMeshLoadedObserver);
+            }
+            this._onMeshLoadedObserver = this.onMeshLoadedObservable.add(callback);
+        }
 
         /**
          * Raised when the loader creates a texture after parsing the glTF properties of the texture.
          */
-        public onTextureLoaded: (texture: BaseTexture) => void;
+        public onTextureLoadedObservable = new Observable<BaseTexture>();
+
+        private _onTextureLoadedObserver: Nullable<Observer<BaseTexture>>;
+        public set onTextureLoaded(callback: (Texture: BaseTexture) => void) {
+            if (this._onTextureLoadedObserver) {
+                this.onTextureLoadedObservable.remove(this._onTextureLoadedObserver);
+            }
+            this._onTextureLoadedObserver = this.onTextureLoadedObservable.add(callback);
+        }
 
         /**
          * Raised when the loader creates a material after parsing the glTF properties of the material.
          */
-        public onMaterialLoaded: (material: Material) => void;
+        public onMaterialLoadedObservable = new Observable<Material>();
+
+        private _onMaterialLoadedObserver: Nullable<Observer<Material>>;
+        public set onMaterialLoaded(callback: (Material: Material) => void) {
+            if (this._onMaterialLoadedObserver) {
+                this.onMaterialLoadedObservable.remove(this._onMaterialLoadedObserver);
+            }
+            this._onMaterialLoadedObserver = this.onMaterialLoadedObservable.add(callback);
+        }
 
         /**
          * Raised when the asset is completely loaded, immediately 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, immediately after onSuccess.
          */
-        public onComplete: () => void;
+        public onCompleteObservable = new Observable<GLTFFileLoader>();
+
+        private _onCompleteObserver: Nullable<Observer<GLTFFileLoader>>;
+        public set onComplete(callback: () => void) {
+            if (this._onCompleteObserver) {
+                this.onCompleteObservable.remove(this._onCompleteObserver);
+            }
+            this._onCompleteObserver = this.onCompleteObservable.add(callback);
+        }
+
+        /**
+        * Raised when the loader is disposed.
+        */
+        public onDisposeObservable = new Observable<GLTFFileLoader>();
+
+        private _onDisposeObserver: Nullable<Observer<GLTFFileLoader>>;
+        public set onDispose(callback: () => void) {
+            if (this._onDisposeObserver) {
+                this.onDisposeObservable.remove(this._onDisposeObserver);
+            }
+            this._onDisposeObserver = this.onDisposeObservable.add(callback);
+        }
 
         // #endregion
 
-        private _loader: IGLTFLoader;
+        private _loader: Nullable<IGLTFLoader> = null;
 
         public name = "gltf";
 
@@ -129,17 +194,22 @@ module BABYLON {
         public dispose(): void {
             if (this._loader) {
                 this._loader.dispose();
+                this._loader = null;
             }
+
+            this.onParsedObservable.clear();
+            this.onMeshLoadedObservable.clear();
+            this.onTextureLoadedObservable.clear();
+            this.onMaterialLoadedObservable.clear();
+            this.onCompleteObservable.clear();
+
+            this.onDisposeObservable.notifyObservers(this);
+            this.onDisposeObservable.clear();
         }
 
         public importMeshAsync(meshesNames: any, scene: Scene, data: any, rootUrl: string, onSuccess?: (meshes: AbstractMesh[], particleSystems: ParticleSystem[], skeletons: Skeleton[]) => void, onProgress?: (event: SceneLoaderProgressEvent) => void, onError?: (message: string, exception?: any) => void): void {
             try {
-                const loaderData = GLTFFileLoader._parse(data);
-
-                if (this.onParsed) {
-                    this.onParsed(loaderData);
-                }
-
+                const loaderData = this._parse(data);
                 this._loader = this._getLoader(loaderData);
                 this._loader.importMeshAsync(meshesNames, scene, loaderData, rootUrl, onSuccess, onProgress, onError);
             }
@@ -155,12 +225,7 @@ module BABYLON {
 
         public loadAsync(scene: Scene, data: string | ArrayBuffer, rootUrl: string, onSuccess?: () => void, onProgress?: (event: SceneLoaderProgressEvent) => void, onError?: (message: string, exception?: any) => void): void {
             try {
-                const loaderData = GLTFFileLoader._parse(data);
-
-                if (this.onParsed) {
-                    this.onParsed(loaderData);
-                }
-
+                const loaderData = this._parse(data);
                 this._loader = this._getLoader(loaderData);
                 this._loader.loadAsync(scene, loaderData, rootUrl, onSuccess, onProgress, onError);
             }
@@ -184,15 +249,20 @@ module BABYLON {
             return new GLTFFileLoader();
         }
 
-        private static _parse(data: string | ArrayBuffer): IGLTFLoaderData {
+        private _parse(data: string | ArrayBuffer): IGLTFLoaderData {
+            let parsedData: IGLTFLoaderData;
             if (data instanceof ArrayBuffer) {
-                return GLTFFileLoader._parseBinary(data);
+                parsedData = GLTFFileLoader._parseBinary(data);
+            }
+            else {
+                parsedData = {
+                    json: JSON.parse(data),
+                    bin: null
+                };
             }
 
-            return {
-                json: JSON.parse(data),
-                bin: null
-            };
+            this.onParsedObservable.notifyObservers(parsedData);
+            return parsedData;
         }
 
         private _getLoader(loaderData: IGLTFLoaderData): IGLTFLoader {
@@ -216,7 +286,7 @@ module BABYLON {
                 }
             }
 
-            const createLoaders: { [key: number]: (parent: GLTFFileLoader) => IGLTFLoader } = {
+            const createLoaders: { [key: number]: () => IGLTFLoader } = {
                 1: GLTFFileLoader.CreateGLTFLoaderV1,
                 2: GLTFFileLoader.CreateGLTFLoaderV2
             };
@@ -226,7 +296,17 @@ module BABYLON {
                 throw new Error("Unsupported version: " + asset.version);
             }
 
-            return createLoader(this);
+            const loader = createLoader();
+            loader.coordinateSystemMode = this.coordinateSystemMode;
+            loader.animationStartMode = this.animationStartMode;
+            loader.compileMaterials = this.compileMaterials;
+            loader.useClipPlane = this.useClipPlane;
+            loader.compileShadowGenerators = this.compileShadowGenerators;
+            loader.onMeshLoadedObservable.add(mesh => this.onMeshLoadedObservable.notifyObservers(mesh));
+            loader.onTextureLoadedObservable.add(texture => this.onTextureLoadedObservable.notifyObservers(texture));
+            loader.onMaterialLoadedObservable.add(material => this.onMaterialLoadedObservable.notifyObservers(material));
+            loader.onCompleteObservable.add(() => this.onCompleteObservable.notifyObservers(this));
+            return loader;
         }
 
         private static _parseBinary(data: ArrayBuffer): IGLTFLoaderData {

+ 1 - 1
src/Audio/babylon.sound.ts

@@ -129,7 +129,7 @@ module BABYLON {
                                 if (codecSupportedFound) {
                                     // Loading sound using XHR2
                                     if (!this._streaming) {
-                                        Tools.LoadFile(url, (data) => { this._soundLoaded(data as ArrayBuffer); }, undefined, this._scene.database, true);
+                                        this._scene._loadFile(url, (data) => { this._soundLoaded(data as ArrayBuffer); }, undefined, true, true);
                                     }
                                     // Streaming sound using HTML5 Audio tag
                                     else {

+ 49 - 37
src/Engine/babylon.engine.ts

@@ -161,38 +161,6 @@
         }
     };
 
-    var partialLoadFile = (url: string, index: number, loadedFiles: (string | ArrayBuffer)[], scene: Nullable<Scene>,
-        onfinish: (files: (string | ArrayBuffer)[]) => void, onErrorCallBack: Nullable<(message?: string, exception?: any) => void> = null) => {
-
-        var onload = (data: string | ArrayBuffer) => {
-            loadedFiles[index] = data;
-            (<any>loadedFiles)._internalCount++;
-
-            if ((<any>loadedFiles)._internalCount === 6) {
-                onfinish(loadedFiles);
-            }
-        };
-
-        const onerror = (request?: XMLHttpRequest, exception?: any) => {
-            if (onErrorCallBack && request) {
-                onErrorCallBack(request.status + " " + request.statusText, exception);
-            }
-        };
-
-        Tools.LoadFile(url, onload, undefined, undefined, true, onerror);
-    }
-
-    var cascadeLoadFiles = (rootUrl: string, scene: Nullable<Scene>,
-        onfinish: (images: (string | ArrayBuffer)[]) => void, files: string[], onError: Nullable<(message?: string, exception?: any) => void> = null) => {
-
-        var loadedFiles: (string | ArrayBuffer)[] = [];
-        (<any>loadedFiles)._internalCount = 0;
-
-        for (let index = 0; index < 6; index++) {
-            partialLoadFile(files[index], index, loadedFiles, scene, onfinish, onError);
-        }
-    };
-
     class BufferPointer {
         public active: boolean;
         public index: number;
@@ -792,6 +760,8 @@
 
         private _nextFreeTextureSlot = 0;
 
+        private _activeRequests = new Array<IFileRequest>();
+
         // Hardware supported Compressed Textures
         private _texturesSupported = new Array<string>();
         private _textureFormatInUse: Nullable<string>;
@@ -3143,7 +3113,7 @@
                 }
 
                 if (!buffer) {
-                    Tools.LoadFile(url, data => {
+                    this._loadFile(url, data => {
                         if (callback) {
                             callback(data);
                         }
@@ -4007,7 +3977,7 @@
             }
 
             if (isKTX) {
-                Tools.LoadFile(rootUrl, data => {
+                this._loadFile(rootUrl, data => {
                     var ktx = new Internals.KhronosTextureContainer(data, 6);
 
                     var loadMipmap = ktx.numberOfMipmapLevels > 1 && !noMipmap;
@@ -4025,7 +3995,7 @@
                 }, undefined, undefined, true, onerror);
             } else if (isDDS) {
                 if (files && files.length === 6) {
-                    cascadeLoadFiles(rootUrl,
+                    this._cascadeLoadFiles(rootUrl,
                         scene,
                         imgs => {
                             var info: Internals.DDSInfo | undefined;
@@ -4063,7 +4033,7 @@
                         onError);
 
                 } else {
-                    Tools.LoadFile(rootUrl,
+                    this._loadFile(rootUrl,
                         data => {
                             var info = Internals.DDSTools.GetDDSInfo(data);
 
@@ -4346,7 +4316,7 @@
                 }
             };
 
-            Tools.LoadFile(url, data => {
+            this._loadFile(url, data => {
                 internalCallback(data);
             }, undefined, scene.database, true, onerror);
 
@@ -5112,6 +5082,11 @@
             this.onEndFrameObservable.clear();
 
             Effect.ResetCache();
+
+            // Abort active requests
+            for (let request of this._activeRequests) {
+                request.abort();
+            }
         }
 
         // Loading screen
@@ -5536,6 +5511,43 @@
             this._gl.bindBufferBase(this._gl.TRANSFORM_FEEDBACK_BUFFER, 0, value);
         }
 
+        public _loadFile(url: string, onSuccess: (data: string | ArrayBuffer, responseURL?: string) => void, onProgress?: (data: any) => void, database?: Database, useArrayBuffer?: boolean, onError?: (request?: XMLHttpRequest, exception?: any) => void): IFileRequest {
+            let request = Tools.LoadFile(url, onSuccess, onProgress, database, useArrayBuffer, onError);
+            this._activeRequests.push(request);
+            request.onCompleteObservable.add(request => {
+                this._activeRequests.splice(this._activeRequests.indexOf(request), 1);
+            });
+            return request;
+        }
+
+        private _partialLoadFile(url: string, index: number, loadedFiles: (string | ArrayBuffer)[], scene: Nullable<Scene>, onfinish: (files: (string | ArrayBuffer)[]) => void, onErrorCallBack: Nullable<(message?: string, exception?: any) => void> = null): void {
+            var onload = (data: string | ArrayBuffer) => {
+                loadedFiles[index] = data;
+                (<any>loadedFiles)._internalCount++;
+
+                if ((<any>loadedFiles)._internalCount === 6) {
+                    onfinish(loadedFiles);
+                }
+            };
+
+            const onerror = (request?: XMLHttpRequest, exception?: any) => {
+                if (onErrorCallBack && request) {
+                    onErrorCallBack(request.status + " " + request.statusText, exception);
+                }
+            };
+
+            this._loadFile(url, onload, undefined, undefined, true, onerror);
+        }
+
+        private _cascadeLoadFiles(rootUrl: string, scene: Nullable<Scene>, onfinish: (images: (string | ArrayBuffer)[]) => void, files: string[], onError: Nullable<(message?: string, exception?: any) => void> = null): void {
+            var loadedFiles: (string | ArrayBuffer)[] = [];
+            (<any>loadedFiles)._internalCount = 0;
+
+            for (let index = 0; index < 6; index++) {
+                this._partialLoadFile(files[index], index, loadedFiles, scene, onfinish, onError);
+            }
+        }
+
         // Statics
         public static isSupported(): boolean {
             try {

+ 48 - 20
src/Loading/babylon.sceneLoader.ts

@@ -155,9 +155,9 @@
             return null;
         }
 
-        private static _loadData(rootUrl: string, sceneFilename: string, scene: Scene, onSuccess: (plugin: ISceneLoaderPlugin | ISceneLoaderPluginAsync, data: any, responseURL?: string) => void, onProgress: ((event: SceneLoaderProgressEvent) => void) | undefined, onError: (message: string, exception?: any) => void, pluginExtension: Nullable<string>): ISceneLoaderPlugin | ISceneLoaderPluginAsync {
-            var directLoad = SceneLoader._getDirectLoad(sceneFilename);
-            var registeredPlugin = pluginExtension ? SceneLoader._getPluginForExtension(pluginExtension) : (directLoad ? SceneLoader._getPluginForDirectLoad(sceneFilename) : SceneLoader._getPluginForFilename(sceneFilename));
+        private static _loadData(rootUrl: string, sceneFilename: string, scene: Scene, onSuccess: (plugin: ISceneLoaderPlugin | ISceneLoaderPluginAsync, data: any, responseURL?: string) => void, onProgress: ((event: SceneLoaderProgressEvent) => void) | undefined, onError: (message: string, exception?: any) => void, onDispose: () => void, pluginExtension: Nullable<string>): ISceneLoaderPlugin | ISceneLoaderPluginAsync {
+            let directLoad = SceneLoader._getDirectLoad(sceneFilename);
+            let registeredPlugin = pluginExtension ? SceneLoader._getPluginForExtension(pluginExtension) : (directLoad ? SceneLoader._getPluginForDirectLoad(sceneFilename) : SceneLoader._getPluginForFilename(sceneFilename));
 
             let plugin: ISceneLoaderPlugin | ISceneLoaderPluginAsync;
             if ((registeredPlugin.plugin as ISceneLoaderPluginFactory).createPlugin) {
@@ -167,12 +167,12 @@
                 plugin = <any>registeredPlugin.plugin;
             }
 
-            var useArrayBuffer = registeredPlugin.isBinary;
-            var database: Database;
+            let useArrayBuffer = registeredPlugin.isBinary;
+            let database: Database;
 
             SceneLoader.OnPluginActivatedObservable.notifyObservers(plugin);
 
-            var dataCallback = (data: any, responseURL?: string) => {
+            let dataCallback = (data: any, responseURL?: string) => {
                 if (scene.isDisposed) {
                     onError("Scene has been disposed");
                     return;
@@ -183,13 +183,32 @@
                 onSuccess(plugin, data, responseURL);
             };
 
-            var manifestChecked = (success: any) => {
-                Tools.LoadFile(rootUrl + sceneFilename, dataCallback, onProgress ? event => {
-                    onProgress(SceneLoaderProgressEvent.FromProgressEvent(event));
-                }: undefined, database, useArrayBuffer, (request, exception) => {
+            let request: Nullable<IFileRequest> = null;
+            let pluginDisposed = false;
+            let onDisposeObservable = (plugin as any).onDisposeObservable as Observable<ISceneLoaderPlugin | ISceneLoaderPluginAsync>;
+            if (onDisposeObservable) {
+                onDisposeObservable.add(() => {
+                    pluginDisposed = true;
+
                     if (request) {
-                        onError(request.status + " " + request.statusText, exception);
+                        request.abort();
+                        request = null;
                     }
+
+                    onDispose();
+                });
+            }
+
+            let manifestChecked = () => {
+                if (pluginDisposed) {
+                    return;
+                }
+
+                let url = rootUrl + sceneFilename;
+                request = Tools.LoadFile(url, dataCallback, onProgress ? event => {
+                    onProgress(SceneLoaderProgressEvent.FromProgressEvent(event));
+                }: undefined, database, useArrayBuffer, (request, exception) => {
+                    onError("Failed to load scene." + (exception ? "" : " " + exception.message), exception);
                 });
             };
 
@@ -204,17 +223,17 @@
                     database = new Database(rootUrl + sceneFilename, manifestChecked);
                 }
                 else {
-                    manifestChecked(true);
+                    manifestChecked();
                 }
             }
             // Loading file from disk via input file or drag'n'drop
             else {
-                var fileOrString = <any>sceneFilename;
+                let fileOrString = <any>sceneFilename;
 
                 if (fileOrString.name) { // File
-                    Tools.ReadFile(fileOrString, dataCallback, onProgress, useArrayBuffer);
+                    request = Tools.ReadFile(fileOrString, dataCallback, onProgress, useArrayBuffer);
                 } else if (FilesInput.FilesToLoad[sceneFilename]) {
-                    Tools.ReadFile(FilesInput.FilesToLoad[sceneFilename], dataCallback, onProgress, useArrayBuffer);
+                    request = Tools.ReadFile(FilesInput.FilesToLoad[sceneFilename], dataCallback, onProgress, useArrayBuffer);
                 } else {
                     onError("Unable to find file named " + sceneFilename);
                 }
@@ -269,6 +288,10 @@
             var loadingToken = {};
             scene._addPendingData(loadingToken);
 
+            var disposeHandler = () => {
+                scene._removePendingData(loadingToken);
+            };
+
             var errorHandler = (message: string, exception?: any) => {
                 let errorMessage = "Unable to import meshes from " + rootUrl + sceneFilename + ": " + message;
 
@@ -279,7 +302,7 @@
                     // should the exception be thrown?
                 }
 
-                scene._removePendingData(loadingToken);
+                disposeHandler();
             };
 
             var progressHandler = onProgress ? (event: SceneLoaderProgressEvent) => {
@@ -331,7 +354,7 @@
                         successHandler(meshes, particleSystems, skeletons);
                     }, progressHandler, errorHandler);
                 }
-            }, progressHandler, errorHandler, pluginExtension);
+            }, progressHandler, errorHandler, disposeHandler, pluginExtension);
         }
 
         /**
@@ -369,6 +392,11 @@
             var loadingToken = {};
             scene._addPendingData(loadingToken);
 
+            var disposeHandler = () => {
+                scene._removePendingData(loadingToken);
+                scene.getEngine().hideLoadingUI();
+            };
+
             var errorHandler = (message: Nullable<string>, exception?: any) => {
                 let errorMessage = "Unable to load from " + rootUrl + sceneFilename + (message ? ": " + message : "");
                 if (onError) {
@@ -377,8 +405,8 @@
                     Tools.Error(errorMessage);
                     // should the exception be thrown?
                 }
-                scene._removePendingData(loadingToken);
-                scene.getEngine().hideLoadingUI();
+
+                disposeHandler();
             };
 
             var progressHandler = onProgress ? (event: SceneLoaderProgressEvent) => {
@@ -425,7 +453,7 @@
                         scene.getEngine().hideLoadingUI();
                     });
                 }
-            }, progressHandler, errorHandler, pluginExtension);
+            }, progressHandler, errorHandler, disposeHandler, pluginExtension);
         }
     };
 }

+ 8 - 1
src/Materials/Textures/babylon.colorGradingTexture.ts

@@ -169,7 +169,14 @@ module BABYLON {
                 }
             }
 
-            Tools.LoadFile(this.url, callback);
+            let scene = this.getScene();
+            if (scene) {
+                scene._loadFile(this.url, callback);
+            }
+            else {
+                this._engine._loadFile(this.url, callback);
+            }
+
             return this._texture;
         }
 

+ 3 - 3
src/Materials/babylon.effect.ts

@@ -303,7 +303,7 @@
             }
 
             // Vertex shader
-            Tools.LoadFile(vertexShaderUrl + ".vertex.fx", callback);
+            this._engine._loadFile(vertexShaderUrl + ".vertex.fx", callback);
         }
 
         public _loadFragmentShader(fragment: any, callback: (data: any) => void): void {
@@ -343,7 +343,7 @@
             }
 
             // Fragment shader
-            Tools.LoadFile(fragmentShaderUrl + ".fragment.fx", callback);
+            this._engine._loadFile(fragmentShaderUrl + ".fragment.fx", callback);
         }
 
         private _dumpShadersSource(vertexCode: string, fragmentCode: string, defines: string): void {
@@ -491,7 +491,7 @@
                 } else {
                     var includeShaderUrl = Engine.ShadersRepository + "ShadersInclude/" + includeFile + ".fx";
 
-                    Tools.LoadFile(includeShaderUrl, (fileContent) => {
+                    this._engine._loadFile(includeShaderUrl, (fileContent) => {
                         Effect.IncludesShadersStore[includeFile] = fileContent as string;
                         this._processIncludes(<string>returnValue, callback);
                     });

+ 2 - 2
src/Mesh/babylon.geometry.ts

@@ -557,7 +557,7 @@
             }
 
             scene._addPendingData(this);
-            Tools.LoadFile(this.delayLoadingFile, data => {
+            scene._loadFile(this.delayLoadingFile, data => {
                 if (!this._delayLoadingFunction) {
                     return;
                 }
@@ -578,7 +578,7 @@
                 if (onLoaded) {
                     onLoaded();
                 }
-            }, () => { }, scene.database);
+            }, undefined, true);
         }
 
         /**

+ 4 - 5
src/Tools/babylon.assetsManager.ts

@@ -119,10 +119,10 @@ module BABYLON {
         }
 
         public runTask(scene: Scene, onSuccess: () => void, onError: (message?: string, exception?: any) => void) {
-            Tools.LoadFile(this.url, (data) => {
+            scene._loadFile(this.url, (data) => {
                 this.text = data as string;
                 onSuccess();
-            }, undefined, scene.database, false, (request, exception) => {
+            }, undefined, false, true, (request, exception) => {
                 if (request) {
                     onError(request.status + " " + request.statusText, exception);
                 }
@@ -141,11 +141,10 @@ module BABYLON {
         }
 
         public runTask(scene: Scene, onSuccess: () => void, onError: (message?: string, exception?: any) => void) {
-            Tools.LoadFile(this.url, (data) => {
-
+            scene._loadFile(this.url, (data) => {
                 this.data = data as ArrayBuffer;
                 onSuccess();
-            }, undefined, scene.database, true, (request, exception) => {
+            }, undefined, true, true, (request, exception) => {
                 if (request) {
                     onError(request.status + " " + request.statusText, exception);
                 }

+ 141 - 67
src/Tools/babylon.tools.ts

@@ -30,6 +30,18 @@
         }
     }
 
+    export interface IFileRequest {
+        /**
+         * Raised when the request is complete (success or error).
+         */
+        onCompleteObservable: Observable<IFileRequest>;
+
+        /**
+         * Aborts the request for a file.
+         */
+        abort: () => void;
+    }
+
     // Screenshots
     var screenshotCanvas: HTMLCanvasElement;
 
@@ -514,100 +526,141 @@
             return img;
         }
 
-        public static LoadFile(url: string, callback: (data: string | ArrayBuffer, responseURL?: string) => void, progressCallBack?: (data: any) => void, database?: Database, useArrayBuffer?: boolean, onError?: (request?: XMLHttpRequest, exception?: any) => void, onRetry?: (oldRequest: XMLHttpRequest, newRequest: XMLHttpRequest) => void, retryStrategy: Nullable<(url: string, request: XMLHttpRequest, retryIndex: number) => number> = null): Nullable<XMLHttpRequest> {
+        public static LoadFile(url: string, onSuccess: (data: string | ArrayBuffer, responseURL?: string) => void, onProgress?: (data: any) => void, database?: Database, useArrayBuffer?: boolean, onError?: (request?: XMLHttpRequest, exception?: any) => void): IFileRequest {
             url = Tools.CleanUrl(url);
 
             url = Tools.PreprocessUrl(url);
 
-            let request: Nullable<XMLHttpRequest> = null;
+            // If file and file input are set
+            if (url.indexOf("file:") !== -1) {
+                const fileName = decodeURIComponent(url.substring(5).toLowerCase());
+                if (FilesInput.FilesToLoad[fileName]) {
+                    return Tools.ReadFile(FilesInput.FilesToLoad[fileName], onSuccess, onProgress, useArrayBuffer);
+                }
+            }
+
+            const loadUrl = Tools.BaseUrl + url;
+
             let aborted = false;
+            const fileRequest: IFileRequest = {
+                onCompleteObservable: new Observable<IFileRequest>(),
+                abort: () => aborted = true,
+            };
 
-            let noIndexedDB = (retryIndex?: number) => {
-                if (aborted) {
-                    return;
-                }
+            const requestFile = () => {
+                let request = new XMLHttpRequest();
+                let retryHandle: Nullable<number> = null;
 
-                let oldRequest = request;
-                request = new XMLHttpRequest();
+                fileRequest.abort = () => {
+                    aborted = true;
 
-                let loadUrl = Tools.BaseUrl + url;
-                request.open('GET', loadUrl, true);
+                    if (request.readyState !== (XMLHttpRequest.DONE || 4)) {
+                        request.abort();
+                    }
 
-                if (useArrayBuffer) {
-                    request.responseType = "arraybuffer";
-                }
+                    if (retryHandle !== null) {
+                        clearTimeout(retryHandle);
+                        retryHandle = null;
+                    }
+                };
 
-                if (progressCallBack) {
-                    request.onprogress = progressCallBack;
-                }
+                const retryLoop = (retryIndex: number) => {
+                    request.open('GET', loadUrl, true);
 
-                request.addEventListener("abort", () => {
-                    aborted = true;
-                });
+                    if (useArrayBuffer) {
+                        request.responseType = "arraybuffer";
+                    }
+
+                    if (onProgress) {
+                        request.addEventListener("progress", onProgress);
+                    }
 
-                let onreadystatechange = () => {
-                    let req = <XMLHttpRequest>request;
-                    // In case of undefined state in some browsers.
-                    if (req.readyState === (XMLHttpRequest.DONE || 4)) {
-                        // Some browsers have issues where onreadystatechange can be called multiple times with the same value.
-                        req.removeEventListener("readystatechange", onreadystatechange);
+                    const onLoadEnd = () => {
+                        fileRequest.onCompleteObservable.notifyObservers(fileRequest);
+                    };
 
-                        if (req.status >= 200 && req.status < 300 || (!Tools.IsWindowObjectExist() && (req.status === 0))) {
-                            callback(!useArrayBuffer ? req.responseText : <ArrayBuffer>req.response, req.responseURL);
+                    request.addEventListener("loadend", onLoadEnd);
+
+                    const onReadyStateChange = () => {
+                        if (aborted) {
                             return;
                         }
 
-                        retryStrategy = retryStrategy || Tools.DefaultRetryStrategy;
-                        if (retryStrategy) {
-                            let waitTime = retryStrategy(loadUrl, req, retryIndex || 0);
-                            if (waitTime !== -1) {
-                                setTimeout(() => noIndexedDB((retryIndex || 0) + 1), waitTime);
+                        // In case of undefined state in some browsers.
+                        if (request.readyState === (XMLHttpRequest.DONE || 4)) {
+                            // Some browsers have issues where onreadystatechange can be called multiple times with the same value.
+                            request.removeEventListener("readystatechange", onReadyStateChange);
+
+                            if (request.status >= 200 && request.status < 300 || (!Tools.IsWindowObjectExist() && (request.status === 0))) {
+                                onSuccess(!useArrayBuffer ? request.responseText : <ArrayBuffer>request.response, request.responseURL);
                                 return;
                             }
-                        }
 
-                        let e = new LoadFileError("Error status: " + req.status + " - Unable to load " + loadUrl);
-                        if (onError) {
-                            onError(req, e);
-                        } else {
-                            throw e;
-                        }
-                    }
-                };
+                            let retryStrategy = Tools.DefaultRetryStrategy;
+                            if (retryStrategy) {
+                                let waitTime = retryStrategy(loadUrl, request, retryIndex);
+                                if (waitTime !== -1) {
+                                    // Prevent the request from completing for retry.
+                                    request.removeEventListener("loadend", onLoadEnd);
+                                    request = new XMLHttpRequest();
+                                    retryHandle = setTimeout(() => retryLoop(retryIndex + 1), waitTime);
+                                    return;
+                                }
+                            }
 
-                request.addEventListener("readystatechange", onreadystatechange);
+                            let e = new LoadFileError("Error status: " + request.status + " " + request.statusText + " - Unable to load " + loadUrl, request);
+                            if (onError) {
+                                onError(request, e);
+                            } else {
+                                throw e;
+                            }
+                        }
+                    };
 
-                request.send();
+                    request.addEventListener("readystatechange", onReadyStateChange);
 
-                if (oldRequest && onRetry) {
-                    onRetry(oldRequest, request);
-                }
-            };
+                    request.send();
+                };
 
-            let loadFromIndexedDB = () => {
-                if (database) {
-                    database.loadFileFromDB(url, callback, progressCallBack, noIndexedDB, useArrayBuffer);
-                }
+                retryLoop(0);
             };
 
-            // If file and file input are set
-            if (url.indexOf("file:") !== -1) {
-                let fileName = decodeURIComponent(url.substring(5).toLowerCase());
-                if (FilesInput.FilesToLoad[fileName]) {
-                    Tools.ReadFile(FilesInput.FilesToLoad[fileName], callback, progressCallBack, useArrayBuffer);
-                    return request;
-                }
-            }
-
             // Caching all files
             if (database && database.enableSceneOffline) {
+                const noIndexedDB = () => {
+                    if (!aborted) {
+                        requestFile();
+                    }
+                };
+
+                const loadFromIndexedDB = () => {
+                    // TODO: database needs to support aborting and should return a IFileRequest
+                    if (aborted) {
+                        return;
+                    }
+
+                    if (database) {
+                        database.loadFileFromDB(url, data => {
+                            if (!aborted) {
+                                onSuccess(data);
+                            }
+
+                            fileRequest.onCompleteObservable.notifyObservers(fileRequest);
+                        }, onProgress ? event => {
+                            if (!aborted) {
+                                onProgress(event);
+                            }
+                        } : undefined, noIndexedDB, useArrayBuffer);
+                    }
+                };
+
                 database.openAsync(loadFromIndexedDB, noIndexedDB);
             }
             else {
-                noIndexedDB();
+                requestFile();
             }
 
-            return request;
+            return fileRequest;
         }
 
         /** 
@@ -635,18 +688,38 @@
             head.appendChild(script);
         }
 
-        public static ReadFileAsDataURL(fileToLoad: Blob, callback: (data: any) => void, progressCallback: (this: MSBaseReader, ev: ProgressEvent) => any): void {
-            var reader = new FileReader();
+        public static ReadFileAsDataURL(fileToLoad: Blob, callback: (data: any) => void, progressCallback: (this: MSBaseReader, ev: ProgressEvent) => any): IFileRequest {
+            let reader = new FileReader();
+
+            let request: IFileRequest = {
+                onCompleteObservable: new Observable<IFileRequest>(),
+                abort: () => reader.abort(),
+            };
+
+            reader.onloadend = e => {
+                request.onCompleteObservable.notifyObservers(request);
+            };
+
             reader.onload = e => {
                 //target doesn't have result from ts 1.3
                 callback((<any>e.target)['result']);
             };
+
             reader.onprogress = progressCallback;
+
             reader.readAsDataURL(fileToLoad);
+
+            return request;
         }
 
-        public static ReadFile(fileToLoad: File, callback: (data: any) => void, progressCallBack?: (this: MSBaseReader, ev: ProgressEvent) => any, useArrayBuffer?: boolean): void {
-            var reader = new FileReader();
+        public static ReadFile(fileToLoad: File, callback: (data: any) => void, progressCallBack?: (this: MSBaseReader, ev: ProgressEvent) => any, useArrayBuffer?: boolean): IFileRequest {
+            let reader = new FileReader();
+            let request: IFileRequest = {
+                onCompleteObservable: new Observable<IFileRequest>(),
+                abort: () => reader.abort(),
+            };
+
+            reader.onloadend = e => request.onCompleteObservable.notifyObservers(request);
             reader.onerror = e => {
                 Tools.Log("Error while reading file: " + fileToLoad.name);
                 callback(JSON.stringify({ autoClear: true, clearColor: [1, 0, 0], ambientColor: [0, 0, 0], gravity: [0, -9.807, 0], meshes: [], cameras: [], lights: [] }));
@@ -655,7 +728,6 @@
                 //target doesn't have result from ts 1.3
                 callback((<any>e.target)['result']);
             };
-
             if (progressCallBack) {
                 reader.onprogress = progressCallBack;
             }
@@ -666,6 +738,8 @@
             else {
                 reader.readAsArrayBuffer(fileToLoad);
             }
+
+            return request;
         }
 
         //returns a downloadable url to a file content.

+ 15 - 0
src/babylon.scene.ts

@@ -870,6 +870,7 @@
         private _alternateProjectionUpdateFlag = -1;
 
         public _toBeDisposed = new SmartArray<Nullable<IDisposable>>(256);
+        private _activeRequests = new Array<IFileRequest>();
         private _pendingData = new Array();
         private _isDisposed = false;
 
@@ -3812,6 +3813,11 @@
             this._meshesForIntersections.dispose();
             this._toBeDisposed.dispose();
 
+            // Abort active requests
+            for (let request of this._activeRequests) {
+                request.abort();
+            }
+
             // Debug layer
             if (this._debugLayer) {
                 this._debugLayer.hide();
@@ -4587,5 +4593,14 @@
                 material.markAsDirty(flag);
             }
         }
+
+        public _loadFile(url: string, onSuccess: (data: string | ArrayBuffer, responseURL?: string) => void, onProgress?: (data: any) => void, useDatabase?: boolean, useArrayBuffer?: boolean, onError?: (request?: XMLHttpRequest, exception?: any) => void): IFileRequest {
+            let request = Tools.LoadFile(url, onSuccess, onProgress, useDatabase ? this.database : undefined, useArrayBuffer, onError);
+            this._activeRequests.push(request);
+            request.onCompleteObservable.add(request => {
+                this._activeRequests.splice(this._activeRequests.indexOf(request), 1);
+            });
+            return request;
+        }
     }
 }