Browse Source

Merge pull request #3970 from RaananW/extending-animations

Changes to fully support animations
David Catuhe 7 years ago
parent
commit
3e930e3500

+ 1 - 1
Viewer/dist/basicExample.html

@@ -17,7 +17,7 @@
     </head>
 
     <body>
-        <babylon configuration="config.json" model.title="Damaged Helmet" model.subtitle="BabylonJS" model.thumbnail="https://www.babylonjs.com/img/favicon/apple-icon-144x144.png"
+        <babylon id="babylon-viewer" configuration="config.json" model.title="Damaged Helmet" model.subtitle="BabylonJS" model.thumbnail="https://www.babylonjs.com/img/favicon/apple-icon-144x144.png"
             model.url="https://www.babylonjs.com/Assets/DamagedHelmet/glTF/DamagedHelmet.gltf" camera.behaviors.auto-rotate="0"
             templates.nav-bar.params.disable-on-fullscreen="true"></babylon>
         <script src="viewer.js"></script>

+ 1 - 0
Viewer/src/configuration/configuration.ts

@@ -68,6 +68,7 @@ export interface ViewerConfiguration {
 
 export interface IModelConfiguration {
     url?: string;
+    root?: string; //optional
     loader?: string; // obj, gltf?
     position?: { x: number, y: number, z: number };
     rotation?: { x: number, y: number, z: number, w?: number };

+ 7 - 1
Viewer/src/index.ts

@@ -1,7 +1,13 @@
+/// <reference path="../../dist/preview release/babylon.d.ts"/>
+/// <reference path="../../dist/babylon.glTF2Interface.d.ts"/>
+/// <reference path="../../dist/preview release/loaders/babylon.glTFFileLoader.d.ts"/>
+
 import { mapperManager } from './configuration/mappers';
 import { viewerManager } from './viewer/viewerManager';
 import { DefaultViewer } from './viewer/defaultViewer';
 import { AbstractViewer } from './viewer/viewer';
+import { ModelLoader } from './model/modelLoader';
+import { ViewerModel } from './model/viewerModel';
 
 /**
  * BabylonJS Viewer
@@ -35,4 +41,4 @@ function disposeAll() {
 }
 
 // public API for initialization
-export { InitTags, DefaultViewer, AbstractViewer, viewerManager, mapperManager, disposeAll };
+export { InitTags, DefaultViewer, AbstractViewer, viewerManager, mapperManager, disposeAll, ModelLoader, ViewerModel };

+ 37 - 0
Viewer/src/model/modelLoader.ts

@@ -0,0 +1,37 @@
+import { AbstractViewer } from "..";
+import { ISceneLoaderPlugin, ISceneLoaderPluginAsync, Tools, SceneLoader, Tags, GLTFFileLoader } from "babylonjs";
+import { IModelConfiguration } from "../configuration/configuration";
+import { ViewerModel, ModelState } from "./viewerModel";
+
+export class ModelLoader {
+
+    private _loadId: number;
+    private _disposed = false;
+
+    private _loaders: Array<ISceneLoaderPlugin | ISceneLoaderPluginAsync>;
+
+    constructor(private _viewer: AbstractViewer) {
+        this._loaders = [];
+        this._loadId = 0;
+    }
+
+    public load(modelConfiguration: IModelConfiguration): ViewerModel {
+
+        const model = new ViewerModel(this._viewer.scene, modelConfiguration);
+
+        model.loadId = this._loadId++;
+        this._loaders.push(model.loader);
+
+        return model;
+    }
+
+    public dispose() {
+        this._loaders.forEach(loader => {
+            if (loader.name === "gltf") {
+                (<GLTFFileLoader>loader).dispose();
+            }
+        });
+        this._loaders.length = 0;
+        this._disposed = true;
+    }
+}

+ 222 - 49
Viewer/src/model/viewerModel.ts

@@ -1,25 +1,47 @@
-import { ISceneLoaderPlugin, ISceneLoaderPluginAsync, AnimationGroup, Animatable, AbstractMesh, Tools, Scene, SceneLoader, Observable, SceneLoaderProgressEvent, Tags, ParticleSystem, Skeleton, IDisposable, Nullable, Animation } from "babylonjs";
+import { ISceneLoaderPlugin, ISceneLoaderPluginAsync, AnimationGroup, Animatable, AbstractMesh, Tools, Scene, SceneLoader, Observable, SceneLoaderProgressEvent, Tags, ParticleSystem, Skeleton, IDisposable, Nullable, Animation, GLTFFileLoader, Quaternion } from "babylonjs";
 import { IModelConfiguration } from "../configuration/configuration";
 import { IModelAnimation, GroupModelAnimation, AnimationPlayMode } from "./modelAnimation";
 
+import * as deepmerge from '../../assets/deepmerge.min.js';
+
+export enum ModelState {
+    INIT,
+    LOADING,
+    LOADED,
+    CANCELED,
+    ERROR
+}
+
 export class ViewerModel implements IDisposable {
 
     public loader: ISceneLoaderPlugin | ISceneLoaderPluginAsync;
 
     private _animations: Array<IModelAnimation>;
-    public meshes: Array<AbstractMesh>;
-    public particleSystems: Array<ParticleSystem>;
-    public skeletons: Array<Skeleton>;
+    public meshes: Array<AbstractMesh> = [];
+    public rootMesh: AbstractMesh;
+    public particleSystems: Array<ParticleSystem> = [];
+    public skeletons: Array<Skeleton> = [];
     public currentAnimation: IModelAnimation;
 
     public onLoadedObservable: Observable<ViewerModel>;
     public onLoadProgressObservable: Observable<SceneLoaderProgressEvent>;
     public onLoadErrorObservable: Observable<{ message: string; exception: any }>;
 
-    constructor(private _modelConfiguration: IModelConfiguration, private _scene: Scene, disableAutoLoad = false) {
+    public onAfterConfigure: Observable<ViewerModel>;
+
+    public state: ModelState;
+    public loadId: number;
+
+    private _loaderDisposed: boolean = false;
+    private _loadedUrl: string;
+
+    constructor(private _scene: Scene, private _modelConfiguration: IModelConfiguration, disableAutoLoad = false) {
         this.onLoadedObservable = new Observable();
         this.onLoadErrorObservable = new Observable();
         this.onLoadProgressObservable = new Observable();
+        this.onAfterConfigure = new Observable();
+
+        this.state = ModelState.INIT;
 
         this._animations = [];
 
@@ -36,9 +58,71 @@ export class ViewerModel implements IDisposable {
         }
     }
 
-    //public getAnimations() {
-    //    return this._animations;
-    //}
+    public cancelLoad() {
+        // ATM only available in the GLTF Loader
+        if (this.loader && this.loader.name === "gltf") {
+            let gltfLoader = (<GLTFFileLoader>this.loader);
+            gltfLoader.dispose();
+            this.state = ModelState.CANCELED;
+        }
+    }
+
+    public get configuration(): IModelConfiguration {
+        return this._modelConfiguration;
+    }
+
+    public set configuration(newConfiguration: IModelConfiguration) {
+        this._modelConfiguration = newConfiguration;
+        this._configureModel();
+    }
+
+    public updateConfiguration(newConfiguration: Partial<IModelConfiguration>) {
+        this._modelConfiguration = deepmerge(this._modelConfiguration, newConfiguration);
+        this._configureModel();
+    }
+
+    public initAnimations() {
+        this._animations.forEach(a => {
+            a.dispose();
+        });
+        this._animations.length = 0;
+
+        // check if this is not a gltf loader and init the animations
+        if (this.loader.name !== 'gltf') {
+            this.skeletons.forEach((skeleton, idx) => {
+                let ag = new AnimationGroup("animation-" + idx, this._scene);
+                skeleton.getAnimatables().forEach(a => {
+                    if (a.animations[0]) {
+                        ag.addTargetedAnimation(a.animations[0], a);
+                    }
+                });
+                this.addAnimationGroup(ag);
+            });
+        }
+
+        if (!this._modelConfiguration) return;
+
+        if (this._modelConfiguration.animation) {
+            if (this._modelConfiguration.animation.playOnce) {
+                this._animations.forEach(a => {
+                    a.playMode = AnimationPlayMode.ONCE;
+                });
+            }
+            if (this._modelConfiguration.animation.autoStart && this._animations.length) {
+                let animationName = this._modelConfiguration.animation.autoStart === true ?
+                    this._animations[0].name : this._modelConfiguration.animation.autoStart;
+                this.playAnimation(animationName);
+            }
+        }
+    }
+
+    public addAnimationGroup(animationGroup: AnimationGroup) {
+        this._animations.push(new GroupModelAnimation(animationGroup));
+    }
+
+    public getAnimations() {
+        return this._animations;
+    }
 
     public getAnimationNames() {
         return this._animations.map(a => a.name);
@@ -69,18 +153,116 @@ export class ViewerModel implements IDisposable {
         }
     }
 
+    private _configureModel() {
+        let meshesWithNoParent: Array<AbstractMesh> = this.meshes.filter(m => !m.parent);
+        let updateMeshesWithNoParent = (variable: string, value: any, param?: string) => {
+            meshesWithNoParent.forEach(mesh => {
+                if (param) {
+                    mesh[variable][param] = value;
+                } else {
+                    mesh[variable] = value;
+                }
+            });
+        }
+        let updateXYZ = (variable: string, configValues: { x: number, y: number, z: number, w?: number }) => {
+            if (configValues.x !== undefined) {
+                updateMeshesWithNoParent(variable, configValues.x, 'x');
+            }
+            if (configValues.y !== undefined) {
+                updateMeshesWithNoParent(variable, configValues.y, 'y');
+            }
+            if (configValues.z !== undefined) {
+                updateMeshesWithNoParent(variable, configValues.z, 'z');
+            }
+            if (configValues.w !== undefined) {
+                updateMeshesWithNoParent(variable, configValues.w, 'w');
+            }
+        }
+        // position?
+        if (this._modelConfiguration.position) {
+            updateXYZ('position', this._modelConfiguration.position);
+        }
+        if (this._modelConfiguration.rotation) {
+            //quaternion?
+            if (this._modelConfiguration.rotation.w) {
+                meshesWithNoParent.forEach(mesh => {
+                    if (!mesh.rotationQuaternion) {
+                        mesh.rotationQuaternion = new Quaternion();
+                    }
+                })
+                updateXYZ('rotationQuaternion', this._modelConfiguration.rotation);
+            } else {
+                updateXYZ('rotation', this._modelConfiguration.rotation);
+            }
+        }
+        if (this._modelConfiguration.scaling) {
+            updateXYZ('scaling', this._modelConfiguration.scaling);
+        }
+
+        if (this._modelConfiguration.castShadow) {
+            this.meshes.forEach(mesh => {
+                Tags.AddTagsTo(mesh, 'castShadow');
+            });
+        }
+
+        if (this._modelConfiguration.normalize) {
+            let center = false;
+            let unitSize = false;
+            let parentIndex;
+            if (this._modelConfiguration.normalize === true) {
+                center = true;
+                unitSize = true;
+                parentIndex = 0;
+            } else {
+                center = !!this._modelConfiguration.normalize.center;
+                unitSize = !!this._modelConfiguration.normalize.unitSize;
+                parentIndex = this._modelConfiguration.normalize.parentIndex;
+            }
+
+            let meshesToNormalize: Array<AbstractMesh> = [];
+            if (parentIndex !== undefined) {
+                meshesToNormalize.push(this.meshes[parentIndex]);
+            } else {
+                meshesToNormalize = meshesWithNoParent;
+            }
+
+            if (unitSize) {
+                meshesToNormalize.forEach(mesh => {
+                    mesh.normalizeToUnitCube(true);
+                    mesh.computeWorldMatrix(true);
+                });
+            }
+            if (center) {
+                meshesToNormalize.forEach(mesh => {
+                    const boundingInfo = mesh.getHierarchyBoundingVectors(true);
+                    const sizeVec = boundingInfo.max.subtract(boundingInfo.min);
+                    const halfSizeVec = sizeVec.scale(0.5);
+                    const center = boundingInfo.min.add(halfSizeVec);
+                    mesh.position = center.scale(-1);
+
+                    // Set on ground.
+                    mesh.position.y += halfSizeVec.y;
+
+                    // Recompute Info.
+                    mesh.computeWorldMatrix(true);
+                });
+            }
+        }
+        this.onAfterConfigure.notifyObservers(this);
+    }
+
     private _initLoad() {
-        if (!this._modelConfiguration || !this._modelConfiguration.url) {
-            return Tools.Error("No model URL to load.");
+        if (!this._modelConfiguration.url) {
+            this.state = ModelState.ERROR;
+            Tools.Error("No URL provided");
+            return;
         }
-        let parts = this._modelConfiguration.url.split('/');
-        let filename = parts.pop() || this._modelConfiguration.url;
-        let base = parts.length ? parts.join('/') + '/' : './';
 
+        let filename = Tools.GetFilename(this._modelConfiguration.url) || this._modelConfiguration.url;
+        let base = this._modelConfiguration.root || Tools.GetFolderPath(this._modelConfiguration.url);
         let plugin = this._modelConfiguration.loader;
+        this._loadedUrl = this._modelConfiguration.url;
 
-        //temp solution for animation group handling
-        let animationsArray = this._scene.animationGroups.slice();
 
         this.loader = SceneLoader.ImportMesh(undefined, base, filename, this._scene, (meshes, particleSystems, skeletons) => {
             meshes.forEach(mesh => {
@@ -90,53 +272,44 @@ export class ViewerModel implements IDisposable {
             this.particleSystems = particleSystems;
             this.skeletons = skeletons;
 
-            // check if this is a gltf loader and load the animations
-            if (this.loader.name === 'gltf') {
-                this._scene.animationGroups.forEach(ag => {
-                    // add animations that didn't exist before
-                    if (animationsArray.indexOf(ag) === -1) {
-                        this._animations.push(new GroupModelAnimation(ag));
-                    }
-                })
-            } else {
-                skeletons.forEach((skeleton, idx) => {
-                    let ag = new AnimationGroup("animation-" + idx, this._scene);
-                    skeleton.getAnimatables().forEach(a => {
-                        if (a.animations[0]) {
-                            ag.addTargetedAnimation(a.animations[0], a);
-                        }
-                    });
-                    this._animations.push(new GroupModelAnimation(ag));
-                });
-            }
-
-            if (this._modelConfiguration.animation) {
-                if (this._modelConfiguration.animation.playOnce) {
-                    this._animations.forEach(a => {
-                        a.playMode = AnimationPlayMode.ONCE;
-                    });
-                }
-                if (this._modelConfiguration.animation.autoStart && this._animations.length) {
-                    let animationName = this._modelConfiguration.animation.autoStart === true ?
-                        this._animations[0].name : this._modelConfiguration.animation.autoStart;
-                    this.playAnimation(animationName);
-                }
-            }
-
+            this.initAnimations();
             this.onLoadedObservable.notifyObserversWithPromise(this);
         }, (progressEvent) => {
             this.onLoadProgressObservable.notifyObserversWithPromise(progressEvent);
         }, (e, m, exception) => {
+            this.state = ModelState.ERROR;
+            Tools.Error("Load Error: There was an error loading the model. " + m);
             this.onLoadErrorObservable.notifyObserversWithPromise({ message: m, exception: exception });
         }, plugin)!;
 
-        this.loader['animationStartMode'] = 0;
+        if (this.loader.name === "gltf") {
+            let gltfLoader = (<GLTFFileLoader>this.loader);
+            gltfLoader.animationStartMode = 0;
+            gltfLoader.onDispose = () => {
+                this._loaderDisposed = true;
+            }
+            gltfLoader.onAnimationGroupLoaded = ag => {
+                this.addAnimationGroup(ag);
+            }
+        }
+
     }
 
     public dispose() {
+        this.onAfterConfigure.clear();
+        this.onLoadedObservable.clear();
+        this.onLoadErrorObservable.clear();
+        this.onLoadProgressObservable.clear();
+        if (this.loader && this.loader.name === "gltf") {
+            (<GLTFFileLoader>this.loader).dispose();
+        }
         this.particleSystems.forEach(ps => ps.dispose());
+        this.particleSystems.length = 0;
         this.skeletons.forEach(s => s.dispose());
+        this.skeletons.length = 0;
         this._animations.forEach(ag => ag.dispose());
+        this._animations.length = 0;
         this.meshes.forEach(m => m.dispose());
+        this.meshes.length = 0;
     }
 }

+ 4 - 3
Viewer/src/viewer/defaultViewer.ts

@@ -104,12 +104,12 @@ export class DefaultViewer extends AbstractViewer {
         this.containerElement.style.display = 'flex';
     }
 
-    protected configureModel(modelConfiguration: Partial<IModelConfiguration>, model: ViewerModel) {
-        super.configureModel(modelConfiguration, model);
-
+    protected configureTemplate(model: ViewerModel) {
         let navbar = this.templateManager.getTemplate('navBar');
         if (!navbar) return;
 
+        let modelConfiguration = model.configuration;
+
         let metadataContainer = navbar.parent.querySelector('#model-metadata');
         if (metadataContainer) {
             if (modelConfiguration.title !== undefined) {
@@ -143,6 +143,7 @@ export class DefaultViewer extends AbstractViewer {
     }
 
     private onModelLoaded = (model: ViewerModel) => {
+        this.configureTemplate(model);
         // with a short timeout, making sure everything is there already.
         let hideLoadingDelay = 500;
         if (this.configuration.lab && this.configuration.lab.hideLoadingDelay !== undefined) {

+ 51 - 140
Viewer/src/viewer/viewer.ts

@@ -1,12 +1,14 @@
 import { viewerManager } from './viewerManager';
 import { TemplateManager } from './../templateManager';
 import { ConfigurationLoader } from './../configuration/loader';
-import { CubeTexture, Color3, IEnvironmentHelperOptions, EnvironmentHelper, Effect, SceneOptimizer, SceneOptimizerOptions, Observable, Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, AbstractMesh, Mesh, HemisphericLight, Database, SceneLoaderProgressEvent, ISceneLoaderPlugin, ISceneLoaderPluginAsync, Quaternion, Light, ShadowLight, ShadowGenerator, Tags, AutoRotationBehavior, BouncingBehavior, FramingBehavior, Behavior, Tools } from 'babylonjs';
+import { Skeleton, AnimationGroup, ParticleSystem, CubeTexture, Color3, IEnvironmentHelperOptions, EnvironmentHelper, Effect, SceneOptimizer, SceneOptimizerOptions, Observable, Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, AbstractMesh, Mesh, HemisphericLight, Database, SceneLoaderProgressEvent, ISceneLoaderPlugin, ISceneLoaderPluginAsync, Quaternion, Light, ShadowLight, ShadowGenerator, Tags, AutoRotationBehavior, BouncingBehavior, FramingBehavior, Behavior, Tools } from 'babylonjs';
 import { ViewerConfiguration, ISceneConfiguration, ISceneOptimizerConfiguration, IObserversConfiguration, IModelConfiguration, ISkyboxConfiguration, IGroundConfiguration, ILightConfiguration, ICameraConfiguration } from '../configuration/configuration';
 
 import * as deepmerge from '../../assets/deepmerge.min.js';
 import { CameraBehavior } from 'src/interfaces';
 import { ViewerModel } from '../model/viewerModel';
+import { GroupModelAnimation } from '../model/modelAnimation';
+import { ModelLoader } from '../model/modelLoader';
 
 export abstract class AbstractViewer {
 
@@ -23,6 +25,7 @@ export abstract class AbstractViewer {
      * The last loader used to load a model. 
      */
     public lastUsedLoader: ISceneLoaderPlugin | ISceneLoaderPluginAsync;
+    public modelLoader: ModelLoader;
 
     protected configuration: ViewerConfiguration;
     public environmentHelper: EnvironmentHelper;
@@ -72,6 +75,7 @@ export abstract class AbstractViewer {
 
         this.registeredOnBeforerenderFunctions = [];
         this.models = [];
+        this.modelLoader = new ModelLoader(this);
 
         // add this viewer to the viewer manager
         viewerManager.addViewer(this);
@@ -526,100 +530,9 @@ export abstract class AbstractViewer {
     }
 
     protected configureModel(modelConfiguration: Partial<IModelConfiguration>, model?: ViewerModel) {
-        let focusMeshes = model ? model.meshes : this.scene.meshes;
-        let meshesWithNoParent: Array<AbstractMesh> = focusMeshes.filter(m => !m.parent);
-        let updateMeshesWithNoParent = (variable: string, value: any, param?: string) => {
-            meshesWithNoParent.forEach(mesh => {
-                if (param) {
-                    mesh[variable][param] = value;
-                } else {
-                    mesh[variable] = value;
-                }
-            });
-        }
-        let updateXYZ = (variable: string, configValues: { x: number, y: number, z: number, w?: number }) => {
-            if (configValues.x !== undefined) {
-                updateMeshesWithNoParent(variable, configValues.x, 'x');
-            }
-            if (configValues.y !== undefined) {
-                updateMeshesWithNoParent(variable, configValues.y, 'y');
-            }
-            if (configValues.z !== undefined) {
-                updateMeshesWithNoParent(variable, configValues.z, 'z');
-            }
-            if (configValues.w !== undefined) {
-                updateMeshesWithNoParent(variable, configValues.w, 'w');
-            }
-        }
-        // position?
-        if (modelConfiguration.position) {
-            updateXYZ('position', modelConfiguration.position);
-        }
-        if (modelConfiguration.rotation) {
-            if (modelConfiguration.rotation.w) {
-                meshesWithNoParent.forEach(mesh => {
-                    if (!mesh.rotationQuaternion) {
-                        mesh.rotationQuaternion = new Quaternion();
-                    }
-                })
-                updateXYZ('rotationQuaternion', modelConfiguration.rotation);
-            } else {
-                updateXYZ('rotation', modelConfiguration.rotation);
-            }
-        }
-        if (modelConfiguration.scaling) {
-            updateXYZ('scaling', modelConfiguration.scaling);
-        }
-
-        if (modelConfiguration.castShadow) {
-            focusMeshes.forEach(mesh => {
-                Tags.AddTagsTo(mesh, 'castShadow');
-            });
-        }
-
-        if (modelConfiguration.normalize) {
-            let center = false;
-            let unitSize = false;
-            let parentIndex;
-            if (modelConfiguration.normalize === true) {
-                center = true;
-                unitSize = true;
-                parentIndex = 0;
-            } else {
-                center = !!modelConfiguration.normalize.center;
-                unitSize = !!modelConfiguration.normalize.unitSize;
-                parentIndex = modelConfiguration.normalize.parentIndex;
-            }
-
-            let meshesToNormalize: Array<AbstractMesh> = [];
-            if (parentIndex !== undefined) {
-                meshesToNormalize.push(focusMeshes[parentIndex]);
-            } else {
-                meshesToNormalize = meshesWithNoParent;
-            }
-
-            if (unitSize) {
-                meshesToNormalize.forEach(mesh => {
-                    mesh.normalizeToUnitCube(true);
-                    mesh.computeWorldMatrix(true);
-                });
-            }
-            if (center) {
-                meshesToNormalize.forEach(mesh => {
-                    const boundingInfo = mesh.getHierarchyBoundingVectors(true);
-                    const sizeVec = boundingInfo.max.subtract(boundingInfo.min);
-                    const halfSizeVec = sizeVec.scale(0.5);
-                    const center = boundingInfo.min.add(halfSizeVec);
-                    mesh.position = center.scale(-1);
-
-                    // Set on ground.
-                    mesh.position.y += halfSizeVec.y;
-
-                    // Recompute Info.
-                    mesh.computeWorldMatrix(true);
-                });
-            }
-        }
+        this.models.forEach(model => {
+            model.configuration = modelConfiguration;
+        })
     }
 
     public dispose() {
@@ -660,6 +573,8 @@ export abstract class AbstractViewer {
             this.scene.activeCamera.detachControl(this.canvas);
         }
 
+        this.modelLoader.dispose();
+
         this.models.forEach(model => {
             model.dispose();
         });
@@ -783,6 +698,43 @@ export abstract class AbstractViewer {
     private isLoading: boolean;
     private nextLoading: Function;
 
+    public initModel(modelConfig: any = this.configuration.model, clearScene: boolean = true): ViewerModel {
+        let model = this.modelLoader.load(modelConfig);
+
+        if (clearScene) {
+            this.models.forEach(m => m.dispose());
+            this.models.length = 0;
+        }
+
+        this.models.push(model);
+        this.lastUsedLoader = model.loader;
+        model.onLoadErrorObservable.add((errorObject) => {
+            this.onModelLoadErrorObservable.notifyObserversWithPromise(errorObject);
+        });
+        model.onLoadProgressObservable.add((progressEvent) => {
+            return this.onModelLoadProgressObservable.notifyObserversWithPromise(progressEvent);
+        });
+        this.onLoaderInitObservable.notifyObserversWithPromise(this.lastUsedLoader);
+
+        model.onLoadedObservable.add(() => {
+            this.onModelLoadedObservable.notifyObserversWithPromise(model)
+                .then(() => {
+                    this.configureLights(this.configuration.lights);
+
+                    if (this.configuration.camera) {
+                        this.configureCamera(this.configuration.camera, model);
+                    }
+                    return this.initEnvironment(model);
+                }).then(() => {
+                    this.isLoading = false;
+                    return model;
+                });
+        });
+
+
+        return model;
+    }
+
     public loadModel(modelConfig: any = this.configuration.model, clearScene: boolean = true): Promise<ViewerModel> {
         // no model was provided? Do nothing!
         let modelUrl = (typeof modelConfig === 'string') ? modelConfig : modelConfig.url;
@@ -790,12 +742,8 @@ export abstract class AbstractViewer {
             return Promise.reject("no model configuration found");
         }
         if (this.isLoading) {
-            //another model is being model. Wait for it to finish, trigger the load afterwards
-            /*this.nextLoading = () => {
-                delete this.nextLoading;
-                return this.loadModel(modelConfig, clearScene);
-            }*/
-            return Promise.reject("sanother model is curently being loaded.");
+            // We can decide here whether or not to cancel the lst load, but the developer can do that.
+            return Promise.reject("another model is curently being loaded.");
         }
         this.isLoading = true;
         if ((typeof modelConfig === 'string')) {
@@ -812,50 +760,13 @@ export abstract class AbstractViewer {
 
         return Promise.resolve(this.scene).then((scene) => {
             if (!scene) return this.initScene();
-
-            if (clearScene) {
-                this.models.forEach(m => m.dispose());
-                this.models.length = 0;
-            }
             return scene;
         }).then(() => {
             return new Promise<ViewerModel>((resolve, reject) => {
                 // at this point, configuration.model is an object, not a string
-                let model = new ViewerModel(<IModelConfiguration>this.configuration.model, this.scene);
-                this.models.push(model);
-                this.lastUsedLoader = model.loader;
-                model.onLoadedObservable.add((model) => {
-                    resolve(model);
-                });
-                model.onLoadErrorObservable.add((errorObject) => {
-                    this.onModelLoadErrorObservable.notifyObserversWithPromise(errorObject).then(() => {
-                        reject(errorObject.exception);
-                    });
-                });
-                model.onLoadProgressObservable.add((progressEvent) => {
-                    return this.onModelLoadProgressObservable.notifyObserversWithPromise(progressEvent);
-                });
-                this.onLoaderInitObservable.notifyObserversWithPromise(this.lastUsedLoader);
+                return this.initModel(modelConfig, clearScene);
             });
-        }).then((model: ViewerModel) => {
-            return this.onModelLoadedObservable.notifyObserversWithPromise(model)
-                .then(() => {
-                    // update the models' configuration
-                    this.configureModel(this.configuration.model || modelConfig, model);
-                    this.configureLights(this.configuration.lights);
-
-                    if (this.configuration.camera) {
-                        this.configureCamera(this.configuration.camera, model);
-                    }
-                    return this.initEnvironment(model);
-                }).then(() => {
-                    this.isLoading = false;
-                    /*if (this.nextLoading) {
-                        return this.nextLoading();
-                    }*/
-                    return model;
-                });
-        });
+        })
     }
 
     protected initEnvironment(model?: ViewerModel): Promise<Scene> {

+ 25 - 1
dist/preview release/viewer/babylon.viewer.d.ts

@@ -205,6 +205,7 @@ declare module BabylonViewer {
 
     export interface IModelConfiguration {
         url?: string;
+        root?: string;
         loader?: string; // obj, gltf?
         position?: { x: number, y: number, z: number };
         rotation?: { x: number, y: number, z: number, w?: number };
@@ -430,9 +431,24 @@ declare module BabylonViewer {
         goToFrame(frameNumber: number): any;
     }
 
-    export interface ViewerModel extends BABYLON.IDisposable {
+    export enum ModelState {
+        INIT,
+        LOADING,
+        LOADED,
+        ERROR
+    }
+
+    export class ModelLoader {
+        constructor(viewer: AbstractViewer);
+        load(modelConfiguration: IModelConfiguration): ViewerModel;
+        dispose(): void;
+    }
+
+    export class ViewerModel {
+        constructor(scene: BABYLON.Scene, modelConfiguration: IModelConfiguration, disableAutoLoad: boolean);
         loader: BABYLON.ISceneLoaderPlugin | BABYLON.ISceneLoaderPluginAsync;
         meshes: Array<BABYLON.AbstractMesh>;
+        rootMesh: BABYLON.AbstractMesh;
         particleSystems: Array<BABYLON.ParticleSystem>;
         skeletons: Array<BABYLON.Skeleton>;
         currentAnimation: IModelAnimation;
@@ -442,7 +458,13 @@ declare module BabylonViewer {
             message: string;
             exception: any;
         }>;
+        onAfterConfigure: BABYLON.Observable<ViewerModel>;
+        state: ModelState;
+        loadId: number;
         load(): void;
+        initAnimations(): void;
+        addAnimationGroup(animationGroup: BABYLON.AnimationGroup): void;
+        getAnimations(): Array<IModelAnimation>;
         getAnimationNames(): string[];
         playAnimation(name: string): IModelAnimation;
         dispose(): void;
@@ -458,6 +480,7 @@ declare module BabylonViewer {
         sceneOptimizer: BABYLON.SceneOptimizer;
         baseId: string;
         models: Array<ViewerModel>;
+        modelLoader: ModelLoader;
         lastUsedLoader: BABYLON.ISceneLoaderPlugin | BABYLON.ISceneLoaderPluginAsync;
         protected configuration: ViewerConfiguration;
         environmentHelper: BABYLON.EnvironmentHelper;
@@ -499,6 +522,7 @@ declare module BabylonViewer {
         protected onTemplatesLoaded(): Promise<AbstractViewer>;
         protected initEngine(): Promise<BABYLON.Engine>;
         protected initScene(): Promise<BABYLON.Scene>;
+        initModel(modelConfig?: any, clearScene?: boolean): ViewerModel
         loadModel(modelConfig?: any, clearScene?: boolean): Promise<ViewerModel>;
         protected initEnvironment(viewerModel?: ViewerModel): Promise<BABYLON.Scene>;
         protected handleHardwareLimitations(): void;

File diff suppressed because it is too large
+ 10 - 10
dist/preview release/viewer/babylon.viewer.js


File diff suppressed because it is too large
+ 532 - 414
dist/preview release/viewer/babylon.viewer.max.js


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

@@ -92,7 +92,7 @@
 - Default fragment shader will clamp negative values to avoid underflow, webVR post processing will render to eye texture size ([trevordev](https://github.com/trevordev))
 - Supports Environment Drag and Drop in Sandbox ([sebavan](https://github.com/sebavan))
 - EnvironmentHelper has no an onError observable to handle errors when loading the textures ([RaananW](https://github.com/RaananW))
-- (Viewer) Viewer supports model animations ([RaananW](https://github.com/RaananW))
+- (Viewer) Viewer supports model animations and multi-model loading ([RaananW](https://github.com/RaananW))
 - Tests for sharpen, chromatic aberration, default pipeline and enable/disable post processes ([trevordev](https://github.com/trevordev))
 - onPointer* callbacks have now the event type as a 3rd variable ([RaananW](https://github.com/RaananW))
 - Lightmap texture in PBR material follow the gammaSpace Flag of the texture ([sebavan](https://github.com/sebavan))

+ 1 - 0
loaders/src/glTF/babylon.glTFFileLoader.ts

@@ -373,6 +373,7 @@ module BABYLON {
             loader.onTextureLoadedObservable.add(texture => this.onTextureLoadedObservable.notifyObservers(texture));
             loader.onMaterialLoadedObservable.add(material => this.onMaterialLoadedObservable.notifyObservers(material));
             loader.onExtensionLoadedObservable.add(extension => this.onExtensionLoadedObservable.notifyObservers(extension));
+            loader.onAnimationGroupLoadedObservable.add(animationGroup => this.onAnimationGroupLoadedObservable.notifyObservers(animationGroup));
 
             loader.onCompleteObservable.add(() => {
                 this.onMeshLoadedObservable.clear();