Prechádzať zdrojové kódy

Merge pull request #6935 from bghgary/range-request

Added support for using HTTP range requests from a glTF binary
David Catuhe 5 rokov pred
rodič
commit
d9a2b9ea8d
29 zmenil súbory, kde vykonal 858 pridanie a 418 odobranie
  1. 2 0
      dist/preview release/what's new.md
  2. 1 1
      inspector/src/components/actionTabs/lines/floatLineComponent.tsx
  3. 4 4
      inspector/src/components/actionTabs/tabs/tools/gltfComponent.tsx
  4. 3 2
      loaders/src/glTF/1.0/glTFBinaryExtension.ts
  5. 3 2
      loaders/src/glTF/2.0/Extensions/EXT_lights_image_based.ts
  6. 2 1
      loaders/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts
  7. 3 2
      loaders/src/glTF/2.0/Extensions/KHR_lights_punctual.ts
  8. 2 1
      loaders/src/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.ts
  9. 2 1
      loaders/src/glTF/2.0/Extensions/KHR_materials_unlit.ts
  10. 2 1
      loaders/src/glTF/2.0/Extensions/KHR_texture_transform.ts
  11. 2 1
      loaders/src/glTF/2.0/Extensions/MSFT_audio_emitter.ts
  12. 71 5
      loaders/src/glTF/2.0/Extensions/MSFT_lod.ts
  13. 2 1
      loaders/src/glTF/2.0/Extensions/MSFT_minecraftMesh.ts
  14. 2 1
      loaders/src/glTF/2.0/Extensions/MSFT_sRGBFactors.ts
  15. 56 32
      loaders/src/glTF/2.0/glTFLoader.ts
  16. 12 2
      loaders/src/glTF/2.0/glTFLoaderExtension.ts
  17. 101 0
      loaders/src/glTF/dataReader.ts
  18. 269 207
      loaders/src/glTF/glTFFileLoader.ts
  19. 103 63
      src/Loading/sceneLoader.ts
  20. 3 11
      src/Materials/Node/nodeMaterial.ts
  21. 12 0
      src/Misc/baseError.ts
  22. 106 35
      src/Misc/fileTools.ts
  23. 2 1
      src/Misc/index.ts
  24. 0 30
      src/Misc/loadFileError.ts
  25. 18 0
      src/Misc/stringTools.ts
  26. 9 8
      src/Misc/tools.ts
  27. 19 0
      src/Misc/webRequest.ts
  28. 47 4
      src/scene.ts
  29. 0 2
      what's new.md

+ 2 - 0
dist/preview release/what's new.md

