Forráskód Böngészése

Merge pull request #2887 from bghgary/node-lod

Add support for progressive loading of node LODs to glTF loader
sebavan 7 éve
szülő
commit
91786d196f

+ 10 - 18
loaders/src/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.ts

@@ -14,21 +14,13 @@ module BABYLON.GLTF2.Extensions {
             return "KHR_materials_pbrSpecularGlossiness";
         }
 
-        protected loadMaterial(loader: GLTFLoader, material: IGLTFMaterial, assign: (babylonMaterial: Material, isNew: boolean) => void): boolean {
-            if (!material.extensions) {
-                return false;
-            }
-
-            var properties = material.extensions[this.name] as IKHRMaterialsPbrSpecularGlossiness;
-            if (!properties) {
-                return false;
-            }
-
-            loader.createPbrMaterial(material);
-            loader.loadMaterialBaseProperties(material);
-            this._loadSpecularGlossinessProperties(loader, material, properties);
-            assign(material.babylonMaterial, true);
-            return true;
+        protected _loadMaterial(loader: GLTFLoader, material: IGLTFMaterial, assign: (babylonMaterial: Material, isNew: boolean) => void): boolean {
+            return this._loadExtension<IKHRMaterialsPbrSpecularGlossiness>(material, (extension, onComplete) => {
+                loader._createPbrMaterial(material);
+                loader._loadMaterialBaseProperties(material);
+                this._loadSpecularGlossinessProperties(loader, material, extension);
+                assign(material.babylonMaterial, true);
+            });
         }
 
         private _loadSpecularGlossinessProperties(loader: GLTFLoader, material: IGLTFMaterial, properties: IKHRMaterialsPbrSpecularGlossiness): void {
@@ -39,16 +31,16 @@ module BABYLON.GLTF2.Extensions {
             babylonMaterial.microSurface = properties.glossinessFactor === undefined ? 1 : properties.glossinessFactor;
 
             if (properties.diffuseTexture) {
-                babylonMaterial.albedoTexture = loader.loadTexture(properties.diffuseTexture);
+                babylonMaterial.albedoTexture = loader._loadTexture(properties.diffuseTexture);
             }
 
             if (properties.specularGlossinessTexture) {
-                babylonMaterial.reflectivityTexture = loader.loadTexture(properties.specularGlossinessTexture);
+                babylonMaterial.reflectivityTexture = loader._loadTexture(properties.specularGlossinessTexture);
                 babylonMaterial.reflectivityTexture.hasAlpha = true;
                 babylonMaterial.useMicroSurfaceFromReflectivityMapAlpha = true;
             }
 
-            loader.loadMaterialAlphaProperties(material, properties.diffuseFactor);
+            loader._loadMaterialAlphaProperties(material, properties.diffuseFactor);
         }
     }
 

+ 57 - 39
loaders/src/glTF/2.0/Extensions/MSFT_lod.ts

@@ -5,6 +5,7 @@ module BABYLON.GLTF2.Extensions {
         ids: number[];
     }
 
+    // See https://github.com/sbtron/glTF/tree/MSFT_lod/extensions/Vendor/MSFT_lod for more information about this extension.
     export class MSFTLOD extends GLTFLoaderExtension {
         /**
          * Specify the minimal delay between LODs in ms (default = 250)
@@ -15,66 +16,83 @@ module BABYLON.GLTF2.Extensions {
             return "MSFT_lod";
         }
 
-        protected loadMaterial(loader: GLTFLoader, material: IGLTFMaterial, assign: (babylonMaterial: Material, isNew: boolean) => void): boolean {
-            if (!material.extensions) {
-                return false;
-            }
+        protected _traverseNode(loader: GLTFLoader, index: number, action: (node: IGLTFNode, index: number, parentNode: IGLTFNode) => boolean, parentNode: IGLTFNode): boolean {
+            var node = loader._gltf.nodes[index];
 
-            var properties = material.extensions[this.name] as IMSFTLOD;
-            if (!properties) {
-                return false;
-            }
+            return this._loadExtension<IMSFTLOD>(node, (extension, onComplete) => {
+                for (var i = extension.ids.length - 1; i >= 0; i--) {
+                    loader._traverseNode(extension.ids[i], action, parentNode);
+                }
 
-            // Clear out the extension so that it won't get loaded again.
-            material.extensions[this.name] = undefined;
+                loader._traverseNode(index, action, parentNode);
+                onComplete();
+            });
+        }
 
-            // Tell the loader not to clear its state until the highest LOD is loaded.
-            var materialLODs = [material.index, ...properties.ids];
+        protected _loadNode(loader: GLTFLoader, node: IGLTFNode): boolean {
+            return this._loadExtension<IMSFTLOD>(node, (extension, onComplete) => {
+                var nodes = [node.index, ...extension.ids].map(index => loader._gltf.nodes[index]);
 
-            loader.addLoaderPendingData(material);
-            for (var index = 0; index < materialLODs.length; index++) {
-                loader.addLoaderNonBlockingPendingData(index);
-            }
+                loader._addLoaderPendingData(node);
+                this._loadNodeLOD(loader, nodes, nodes.length - 1, () => {
+                    loader._removeLoaderPendingData(node);
+                    onComplete();
+                });
+            });
+        }
 
-            // Start with the lowest quality LOD.
-            this.loadMaterialLOD(loader, material, materialLODs, materialLODs.length - 1, assign);
+        private _loadNodeLOD(loader: GLTFLoader, nodes: IGLTFNode[], index: number, onComplete: () => void): void {
+            loader._whenAction(() => {
+                loader._loadNode(nodes[index]);
+            }, () => {
+                if (index !== nodes.length - 1) {
+                    var previousNode = nodes[index + 1];
+                    previousNode.babylonMesh.setEnabled(false);
+                }
 
-            return true;
-        }
+                if (index === 0) {
+                    onComplete();
+                    return;
+                }
 
-        private loadMaterialLOD(loader: GLTFLoader, material: IGLTFMaterial, materialLODs: number[], lod: number, assign: (babylonMaterial: Material, isNew: boolean) => void): void {
-            var materialLOD = loader.gltf.materials[materialLODs[lod]];
+                setTimeout(() => {
+                    this._loadNodeLOD(loader, nodes, index - 1, onComplete);
+                }, MSFTLOD.MinimalLODDelay);
+            });
+        }
 
-            if (lod !== materialLODs.length - 1) {
-                loader.blockPendingTracking = true;
-            }
-            
-            loader.loadMaterial(materialLOD, (babylonMaterial, isNew) => {
-                assign(babylonMaterial, isNew);
+        protected _loadMaterial(loader: GLTFLoader, material: IGLTFMaterial, assign: (babylonMaterial: Material, isNew: boolean) => void): boolean {
+            return this._loadExtension<IMSFTLOD>(material, (extension, onComplete) => {
+                var materials = [material.index, ...extension.ids].map(index => loader._gltf.materials[index]);
 
-                loader.removeLoaderPendingData(lod);
+                loader._addLoaderPendingData(material);
+                this._loadMaterialLOD(loader, materials, materials.length - 1, assign, () => {
+                    material.extensions[this.name] = extension;
+                    loader._removeLoaderPendingData(material);
+                    onComplete();
+                });
+            });
+        }
 
-                // Loading is considered complete if this is the lowest quality LOD.
-                if (lod === materialLODs.length - 1) {
-                    loader.removeLoaderPendingData(material);
-                }
+        private _loadMaterialLOD(loader: GLTFLoader, materials: IGLTFMaterial[], index: number, assign: (babylonMaterial: Material, isNew: boolean) => void, onComplete: () => void): void {
+            loader._loadMaterial(materials[index], (babylonMaterial, isNew) => {
+                assign(babylonMaterial, isNew);
 
-                if (lod === 0) {
-                    loader.blockPendingTracking = false;
+                if (index === 0) {
+                    onComplete();
                     return;
                 }
 
                 // Load the next LOD when the loader is ready to render and
                 // all active material textures of the current LOD are loaded.
-                loader.executeWhenRenderReady(() => {
+                loader._executeWhenRenderReady(() => {
                     BaseTexture.WhenAllReady(babylonMaterial.getActiveTextures(), () => {
-                        setTimeout(()=> {
-                            this.loadMaterialLOD(loader, material, materialLODs, lod - 1, assign);
+                        setTimeout(() => {
+                            this._loadMaterialLOD(loader, materials, index - 1, assign, onComplete);
                         }, MSFTLOD.MinimalLODDelay);
                     });
                 });
             });
-
         }
     }
 

+ 185 - 179
loaders/src/glTF/2.0/babylon.glTFLoader.ts

@@ -1,29 +1,47 @@
 /// <reference path="../../../../dist/preview release/babylon.d.ts"/>
 
 module BABYLON.GLTF2 {
+    class GLTFLoaderTracker {
+        private _pendingCount = 0;
+        private _callback: () => void;
+
+        constructor(onComplete: () => void) {
+            this._callback = onComplete;
+        }
+
+        public _addPendingData(data: any): void {
+            this._pendingCount++;
+        }
+
+        public _removePendingData(data: any): void {
+            if (--this._pendingCount === 0) {
+                this._callback();
+            }
+        }
+   }
+
     export class GLTFLoader implements IGLTFLoader, IDisposable {
+        public _gltf: IGLTF;
+        public _babylonScene: Scene;
+
         private _parent: GLTFFileLoader;
-        private _gltf: IGLTF;
-        private _babylonScene: Scene;
         private _rootUrl: string;
         private _defaultMaterial: PBRMaterial;
         private _successCallback: () => void;
         private _progressCallback: (event: ProgressEvent) => void;
         private _errorCallback: (message: string) => void;
-        private _renderReady: boolean = false;
-        private _disposed: boolean = false;
-        private _blockPendingTracking: boolean = false;
-        private _nonBlockingData: Array<any>;
-        private _rootMesh: Mesh;
+        private _renderReady = false;
+        private _disposed = false;
 
-        // Observable with boolean indicating success or error.
         private _renderReadyObservable = new Observable<GLTFLoader>();
 
         // Count of pending work that needs to complete before the asset is rendered.
-        private _renderPendingCount: number = 0;
+        private _renderPendingCount = 0;
 
-        // Count of pending work that needs to complete before the loader is cleared.
-        private _loaderPendingCount: number = 0;
+        // Count of pending work that needs to complete before the loader is disposed.
+        private _loaderPendingCount = 0;
+
+        private _loaderTrackers = new Array<GLTFLoaderTracker>();
 
         public static Extensions: { [name: string]: GLTFLoaderExtension } = {};
 
@@ -39,23 +57,6 @@ module BABYLON.GLTF2 {
             GLTFLoaderExtension._Extensions.push(extension);
         }
 
-        public get gltf(): IGLTF {
-            return this._gltf;
-        }
-
-        public get babylonScene(): Scene {
-            return this._babylonScene;
-        }
-
-        public executeWhenRenderReady(func: () => void) {
-            if (this._renderReady) {
-                func();
-            }
-            else {
-                this._renderReadyObservable.add(func);
-            }
-        }
-
         public constructor(parent: GLTFFileLoader) {
             this._parent = parent;
         }
@@ -107,57 +108,54 @@ module BABYLON.GLTF2 {
             this._progressCallback = onProgress;
             this._errorCallback = onError;
 
-            this.addPendingData(this);
+            this._addPendingData(this);
             this._loadScene(nodeNames);
             this._loadAnimations();
-            this.removePendingData(this);
+            this._removePendingData(this);
         }
 
         private _onError(message: string): void {
             if (this._errorCallback) {
                 this._errorCallback(message);
             }
+
             this.dispose();
         }
 
         private _onProgress(event: ProgressEvent): void {
-            this._progressCallback(event);
+            if (this._progressCallback) {
+                this._progressCallback(event);
+            }
         }
 
-        private _onRenderReady(): void {
-            switch (this._parent.coordinateSystemMode) {
-                case GLTFLoaderCoordinateSystemMode.AUTO:
-                    if (!this._babylonScene.useRightHandedSystem) {
-                        this._addRightHandToLeftHandRootTransform();
-                    }
-                    break;
-                case GLTFLoaderCoordinateSystemMode.PASS_THROUGH:
-                    // do nothing
-                    break;
-                case GLTFLoaderCoordinateSystemMode.FORCE_RIGHT_HANDED:
-                    this._babylonScene.useRightHandedSystem = true;
-                    break;
-                default:
-                    Tools.Error("Invalid coordinate system mode (" + this._parent.coordinateSystemMode + ")");
-                    break;
+        public _executeWhenRenderReady(func: () => void): void {
+            if (this._renderReady) {
+                func();
             }
+            else {
+                this._renderReadyObservable.add(func);
+            }
+        }
+
+        private _onRenderReady(): void {
+            var rootNode = this._gltf.nodes[this._gltf.nodes.length - 1];
+            rootNode.babylonMesh.setEnabled(true);
 
-            this._showMeshes();
             this._startAnimations();
             this._successCallback();
             this._renderReadyObservable.notifyObservers(this);
+
+            if (this._parent.onReady) {
+                this._parent.onReady();
+            }
         }
 
-        private _onLoaderComplete(): void {
+        private _onComplete(): void {
             if (this._parent.onComplete) {
                 this._parent.onComplete();
             }
-        }
 
-        private _onLoaderFirstLODComplete(): void {
-            if (this._parent.onFirstLODComplete) {
-                this._parent.onFirstLODComplete();
-            }
+            this.dispose();
         }
 
         private _loadData(data: IGLTFLoaderData): void {
@@ -183,30 +181,9 @@ module BABYLON.GLTF2 {
             }
         }
 
-        private _addRightHandToLeftHandRootTransform(): void {
-            this._rootMesh = new Mesh("root", this._babylonScene);
-            this._rootMesh.isVisible = false;
-            this._rootMesh.scaling = new Vector3(1, 1, -1);
-            this._rootMesh.rotation.y = Math.PI;
-
-            var nodes = this._gltf.nodes;
-            if (nodes) {
-                for (var i = 0; i < nodes.length; i++) {
-                    var mesh = nodes[i].babylonMesh;
-                    if (mesh && !mesh.parent) {
-                        mesh.parent = this._rootMesh;
-                    }
-                }
-            }
-        }
-
         private _getMeshes(): Mesh[] {
             var meshes = [];
 
-            if (this._rootMesh) {
-                meshes.push(this._rootMesh);
-            }
-
             var nodes = this._gltf.nodes;
             if (nodes) {
                 nodes.forEach(node => {
@@ -247,16 +224,38 @@ module BABYLON.GLTF2 {
             return targets;
         }
 
-        private _showMeshes(): void {
-            this._getMeshes().forEach(mesh => mesh.isVisible = true);
-        }
-
         private _startAnimations(): void {
             this._getAnimationTargets().forEach(target => this._babylonScene.beginAnimation(target, 0, Number.MAX_VALUE, true));
         }
 
         private _loadScene(nodeNames: any): void {
             var scene = this._gltf.scenes[this._gltf.scene || 0];
+
+            var rootNode = { name: "root", children: scene.nodes } as IGLTFNode;
+
+            switch (this._parent.coordinateSystemMode) {
+                case GLTFLoaderCoordinateSystemMode.AUTO:
+                    if (!this._babylonScene.useRightHandedSystem) {
+                        rootNode.rotation = [0, 1, 0, 0];
+                        rootNode.scale = [1, 1, -1];
+                    }
+                    break;
+                case GLTFLoaderCoordinateSystemMode.PASS_THROUGH:
+                    // do nothing
+                    break;
+                case GLTFLoaderCoordinateSystemMode.FORCE_RIGHT_HANDED:
+                    this._babylonScene.useRightHandedSystem = true;
+                    break;
+                default:
+                    Tools.Error("Invalid coordinate system mode (" + this._parent.coordinateSystemMode + ")");
+                    break;
+            }
+
+            // Inject a root node into the scene.
+            this._gltf.nodes = this._gltf.nodes || [];
+            scene.nodes = [this._gltf.nodes.length];
+            this._gltf.nodes.push(rootNode);
+
             var nodeIndices = scene.nodes;
 
             this._traverseNodes(nodeIndices, (node, index, parentNode) => {
@@ -277,23 +276,31 @@ module BABYLON.GLTF2 {
 
                 var filteredNodeIndices = new Array<number>();
                 this._traverseNodes(nodeIndices, node => {
-                    if (nodeNames.indexOf(node.name) === -1) {
-                        return true;
+                    if (nodeNames.indexOf(node.name) !== -1) {
+                        filteredNodeIndices.push(node.index);
+                        return false;
                     }
 
-                    filteredNodeIndices.push(node.index);
-                    return false;
+                    return true;
                 });
 
                 nodeIndices = filteredNodeIndices;
             }
 
-            this._traverseNodes(nodeIndices, node => this._loadNode(node));
+            for (var i = 0; i < nodeIndices.length; i++) {
+                this._loadNode(this._gltf.nodes[nodeIndices[i]]);
+            }
+
+            // Disable the root mesh until the asset is ready to render.
+            rootNode.babylonMesh.setEnabled(false);
         }
 
-        private _loadNode(node: IGLTFNode): boolean {
+        public _loadNode(node: IGLTFNode): void {
+            if (GLTFLoaderExtension.LoadNode(this, node)) {
+                return;
+            }
+
             node.babylonMesh = new Mesh(node.name || "mesh" + node.index, this._babylonScene);
-            node.babylonMesh.isVisible = false;
 
             this._loadTransform(node);
 
@@ -317,7 +324,11 @@ module BABYLON.GLTF2 {
                 // TODO: handle cameras
             }
 
-            return true;
+            if (node.children) {
+                for (var i = 0; i < node.children.length; i++) {
+                    this._loadNode(this._gltf.nodes[node.children[i]]);
+                }
+            }
         }
 
         private _loadMesh(node: IGLTFNode, mesh: IGLTFMesh): void {
@@ -356,25 +367,25 @@ module BABYLON.GLTF2 {
                         loadMaterial: () => {
                             if (primitive.material === undefined) {
                                 babylonMultiMaterial.subMaterials[i] = this._getDefaultMaterial();
+                                return;
                             }
-                            else {
-                                var material = this._gltf.materials[primitive.material];
-                                this.loadMaterial(material, (babylonMaterial, isNew) => {
-                                    if (isNew && this._parent.onMaterialLoaded) {
-                                        this._parent.onMaterialLoaded(babylonMaterial);
-                                    }
-                                    
-                                    if (this._parent.onBeforeMaterialReadyAsync) {
-                                        this.addLoaderPendingData(material);
-                                        this._parent.onBeforeMaterialReadyAsync(babylonMaterial, node.babylonMesh, babylonMultiMaterial.subMaterials[i] != null, () => {
-                                            babylonMultiMaterial.subMaterials[i] = babylonMaterial;
-                                            this.removeLoaderPendingData(material);
-                                        });
-                                    } else {
+
+                            var material = this._gltf.materials[primitive.material];
+                            this._loadMaterial(material, (babylonMaterial, isNew) => {
+                                if (isNew && this._parent.onMaterialLoaded) {
+                                    this._parent.onMaterialLoaded(babylonMaterial);
+                                }
+
+                                if (this._parent.onBeforeMaterialReadyAsync) {
+                                    this._addLoaderPendingData(material);
+                                    this._parent.onBeforeMaterialReadyAsync(babylonMaterial, node.babylonMesh, babylonMultiMaterial.subMaterials[i] != null, () => {
                                         babylonMultiMaterial.subMaterials[i] = babylonMaterial;
-                                    }
-                                });
-                            }
+                                        this._removeLoaderPendingData(material);
+                                    });
+                                } else {
+                                    babylonMultiMaterial.subMaterials[i] = babylonMaterial;
+                                }
+                            });
                         }
                     });
 
@@ -456,7 +467,7 @@ module BABYLON.GLTF2 {
             }
         }
 
-        private _createMorphTargets(node: IGLTFNode, mesh: IGLTFMesh, primitive: IGLTFMeshPrimitive, babylonMesh: Mesh) {
+        private _createMorphTargets(node: IGLTFNode, mesh: IGLTFMesh, primitive: IGLTFMeshPrimitive, babylonMesh: Mesh): void {
             var targets = primitive.targets;
             if (!targets) {
                 return;
@@ -618,17 +629,18 @@ module BABYLON.GLTF2 {
             }
         }
 
-        private _traverseNode(index: number, action: (node: IGLTFNode, index: number, parentNode: IGLTFNode) => boolean, parentNode: IGLTFNode = null): void {
-            var node = this._gltf.nodes[index];
+        public _traverseNode(index: number, action: (node: IGLTFNode, index: number, parentNode: IGLTFNode) => boolean, parentNode: IGLTFNode = null): void {
+            if (GLTFLoaderExtension.TraverseNode(this, index, action, parentNode)) {
+                return;
+            }
 
+            var node = this._gltf.nodes[index];
             if (!action(node, index, parentNode)) {
                 return;
             }
 
             if (node.children) {
-                for (var i = 0; i < node.children.length; i++) {
-                    this._traverseNode(node.children[i], action, node);
-                }
+                this._traverseNodes(node.children, action, node);
             }
         }
 
@@ -761,14 +773,12 @@ module BABYLON.GLTF2 {
                 }
             };
 
-            this._loadAccessorAsync(this._gltf.accessors[sampler.input], data =>
-            {
+            this._loadAccessorAsync(this._gltf.accessors[sampler.input], data => {
                 inputData = <Float32Array>data;
                 checkSuccess();
             });
 
-            this._loadAccessorAsync(this._gltf.accessors[sampler.output], data =>
-            {
+            this._loadAccessorAsync(this._gltf.accessors[sampler.output], data => {
                 outputData = <Float32Array>data;
                 checkSuccess();
             });
@@ -776,12 +786,12 @@ module BABYLON.GLTF2 {
 
         private _loadBufferAsync(index: number, onSuccess: (data: ArrayBufferView) => void): void {
             var buffer = this._gltf.buffers[index];
-            this.addPendingData(buffer);
+            this._addPendingData(buffer);
 
             if (buffer.loadedData) {
                 setTimeout(() => {
                     onSuccess(buffer.loadedData);
-                    this.removePendingData(buffer);
+                    this._removePendingData(buffer);
                 });
             }
             else if (GLTFUtils.IsBase64(buffer.uri)) {
@@ -789,20 +799,20 @@ module BABYLON.GLTF2 {
                 buffer.loadedData = new Uint8Array(data);
                 setTimeout(() => {
                     onSuccess(buffer.loadedData);
-                    this.removePendingData(buffer);
+                    this._removePendingData(buffer);
                 });
             }
             else if (buffer.loadedObservable) {
                 buffer.loadedObservable.add(buffer => {
                     onSuccess(buffer.loadedData);
-                    this.removePendingData(buffer);
+                    this._removePendingData(buffer);
                 });
             }
             else {
                 buffer.loadedObservable = new Observable<IGLTFBuffer>();
                 buffer.loadedObservable.add(buffer => {
                     onSuccess(buffer.loadedData);
-                    this.removePendingData(buffer);
+                    this._removePendingData(buffer);
                 });
 
                 Tools.LoadFile(this._rootUrl + buffer.uri, data => {
@@ -816,7 +826,7 @@ module BABYLON.GLTF2 {
                 }, this._babylonScene.database, true, request => {
                     if (!this._disposed) {
                         this._onError("Failed to load file '" + buffer.uri + "': " + request.status + " " + request.statusText);
-                        this.removePendingData(buffer);
+                        this._removePendingData(buffer);
                     }
                 });
             }
@@ -832,7 +842,7 @@ module BABYLON.GLTF2 {
 
             this._extractInterleavedData(sourceBuffer, targetBuffer, bytePerComponent, byteStride, targetBuffer.length);
 
-            return targetBuffer;              
+            return targetBuffer;
         }
 
         private _buildUint8ArrayBuffer(buffer: ArrayBuffer, byteOffset: number, byteLength: number, byteStride: number, bytePerComponent: number): Uint8Array {
@@ -845,8 +855,8 @@ module BABYLON.GLTF2 {
 
             this._extractInterleavedData(sourceBuffer, targetBuffer, bytePerComponent, byteStride, targetBuffer.length);
 
-            return targetBuffer;              
-        }        
+            return targetBuffer;
+        }
 
         private _buildInt16ArrayBuffer(buffer: ArrayBuffer, byteOffset: number, byteLength: number, byteStride: number, bytePerComponent: number): Int16Array {
             if (!byteStride) {
@@ -858,8 +868,8 @@ module BABYLON.GLTF2 {
 
             this._extractInterleavedData(sourceBuffer, targetBuffer, bytePerComponent, byteStride / 2, targetBuffer.length);
 
-            return targetBuffer;             
-        }   
+            return targetBuffer;
+        }
 
         private _buildUint16ArrayBuffer(buffer: ArrayBuffer, byteOffset: number, byteLength: number, byteStride: number, bytePerComponent: number): Uint16Array {
             if (!byteStride) {
@@ -871,9 +881,9 @@ module BABYLON.GLTF2 {
 
             this._extractInterleavedData(sourceBuffer, targetBuffer, bytePerComponent, byteStride / 2, targetBuffer.length);
 
-            return targetBuffer;             
-        }          
-        
+            return targetBuffer;
+        }
+
         private _buildUint32ArrayBuffer(buffer: ArrayBuffer, byteOffset: number, byteLength: number, byteStride: number, bytePerComponent: number): Uint32Array {
             if (!byteStride) {
                 return new Uint32Array(buffer, byteOffset, byteLength);
@@ -884,9 +894,9 @@ module BABYLON.GLTF2 {
 
             this._extractInterleavedData(sourceBuffer, targetBuffer, bytePerComponent, byteStride / 4, targetBuffer.length);
 
-            return targetBuffer;            
-        }     
-        
+            return targetBuffer;
+        }
+
         private _buildFloat32ArrayBuffer(buffer: ArrayBuffer, byteOffset: number, byteLength: number, byteStride: number, bytePerComponent: number): Float32Array {
             if (!byteStride) {
                 return new Float32Array(buffer, byteOffset, byteLength);
@@ -898,11 +908,11 @@ module BABYLON.GLTF2 {
             this._extractInterleavedData(sourceBuffer, targetBuffer, bytePerComponent, byteStride / 4, targetBuffer.length);
 
             return targetBuffer;
-        }    
-        
+        }
+
         private _extractInterleavedData(sourceBuffer: ArrayBufferView, targetBuffer: ArrayBufferView, bytePerComponent: number, stride: number, length: number): void {
             let tempIndex = 0;
-            let sourceIndex = 0;            
+            let sourceIndex = 0;
             let storageSize = bytePerComponent;
 
             while (tempIndex < length) {
@@ -978,19 +988,15 @@ module BABYLON.GLTF2 {
             }
         }
 
-        public set blockPendingTracking(value: boolean) {
-            this._blockPendingTracking = value;
-        }
-
-        public addPendingData(data: any) {
+        public _addPendingData(data: any): void {
             if (!this._renderReady) {
                 this._renderPendingCount++;
             }
 
-            this.addLoaderPendingData(data);
+            this._addLoaderPendingData(data);
         }
 
-        public removePendingData(data: any) {
+        public _removePendingData(data: any): void {
             if (!this._renderReady) {
                 if (--this._renderPendingCount === 0) {
                     this._renderReady = true;
@@ -998,36 +1004,36 @@ module BABYLON.GLTF2 {
                 }
             }
 
-            this.removeLoaderPendingData(data);
+            this._removeLoaderPendingData(data);
         }
 
-        public addLoaderNonBlockingPendingData(data: any): void {
-            if (!this._nonBlockingData) {
-                this._nonBlockingData = new Array<any>();
-            }
-            this._nonBlockingData.push(data);
+        public _addLoaderPendingData(data: any): void {
+            this._loaderPendingCount++;
+
+            this._loaderTrackers.forEach(tracker => tracker._addPendingData(data));
         }
 
-        public addLoaderPendingData(data: any) {
-            if (this._blockPendingTracking) {
-                this.addLoaderNonBlockingPendingData(data);
-                return;
+        public _removeLoaderPendingData(data: any): void {
+            this._loaderTrackers.forEach(tracker => tracker._removePendingData(data));
+
+            if (--this._loaderPendingCount === 0) {
+                this._onComplete();
             }
-            this._loaderPendingCount++;
         }
 
-        public removeLoaderPendingData(data: any) {
-            var indexInPending = this._nonBlockingData ? this._nonBlockingData.indexOf(data) : -1;
-            if (indexInPending !== -1) {
-                this._nonBlockingData.splice(indexInPending, 1);
-            } else if (--this._loaderPendingCount === 0) {
-                this._onLoaderFirstLODComplete();
-            }
+        public _whenAction(action: () => void, onComplete: () => void): void {
+            var tracker = new GLTFLoaderTracker(() => {
+                this._loaderTrackers.splice(this._loaderTrackers.indexOf(tracker));
+                onComplete();
+            });
 
-            if ((!this._nonBlockingData || this._nonBlockingData.length === 0) && this._loaderPendingCount === 0) {
-                this._onLoaderComplete();
-                this.dispose();
-            }
+            this._loaderTrackers.push(tracker);
+
+            this._addLoaderPendingData(tracker);
+
+            action();
+
+            this._removeLoaderPendingData(tracker);
         }
 
         private _getDefaultMaterial(): Material {
@@ -1064,20 +1070,20 @@ module BABYLON.GLTF2 {
             babylonMaterial.roughness = properties.roughnessFactor === undefined ? 1 : properties.roughnessFactor;
 
             if (properties.baseColorTexture) {
-                babylonMaterial.albedoTexture = this.loadTexture(properties.baseColorTexture);
+                babylonMaterial.albedoTexture = this._loadTexture(properties.baseColorTexture);
             }
 
             if (properties.metallicRoughnessTexture) {
-                babylonMaterial.metallicTexture = this.loadTexture(properties.metallicRoughnessTexture);
+                babylonMaterial.metallicTexture = this._loadTexture(properties.metallicRoughnessTexture);
                 babylonMaterial.useMetallnessFromMetallicTextureBlue = true;
                 babylonMaterial.useRoughnessFromMetallicTextureGreen = true;
                 babylonMaterial.useRoughnessFromMetallicTextureAlpha = false;
             }
 
-            this.loadMaterialAlphaProperties(material, properties.baseColorFactor);
+            this._loadMaterialAlphaProperties(material, properties.baseColorFactor);
         }
 
-        public loadMaterial(material: IGLTFMaterial, assign: (babylonMaterial: Material, isNew: boolean) => void): void {
+        public _loadMaterial(material: IGLTFMaterial, assign: (babylonMaterial: Material, isNew: boolean) => void): void {
             if (material.babylonMaterial) {
                 assign(material.babylonMaterial, false);
                 return;
@@ -1087,19 +1093,19 @@ module BABYLON.GLTF2 {
                 return;
             }
 
-            this.createPbrMaterial(material);
-            this.loadMaterialBaseProperties(material);
+            this._createPbrMaterial(material);
+            this._loadMaterialBaseProperties(material);
             this._loadMaterialMetallicRoughnessProperties(material);
             assign(material.babylonMaterial, true);
         }
 
-        public createPbrMaterial(material: IGLTFMaterial): void {
+        public _createPbrMaterial(material: IGLTFMaterial): void {
             var babylonMaterial = new PBRMaterial(material.name || "mat" + material.index, this._babylonScene);
             babylonMaterial.sideOrientation = Material.CounterClockWiseSideOrientation;
             material.babylonMaterial = babylonMaterial;
         }
 
-        public loadMaterialBaseProperties(material: IGLTFMaterial): void {
+        public _loadMaterialBaseProperties(material: IGLTFMaterial): void {
             var babylonMaterial = material.babylonMaterial as PBRMaterial;
 
             babylonMaterial.emissiveColor = material.emissiveFactor ? Color3.FromArray(material.emissiveFactor) : new Color3(0, 0, 0);
@@ -1109,7 +1115,7 @@ module BABYLON.GLTF2 {
             }
 
             if (material.normalTexture) {
-                babylonMaterial.bumpTexture = this.loadTexture(material.normalTexture);
+                babylonMaterial.bumpTexture = this._loadTexture(material.normalTexture);
                 babylonMaterial.invertNormalMapX = !this._babylonScene.useRightHandedSystem;
                 babylonMaterial.invertNormalMapY = this._babylonScene.useRightHandedSystem;
                 if (material.normalTexture.scale !== undefined) {
@@ -1118,7 +1124,7 @@ module BABYLON.GLTF2 {
             }
 
             if (material.occlusionTexture) {
-                babylonMaterial.ambientTexture = this.loadTexture(material.occlusionTexture);
+                babylonMaterial.ambientTexture = this._loadTexture(material.occlusionTexture);
                 babylonMaterial.useAmbientInGrayScale = true;
                 if (material.occlusionTexture.strength !== undefined) {
                     babylonMaterial.ambientTextureStrength = material.occlusionTexture.strength;
@@ -1126,11 +1132,11 @@ module BABYLON.GLTF2 {
             }
 
             if (material.emissiveTexture) {
-                babylonMaterial.emissiveTexture = this.loadTexture(material.emissiveTexture);
+                babylonMaterial.emissiveTexture = this._loadTexture(material.emissiveTexture);
             }
         }
 
-        public loadMaterialAlphaProperties(material: IGLTFMaterial, colorFactor?: number[]): void {
+        public _loadMaterialAlphaProperties(material: IGLTFMaterial, colorFactor?: number[]): void {
             var babylonMaterial = material.babylonMaterial as PBRMaterial;
 
             var alphaMode = material.alphaMode || "OPAQUE";
@@ -1157,7 +1163,7 @@ module BABYLON.GLTF2 {
             babylonMaterial.alphaCutOff = material.alphaCutoff === undefined ? 0.5 : material.alphaCutoff;
         }
 
-        public loadTexture(textureInfo: IGLTFTextureInfo): Texture {
+        public _loadTexture(textureInfo: IGLTFTextureInfo): Texture {
             var texture = this._gltf.textures[textureInfo.index];
             var texCoord = textureInfo.texCoord || 0;
 
@@ -1170,15 +1176,15 @@ module BABYLON.GLTF2 {
             var noMipMaps = (sampler.minFilter === ETextureMinFilter.NEAREST || sampler.minFilter === ETextureMinFilter.LINEAR);
             var samplingMode = GLTFUtils.GetTextureSamplingMode(sampler.magFilter, sampler.minFilter);
 
-            this.addPendingData(texture);
+            this._addPendingData(texture);
             var babylonTexture = new Texture(null, this._babylonScene, noMipMaps, false, samplingMode, () => {
                 if (!this._disposed) {
-                    this.removePendingData(texture);
+                    this._removePendingData(texture);
                 }
             }, () => {
                 if (!this._disposed) {
                     this._onError("Failed to load texture '" + source.uri + "'");
-                    this.removePendingData(texture);
+                    this._removePendingData(texture);
                 }
             });
 

+ 35 - 2
loaders/src/glTF/2.0/babylon.glTFLoaderExtension.ts

@@ -6,7 +6,32 @@ module BABYLON.GLTF2 {
 
         public abstract get name(): string;
 
-        protected loadMaterial(loader: GLTFLoader, material: IGLTFMaterial, assign: (babylonMaterial: Material, isNew: boolean) => void): boolean { return false; }
+        protected _traverseNode(loader: GLTFLoader, index: number, action: (node: IGLTFNode, index: number, parentNode: IGLTFNode) => boolean, parentNode: IGLTFNode): boolean { return false; }
+
+        protected _loadNode(loader: GLTFLoader, node: IGLTFNode): boolean { return false; }
+
+        protected _loadMaterial(loader: GLTFLoader, material: IGLTFMaterial, assign: (babylonMaterial: Material, isNew: boolean) => void): boolean { return false; }
+
+        protected _loadExtension<T>(property: IGLTFProperty, action: (extension: T, onComplete: () => void) => void): boolean {
+            if (!property.extensions) {
+                return false;
+            }
+
+            var extension = property.extensions[this.name] as T;
+            if (!extension) {
+                return false;
+            }
+
+            // Clear out the extension before executing the action to avoid recursing into the same property.
+            property.extensions[this.name] = undefined;
+
+            action(extension, () => {
+                // Restore the extension after completing the action.
+                property.extensions[this.name] = extension;
+            });
+
+            return true;
+        }
 
         //
         // Utilities
@@ -14,8 +39,16 @@ module BABYLON.GLTF2 {
 
         public static _Extensions: GLTFLoaderExtension[] = [];
 
+        public static TraverseNode(loader: GLTFLoader, index: number, action: (node: IGLTFNode, index: number, parentNode: IGLTFNode) => boolean, parentNode: IGLTFNode): boolean {
+            return this._ApplyExtensions(extension => extension._traverseNode(loader, index, action, parentNode));
+        }
+
+        public static LoadNode(loader: GLTFLoader, node: IGLTFNode): boolean {
+            return this._ApplyExtensions(extension => extension._loadNode(loader, node));
+        }
+
         public static LoadMaterial(loader: GLTFLoader, material: IGLTFMaterial, assign: (babylonMaterial: Material, isNew: boolean) => void): boolean {
-            return this._ApplyExtensions(extension => extension.loadMaterial(loader, material, assign));
+            return this._ApplyExtensions(extension => extension._loadMaterial(loader, material, assign));
         }
 
         private static _ApplyExtensions(action: (extension: GLTFLoaderExtension) => boolean) {

+ 11 - 4
loaders/src/glTF/babylon.glTFFileLoader.ts

@@ -38,18 +38,25 @@ module BABYLON {
         public coordinateSystemMode: GLTFLoaderCoordinateSystemMode = GLTFLoaderCoordinateSystemMode.AUTO;
         public onTextureLoaded: (texture: BaseTexture) => void;
         public onMaterialLoaded: (material: Material) => void;
+
         /**
          * Let the user decides if he needs to process the material (like precompilation) before affecting it to meshes
          */
         public onBeforeMaterialReadyAsync: (material: Material, targetMesh: AbstractMesh, isLOD: boolean, callback: () => void) => void;
+
         /**
-         * Raised when all LODs are complete (or if there is no LOD and model is complete)
+         * Raised when the visible components (geometry, materials, textures, etc.) are first ready to be rendered.
+         * For assets with LODs, raised when the first LOD is complete.
+         * For assets without LODs, raised when the model is complete just before onComplete.
          */
-        public onComplete: () => void;
+        public onReady: () => void;
+
         /**
-         * Raised when first LOD complete (or if there is no LOD and model is complete)
+         * Raised when the asset is completely loaded, just before the loader is disposed.
+         * For assets with LODs, raised when all of the LODs are complete.
+         * For assets without LODs, raised when the model is complete just after onReady.
          */
-        public onFirstLODComplete: () => void;
+        public onComplete: () => void;
 
         public name = "gltf";