@@ -83,6 +83,8 @@
 - Added support for non-float accessors in animation data for glTF loader. ([bghgary](https://github.com/bghgary))
 - Support loading cube data in the .basis loader ([TrevorDev](https://github.com/TrevorDev))
 - Load glTF extras into BJS metadata ([pjoe](https://github.com/pjoe))
+- Added support for morph target names via `mesh.extras.targetNames` when loading a glTF ([zeux](https://github.com/zeux))
+- Added support for using HTTP range requests when loading `MSFT_lod` extension from a glTF binary. ([bghgary](https://github.com/bghgary))
 
 ### Materials
 - Added `ShaderMaterial.setColor4Array` ([JonathanTron](https://github.com/JonathanTron/))

+ 1 - 1
inspector/src/components/actionTabs/lines/floatLineComponent.tsx

@@ -125,7 +125,7 @@ export class FloatLineComponent extends React.Component<IFloatLineComponentProps
                             {this.props.label}
                         </div>
                         <div className="value">
-                            <input type="number" step={this.props.step || "0.01"} className="numeric-input" value={this.state.value} onBlur={() => this.unlock()} onFocus={() => this.lock()} onChange={evt => this.updateValue(evt.target.value)} />
+                            <input type="number" step={this.props.step || this.props.isInteger ? "1" : "0.01"} className="numeric-input" value={this.state.value} onBlur={() => this.unlock()} onFocus={() => this.lock()} onChange={evt => this.updateValue(evt.target.value)} />
                         </div>
                     </div>
                 }

+ 4 - 4
inspector/src/components/actionTabs/tabs/tools/gltfComponent.tsx

@@ -11,8 +11,8 @@ import { TextLineComponent } from "../../lines/textLineComponent";
 import { GLTFLoaderCoordinateSystemMode, GLTFLoaderAnimationStartMode } from "babylonjs-loaders/glTF/index";
 
 interface IGLTFComponentProps {
-    scene: Scene,
-    globalState: GlobalState
+    scene: Scene;
+    globalState: GlobalState;
 }
 
 export class GLTFComponent extends React.Component<IGLTFComponentProps> {
@@ -21,7 +21,7 @@ export class GLTFComponent extends React.Component<IGLTFComponentProps> {
 
         const extensionStates = this.props.globalState.glTFLoaderExtensionDefaults;
 
-        extensionStates["MSFT_lod"] = extensionStates["MSFT_lod"] || { enabled: true, maxLODsToLoad: Number.MAX_VALUE };
+        extensionStates["MSFT_lod"] = extensionStates["MSFT_lod"] || { enabled: true, maxLODsToLoad: 10 };
         extensionStates["MSFT_minecraftMesh"] = extensionStates["MSFT_minecraftMesh"] || { enabled: true };
         extensionStates["MSFT_sRGBFactors"] = extensionStates["MSFT_sRGBFactors"] || { enabled: true };
         extensionStates["MSFT_audio_emitter"] = extensionStates["MSFT_audio_emitter"] || { enabled: true };
@@ -123,7 +123,7 @@ export class GLTFComponent extends React.Component<IGLTFComponentProps> {
                 </LineContainerComponent>
                 <LineContainerComponent globalState={this.props.globalState} title="GLTF EXTENSIONS" closed={true}>
                     <CheckBoxLineComponent label="MSFT_lod" isSelected={() => extensionStates["MSFT_lod"].enabled} onSelect={value => extensionStates["MSFT_lod"].enabled = value} />
-                    <FloatLineComponent label="Maximum LODs" target={extensionStates["MSFT_lod"]} propertyName="maxLODsToLoad" additionalClass="gltf-extension-property" />
+                    <FloatLineComponent label="Maximum LODs" target={extensionStates["MSFT_lod"]} propertyName="maxLODsToLoad" additionalClass="gltf-extension-property" isInteger={true} />
                     <CheckBoxLineComponent label="MSFT_minecraftMesh" isSelected={() => extensionStates["MSFT_minecraftMesh"].enabled} onSelect={value => extensionStates["MSFT_minecraftMesh"].enabled = value} />
                     <CheckBoxLineComponent label="MSFT_sRGBFactors" isSelected={() => extensionStates["MSFT_sRGBFactors"].enabled} onSelect={value => extensionStates["MSFT_sRGBFactors"].enabled = value} />
                     <CheckBoxLineComponent label="MSFT_audio_emitter" isSelected={() => extensionStates["MSFT_audio_emitter"].enabled} onSelect={value => extensionStates["MSFT_audio_emitter"].enabled = value} />

+ 3 - 2
loaders/src/glTF/1.0/glTFBinaryExtension.ts

@@ -4,6 +4,7 @@ import { Scene } from "babylonjs/scene";
 import { IGLTFLoaderData } from "../glTFFileLoader";
 import { IGLTFRuntime, IGLTFTexture, IGLTFImage, IGLTFBufferView, EComponentType, IGLTFShader } from "./glTFLoaderInterfaces";
 import { GLTFLoader, GLTFLoaderBase } from "./glTFLoader";
+import { IDataBuffer } from '../dataReader';
 
 const BinaryExtensionBufferName = "binary_glTF";
 
@@ -20,7 +21,7 @@ interface IGLTFBinaryExtensionImage {
 
 /** @hidden */
 export class GLTFBinaryExtension extends GLTFLoaderExtension {
-    private _bin: ArrayBufferView;
+    private _bin: IDataBuffer;
 
     public constructor() {
         super("KHR_binary_glTF");
@@ -46,7 +47,7 @@ export class GLTFBinaryExtension extends GLTFLoaderExtension {
             return false;
         }
 
-        onSuccess(this._bin);
+        this._bin.readAsync(0, this._bin.byteLength).then(onSuccess, (error) => onError(error.message));
         return true;
     }
 

+ 3 - 2
loaders/src/glTF/2.0/Extensions/EXT_lights_image_based.ts

@@ -32,14 +32,14 @@ interface ILights {
 }
 
 /**
- * [Specification](https://github.com/KhronosGroup/glTF/blob/eb3e32332042e04691a5f35103f8c261e50d8f1e/extensions/2.0/Khronos/EXT_lights_image_based/README.md) (Experimental)
+ * [Specification](https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Vendor/EXT_lights_image_based/README.md)
  */
 export class EXT_lights_image_based implements IGLTFLoaderExtension {
     /** The name of this extension. */
     public readonly name = NAME;
 
     /** Defines whether this extension is enabled. */
-    public enabled = true;
+    public enabled: boolean;
 
     private _loader: GLTFLoader;
     private _lights?: ILight[];
@@ -47,6 +47,7 @@ export class EXT_lights_image_based implements IGLTFLoaderExtension {
     /** @hidden */
     constructor(loader: GLTFLoader) {
         this._loader = loader;
+        this.enabled = this._loader.isExtensionUsed(NAME);
     }
 
     /** @hidden */

+ 2 - 1
loaders/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts

@@ -31,13 +31,14 @@ export class KHR_draco_mesh_compression implements IGLTFLoaderExtension {
     public dracoCompression?: DracoCompression;
 
     /** Defines whether this extension is enabled. */
-    public enabled = DracoCompression.DecoderAvailable;
+    public enabled: boolean;
 
     private _loader: GLTFLoader;
 
     /** @hidden */
     constructor(loader: GLTFLoader) {
         this._loader = loader;
+        this.enabled = DracoCompression.DecoderAvailable && this._loader.isExtensionUsed(NAME);
     }
 
     /** @hidden */

+ 3 - 2
loaders/src/glTF/2.0/Extensions/KHR_lights_punctual.ts

@@ -39,14 +39,14 @@ interface ILights {
 }
 
 /**
- * [Specification](https://github.com/KhronosGroup/glTF/blob/1048d162a44dbcb05aefc1874bfd423cf60135a6/extensions/2.0/Khronos/KHR_lights_punctual/README.md) (Experimental)
+ * [Specification](https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_lights_punctual/README.md)
  */
 export class KHR_lights implements IGLTFLoaderExtension {
     /** The name of this extension. */
     public readonly name = NAME;
 
     /** Defines whether this extension is enabled. */
-    public enabled = true;
+    public enabled: boolean;
 
     private _loader: GLTFLoader;
     private _lights?: ILight[];
@@ -54,6 +54,7 @@ export class KHR_lights implements IGLTFLoaderExtension {
     /** @hidden */
     constructor(loader: GLTFLoader) {
         this._loader = loader;
+        this.enabled = this._loader.isExtensionUsed(NAME);
     }
 
     /** @hidden */

+ 2 - 1
loaders/src/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.ts

@@ -25,13 +25,14 @@ export class KHR_materials_pbrSpecularGlossiness implements IGLTFLoaderExtension
     public readonly name = NAME;
 
     /** Defines whether this extension is enabled. */
-    public enabled = true;
+    public enabled: boolean;
 
     private _loader: GLTFLoader;
 
     /** @hidden */
     constructor(loader: GLTFLoader) {
         this._loader = loader;
+        this.enabled = this._loader.isExtensionUsed(NAME);
     }
 
     /** @hidden */

+ 2 - 1
loaders/src/glTF/2.0/Extensions/KHR_materials_unlit.ts

@@ -17,13 +17,14 @@ export class KHR_materials_unlit implements IGLTFLoaderExtension {
     public readonly name = NAME;
 
     /** Defines whether this extension is enabled. */
-    public enabled = true;
+    public enabled: boolean;
 
     private _loader: GLTFLoader;
 
     /** @hidden */
     constructor(loader: GLTFLoader) {
         this._loader = loader;
+        this.enabled = this._loader.isExtensionUsed(NAME);
     }
 
     /** @hidden */

+ 2 - 1
loaders/src/glTF/2.0/Extensions/KHR_texture_transform.ts

@@ -23,13 +23,14 @@ export class KHR_texture_transform implements IGLTFLoaderExtension {
     public readonly name = NAME;
 
     /** Defines whether this extension is enabled. */
-    public enabled = true;
+    public enabled: boolean;
 
     private _loader: GLTFLoader;
 
     /** @hidden */
     constructor(loader: GLTFLoader) {
         this._loader = loader;
+        this.enabled = this._loader.isExtensionUsed(NAME);
     }
 
     /** @hidden */

+ 2 - 1
loaders/src/glTF/2.0/Extensions/MSFT_audio_emitter.ts

@@ -97,7 +97,7 @@ export class MSFT_audio_emitter implements IGLTFLoaderExtension {
     public readonly name = NAME;
 
     /** Defines whether this extension is enabled. */
-    public enabled = true;
+    public enabled: boolean;
 
     private _loader: GLTFLoader;
     private _clips: Array<ILoaderClip>;
@@ -106,6 +106,7 @@ export class MSFT_audio_emitter implements IGLTFLoaderExtension {
     /** @hidden */
     constructor(loader: GLTFLoader) {
         this._loader = loader;
+        this.enabled = this._loader.isExtensionUsed(NAME);
     }
 
     /** @hidden */

+ 71 - 5
loaders/src/glTF/2.0/Extensions/MSFT_lod.ts

@@ -5,7 +5,7 @@ import { Material } from "babylonjs/Materials/material";
 import { TransformNode } from "babylonjs/Meshes/transformNode";
 import { Mesh } from "babylonjs/Meshes/mesh";
 
-import { INode, IMaterial } from "../glTFLoaderInterfaces";
+import { INode, IMaterial, IBuffer } from "../glTFLoaderInterfaces";
 import { IGLTFLoaderExtension } from "../glTFLoaderExtension";
 import { GLTFLoader, ArrayItem } from "../glTFLoader";
 import { IProperty } from 'babylonjs-gltf2interface';
@@ -24,12 +24,12 @@ export class MSFT_lod implements IGLTFLoaderExtension {
     public readonly name = NAME;
 
     /** Defines whether this extension is enabled. */
-    public enabled = true;
+    public enabled: boolean;
 
     /**
      * Maximum number of LODs to load, starting from the lowest LOD.
      */
-    public maxLODsToLoad = Number.MAX_VALUE;
+    public maxLODsToLoad = 10;
 
     /**
      * Observable raised when all node LODs of one level are loaded.
@@ -55,9 +55,13 @@ export class MSFT_lod implements IGLTFLoaderExtension {
     private _materialSignalLODs = new Array<Deferred<void>>();
     private _materialPromiseLODs = new Array<Array<Promise<any>>>();
 
+    private _indexLOD: Nullable<number> = null;
+    private _bufferLODs = new Array<{ start: number, end: number, loaded: Deferred<ArrayBufferView> }>();
+
     /** @hidden */
     constructor(loader: GLTFLoader) {
         this._loader = loader;
+        this.enabled = this._loader.isExtensionUsed(NAME);
     }
 
     /** @hidden */
@@ -72,6 +76,9 @@ export class MSFT_lod implements IGLTFLoaderExtension {
         this._materialSignalLODs.length = 0;
         this._materialPromiseLODs.length = 0;
 
+        this._indexLOD = null;
+        this._bufferLODs.length = 0;
+
         this.onMaterialLODsLoadedObservable.clear();
         this.onNodeLODsLoadedObservable.clear();
     }
@@ -117,6 +124,10 @@ export class MSFT_lod implements IGLTFLoaderExtension {
 
             this._loader._completePromises.push(promise);
         }
+
+        for (let indexLOD = 1; indexLOD < this._bufferLODs.length; indexLOD++) {
+            this._loadBufferLOD(indexLOD);
+        }
     }
 
     /** @hidden */
@@ -130,6 +141,8 @@ export class MSFT_lod implements IGLTFLoaderExtension {
             for (let indexLOD = 0; indexLOD < nodeLODs.length; indexLOD++) {
                 const nodeLOD = nodeLODs[indexLOD];
 
+                this._indexLOD = indexLOD;
+
                 if (indexLOD !== 0) {
                     this._nodeIndexLOD = indexLOD;
                     this._nodeSignalLODs[indexLOD] = this._nodeSignalLODs[indexLOD] || new Deferred();
@@ -138,7 +151,7 @@ export class MSFT_lod implements IGLTFLoaderExtension {
                 const assign = (babylonTransformNode: TransformNode) => { babylonTransformNode.setEnabled(false); };
                 const promise = this._loader.loadNodeAsync(`#/nodes/${nodeLOD.index}`, nodeLOD, assign).then((babylonMesh) => {
                     if (indexLOD !== 0) {
-                        // TODO: should not rely on _babylonMesh
+                        // TODO: should not rely on _babylonTransformNode
                         const previousNodeLOD = nodeLODs[indexLOD - 1];
                         if (previousNodeLOD._babylonTransformNode) {
                             previousNodeLOD._babylonTransformNode.dispose();
@@ -153,11 +166,17 @@ export class MSFT_lod implements IGLTFLoaderExtension {
 
                 if (indexLOD === 0) {
                     firstPromise = promise;
+
+                    if (this._bufferLODs.length !== 0) {
+                        this._loadBufferLOD(0);
+                    }
                 }
                 else {
                     this._nodeIndexLOD = null;
                 }
 
+                this._indexLOD = null;
+
                 this._nodePromiseLODs[indexLOD] = this._nodePromiseLODs[indexLOD] || [];
                 this._nodePromiseLODs[indexLOD].push(promise);
             }
@@ -170,7 +189,7 @@ export class MSFT_lod implements IGLTFLoaderExtension {
     /** @hidden */
     public _loadMaterialAsync(context: string, material: IMaterial, babylonMesh: Mesh, babylonDrawMode: number, assign: (babylonMaterial: Material) => void): Nullable<Promise<Material>> {
         // Don't load material LODs if already loading a node LOD.
-        if (this._nodeIndexLOD) {
+        if (this._indexLOD) {
             return null;
         }
 
@@ -183,6 +202,8 @@ export class MSFT_lod implements IGLTFLoaderExtension {
             for (let indexLOD = 0; indexLOD < materialLODs.length; indexLOD++) {
                 const materialLOD = materialLODs[indexLOD];
 
+                this._indexLOD = indexLOD;
+
                 if (indexLOD !== 0) {
                     this._materialIndexLOD = indexLOD;
                 }
@@ -208,11 +229,17 @@ export class MSFT_lod implements IGLTFLoaderExtension {
 
                 if (indexLOD === 0) {
                     firstPromise = promise;
+
+                    if (this._bufferLODs.length !== 0) {
+                        this._loadBufferLOD(0);
+                    }
                 }
                 else {
                     this._materialIndexLOD = null;
                 }
 
+                this._indexLOD = null;
+
                 this._materialPromiseLODs[indexLOD] = this._materialPromiseLODs[indexLOD] || [];
                 this._materialPromiseLODs[indexLOD].push(promise);
             }
@@ -245,6 +272,45 @@ export class MSFT_lod implements IGLTFLoaderExtension {
         return null;
     }
 
+    /** @hidden */
+    public loadBufferAsync(context: string, buffer: IBuffer, byteOffset: number, byteLength: number): Nullable<Promise<ArrayBufferView>> {
+        if (this._loader.parent.useRangeRequests && !buffer.uri) {
+            if (!this._loader.bin) {
+                throw new Error(`${context}: Uri is missing or the binary glTF is missing its binary chunk`);
+            }
+
+            // Non-LOD buffers will be bucketed into the first LOD.
+            const indexLOD = this._indexLOD || 0;
+
+            const start = byteOffset;
+            const end = start + byteLength - 1;
+            let bufferLOD = this._bufferLODs[indexLOD];
+            if (bufferLOD) {
+                bufferLOD.start = Math.min(bufferLOD.start, start);
+                bufferLOD.end = Math.max(bufferLOD.end, end);
+            }
+            else {
+                bufferLOD = { start: start, end: end, loaded: new Deferred() };
+                this._bufferLODs[indexLOD] = bufferLOD;
+            }
+
+            return bufferLOD.loaded.promise.then((data) => {
+                return new Uint8Array(data.buffer, data.byteOffset + byteOffset - bufferLOD.start, byteLength);
+            });
+        }
+
+        return null;
+    }
+
+    private _loadBufferLOD(indexLOD: number): void {
+        const bufferLOD = this._bufferLODs[indexLOD];
+        this._loader.bin!.readAsync(bufferLOD.start, bufferLOD.end - bufferLOD.start + 1).then((data) => {
+            bufferLOD.loaded.resolve(data);
+        }, (error) => {
+            bufferLOD.loaded.reject(error);
+        });
+    }
+
     /**
      * Gets an array of LOD properties from lowest to highest.
      */

+ 2 - 1
loaders/src/glTF/2.0/Extensions/MSFT_minecraftMesh.ts

@@ -11,12 +11,13 @@ const NAME = "MSFT_minecraftMesh";
 /** @hidden */
 export class MSFT_minecraftMesh implements IGLTFLoaderExtension {
     public readonly name = NAME;
-    public enabled = true;
+    public enabled: boolean;
 
     private _loader: GLTFLoader;
 
     constructor(loader: GLTFLoader) {
         this._loader = loader;
+        this.enabled = this._loader.isExtensionUsed(NAME);
     }
 
     public dispose() {

+ 2 - 1
loaders/src/glTF/2.0/Extensions/MSFT_sRGBFactors.ts

@@ -11,12 +11,13 @@ const NAME = "MSFT_sRGBFactors";
 /** @hidden */
 export class MSFT_sRGBFactors implements IGLTFLoaderExtension {
     public readonly name = NAME;
-    public enabled = true;
+    public enabled: boolean;
 
     private _loader: GLTFLoader;
 
     constructor(loader: GLTFLoader) {
         this._loader = loader;
+        this.enabled = this._loader.isExtensionUsed(NAME);
     }
 
     public dispose() {

+ 56 - 32
loaders/src/glTF/2.0/glTFLoader.ts

@@ -3,7 +3,6 @@ import { Deferred } from "babylonjs/Misc/deferred";
 import { Quaternion, Color3, Vector3, Matrix } from "babylonjs/Maths/math";
 import { Tools } from "babylonjs/Misc/tools";
 import { IFileRequest } from "babylonjs/Misc/fileRequest";
-import { LoadFileError } from "babylonjs/Misc/loadFileError";
 import { Camera } from "babylonjs/Cameras/camera";
 import { FreeCamera } from "babylonjs/Cameras/freeCamera";
 import { AnimationGroup } from "babylonjs/Animations/animationGroup";
@@ -30,6 +29,8 @@ import { IGLTFLoaderExtension } from "./glTFLoaderExtension";
 import { IGLTFLoader, GLTFFileLoader, GLTFLoaderState, IGLTFLoaderData, GLTFLoaderCoordinateSystemMode, GLTFLoaderAnimationStartMode } from "../glTFFileLoader";
 import { IAnimationKey, AnimationKeyInterpolation } from 'babylonjs/Animations/animationKey';
 import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
+import { IDataBuffer } from '../dataReader';
+import { LoadFileError } from 'babylonjs/Misc/fileTools';
 
 interface TypedArrayLike extends ArrayBufferView {
     readonly length: number;
@@ -100,6 +101,7 @@ export class GLTFLoader implements IGLTFLoader {
     private _fileName: string;
     private _uniqueRootUrl: string;
     private _gltf: IGLTF;
+    private _bin: Nullable<IDataBuffer>;
     private _babylonScene: Scene;
     private _rootBabylonMesh: Mesh;
     private _defaultBabylonMaterialData: { [drawMode: number]: Material } = {};
@@ -155,13 +157,27 @@ export class GLTFLoader implements IGLTFLoader {
     }
 
     /**
-     * The glTF object parsed from the JSON.
+     * The object that represents the glTF JSON.
      */
     public get gltf(): IGLTF {
         return this._gltf;
     }
 
     /**
+     * The BIN chunk of a binary glTF.
+     */
+    public get bin(): Nullable<IDataBuffer> {
+        return this._bin;
+    }
+
+    /**
+     * The parent file loader.
+     */
+    public get parent(): GLTFFileLoader {
+        return this._parent;
+    }
+
+    /**
      * The Babylon scene when loading the asset.
      */
     public get babylonScene(): Scene {
@@ -363,20 +379,7 @@ export class GLTFLoader implements IGLTFLoader {
         this._gltf = data.json as IGLTF;
         this._setupData();
 
-        if (data.bin) {
-            const buffers = this._gltf.buffers;
-            if (buffers && buffers[0] && !buffers[0].uri) {
-                const binaryBuffer = buffers[0];
-                if (binaryBuffer.byteLength < data.bin.byteLength - 3 || binaryBuffer.byteLength > data.bin.byteLength) {
-                    Tools.Warn(`Binary buffer length (${binaryBuffer.byteLength}) from JSON does not match chunk length (${data.bin.byteLength})`);
-                }
-
-                binaryBuffer._data = Promise.resolve(data.bin);
-            }
-            else {
-                Tools.Warn("Unexpected BIN chunk");
-            }
-        }
+        this._bin = data.bin;
     }
 
     private _setupData(): void {
@@ -1379,18 +1382,33 @@ export class GLTFLoader implements IGLTFLoader {
         return sampler._data;
     }
 
-    private _loadBufferAsync(context: string, buffer: IBuffer): Promise<ArrayBufferView> {
-        if (buffer._data) {
-            return buffer._data;
+    private _loadBufferAsync(context: string, buffer: IBuffer, byteOffset: number, byteLength: number): Promise<ArrayBufferView> {
+        const extensionPromise = this._extensionsLoadBufferAsync(context, buffer, byteOffset, byteLength);
+        if (extensionPromise) {
+            return extensionPromise;
         }
 
-        if (!buffer.uri) {
-            throw new Error(`${context}/uri: Value is missing`);
-        }
+        if (!buffer._data) {
+            if (buffer.uri) {
+                buffer._data = this.loadUriAsync(`${context}/uri`, buffer, buffer.uri);
+            }
+            else {
+                if (!this._bin) {
+                    throw new Error(`${context}: Uri is missing or the binary glTF is missing its binary chunk`);
+                }
 
-        buffer._data = this.loadUriAsync(`${context}/uri`, buffer, buffer.uri);
+                buffer._data = this._bin.readAsync(0, buffer.byteLength);
+            }
+        }
 
-        return buffer._data;
+        return buffer._data.then((data) => {
+            try {
+                return new Uint8Array(data.buffer, data.byteOffset + byteOffset, byteLength);
+            }
+            catch (e) {
+                throw new Error(`${context}: ${e.message}`);
+            }
+        });
     }
 
     /**
@@ -1410,14 +1428,7 @@ export class GLTFLoader implements IGLTFLoader {
         }
 
         const buffer = ArrayItem.Get(`${context}/buffer`, this._gltf.buffers, bufferView.buffer);
-        bufferView._data = this._loadBufferAsync(`/buffers/${buffer.index}`, buffer).then((data) => {
-            try {
-                return new Uint8Array(data.buffer, data.byteOffset + (bufferView.byteOffset || 0), bufferView.byteLength);
-            }
-            catch (e) {
-                throw new Error(`${context}: ${e.message}`);
-            }
-        });
+        bufferView._data = this._loadBufferAsync(`/buffers/${buffer.index}`, buffer, (bufferView.byteOffset || 0), bufferView.byteLength);
 
         return bufferView._data;
     }
@@ -2286,6 +2297,10 @@ export class GLTFLoader implements IGLTFLoader {
         return this._applyExtensions(bufferView, "loadBufferView", (extension) => extension.loadBufferViewAsync && extension.loadBufferViewAsync(context, bufferView));
     }
 
+    private _extensionsLoadBufferAsync(context: string, buffer: IBuffer, byteOffset: number, byteLength: number): Nullable<Promise<ArrayBufferView>> {
+        return this._applyExtensions(buffer, "loadBuffer", (extension) => extension.loadBufferAsync && extension.loadBufferAsync(context, buffer, byteOffset, byteLength));
+    }
+
     /**
      * Helper method called by a loader extension to load an glTF extension.
      * @param context The context when loading the asset
@@ -2333,6 +2348,15 @@ export class GLTFLoader implements IGLTFLoader {
     }
 
     /**
+     * Checks for presence of an extension.
+     * @param name The name of the extension to check
+     * @returns A boolean indicating the presence of the given extension name in `extensionsUsed`
+     */
+    public isExtensionUsed(name: string): boolean {
+        return !!this._gltf.extensionsUsed && this._gltf.extensionsUsed.indexOf(name) !== -1;
+    }
+
+    /**
      * Increments the indentation level and logs a message.
      * @param message The message to log
      */

+ 12 - 2
loaders/src/glTF/2.0/glTFLoaderExtension.ts

@@ -9,7 +9,7 @@ import { Mesh } from "babylonjs/Meshes/mesh";
 import { AbstractMesh } from "babylonjs/Meshes/abstractMesh";
 import { IDisposable } from "babylonjs/scene";
 
-import { IScene, INode, IMesh, ISkin, ICamera, IMeshPrimitive, IMaterial, ITextureInfo, IAnimation, IBufferView } from "./glTFLoaderInterfaces";
+import { IScene, INode, IMesh, ISkin, ICamera, IMeshPrimitive, IMaterial, ITextureInfo, IAnimation, IBufferView, IBuffer } from "./glTFLoaderInterfaces";
 import { IGLTFLoaderExtension as IGLTFBaseLoaderExtension } from "../glTFFileLoader";
 import { IProperty } from 'babylonjs-gltf2interface';
 
@@ -139,7 +139,17 @@ export interface IGLTFLoaderExtension extends IGLTFBaseLoaderExtension, IDisposa
      * Define this method to modify the default behavior when loading buffer views.
      * @param context The context when loading the asset
      * @param bufferView The glTF buffer view property
-     * @returns A promise that resolves with the loaded buffer view when the load is complete or null if not handled
+     * @returns A promise that resolves with the loaded data when the load is complete or null if not handled
      */
     loadBufferViewAsync?(context: string, bufferView: IBufferView): Nullable<Promise<ArrayBufferView>>;
+
+    /**
+     * Define this method to modify the default behavior when loading buffers.
+     * @param context The context when loading the asset
+     * @param buffer The glTF buffer property
+     * @param byteOffset The byte offset to load
+     * @param byteLength The byte length to load
+     * @returns A promise that resolves with the loaded data when the load is complete or null if not handled
+     */
+    loadBufferAsync?(context: string, buffer: IBuffer, byteOffset: number, byteLength: number): Nullable<Promise<ArrayBufferView>>;
 }

+ 101 - 0
loaders/src/glTF/dataReader.ts

@@ -0,0 +1,101 @@
+import { StringTools } from 'babylonjs/Misc/stringTools';
+
+/**
+ * Interface for a data buffer
+ */
+export interface IDataBuffer {
+    /**
+     * Reads bytes from the data buffer.
+     * @param byteOffset The byte offset to read
+     * @param byteLength The byte length to read
+     * @returns A promise that resolves when the bytes are read
+     */
+    readAsync(byteOffset: number, byteLength: number): Promise<ArrayBufferView>;
+
+    /**
+     * The byte length of the buffer.
+     */
+    readonly byteLength: number;
+}
+
+/**
+ * Utility class for reading from a data buffer
+ */
+export class DataReader {
+    /**
+     * The data buffer associated with this data reader.
+     */
+    public readonly buffer: IDataBuffer;
+
+    /**
+     * The current byte offset from the beginning of the data buffer.
+     */
+    public byteOffset = 0;
+
+    private _dataView: DataView;
+    private _dataByteOffset: number;
+
+    /**
+     * Constructor
+     * @param buffer The buffer to read
+     */
+    constructor(buffer: IDataBuffer) {
+        this.buffer = buffer;
+    }
+
+    /**
+     * Loads the given byte length.
+     * @param byteLength The byte length to load
+     * @returns A promise that resolves when the load is complete
+     */
+    public loadAsync(byteLength: number): Promise<void> {
+        delete this._dataView;
+        delete this._dataByteOffset;
+
+        return this.buffer.readAsync(this.byteOffset, byteLength).then((data) => {
+            this._dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
+            this._dataByteOffset = 0;
+        });
+    }
+
+    /**
+     * Read a unsigned 32-bit integer from the currently loaded data range.
+     * @returns The 32-bit integer read
+     */
+    public readUint32(): number {
+        const value = this._dataView.getUint32(this._dataByteOffset, true);
+        this._dataByteOffset += 4;
+        this.byteOffset += 4;
+        return value;
+    }
+
+    /**
+     * Read a byte array from the currently loaded data range.
+     * @param byteLength The byte length to read
+     * @returns The byte array read
+     */
+    public readUint8Array(byteLength: number): Uint8Array {
+        const value = new Uint8Array(this._dataView.buffer, this._dataView.byteOffset + this._dataByteOffset, byteLength);
+        this._dataByteOffset += byteLength;
+        this.byteOffset += byteLength;
+        return value;
+    }
+
+    /**
+     * Read a string from the currently loaded data range.
+     * @param byteLength The byte length to read
+     * @returns The string read
+     */
+    public readString(byteLength: number): string {
+        return StringTools.Decode(this.readUint8Array(byteLength));
+    }
+
+    /**
+     * Skips the given byte length the currently loaded data range.
+     * @param byteLength The byte length to skip
+     */
+    public skipBytes(byteLength: number): void {
+        this._dataByteOffset += byteLength;
+        this.byteOffset += byteLength;
+    }
+}

+ 269 - 207
loaders/src/glTF/glTFFileLoader.ts

@@ -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());
 }

+ 103 - 63
src/Loading/sceneLoader.ts

@@ -8,14 +8,16 @@ import { EngineStore } from "../Engines/engineStore";
 import { AbstractMesh } from "../Meshes/abstractMesh";
 import { AnimationGroup } from "../Animations/animationGroup";
 import { _TimeToken } from "../Instrumentation/timeToken";
-import { IOfflineProvider } from "../Offline/IOfflineProvider";
 import { AssetContainer } from "../assetContainer";
 import { IParticleSystem } from "../Particles/IParticleSystem";
 import { Skeleton } from "../Bones/skeleton";
 import { Logger } from "../Misc/logger";
 import { Constants } from "../Engines/constants";
 import { SceneLoaderFlags } from "./sceneLoaderFlags";
-import { IFileRequest } from '../Misc/fileRequest';
+import { IFileRequest } from "../Misc/fileRequest";
+import { WebRequest } from "../Misc/webRequest";
+import { RequestFileError, ReadFileError } from '../Misc/fileTools';
+
 /**
  * Class used to represent data loading progression
  */
@@ -65,21 +67,25 @@ export interface ISceneLoaderPluginFactory {
      * Defines the name of the factory
      */
     name: string;
+
     /**
      * Function called to create a new plugin
      * @return the new plugin
      */
     createPlugin(): ISceneLoaderPlugin | ISceneLoaderPluginAsync;
+
     /**
-     * Boolean indicating if the plugin can direct load specific 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
      */
-    canDirectLoad?: (data: string) => boolean;
+    canDirectLoad?(data: string): boolean;
 }
 
 /**
- * Interface used to define a SceneLoader plugin
+ * Interface used to define the base of ISceneLoaderPlugin and ISceneLoaderPluginAsync
  */
-export interface ISceneLoaderPlugin {
+export interface ISceneLoaderPluginBase {
     /**
      * The friendly name of this plugin.
      */
@@ -91,6 +97,58 @@ export interface ISceneLoaderPlugin {
     extensions: string | ISceneLoaderPluginExtensions;
 
     /**
+     * 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
+     */
+    requestFile?(scene: Scene, url: string, onSuccess: (data: any, request?: WebRequest) => void, onProgress?: (ev: ProgressEvent) => void, useArrayBuffer?: boolean, onError?: (error: any) => void): IFileRequest;
+
+    /**
+     * 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
+     */
+    readFile?(scene: Scene, file: File, onSuccess: (data: any) => void, onProgress?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean, onError?: (error: any) => void): IFileRequest;
+
+    /**
+     * 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
+     */
+    canDirectLoad?(data: string): boolean;
+
+    /**
+     * 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
+     */
+    directLoad?(scene: Scene, data: string): any;
+
+    /**
+     * 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
+     */
+    rewriteRootURL?(rootUrl: string, responseURL?: string): string;
+}
+
+/**
+ * Interface used to define a SceneLoader plugin
+ */
+export interface ISceneLoaderPlugin extends ISceneLoaderPluginBase {
+    /**
      * Import meshes into a scene.
      * @param meshesNames An array of mesh names, a single mesh name, or empty string for all meshes that filter what meshes are imported
      * @param scene The scene to import into
@@ -110,19 +168,9 @@ export interface ISceneLoaderPlugin {
      * @param data The data to import
      * @param rootUrl The root url for scene and resources
      * @param onError The callback when import fails
-     * @returns true if successful or false otherwise
-     */
-    load(scene: Scene, data: string, rootUrl: string, onError?: (message: string, exception?: any) => void): boolean;
-
-    /**
-     * The callback that returns true if the data can be directly loaded.
-     */
-    canDirectLoad?: (data: string) => boolean;
-
-    /**
-     * The callback that allows custom handling of the root url based on the response url.
+     * @returns True if successful or false otherwise
      */
-    rewriteRootURL?: (rootUrl: string, responseURL?: string) => string;
+    load(scene: Scene, data: any, rootUrl: string, onError?: (message: string, exception?: any) => void): boolean;
 
     /**
      * Load into an asset container.
@@ -132,23 +180,13 @@ export interface ISceneLoaderPlugin {
      * @param onError The callback when import fails
      * @returns The loaded asset container
      */
-    loadAssetContainer(scene: Scene, data: string, rootUrl: string, onError?: (message: string, exception?: any) => void): AssetContainer;
+    loadAssetContainer(scene: Scene, data: any, rootUrl: string, onError?: (message: string, exception?: any) => void): AssetContainer;
 }
 
 /**
  * Interface used to define an async SceneLoader plugin
  */
-export interface ISceneLoaderPluginAsync {
-    /**
-     * The friendly name of this plugin.
-     */
-    name: string;
-
-    /**
-     * The file extensions supported by this plugin.
-     */
-    extensions: string | ISceneLoaderPluginExtensions;
-
+export interface ISceneLoaderPluginAsync extends ISceneLoaderPluginBase {
     /**
      * Import meshes into a scene.
      * @param meshesNames An array of mesh names, a single mesh name, or empty string for all meshes that filter what meshes are imported
@@ -170,17 +208,7 @@ export interface ISceneLoaderPluginAsync {
      * @param fileName Defines the name of the file to load
      * @returns Nothing
      */
-    loadAsync(scene: Scene, data: string, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<void>;
-
-    /**
-     * The callback that returns true if the data can be directly loaded.
-     */
-    canDirectLoad?: (data: string) => boolean;
-
-    /**
-     * The callback that allows custom handling of the root url based on the response url.
-     */
-    rewriteRootURL?: (rootUrl: string, responseURL?: string) => string;
+    loadAsync(scene: Scene, data: any, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<void>;
 
     /**
      * Load into an asset container.
@@ -191,7 +219,7 @@ export interface ISceneLoaderPluginAsync {
      * @param fileName Defines the name of the file to load
      * @returns The loaded asset container
      */
-    loadAssetContainerAsync(scene: Scene, data: string, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<AssetContainer>;
+    loadAssetContainerAsync(scene: Scene, data: any, rootUrl: string, onProgress?: (event: SceneLoaderProgressEvent) => void, fileName?: string): Promise<AssetContainer>;
 }
 
 /**
@@ -347,7 +375,6 @@ export class SceneLoader {
         return SceneLoader._getPluginForExtension(extension);
     }
 
-    // use babylon file loader directly if sceneFilename is prefixed with "data:"
     private static _getDirectLoad(sceneFilename: string): Nullable<string> {
         if (sceneFilename.substr(0, 5) === "data:") {
             return sceneFilename.substr(5);
@@ -356,9 +383,9 @@ export class SceneLoader {
         return null;
     }
 
-    private static _loadData(fileInfo: IFileInfo, 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(fileInfo.name);
-        let registeredPlugin = pluginExtension ? SceneLoader._getPluginForExtension(pluginExtension) : (directLoad ? SceneLoader._getPluginForDirectLoad(fileInfo.name) : SceneLoader._getPluginForFilename(fileInfo.name));
+    private static _loadData(fileInfo: IFileInfo, 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>): Nullable<ISceneLoaderPlugin | ISceneLoaderPluginAsync> {
+        const directLoad = SceneLoader._getDirectLoad(fileInfo.name);
+        const registeredPlugin = pluginExtension ? SceneLoader._getPluginForExtension(pluginExtension) : (directLoad ? SceneLoader._getPluginForDirectLoad(fileInfo.name) : SceneLoader._getPluginForFilename(fileInfo.name));
 
         let plugin: ISceneLoaderPlugin | ISceneLoaderPluginAsync;
         if ((registeredPlugin.plugin as ISceneLoaderPluginFactory).createPlugin) {
@@ -372,19 +399,21 @@ export class SceneLoader {
             throw "The loader plugin corresponding to the file type you are trying to load has not been found. If using es6, please import the plugin you wish to use before.";
         }
 
-        let useArrayBuffer = registeredPlugin.isBinary;
-        let offlineProvider: IOfflineProvider;
-
         SceneLoader.OnPluginActivatedObservable.notifyObservers(plugin);
 
+        if (directLoad) {
+            onSuccess(plugin, plugin.directLoad ? plugin.directLoad(scene, directLoad) : directLoad);
+            return plugin;
+        }
+
+        let useArrayBuffer = registeredPlugin.isBinary;
+
         let dataCallback = (data: any, responseURL?: string) => {
             if (scene.isDisposed) {
                 onError("Scene has been disposed");
                 return;
             }
 
-            scene.offlineProvider = offlineProvider;
-
             onSuccess(plugin, data, responseURL);
         };
 
@@ -404,22 +433,27 @@ export class SceneLoader {
             });
         }
 
+        const progressCallback = onProgress ? (event: ProgressEvent) => {
+            onProgress(SceneLoaderProgressEvent.FromProgressEvent(event));
+        } : undefined;
+
         let manifestChecked = () => {
             if (pluginDisposed) {
                 return;
             }
 
-            request = Tools.LoadFile(fileInfo.url, dataCallback, onProgress ? (event) => {
-                onProgress(SceneLoaderProgressEvent.FromProgressEvent(event));
-            } : undefined, offlineProvider, useArrayBuffer, (request, exception) => {
-                onError("Failed to load scene." + (exception ? " " + exception.message : ""), exception);
-            });
-        };
+            const successCallback = (data: string | ArrayBuffer, request?: WebRequest) => {
+                dataCallback(data, request ? request.responseURL : undefined);
+            };
 
-        if (directLoad) {
-            dataCallback(directLoad);
-            return plugin;
-        }
+            const errorCallback = (error: RequestFileError) => {
+                onError(error.message, error);
+            };
+
+            request = plugin.requestFile
+                ? plugin.requestFile(scene, fileInfo.url, successCallback, progressCallback, useArrayBuffer, errorCallback)
+                : scene._requestFile(fileInfo.url, successCallback, progressCallback, true, useArrayBuffer, errorCallback);
+        };
 
         const file = fileInfo.file || FilesInputStore.FilesToLoad[fileInfo.name.toLowerCase()];
 
@@ -441,7 +475,7 @@ export class SceneLoader {
 
             if (canUseOfflineSupport && Engine.OfflineProviderFactory) {
                 // Checking if a manifest file has been set for this scene and if offline mode has been requested
-                offlineProvider = Engine.OfflineProviderFactory(fileInfo.url, manifestChecked, engine.disableManifestCheck);
+                scene.offlineProvider = Engine.OfflineProviderFactory(fileInfo.url, manifestChecked, engine.disableManifestCheck);
             }
             else {
                 manifestChecked();
@@ -450,7 +484,13 @@ export class SceneLoader {
         // Loading file from disk via input file or drag'n'drop
         else {
             if (file) {
-                request = Tools.ReadFile(file, dataCallback, onProgress, useArrayBuffer);
+                const errorCallback = (error: ReadFileError) => {
+                    onError(error.message, error);
+                };
+
+                request = plugin.readFile
+                    ? plugin.readFile(scene, file, dataCallback, progressCallback, useArrayBuffer, errorCallback)
+                    : scene._readFile(file, dataCallback, progressCallback, useArrayBuffer, errorCallback);
             } else {
                 onError("Unable to find file named " + fileInfo.name);
             }

+ 3 - 11
src/Materials/Node/nodeMaterial.ts

@@ -27,7 +27,6 @@ import { _TypeStore } from '../../Misc/typeStore';
 import { SerializationHelper } from '../../Misc/decorators';
 import { TextureBlock } from './Blocks/Dual/textureBlock';
 import { ReflectionTextureBlock } from './Blocks/Dual/reflectionTextureBlock';
-import { FileTools } from '../../Misc/fileTools';
 import { EffectFallbacks } from '../effectFallbacks';
 
 // declare NODEEDITOR namespace for compilation issue
@@ -1008,16 +1007,9 @@ export class NodeMaterial extends PushMaterial {
      * @returns a promise that will fullfil when the material is fully loaded
      */
     public loadAsync(url: string) {
-        return new Promise((resolve, reject) => {
-            FileTools.LoadFile(url, (data) => {
-                let serializationObject = JSON.parse(data as string);
-
-                this.loadFromSerialization(serializationObject, "");
-
-                resolve();
-            }, undefined, undefined, false, (request, exception) => {
-                reject(exception.message);
-            });
+        return this.getScene()._loadFileAsync(url).then((data) => {
+            const serializationObject = JSON.parse(data as string);
+            this.loadFromSerialization(serializationObject, "");
         });
     }
 

+ 12 - 0
src/Misc/baseError.ts

@@ -0,0 +1,12 @@
+/**
+ * @ignore
+ * Application error to support additional information when loading a file
+ */
+export abstract class BaseError extends Error {
+    // See https://stackoverflow.com/questions/12915412/how-do-i-extend-a-host-object-e-g-error-in-typescript
+    // and https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
+
+    // Polyfill for Object.setPrototypeOf if necessary.
+    protected static _setPrototypeOf: (o: any, proto: object | null) => any =
+        (Object as any).setPrototypeOf || ((o, proto) => { o.__proto__ = proto; return o; });
+}

+ 106 - 35
src/Misc/fileTools.ts

@@ -1,5 +1,4 @@
 import { WebRequest } from './webRequest';
-import { LoadFileError } from './loadFileError';
 import { DomManagement } from './domManagement';
 import { Nullable } from '../types';
 import { IOfflineProvider } from '../Offline/IOfflineProvider';
@@ -7,7 +6,61 @@ import { IFileRequest } from './fileRequest';
 import { Observable } from './observable';
 import { FilesInputStore } from './filesInputStore';
 import { RetryStrategy } from './retryStrategy';
+import { BaseError } from './baseError';
 
+/** @ignore */
+export class LoadFileError extends BaseError {
+    public request?: WebRequest;
+    public file?: File;
+
+    /**
+     * Creates a new LoadFileError
+     * @param message defines the message of the error
+     * @param request defines the optional web request
+     * @param file defines the optional file
+     */
+    constructor(message: string, object?: WebRequest | File) {
+        super(message);
+
+        this.name = "LoadFileError";
+        BaseError._setPrototypeOf(this, LoadFileError.prototype);
+
+        if (object instanceof WebRequest) {
+            this.request = object;
+        }
+        else {
+            this.file = object;
+        }
+    }
+}
+
+/** @ignore */
+export class RequestFileError extends BaseError {
+    /**
+     * Creates a new LoadFileError
+     * @param message defines the message of the error
+     * @param request defines the optional web request
+     */
+    constructor(message: string, public request: WebRequest) {
+        super(message);
+        this.name = "RequestFileError";
+        BaseError._setPrototypeOf(this, RequestFileError.prototype);
+    }
+}
+
+/** @ignore */
+export class ReadFileError extends BaseError {
+    /**
+     * Creates a new ReadFileError
+     * @param message defines the message of the error
+     * @param file defines the optional file
+     */
+    constructor(message: string, public file: File) {
+        super(message);
+        this.name = "ReadFileError";
+        BaseError._setPrototypeOf(this, ReadFileError.prototype);
+    }
+}
 /**
  * @hidden
  */
@@ -205,14 +258,15 @@ export class FileTools {
     }
 
     /**
-     * Loads a file
-     * @param fileToLoad defines the file to load
-     * @param callback defines the callback to call when data is loaded
-     * @param progressCallBack defines the callback to call during loading process
+     * Reads a file from a File object
+     * @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 static ReadFile(fileToLoad: File, callback: (data: any) => void, progressCallBack?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean): IFileRequest {
+    public static ReadFile(file: File, onSuccess: (data: any) => void, onProgress?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean, onError?: (error: ReadFileError) => void): IFileRequest {
         let reader = new FileReader();
         let request: IFileRequest = {
             onCompleteObservable: new Observable<IFileRequest>(),
@@ -220,30 +274,32 @@ export class FileTools {
         };
 
         reader.onloadend = (e) => request.onCompleteObservable.notifyObservers(request);
-        reader.onerror = (e) => {
-            callback(JSON.stringify({ autoClear: true, clearColor: [1, 0, 0], ambientColor: [0, 0, 0], gravity: [0, -9.807, 0], meshes: [], cameras: [], lights: [] }));
-        };
+        if (onError) {
+            reader.onerror = (e) => {
+                onError(new ReadFileError(`Unable to read ${file.name}`, file));
+            };
+        }
         reader.onload = (e) => {
             //target doesn't have result from ts 1.3
-            callback((<any>e.target)['result']);
+            onSuccess((<any>e.target)['result']);
         };
-        if (progressCallBack) {
-            reader.onprogress = progressCallBack;
+        if (onProgress) {
+            reader.onprogress = onProgress;
         }
         if (!useArrayBuffer) {
             // Asynchronous read
-            reader.readAsText(fileToLoad);
+            reader.readAsText(file);
         }
         else {
-            reader.readAsArrayBuffer(fileToLoad);
+            reader.readAsArrayBuffer(file);
         }
 
         return request;
     }
 
     /**
-     * Loads a file
-     * @param url url string, ArrayBuffer, or Blob to load
+     * Loads a file from a 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 offlineProvider defines the offline provider for caching
@@ -251,19 +307,37 @@ export class FileTools {
      * @param onError callback called when the file fails to load
      * @returns a file request object
      */
-    public static LoadFile(url: string, onSuccess: (data: string | ArrayBuffer, responseURL?: string) => void, onProgress?: (data: any) => void, offlineProvider?: IOfflineProvider, useArrayBuffer?: boolean, onError?: (request?: WebRequest, exception?: any) => void): IFileRequest {
-        url = this._CleanUrl(url);
-
-        url = this.PreprocessUrl(url);
-
+    public static LoadFile(url: string, onSuccess: (data: string | ArrayBuffer, responseURL?: string) => void, onProgress?: (ev: ProgressEvent) => void, offlineProvider?: IOfflineProvider, useArrayBuffer?: boolean, onError?: (request?: WebRequest, exception?: LoadFileError) => void): IFileRequest {
         // If file and file input are set
         if (url.indexOf("file:") !== -1) {
             const fileName = decodeURIComponent(url.substring(5).toLowerCase());
-            if (FilesInputStore.FilesToLoad[fileName]) {
-                return this.ReadFile(FilesInputStore.FilesToLoad[fileName], onSuccess, onProgress, useArrayBuffer);
+            const file = FilesInputStore.FilesToLoad[fileName];
+            if (file) {
+                return this.ReadFile(file, onSuccess, onProgress, useArrayBuffer, onError ? (error) => onError(undefined, new LoadFileError(error.message, error.file)) : undefined);
             }
         }
 
+        return this.RequestFile(url, (data, request) => {
+            onSuccess(data, request ? request.responseURL : undefined);
+        }, onProgress, offlineProvider, useArrayBuffer, onError ? (error) => {
+            onError(error.request, new LoadFileError(error.message, error.request));
+        } : undefined);
+    }
+
+    /**
+     * Loads a file
+     * @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
+     * @param onOpened callback called when the web request is opened
+     * @returns a file request object
+     */
+    public static RequestFile(url: string, onSuccess: (data: string | ArrayBuffer, request?: WebRequest) => void, onProgress?: (event: ProgressEvent) => void, offlineProvider?: IOfflineProvider, useArrayBuffer?: boolean, onError?: (error: RequestFileError) => void, onOpened?: (request: WebRequest) => void): IFileRequest {
+        url = this._CleanUrl(url);
+        url = this.PreprocessUrl(url);
+
         const loadUrl = this.BaseUrl + url;
 
         let aborted = false;
@@ -292,6 +366,10 @@ export class FileTools {
             const retryLoop = (retryIndex: number) => {
                 request.open('GET', loadUrl);
 
+                if (onOpened) {
+                    onOpened(request);
+                }
+
                 if (useArrayBuffer) {
                     request.responseType = "arraybuffer";
                 }
@@ -319,7 +397,7 @@ export class FileTools {
                         request.removeEventListener("readystatechange", onReadyStateChange);
 
                         if ((request.status >= 200 && request.status < 300) || (request.status === 0 && (!DomManagement.IsWindowObjectExist() || this.IsFileURL()))) {
-                            onSuccess(!useArrayBuffer ? request.responseText : <ArrayBuffer>request.response, request.responseURL);
+                            onSuccess(useArrayBuffer ? request.response : request.responseText, request);
                             return;
                         }
 
@@ -335,11 +413,9 @@ export class FileTools {
                             }
                         }
 
-                        let e = new LoadFileError("Error status: " + request.status + " " + request.statusText + " - Unable to load " + loadUrl, request);
+                        const error = new RequestFileError("Error status: " + request.status + " " + request.statusText + " - Unable to load " + loadUrl, request);
                         if (onError) {
-                            onError(request, e);
-                        } else {
-                            throw e;
+                            onError(error);
                         }
                     }
                 };
@@ -360,20 +436,15 @@ export class FileTools {
                         onError(request);
                     }
                 } else {
-                    if (!aborted) {
-                        requestFile();
-                    }
+                    requestFile();
                 }
             };
 
             const loadFromOfflineSupport = () => {
                 // TODO: database needs to support aborting and should return a IFileRequest
-                if (aborted) {
-                    return;
-                }
 
                 if (offlineProvider) {
-                    offlineProvider.loadFile(url, (data) => {
+                    offlineProvider.loadFile(this.BaseUrl + url, (data) => {
                         if (!aborted) {
                             onSuccess(data);
                         }

+ 2 - 1
src/Misc/index.ts

@@ -39,6 +39,7 @@ export * from "./perfCounter";
 export * from "./fileRequest";
 export * from "./customAnimationFrameRequester";
 export * from "./retryStrategy";
-export * from "./loadFileError";
 export * from "./interfaces/screenshotSize";
 export * from "./canvasGenerator";
+export * from "./fileTools";
+export * from "./stringTools";

+ 0 - 30
src/Misc/loadFileError.ts

@@ -1,30 +0,0 @@
-import { WebRequest } from './webRequest';
-
-/**
- * @ignore
- * Application error to support additional information when loading a file
- */
-export class LoadFileError extends Error {
-    // See https://stackoverflow.com/questions/12915412/how-do-i-extend-a-host-object-e-g-error-in-typescript
-    // and https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
-
-    // Polyfill for Object.setPrototypeOf if necessary.
-    private static _setPrototypeOf: (o: any, proto: object | null) => any =
-        (Object as any).setPrototypeOf || ((o, proto) => { o.__proto__ = proto; return o; });
-
-    /**
-     * Creates a new LoadFileError
-     * @param message defines the message of the error
-     * @param request defines the optional web request
-     */
-    constructor(
-        message: string,
-        /** defines the optional web request */
-        public request?: WebRequest
-    ) {
-        super(message);
-        this.name = "LoadFileError";
-
-        LoadFileError._setPrototypeOf(this, LoadFileError.prototype);
-    }
-}

+ 18 - 0
src/Misc/stringTools.ts

@@ -21,4 +21,22 @@ export class StringTools {
     public static StartsWith(str: string, suffix: string): boolean {
         return str.indexOf(suffix) === 0;
     }
+
+    /**
+     * Decodes a buffer into a string
+     * @param buffer The buffer to decode
+     * @returns The decoded string
+     */
+    public static Decode(buffer: Uint8Array | Uint16Array): string {
+        if (typeof TextDecoder !== "undefined") {
+            return new TextDecoder().decode(buffer);
+        }
+
+        let result = "";
+        for (let i = 0; i < buffer.byteLength; i++) {
+            result += String.fromCharCode(buffer[i]);
+        }
+
+        return result;
+    }
 }

+ 9 - 8
src/Misc/tools.ts

@@ -9,7 +9,7 @@ import { _DevTools } from './devTools';
 import { WebRequest } from './webRequest';
 import { IFileRequest } from './fileRequest';
 import { EngineStore } from '../Engines/engineStore';
-import { FileTools } from './fileTools';
+import { FileTools, ReadFileError } from './fileTools';
 import { IOfflineProvider } from '../Offline/IOfflineProvider';
 import { PromisePolyfill } from './promise';
 import { TimingTools } from './timingTools';
@@ -368,7 +368,7 @@ export class Tools {
     }
 
     /**
-     * Loads a file
+     * Loads a file from a url
      * @param url url string, ArrayBuffer, or Blob 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)
@@ -496,15 +496,16 @@ export class Tools {
     }
 
     /**
-     * Loads a file
-     * @param fileToLoad defines the file to load
-     * @param callback defines the callback to call when data is loaded
-     * @param progressCallBack defines the callback to call during loading process
+     * Reads a file from a File object
+     * @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 static ReadFile(fileToLoad: File, callback: (data: any) => void, progressCallBack?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean): IFileRequest {
-        return FileTools.ReadFile(fileToLoad, callback, progressCallBack, useArrayBuffer);
+    public static ReadFile(file: File, onSuccess: (data: any) => void, onProgress?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean, onError?: (error: ReadFileError) => void): IFileRequest {
+        return FileTools.ReadFile(file, onSuccess, onProgress, useArrayBuffer, onError);
     }
 
     /**

+ 19 - 0
src/Misc/webRequest.ts

@@ -1,4 +1,5 @@
 import { IWebRequest } from './interfaces/iWebRequest';
+import { Nullable } from '../types';
 
 /**
  * Extended version of XMLHttpRequest with support for customizations (headers, ...)
@@ -137,4 +138,22 @@ export class WebRequest implements IWebRequest {
 
         return this._xhr.open(method, url, true);
     }
+
+    /**
+     * Sets the value of a request header.
+     * @param name The name of the header whose value is to be set
+     * @param value The value to set as the body of the header
+     */
+    setRequestHeader(name: string, value: string): void {
+        this._xhr.setRequestHeader(name, value);
+    }
+
+    /**
+     * Get the string containing the text of a particular header's value.
+     * @param name The name of the header
+     * @returns The string containing the text of the given header name
+     */
+    getResponseHeader(name: string): Nullable<string> {
+        return this._xhr.getResponseHeader(name);
+    }
 }

+ 47 - 4
src/scene.ts

@@ -54,6 +54,7 @@ import { Color4, Color3 } from './Maths/math.color';
 import { Plane } from './Maths/math.plane';
 import { Frustum } from './Maths/math.frustum';
 import { UniqueIdGenerator } from './Misc/uniqueIdGenerator';
+import { FileTools, LoadFileError, RequestFileError, ReadFileError } from './Misc/fileTools';
 
 declare type Ray = import("./Culling/ray").Ray;
 declare type TrianglePickingPredicate = import("./Culling/ray").TrianglePickingPredicate;
@@ -4526,8 +4527,8 @@ export class Scene extends AbstractScene implements IAnimatable {
     }
 
     /** @hidden */
-    public _loadFile(url: string, onSuccess: (data: string | ArrayBuffer, responseURL?: string) => void, onProgress?: (data: any) => void, useOfflineSupport?: boolean, useArrayBuffer?: boolean, onError?: (request?: WebRequest, exception?: any) => void): IFileRequest {
-        let request = Tools.LoadFile(url, onSuccess, onProgress, useOfflineSupport ? this.offlineProvider : undefined, useArrayBuffer, onError);
+    public _loadFile(url: string, onSuccess: (data: string | ArrayBuffer, responseURL?: string) => void, onProgress?: (ev: ProgressEvent) => void, useOfflineSupport?: boolean, useArrayBuffer?: boolean, onError?: (request?: WebRequest, exception?: LoadFileError) => void): IFileRequest {
+        const request = FileTools.LoadFile(url, onSuccess, onProgress, useOfflineSupport ? this.offlineProvider : undefined, useArrayBuffer, onError);
         this._activeRequests.push(request);
         request.onCompleteObservable.add((request) => {
             this._activeRequests.splice(this._activeRequests.indexOf(request), 1);
@@ -4536,13 +4537,55 @@ export class Scene extends AbstractScene implements IAnimatable {
     }
 
     /** @hidden */
-    public _loadFileAsync(url: string, useOfflineSupport?: boolean, useArrayBuffer?: boolean): Promise<string | ArrayBuffer> {
+    public _loadFileAsync(url: string, onProgress?: (data: any) => void, useOfflineSupport?: boolean, useArrayBuffer?: boolean): Promise<string | ArrayBuffer> {
         return new Promise((resolve, reject) => {
             this._loadFile(url, (data) => {
                 resolve(data);
-            }, undefined, useOfflineSupport, useArrayBuffer, (request, exception) => {
+            }, onProgress, useOfflineSupport, useArrayBuffer, (request, exception) => {
                 reject(exception);
             });
         });
     }
+
+    /** @hidden */
+    public _requestFile(url: string, onSuccess: (data: string | ArrayBuffer, request?: WebRequest) => void, onProgress?: (ev: ProgressEvent) => void, useOfflineSupport?: boolean, useArrayBuffer?: boolean, onError?: (error: RequestFileError) => void, onOpened?: (request: WebRequest) => void): IFileRequest {
+        const request = FileTools.RequestFile(url, onSuccess, onProgress, useOfflineSupport ? this.offlineProvider : undefined, useArrayBuffer, onError, onOpened);
+        this._activeRequests.push(request);
+        request.onCompleteObservable.add((request) => {
+            this._activeRequests.splice(this._activeRequests.indexOf(request), 1);
+        });
+        return request;
+    }
+
+    /** @hidden */
+    public _requestFileAsync(url: string, onProgress?: (ev: ProgressEvent) => void, useOfflineSupport?: boolean, useArrayBuffer?: boolean, onOpened?: (request: WebRequest) => void): Promise<string | ArrayBuffer> {
+        return new Promise((resolve, reject) => {
+            this._requestFile(url, (data) => {
+                resolve(data);
+            }, onProgress, useOfflineSupport, useArrayBuffer, (error) => {
+                reject(error);
+            }, onOpened);
+        });
+    }
+
+    /** @hidden */
+    public _readFile(file: File, onSuccess: (data: string | ArrayBuffer) => void, onProgress?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean, onError?: (error: ReadFileError) => void): IFileRequest {
+        const request = FileTools.ReadFile(file, onSuccess, onProgress, useArrayBuffer, onError);
+        this._activeRequests.push(request);
+        request.onCompleteObservable.add((request) => {
+            this._activeRequests.splice(this._activeRequests.indexOf(request), 1);
+        });
+        return request;
+    }
+
+    /** @hidden */
+    public _readFileAsync(file: File, onProgress?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean): Promise<string | ArrayBuffer> {
+        return new Promise((resolve, reject) => {
+            this._readFile(file, (data) => {
+                resolve(data);
+            }, onProgress, useArrayBuffer, (error) => {
+                reject(error);
+            });
+        });
+    }
 }

+ 0 - 2
what's new.md

@@ -146,8 +146,6 @@
 - Added support for MSFT_audio_emitter ([najadojo](http://www.github.com/najadojo))
 - Added support for custom loader extensions ([bghgary](http://www.github.com/bghgary))
 - Added support for validating assets using [glTF-Validator](https://github.com/KhronosGroup/glTF-Validator) ([bghgary](http://www.github.com/bghgary))
-- Added automatically renormalizes skinweights when loading geometry. Calls core mesh functions to do this ([Bolloxim](https://github.com/Bolloxim))
-- Added support for morph target names via `mesh.extras.targetNames` ([zeux](https://github.com/zeux))
 
 ### glTF Serializer
 - Added support for exporting the scale, rotation and offset texture properties ([kcoley](http://www.github.com/kcoley))