David Catuhe преди 7 години
родител
ревизия
cb50871d5b
променени са 87 файла, в които са добавени 24658 реда и са изтрити 5654 реда
  1. 6 0
      .gitignore
  2. 8 8
      .vscode/settings.json
  3. 900 900
      Playground/babylon.d.txt
  4. 6 2
      Tools/Gulp/config.json
  5. 118 5
      Tools/Gulp/gulpfile.js
  6. 1 1
      Tools/Gulp/package.json
  7. 40 0
      Tools/Gulp/processViewerDeclaration.js
  8. 10 1
      Viewer/assets/templates/default/navbar.html
  9. 9162 0
      Viewer/dist/assets/BrainStem/BrainStem.gltf
  10. BIN
      Viewer/dist/assets/BrainStem/BrainStem0.bin
  11. BIN
      Viewer/dist/assets/environment/DefaultSkybox_NX_256.png
  12. BIN
      Viewer/dist/assets/environment/DefaultSkybox_NY_256.png
  13. BIN
      Viewer/dist/assets/environment/DefaultSkybox_NZ_256.png
  14. BIN
      Viewer/dist/assets/environment/DefaultSkybox_PX_256.png
  15. BIN
      Viewer/dist/assets/environment/DefaultSkybox_PY_256.png
  16. BIN
      Viewer/dist/assets/environment/DefaultSkybox_PZ_256.png
  17. BIN
      Viewer/dist/assets/environment/DefaultSkybox_cube_radiance_256.dds
  18. BIN
      Viewer/dist/assets/environment/EnvMap_1.2-256.env
  19. BIN
      Viewer/dist/assets/environment/EnvMap_2.0-256.env
  20. BIN
      Viewer/dist/assets/environment/Ground_1.0-1024.png
  21. BIN
      Viewer/dist/assets/environment/Ground_2.0-1024.png
  22. BIN
      Viewer/dist/assets/environment/Skybox_1.0-128.dds
  23. 66 0
      Viewer/dist/sphereExample.html
  24. 33 0
      Viewer/dist/ufoExample.html
  25. 220 9
      Viewer/src/configuration/configuration.ts
  26. 8 0
      Viewer/src/configuration/globals.ts
  27. 1 1
      Viewer/src/configuration/mappers.ts
  28. 10 20
      Viewer/src/configuration/types/default.ts
  29. 12 0
      Viewer/src/configuration/types/environmentMap.ts
  30. 93 0
      Viewer/src/configuration/types/extended.ts
  31. 37 11
      Viewer/src/configuration/types/index.ts
  32. 60 0
      Viewer/src/configuration/types/shadowLight.ts
  33. 5 0
      Viewer/src/externalModules.d.ts
  34. 24 0
      Viewer/src/helper.ts
  35. 15 14
      Viewer/src/index.ts
  36. 15 0
      Viewer/src/initializer.ts
  37. 333 0
      Viewer/src/labs/environmentSerializer.ts
  38. 410 0
      Viewer/src/labs/texture.ts
  39. 141 0
      Viewer/src/labs/viewerLabs.ts
  40. 61 9
      Viewer/src/model/modelLoader.ts
  41. 14 0
      Viewer/src/loader/plugins/extendedMaterialLoaderPlugin.ts
  42. 28 0
      Viewer/src/loader/plugins/index.ts
  43. 16 0
      Viewer/src/loader/plugins/loaderPlugin.ts
  44. 39 0
      Viewer/src/loader/plugins/minecraftLoaderPlugin.ts
  45. 23 0
      Viewer/src/loader/plugins/msftLodLoaderPlugin.ts
  46. 46 0
      Viewer/src/loader/plugins/telemetryLoaderPlugin.ts
  47. 180 48
      Viewer/src/model/viewerModel.ts
  48. 128 0
      Viewer/src/telemetryManager.ts
  49. 51 14
      Viewer/src/templateManager.ts
  50. 65 33
      Viewer/src/viewer/defaultViewer.ts
  51. 1084 0
      Viewer/src/viewer/sceneManager.ts
  52. 219 645
      Viewer/src/viewer/viewer.ts
  53. 78 0
      Viewer/tests/commons/boot.ts
  54. 238 0
      Viewer/tests/commons/helper.ts
  55. 583 0
      Viewer/tests/commons/mockWebGL.ts
  56. 39 0
      Viewer/tests/karma.conf.js
  57. 16 0
      Viewer/tests/package.json
  58. 6 0
      Viewer/tests/unit/src/index.ts
  59. 433 0
      Viewer/tests/unit/src/viewer/viewer.ts
  60. 9 0
      Viewer/tests/unit/src/viewerReference.ts
  61. 38 0
      Viewer/tests/unit/tsconfig.json
  62. 55 0
      Viewer/tests/unit/webpack.config.js
  63. BIN
      Viewer/tests/validation/LogoV3.png
  64. BIN
      Viewer/tests/validation/ReferenceImages/BrainStem.png
  65. BIN
      Viewer/tests/validation/ReferenceImages/BrainStemTransformation.png
  66. BIN
      Viewer/tests/validation/ReferenceImages/Control.png
  67. BIN
      Viewer/tests/validation/ReferenceImages/Diffuse.png
  68. 92 0
      Viewer/tests/validation/config.json
  69. 88 0
      Viewer/tests/validation/index.css
  70. 55 0
      Viewer/tests/validation/index.html
  71. 125 0
      Viewer/tests/validation/integration.js
  72. 94 0
      Viewer/tests/validation/karma.conf.browserstack.js
  73. 40 0
      Viewer/tests/validation/karma.conf.js
  74. BIN
      Viewer/tests/validation/loading.gif
  75. 60 0
      Viewer/tests/validation/validate.html
  76. 288 0
      Viewer/tests/validation/validation.js
  77. 3 2
      Viewer/tsconfig-gulp.json
  78. 1 1
      Viewer/tsconfig.json
  79. 2100 0
      dist/preview release/viewer/babylon.viewer.d.ts
  80. 11 10
      dist/preview release/viewer/babylon.viewer.js
  81. 5303 3467
      dist/preview release/viewer/babylon.viewer.max.js
  82. 1163 404
      dist/preview release/viewer/babylon.viewer.module.d.ts
  83. 2 0
      dist/preview release/viewer/package.json
  84. 1 0
      dist/preview release/what's new.md
  85. 18 18
      src/Mesh/babylon.mesh.ts
  86. 27 27
      src/Mesh/babylon.meshBuilder.ts
  87. 4 4
      what's new.md

+ 6 - 0
.gitignore

@@ -175,4 +175,10 @@ dist/preview release/package/
 /Viewer/dist/viewer.js
 /Viewer/dist/viewer.min.js
 dist/preview release/viewer/babylon.d.ts
+dist/preview release/viewer/babylonjs.loaders.d.ts
+dist/preview release/viewer/babylon.glTF2Interface.d.ts
 Viewer/dist/viewer.max.js
+!Viewer/src/externalModules.d.ts
+Viewer/tests/unit/src/**/*.js
+Viewer/tests/Lib/**/*.js
+Viewer/tests/commons/**/*.js

+ 8 - 8
.vscode/settings.json

@@ -6,7 +6,7 @@
         "**/.svn": true,
         "**/.hg": true,
         "**/.DS_Store": true,
-        "**/.vs": true,        
+        "**/.vs": true,
         "**/.tempChromeProfileForDebug": true,
         "**/node_modules": true,
         "**/temp": true,
@@ -22,8 +22,8 @@
         "Viewer/**/*.d.ts": true,
         "**/*.js.map": true,
         "**/*.js.fx": true,
-        "**/*.js": { 
-            "when":"$(basename).ts"
+        "**/*.js": {
+            "when": "$(basename).ts"
         }
     },
     "files.associations": {
@@ -37,11 +37,11 @@
         "**/.temp": true,
         "**/dist": true,
         "**/*.map": true,
-        "**/*.js": { 
-             "when":"$(basename).ts"
-         },
+        "**/*.js": {
+            "when": "$(basename).ts"
+        },
         "**/*.d.ts": true,
-        "assets":true
+        "assets": true
     },
     "typescript.tsdk": "./Tools/Gulp/node_modules/typescript/lib"
-}
+}

Файловите разлики са ограничени, защото са твърде много
+ 900 - 900
Playground/babylon.d.txt


+ 6 - 2
Tools/Gulp/config.json

@@ -1764,7 +1764,7 @@
                 "name": "babylonjs-viewer",
                 "main": "../../Viewer/src/index.d.ts",
                 "out": "../../dist/preview release/viewer/babylon.viewer.module.d.ts",
-                "prependText": "/// <reference path=\"./babylon.d.ts\"/>"
+                "prependText": "/// <reference path=\"./babylon.d.ts\"/>\n/// <reference path=\"./babylon.glTF2Interface.d.ts\"/>\n/// <reference path=\"./babylonjs.loaders.d.ts\"/>\n"
             },
             "outputs": [
                 {
@@ -1776,7 +1776,11 @@
                         {
                             "filename": "babylon.viewer.js",
                             "outputDirectory": "/viewer/",
-                            "addBabylonDeclaration": true
+                            "addBabylonDeclaration": [
+                                "babylon.d.ts",
+                                "loaders/babylonjs.loaders.d.ts",
+                                "gltf2Interface/babylon.glTF2Interface.d.ts"
+                            ]
                         }
                     ],
                     "minified": true

+ 118 - 5
Tools/Gulp/gulpfile.js

@@ -34,6 +34,9 @@ var dtsBundle = require('dts-bundle');
 const through = require('through2');
 var karmaServer = require('karma').Server;
 
+//viewer declaration
+var processViewerDeclaration = require('./processViewerDeclaration');
+
 var config = require("./config.json");
 
 var del = require("del");
@@ -467,6 +470,8 @@ var buildExternalLibrary = function (library, settings, watch) {
                             if (err) throw err;
                             data = settings.build.dtsBundle.prependText + '\n' + data.toString();
                             fs.writeFile(settings.build.dtsBundle.out, data);
+                            var newData = processViewerDeclaration(data);
+                            fs.writeFile(settings.build.dtsBundle.out.replace('.module', ''), newData);
                         });
                     });
                 }
@@ -502,7 +507,16 @@ var buildExternalLibrary = function (library, settings, watch) {
 
                     if (library.babylonIncluded && dest.addBabylonDeclaration) {
                         // include the babylon declaration
-                        sequence.unshift(gulp.src(config.build.outputDirectory + '/' + config.build.declarationFilename)
+                        if (dest.addBabylonDeclaration === true) {
+                            dest.addBabylonDeclaration = [config.build.declarationFilename];
+                        }
+                        var decsToAdd = dest.addBabylonDeclaration.map(function (dec) {
+                            return config.build.outputDirectory + '/' + dec;
+                        });
+                        sequence.unshift(gulp.src(decsToAdd)
+                            .pipe(rename(function (path) {
+                                path.dirname = '';
+                            }))
                             .pipe(gulp.dest(outputDirectory)))
                     }
                 }
@@ -884,8 +898,8 @@ gulp.task("modules", ["prepare-dependency-tree"], function () {
  */
 gulp.task("typedoc-generate", function () {
     return gulp
-        .src(["../../dist/preview release/babylon.d.ts", 
-            "../../dist/preview release/loaders/babylon.glTF2FileLoader.d.ts", 
+        .src(["../../dist/preview release/babylon.d.ts",
+            "../../dist/preview release/loaders/babylon.glTF2FileLoader.d.ts",
             "../../dist/preview release/serializers/babylon.glTF2Serializer.d.ts",
             "../../dist/preview release/gltf2Interface/babylon.glTF2Interface.d.ts"])
         .pipe(typedoc({
@@ -1026,13 +1040,112 @@ gulp.task("tests-unit-debug", ["tests-unit-transpile"], function (done) {
     server.start();
 });
 
+gulp.task("tests-babylon-unit", ["tests-unit-transpile"], function (done) {
+    var kamaServerOptions = {
+        configFile: __dirname + "/../../tests/unit/karma.conf.js",
+        singleRun: true
+    };
+
+    var server = new karmaServer(kamaServerOptions, done);
+    server.start();
+});
+
 /**
  * Launches the KARMA unit tests in phantomJS.
  * (Can only be launch on any branches.)
  */
-gulp.task("tests-unit", ["tests-unit-transpile"], function (done) {
+gulp.task("tests-unit", function (cb) {
+    runSequence("tests-babylon-unit", "tests-viewer-unit", cb);
+});
+
+var rmDir = function (dirPath) {
+    try { var files = fs.readdirSync(dirPath); }
+    catch (e) { return; }
+    if (files.length > 0)
+        for (var i = 0; i < files.length; i++) {
+            var filePath = dirPath + '/' + files[i];
+            if (fs.statSync(filePath).isFile())
+                fs.unlinkSync(filePath);
+            else
+                rmDir(filePath);
+        }
+    fs.rmdirSync(dirPath);
+};
+
+/**
+ * Launches the viewer's KARMA validation tests in chrome in order to debug them.
+ * (Can only be launch locally.)
+ */
+gulp.task("tests-viewer-validation-karma", ["tests-viewer-validation-transpile"], function (done) {
     var kamaServerOptions = {
-        configFile: __dirname + "/../../tests/unit/karma.conf.js",
+        configFile: __dirname + "/../../Viewer/tests/validation/karma.conf.js",
+        singleRun: false
+    };
+
+    var server = new karmaServer(kamaServerOptions, done);
+    server.start();
+});
+
+/**
+ * Transpiles viewer typescript unit tests. 
+ */
+gulp.task("tests-viewer-validation-transpile", function (done) {
+
+    let wpBuild = webpack(require('../../Viewer//webpack.gulp.config.js'));
+
+    // clean the built directory
+    rmDir("../../Viewer/tests/build/");
+
+    return wpBuild
+        .pipe(rename(function (path) {
+            if (path.extname === '.js') {
+                path.basename = "test";
+            }
+        }))
+        .pipe(gulp.dest("../../Viewer/tests/build/"));
+});
+
+/**
+ * Transpiles viewer typescript unit tests. 
+ */
+gulp.task("tests-viewer-transpile", function (done) {
+
+    let wpBuild = webpack(require('../../Viewer/tests/unit/webpack.config.js'));
+
+    // clean the built directory
+    rmDir("../../Viewer/tests/build/");
+
+    return wpBuild
+        .pipe(rename(function (path) {
+            if (path.extname === '.js') {
+                path.basename = "test";
+            }
+        }))
+        .pipe(gulp.dest("../../Viewer/tests/build/"));
+});
+
+/**
+ * Launches the KARMA unit tests in chrome.
+ * (Can be launch on any branches.)
+ */
+gulp.task("tests-viewer-unit-debug", ["tests-viewer-transpile"], function (done) {
+    var kamaServerOptions = {
+        configFile: __dirname + "/../../Viewer/tests/karma.conf.js",
+        singleRun: false,
+        browsers: ['Chrome']
+    };
+
+    var server = new karmaServer(kamaServerOptions, done);
+    server.start();
+});
+
+/**
+ * Launches the KARMA unit tests in phantomJS.
+ * (Can be launch on any branches.)
+ */
+gulp.task("tests-viewer-unit", ["tests-viewer-transpile"], function (done) {
+    var kamaServerOptions = {
+        configFile: __dirname + "/../../Viewer/tests/karma.conf.js",
         singleRun: true
     };
 

+ 1 - 1
Tools/Gulp/package.json

@@ -61,7 +61,7 @@
         "webpack-stream": "^4.0.1"
     },
     "scripts": {
-        "install": "npm --prefix ../../Playground/ install ../../Playground/ && npm --prefix ../../tests/unit/ install ../../tests/unit/ && gulp deployLocalDev"
+        "install": "npm --prefix ../../Playground/ install ../../Playground/ && npm --prefix ../../tests/unit/ install ../../tests/unit/ && npm --prefix ../../Viewer/tests/ install ../../Viewer/tests/ && gulp deployLocalDev"
     },
     "dependencies": {
         "dts-bundle": "^0.7.3",

+ 40 - 0
Tools/Gulp/processViewerDeclaration.js

@@ -0,0 +1,40 @@
+module.exports = function (data) {
+
+    var str = "" + data;
+
+    // this regex is not working on node 6 for some reason:
+    // str = str.replace(/declare module 'babylonjs-viewer\/' {((?!(declare))(.|\n))*\n}/g, '');
+
+    let lines = str.split('\n');
+    var firstIndex = lines.findIndex((line => { return line.indexOf("'babylonjs-viewer/'") !== -1 }));
+    var lastIndex = lines.findIndex(((line, idx) => { return line.trim() === '}' && idx > firstIndex }));
+    lines.splice(firstIndex, lastIndex - firstIndex + 1);
+    str = lines.join('\n');
+
+    str = str.replace(/declare module (.*) {/g, 'declare module BabylonViewer {').replace("import * as BABYLON from 'babylonjs';", "");
+    str = str.replace(/import {(.*)} from ['"]babylonjs-viewer(.*)['"];/g, '').replace(/import 'babylonjs-loaders';/, '').replace(/import 'pep';/, '');
+
+    //find all used BABYLON and BABYLON-Loaders classes:
+
+    var babylonRegex = /import {(.*)} from ['"](babylonjs|babylonjs-loaders)['"];/g
+
+    var match = babylonRegex.exec(str);
+    let classes = new Set();
+    while (match != null) {
+        if (match[1]) {
+            match[1].split(",").forEach(element => {
+                classes.add(element.trim());
+            });
+        }
+        match = babylonRegex.exec(str);
+    }
+    str = str.replace(babylonRegex, '');
+    classes.forEach(cls => {
+        let rg = new RegExp(`([ <])(${cls})([^\\w])`, "g")
+        str = str.replace(rg, "$1BABYLON.$2$3");
+    });
+
+    str = str.replace(/export {(.*)};/g, '')
+
+    return str;
+}

+ 10 - 1
Viewer/assets/templates/default/navbar.html

@@ -56,7 +56,8 @@
         font-size: 90%;
     }
 
-    div.button-container {
+    div.button-container,
+    div.animation-container {
         align-items: center;
         justify-content: flex-end;
     }
@@ -100,6 +101,14 @@
         <span class="model-subtitle"> {{#if subtitle}}{{subtitle}} {{/if}}</span>
     </div>
 </div>
+{{#if animations}} {{#if hideAnimations}}{{else}}
+<div class="animation-container flex-container">
+    <select id="animation-selector" name="animations">
+        {{#each animations}}
+        <option value="{{this}}">{{this}}</option>> {{/each}}
+    </select>
+</div>
+{{/if}} {{/if}}
 <div class="button-container flex-container">
     <!-- holding the buttons -->
     {{#eachInMap buttons}}

Файловите разлики са ограничени, защото са твърде много
+ 9162 - 0
Viewer/dist/assets/BrainStem/BrainStem.gltf


BIN
Viewer/dist/assets/BrainStem/BrainStem0.bin


BIN
Viewer/dist/assets/environment/DefaultSkybox_NX_256.png


BIN
Viewer/dist/assets/environment/DefaultSkybox_NY_256.png


BIN
Viewer/dist/assets/environment/DefaultSkybox_NZ_256.png


BIN
Viewer/dist/assets/environment/DefaultSkybox_PX_256.png


BIN
Viewer/dist/assets/environment/DefaultSkybox_PY_256.png


BIN
Viewer/dist/assets/environment/DefaultSkybox_PZ_256.png


BIN
Viewer/dist/assets/environment/DefaultSkybox_cube_radiance_256.dds


BIN
Viewer/dist/assets/environment/EnvMap_1.2-256.env


BIN
Viewer/dist/assets/environment/EnvMap_2.0-256.env


BIN
Viewer/dist/assets/environment/Ground_1.0-1024.png


BIN
Viewer/dist/assets/environment/Ground_2.0-1024.png


BIN
Viewer/dist/assets/environment/Skybox_1.0-128.dds


+ 66 - 0
Viewer/dist/sphereExample.html

@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="en">
+
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="X-UA-Compatible" content="ie=edge">
+        <title>BabylonJS Viewer - Sphere</title>
+        <style>
+            babylon,
+            #viewport {
+                max-width: 800px;
+                max-height: 500px;
+                width: 100%;
+                height: 600px;
+            }
+        </style>
+    </head>
+
+    <body>
+        <babylon id="babylon-viewer" scene.environment-texture="/environment.dds" extends="extended, shadowDirectionalLight" lab.environmentMap.texture=""></babylon>
+        <div id="viewport" touch-action="none"></div>
+        <script src="viewer.js"></script>
+        <script>
+            BabylonViewer.viewerManager.onViewerAdded = function (viewer) {
+                console.log('Using viewerManager.onViewerAdded: ', 'viewer - ' + viewer.getBaseId());
+
+                let specular = 0.6;
+                let ms = 0.6;
+                viewer.onInitDoneObservable.add(() => {
+                    let meshModel = new BabylonViewer.ViewerModel(viewer, {
+                        material: {
+                            albedoColor: {
+                                r: 0,
+                                g: 0,
+                                b: 1
+                            },
+                            reflectivityColor: {
+                                r: specular,
+                                g: specular,
+                                b: specular,
+                            },
+                            microSurface: ms
+                        }
+                    });
+
+                    let sphereMesh = BABYLON.Mesh.CreateSphere('sphere-', 20, 1.0, viewer.sceneManager.scene);
+                    let material = new BABYLON.PBRMaterial("sphereMat", viewer.sceneManager.scene);
+                    material.environmentBRDFTexture = null;
+                    material.useAlphaFresnel = material.needAlphaBlending();
+                    material.backFaceCulling = material.forceDepthWrite;
+                    material.twoSidedLighting = true;
+                    material.useSpecularOverAlpha = false;
+                    material.useRadianceOverAlpha = false;
+                    material.usePhysicalLightFalloff = true;
+                    material.forceNormalForward = true;
+                    sphereMesh.material = material;
+                    meshModel.addMesh(sphereMesh, true);
+                })
+
+            }
+
+        </script>
+    </body>
+
+</html>

+ 33 - 0
Viewer/dist/ufoExample.html

@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="en">
+
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="X-UA-Compatible" content="ie=edge">
+        <title>BabylonJS Viewer - UFO</title>
+        <style>
+            babylon,
+            #viewport {
+                max-width: 900px;
+                max-height: 600px;
+                width: 100%;
+                height: 600px;
+            }
+        </style>
+    </head>
+
+    <body>
+        <babylon extends="default, shadowDirectionalLight, environmentMap" templates.nav-bar.params.hide-animations="true" templates.nav-bar.params.disable-on-fullscreen="true">
+            <scene glow="true">
+                <main-color r="0.5" g="0.2" b="0.2"></main-color>
+            </scene>
+            <model url="https://models.babylonjs.com/ufo.glb">
+                <animation auto-start="true"></animation>
+            </model>
+            <camera beta="0.8"></camera>
+        </babylon>
+        <script src="viewer.js"></script>
+    </body>
+
+</html>

+ 220 - 9
Viewer/src/configuration/configuration.ts

@@ -1,4 +1,5 @@
 import { ITemplateConfiguration } from './../templateManager';
+import { EngineOptions, IGlowLayerOptions } from 'babylonjs';
 
 export interface ViewerConfiguration {
 
@@ -33,7 +34,7 @@ export interface ViewerConfiguration {
     engine?: {
         antialiasing?: boolean;
         disableResize?: boolean;
-        engineOptions?: { [key: string]: any };
+        engineOptions?: EngineOptions;
         adaptiveQuality?: boolean;
     },
     //templateStructure?: ITemplateStructure,
@@ -51,6 +52,15 @@ export interface ViewerConfiguration {
         }
     }
 
+    loaderPlugins?: {
+        extendedMaterial?: boolean;
+        msftLod?: boolean;
+        telemetry?: boolean;
+        minecraft?: boolean;
+
+        [propName: string]: boolean | undefined;
+    };
+
     // features that are being tested.
     // those features' syntax will change and move out! 
     // Don't use in production (or be ready to make the changes :) )
@@ -63,10 +73,45 @@ export interface ViewerConfiguration {
             specular?: { r: number, g: number, b: number };
         }
         hideLoadingDelay?: number;
+        environmentAssetsRootURL?: string;
+        environmentMap?: {
+            /**
+             * Environment map texture path in relative to the asset folder.
+             */
+            texture: string;
+
+            /**
+             * Default rotation to apply to the environment map.
+             */
+            rotationY: number;
+
+            /**
+             * Tint level of the main color on the environment map.
+             */
+            tintLevel: number;
+        }
+        renderingPipelines?: {
+            default?: boolean | {
+                [propName: string]: any;
+            };
+            standard?: boolean | {
+                [propName: string]: any;
+            };
+            /*lens?: boolean | {
+                [propName: string]: boolean | string | number | undefined;
+            };*/
+            ssao?: boolean | {
+                [propName: string]: any;
+            };
+            ssao2?: boolean | {
+                [propName: string]: any;
+            };
+        }
     }
 }
 
 export interface IModelConfiguration {
+    id?: string;
     url?: string;
     root?: string; //optional
     loader?: string; // obj, gltf?
@@ -76,11 +121,12 @@ export interface IModelConfiguration {
     parentObjectIndex?: number; // the index of the parent object of the model in the loaded meshes array.
 
     castShadow?: boolean;
+    receiveShadows?: boolean;
     normalize?: boolean | {
         center?: boolean;
         unitSize?: boolean;
         parentIndex?: number;
-    }; // shoud the model be scaled to unit-size
+    }; // should the model be scaled to unit-size
 
     title?: string;
     subtitle?: string;
@@ -91,6 +137,33 @@ export interface IModelConfiguration {
         playOnce?: boolean;
     }
 
+    material?: {
+        directEnabled?: boolean;
+        directIntensity?: number;
+        emissiveIntensity?: number;
+        environmentIntensity?: number;
+        [propName: string]: any;
+    }
+
+    /** 
+     * Rotation offset axis definition
+     */
+    rotationOffsetAxis?: {
+        x: number;
+        y: number;
+        z: number;
+    };
+
+    /**
+     * the offset angle
+     */
+    rotationOffsetAngle?: number;
+
+    loaderConfiguration?: {
+        maxLODsToLoad?: number;
+        progressiveLoading?: boolean;
+    }
+
     // [propName: string]: any; // further configuration, like title and creator
 }
 
@@ -108,7 +181,7 @@ export interface ISkyboxConfiguration {
         imageProcessingConfiguration?: IImageProcessingConfiguration;
         [propName: string]: any;
     };
-    infiniteDIstance?: boolean;
+    infiniteDistance?: boolean;
 
 }
 
@@ -128,20 +201,145 @@ export interface IGroundConfiguration {
     texture?: string;
     color?: { r: number, g: number, b: number };
     opacity?: number;
-    material?: { // deprecated!
+    material?: {
         [propName: string]: any;
     };
 }
 
 export interface ISceneConfiguration {
     debug?: boolean;
-    autoRotate?: boolean; // deprecated
-    rotationSpeed?: number; // deprecated
-    defaultCamera?: boolean; // deprecated
-    defaultLight?: boolean; // deprecated
     clearColor?: { r: number, g: number, b: number, a: number };
+    mainColor?: { r: number, g: number, b: number };
     imageProcessingConfiguration?: IImageProcessingConfiguration;
     environmentTexture?: string;
+    colorGrading?: IColorGradingConfiguration;
+    environmentRotationY?: number;
+    glow?: boolean | IGlowLayerOptions;
+    disableHdr?: boolean;
+    renderInBackground?: boolean;
+    disableCameraControl?: boolean;
+    animationPropertiesOverride?: {
+        [propName: string]: any;
+    };
+    defaultMaterial?: {
+        materialType: "standard" | "pbr";
+        [propName: string]: any;
+    };
+    flags?: {
+        shadowsEnabled?: boolean;
+        particlesEnabled?: boolean;
+        collisionsEnabled?: boolean;
+        lightsEnabled?: boolean;
+        texturesEnabled?: boolean;
+        lensFlaresEnabled?: boolean;
+        proceduralTexturesEnabled?: boolean;
+        renderTargetsEnabled?: boolean;
+        spritesEnabled?: boolean;
+        skeletonsEnabled?: boolean;
+        audioEnabled?: boolean;
+    }
+}
+
+/**
+ * The Color Grading Configuration groups the different settings used to define the color grading used in the viewer.
+ */
+export interface IColorGradingConfiguration {
+
+    /**
+     * Transform data string, encoded as determined by transformDataFormat.
+     */
+    transformData: string;
+
+    /**
+     * The encoding format of TransformData (currently only raw-base16 is supported).
+     */
+    transformDataFormat: string;
+
+    /**
+     * The weight of the transform
+     */
+    transformWeight: number;
+
+    /**
+     * Color curve colorFilterHueGlobal value
+     */
+    colorFilterHueGlobal: number;
+
+    /**
+     * Color curve colorFilterHueShadows value
+     */
+    colorFilterHueShadows: number;
+
+    /**
+     * Color curve colorFilterHueMidtones value
+     */
+    colorFilterHueMidtones: number;
+
+    /**
+     * Color curve colorFilterHueHighlights value
+     */
+    colorFilterHueHighlights: number;
+
+    /**
+     * Color curve colorFilterDensityGlobal value
+     */
+    colorFilterDensityGlobal: number;
+
+    /**
+     * Color curve colorFilterDensityShadows value
+     */
+    colorFilterDensityShadows: number;
+
+    /**
+     * Color curve colorFilterDensityMidtones value
+     */
+    colorFilterDensityMidtones: number;
+
+    /**
+     * Color curve colorFilterDensityHighlights value
+     */
+    colorFilterDensityHighlights: number;
+
+    /**
+     * Color curve saturationGlobal value
+     */
+    saturationGlobal: number;
+
+    /**
+     * Color curve saturationShadows value
+     */
+    saturationShadows: number;
+
+    /**
+     * Color curve saturationMidtones value
+     */
+    saturationMidtones: number;
+
+    /**
+     * Color curve saturationHighlights value
+     */
+    saturationHighlights: number;
+
+    /**
+     * Color curve exposureGlobal value
+     */
+    exposureGlobal: number;
+
+    /**
+     * Color curve exposureShadows value
+     */
+    exposureShadows: number;
+
+    /**
+     * Color curve exposureMidtones value
+     */
+    exposureMidtones: number;
+
+    /**
+     * Color curve exposureHighlights value
+     */
+    exposureHighlights: number;
+
 }
 
 export interface ISceneOptimizerConfiguration {
@@ -176,12 +374,17 @@ export interface ICameraConfiguration {
     minZ?: number;
     maxZ?: number;
     inertia?: number;
+    exposure?: number;
+    pinchPrecision?: number;
     behaviors?: {
         [name: string]: number | {
             type: number;
             [propName: string]: any;
         };
     };
+    disableCameraControl?: boolean;
+    disableCtrlForPanning?: boolean;
+    disableAutoFocus?: boolean;
 
     [propName: string]: any;
 }
@@ -201,6 +404,7 @@ export interface ILightConfiguration {
     shadownEnabled?: boolean; // only on specific lights!
     shadowConfig?: {
         useBlurExponentialShadowMap?: boolean;
+        useBlurCloseExponentialShadowMap?: boolean;
         useKernelBlur?: boolean;
         blurKernel?: number;
         blurScale?: number;
@@ -208,8 +412,15 @@ export interface ILightConfiguration {
         maxZ?: number;
         frustumSize?: number;
         angleScale?: number;
+        frustumEdgeFalloff?: number;
         [propName: string]: any;
-    }
+    };
+    spotAngle?: number;
+    shadowFieldOfView?: number;
+    shadowBufferSize?: number;
+    shadowFrustumSize?: number;
+    shadowMinZ?: number;
+    shadowMaxZ?: number;
     [propName: string]: any;
 
     // no behaviors for light at the moment, but allowing configuration for future reference.

+ 8 - 0
Viewer/src/configuration/globals.ts

@@ -0,0 +1,8 @@
+export class ViewerGlobals {
+
+    public disableInit: boolean = false;
+    public disableWebGL2Support: boolean = false;
+
+}
+
+export let viewerGlobals: ViewerGlobals = new ViewerGlobals();

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

@@ -53,7 +53,7 @@ class HTMLMapper implements IMapper {
                     } else if (val === "false") {
                         val = false;
                     } else {
-                        var isnum = /^\d+$/.test(val);
+                        var isnum = !isNaN(parseFloat(val)) && isFinite(val);///^\d+$/.test(val);
                         if (isnum) {
                             let number = parseFloat(val);
                             if (!isNaN(number)) {

+ 10 - 20
Viewer/src/configuration/types/default.ts

@@ -38,7 +38,8 @@ export let defaultConfiguration: ViewerConfiguration = {
             },
             events: {
                 pointerdown: { 'fullscreen-button': true/*, '#help-button': true*/ },
-                pointerover: true
+                pointerover: true,
+                change: { 'animation-selector': true }
             }
         },
         overlay: {
@@ -66,24 +67,11 @@ export let defaultConfiguration: ViewerConfiguration = {
                 type: 2,
                 zoomOnBoundingInfo: true,
                 zoomStopsAnimation: false
-            }
-        }
+            },
+            bouncing: 1
+        },
+        wheelPrecision: 200,
     },
-    /*lights: {
-        "default": {
-            type: 1,
-            shadowEnabled: true,
-            direction: { x: -0.2, y: -0.8, z: 0 },
-            position: { x: 10, y: 10, z: 0 },
-            intensity: 4.5,
-            shadowConfig: {
-                useBlurExponentialShadowMap: true,
-                useKernelBlur: true,
-                blurKernel: 64,
-                blurScale: 4
-            }
-        }
-    },*/
     skybox: {
         /*cubeTexture: {
             url: 'https://playground.babylonjs.com/textures/environment.dds',
@@ -91,7 +79,7 @@ export let defaultConfiguration: ViewerConfiguration = {
         },*/
         pbr: true,
         blur: 0.7,
-        infiniteDIstance: false,
+        infiniteDistance: false,
         /*material: {
             imageProcessingConfiguration: {
                 colorCurves: {
@@ -110,7 +98,9 @@ export let defaultConfiguration: ViewerConfiguration = {
             }
         }*/
     },
-    ground: true,
+    ground: {
+        receiveShadows: true
+    },
     engine: {
         antialiasing: true
     },

+ 12 - 0
Viewer/src/configuration/types/environmentMap.ts

@@ -0,0 +1,12 @@
+import { ViewerConfiguration } from './../configuration';
+
+export const environmentMapConfiguration: ViewerConfiguration = {
+    lab: {
+        environmentAssetsRootURL: '/assets/environment/',
+        environmentMap: {
+            texture: 'EnvMap_2.0-256.env',
+            rotationY: 0,
+            tintLevel: 0.4
+        }
+    }
+}

+ 93 - 0
Viewer/src/configuration/types/extended.ts

@@ -0,0 +1,93 @@
+import { ViewerConfiguration } from './../configuration';
+
+export let extendedConfiguration: ViewerConfiguration = {
+    version: "3.2.0",
+    extends: "default",
+    camera: {
+        radius: 2,
+        alpha: -1.5708,
+        beta: Math.PI * 0.5 - 0.2618,
+        wheelPrecision: 300,
+        minZ: 0.1,
+        maxZ: 50,
+    },
+    lights: {
+        "light1": {
+            type: 0,
+            shadowEnabled: false,
+            position: { x: -1.78, y: 2.298, z: 2.62 },
+            diffuse: { r: 0.8, g: 0.8, b: 0.8 },
+            intensity: 3,
+            intensityMode: 0,
+            radius: 3.135,
+        },
+        "light3": {
+            type: 2,
+            shadowEnabled: false,
+            position: { x: -4, y: 2, z: -2.23 },
+            diffuse: { r: 0.718, g: 0.772, b: 0.749 },
+            intensity: 2.052,
+            intensityMode: 0,
+            radius: 0.5,
+            spotAngle: 42.85
+        }
+    },
+    ground: {
+        receiveShadows: true
+    },
+    scene: {
+        imageProcessingConfiguration: {
+            colorCurves: {
+                shadowsHue: 43.359,
+                shadowsDensity: 1,
+                shadowsSaturation: -25,
+                shadowsExposure: -3.0,
+                midtonesHue: 93.65,
+                midtonesDensity: -15.24,
+                midtonesExposure: 7.37,
+                midtonesSaturation: -15,
+                highlightsHue: 37.2,
+                highlightsDensity: -22.43,
+                highlightsExposure: 45.0,
+                highlightsSaturation: -15
+            }
+        },
+        mainColor: {
+            r: 0.7,
+            g: 0.7,
+            b: 0.7
+        }
+    },
+    loaderPlugins: {
+        extendedMaterial: true,
+        minecraft: true,
+        msftLod: true,
+        telemetry: true
+    },
+    model: {
+        rotationOffsetAxis: {
+            x: 0,
+            y: 1,
+            z: 0
+        },
+        rotationOffsetAngle: 3.66519,
+        material: {
+            directEnabled: true,
+            directIntensity: 0.884,
+            emissiveIntensity: 1.04,
+            environmentIntensity: 0.868
+        },
+        normalize: true,
+        castShadow: true,
+        receiveShadows: true
+    },
+    lab: {
+        renderingPipelines: {
+            default: {
+                bloomEnabled: true,
+                bloomThreshold: 1.0,
+                fxaaEnabled: true
+            }
+        }
+    }
+}

+ 37 - 11
Viewer/src/configuration/types/index.ts

@@ -1,18 +1,44 @@
 import { minimalConfiguration } from './minimal';
 import { defaultConfiguration } from './default';
+import { extendedConfiguration } from './extended';
 import { ViewerConfiguration } from '../configuration';
+import { shadowDirectionalLightConfiguration, shadowSpotlLightConfiguration } from './shadowLight';
+import { environmentMapConfiguration } from './environmentMap';
+import * as deepmerge from '../../../assets/deepmerge.min.js';
 
-let getConfigurationType = function (type: string): ViewerConfiguration {
-    switch (type) {
-        case 'default':
-            return defaultConfiguration;
-        case 'minimal':
-            return minimalConfiguration;
-        case 'none':
-            return {};
-        default:
-            return defaultConfiguration;
-    }
+let getConfigurationType = function (types: string): ViewerConfiguration {
+    let config: ViewerConfiguration = {};
+    let typesSeparated = types.split(",");
+    typesSeparated.forEach(type => {
+        switch (type.trim()) {
+            case 'environmentMap':
+                config = deepmerge(config, environmentMapConfiguration);
+                break;
+            case 'shadowDirectionalLight':
+                config = deepmerge(config, shadowDirectionalLightConfiguration);
+                break;
+            case 'shadowSpotLight':
+                config = deepmerge(config, shadowSpotlLightConfiguration);
+                break;
+            case 'extended':
+                config = deepmerge(config, extendedConfiguration);
+                break;
+            case 'minimal':
+                config = deepmerge(config, minimalConfiguration);
+                break;
+            case 'none':
+                break;
+            case 'default':
+            default:
+                config = deepmerge(config, defaultConfiguration);
+                break;
+        }
+
+        if (config.extends) {
+            config = deepmerge(config, getConfigurationType(config.extends));
+        }
+    });
+    return config;
 
 }
 

+ 60 - 0
Viewer/src/configuration/types/shadowLight.ts

@@ -0,0 +1,60 @@
+import { ViewerConfiguration } from './../configuration';
+
+export const shadowDirectionalLightConfiguration: ViewerConfiguration = {
+    model: {
+        receiveShadows: true,
+        castShadow: true
+    },
+    ground: {
+        receiveShadows: true
+    },
+    lights: {
+        shadowDirectionalLight: {
+            type: 1,
+            shadowEnabled: true,
+            target: { x: 0, y: 0, z: 0.5 },
+            position: { x: 1.49, y: 2.39, z: -1.33 },
+            diffuse: { r: 0.867, g: 0.816, b: 0.788 },
+            intensity: 4.887,
+            intensityMode: 0,
+            shadowBufferSize: 1024,
+            shadowFrustumSize: 8.0,
+            shadowFieldOfView: 50.977,
+            shadowMinZ: 0.1,
+            shadowMaxZ: 12.0,
+            shadowConfig: {
+                blurKernel: 32,
+                useBlurExponentialShadowMap: true
+            }
+        }
+    }
+}
+
+export const shadowSpotlLightConfiguration: ViewerConfiguration = {
+    model: {
+        receiveShadows: true,
+        castShadow: true
+    },
+    ground: {
+        receiveShadows: true
+    },
+    lights: {
+        shadowSpotLight: {
+            type: 2,
+            intensity: 2,
+            shadowEnabled: true,
+            target: { x: 0, y: 0, z: 0.5 },
+            position: { x: 0, y: 3.5, z: 3.7 },
+            angle: 1,
+            shadowOrthoScale: 0.5,
+            shadowBufferSize: 1024,
+            shadowMinZ: 0.1,
+            shadowMaxZ: 50.0,
+            shadowConfig: {
+                frustumEdgeFalloff: 0.5,
+                blurKernel: 32,
+                useBlurExponentialShadowMap: true
+            }
+        }
+    }
+}

+ 5 - 0
Viewer/src/externalModules.d.ts

@@ -0,0 +1,5 @@
+/// <reference path="../../dist/preview release/loaders/babylonjs.loaders.d.ts"/>
+
+declare module "babylonjs-loaders" {
+    export = BABYLON;
+}

+ 24 - 0
Viewer/src/helper.ts

@@ -25,4 +25,28 @@ export function kebabToCamel(s) {
  */
 export function camelToKebab(str) {
     return !str ? null : str.replace(/([A-Z])/g, function (g) { return '-' + g[0].toLowerCase() });
+}
+
+/**
+ * This will extend an object with configuration values.
+ * What it practically does it take the keys from the configuration and set them on the object.
+ * I the configuration is a tree, it will traverse into the tree.
+ * @param object the object to extend
+ * @param config the configuration object that will extend the object
+ */
+export function extendClassWithConfig(object: any, config: any) {
+    if (!config) return;
+    Object.keys(config).forEach(key => {
+        if (key in object && typeof object[key] !== 'function') {
+            // if (typeof object[key] === 'function') return;
+            // if it is an object, iterate internally until reaching basic types
+            if (typeof object[key] === 'object') {
+                extendClassWithConfig(object[key], config[key]);
+            } else {
+                if (config[key] !== undefined) {
+                    object[key] = config[key];
+                }
+            }
+        }
+    });
 }

+ 15 - 14
Viewer/src/index.ts

@@ -1,11 +1,14 @@
 /// <reference path="../../dist/babylon.glTF2Interface.d.ts"/>
 import { mapperManager } from './configuration/mappers';
+import { viewerGlobals } from './configuration/globals';
 import { viewerManager } from './viewer/viewerManager';
 import { DefaultViewer } from './viewer/defaultViewer';
 import { AbstractViewer } from './viewer/viewer';
-import { ModelLoader } from './model/modelLoader';
+import { telemetryManager } from './telemetryManager';
+import { ModelLoader } from './loader/modelLoader';
 import { ViewerModel, ModelState } from './model/viewerModel';
 import { AnimationPlayMode, AnimationState } from './model/modelAnimation';
+import { ILoaderPlugin } from './loader/plugins/loaderPlugin';
 
 /**
  * BabylonJS Viewer
@@ -13,25 +16,20 @@ import { AnimationPlayMode, AnimationState } from './model/modelAnimation';
  * An HTML-Based viewer for 3D models, based on BabylonJS and its extensions.
  */
 
-import { PromisePolyfill } from 'babylonjs';
+import * as BABYLON from 'babylonjs';
 
 // load needed modules.
 import 'babylonjs-loaders';
 import 'pep';
 
-
-import { InitTags } from './initializer';
+import { initListeners, InitTags } from './initializer';
 
 // promise polyfill, if needed!
-PromisePolyfill.Apply();
-
-export let disableInit: boolean = false;
-document.addEventListener("DOMContentLoaded", init);
-function init(event) {
-    document.removeEventListener("DOMContentLoaded", init);
-    if (disableInit) return;
-    InitTags();
-}
+BABYLON.PromisePolyfill.Apply();
+initListeners();
+
+//deprectaed, here for backwards compatibility
+let disableInit: boolean = viewerGlobals.disableInit;
 
 /**
  * Dispose all viewers currently registered
@@ -39,7 +37,10 @@ function init(event) {
 function disposeAll() {
     viewerManager.dispose();
     mapperManager.dispose();
+    telemetryManager.dispose();
 }
 
+const Version = BABYLON.Engine.Version;
+
 // public API for initialization
-export { InitTags, DefaultViewer, AbstractViewer, viewerManager, mapperManager, disposeAll, ModelLoader, ViewerModel, AnimationPlayMode, AnimationState, ModelState };
+export { BABYLON, Version, InitTags, DefaultViewer, AbstractViewer, viewerGlobals, telemetryManager, disableInit, viewerManager, mapperManager, disposeAll, ModelLoader, ViewerModel, AnimationPlayMode, AnimationState, ModelState, ILoaderPlugin };

+ 15 - 0
Viewer/src/initializer.ts

@@ -1,5 +1,20 @@
 import { DefaultViewer } from './viewer/defaultViewer';
 import { mapperManager } from './configuration/mappers';
+import { viewerGlobals, disableInit } from './';
+
+
+/**
+ * Will attach an init function the the DOMContentLoaded event.
+ * The init function will be removed automatically after the event was triggered.
+ */
+export function initListeners() {
+    document.addEventListener("DOMContentLoaded", init);
+    function init(event) {
+        document.removeEventListener("DOMContentLoaded", init);
+        if (viewerGlobals.disableInit || disableInit) return;
+        InitTags();
+    }
+}
 
 /**
  * Select all HTML tags on the page that match the selector and initialize a viewer

+ 333 - 0
Viewer/src/labs/environmentSerializer.ts

@@ -0,0 +1,333 @@
+import { Vector3, Tools } from "babylonjs";
+import { TextureCube, PixelFormat, PixelType } from './texture';
+
+/**
+	 * Spherical polynomial coefficients (counter part to spherical harmonic coefficients used in shader irradiance calculation)
+	 * @ignoreChildren
+	 */
+export interface SphericalPolynomalCoefficients {
+    x: Vector3;
+    y: Vector3;
+    z: Vector3;
+    xx: Vector3;
+    yy: Vector3;
+    zz: Vector3;
+    yz: Vector3;
+    zx: Vector3;
+    xy: Vector3;
+}
+
+/**
+ * Wraps data and maps required for environments with physically based rendering
+ */
+export interface PBREnvironment {
+
+    /**
+     * Spherical Polynomial Coefficients representing an irradiance map
+     */
+    irradiancePolynomialCoefficients: SphericalPolynomalCoefficients;
+
+    /**
+     * Specular cubemap
+     */
+    specularTexture?: TextureCube;
+    /**
+     * A scale factor applied to RGB values after reading from environment maps
+     */
+    textureIntensityScale: number;
+}
+
+
+/**
+		 * Environment map representations: layouts, projections and approximations
+		 */
+export type MapType =
+    'irradiance_sh_coefficients_9' |
+    'cubemap_faces';
+
+/**
+ * Image type used for environment map
+ */
+export type ImageType = 'png';
+
+//Payload Descriptor
+
+/**
+ * A generic field in JSON that report's its type
+ */
+export interface TypedObject<T> {
+    type: T;
+}
+
+/**
+ * Describes a range of bytes starting at byte pos (inclusive) and finishing at byte pos + length - 1
+ */
+export interface ByteRange {
+    pos: number;
+    length: number;
+}
+
+/**
+ * Complete Spectre Environment JSON Descriptor
+ */
+export interface EnvJsonDescriptor {
+    radiance: TypedObject<MapType>;
+    irradiance: TypedObject<MapType>;
+    specular: TypedObject<MapType>;
+}
+
+/**
+ * Spherical harmonic coefficients to provide an irradiance map
+ */
+export interface IrradianceSHCoefficients9 extends TypedObject<MapType> {
+    l00: Array<number>;
+
+    l1_1: Array<number>;
+    l10: Array<number>;
+    l11: Array<number>;
+
+    l2_2: Array<number>;
+    l2_1: Array<number>;
+    l20: Array<number>;
+    l21: Array<number>;
+    l22: Array<number>;
+}
+
+/**
+ * A generic set of images, where the image content is specified by byte ranges in the mipmaps field
+ */
+export interface ImageSet<T> extends TypedObject<MapType> {
+    imageType: ImageType;
+    width: number;
+    height: number;
+    mipmaps: Array<T>;
+    multiplier: number;
+}
+
+/**
+ * A set of cubemap faces
+ */
+export type CubemapFaces = ImageSet<Array<ByteRange>>;
+
+/**
+ * A single image containing an atlas of equirectangular-projection maps across all mip levels
+ */
+export type EquirectangularMipmapAtlas = ImageSet<ByteRange>;
+
+/**
+ * A static class proving methods to aid parsing Spectre environment files
+ */
+export class EnvironmentDeserializer {
+
+    /**
+     * Parses an arraybuffer into a new PBREnvironment object
+     * @param arrayBuffer The arraybuffer of the Spectre environment file
+     * @return a PBREnvironment object
+     */
+    public static Parse(arrayBuffer: ArrayBuffer): PBREnvironment {
+        var environment: PBREnvironment = {
+            //irradiance
+            irradiancePolynomialCoefficients: {
+                x: new Vector3(0, 0, 0),
+                y: new Vector3(0, 0, 0),
+                z: new Vector3(0, 0, 0),
+                xx: new Vector3(0, 0, 0),
+                yy: new Vector3(0, 0, 0),
+                zz: new Vector3(0, 0, 0),
+                yz: new Vector3(0, 0, 0),
+                zx: new Vector3(0, 0, 0),
+                xy: new Vector3(0, 0, 0)
+            },
+
+            //specular
+            textureIntensityScale: 1.0,
+        };
+
+        //read .env
+        let littleEndian = false;
+
+        let magicBytes = [0x86, 0x16, 0x87, 0x96, 0xf6, 0xd6, 0x96, 0x36];
+
+        let dataView = new DataView(arrayBuffer);
+        let pos = 0;
+
+        for (let i = 0; i < magicBytes.length; i++) {
+            if (dataView.getUint8(pos++) !== magicBytes[i]) {
+                Tools.Error('Not a Spectre environment map');
+            }
+        }
+
+        let version = dataView.getUint16(pos, littleEndian); pos += 2;
+
+        if (version !== 1) {
+            Tools.Warn('Unsupported Spectre environment map version "' + version + '"');
+        }
+
+        //read json descriptor - collect characters up to null terminator
+        let descriptorString = '';
+        let charCode = 0x00;
+        while ((charCode = dataView.getUint8(pos++))) {
+            descriptorString += String.fromCharCode(charCode);
+        }
+
+        let descriptor: EnvJsonDescriptor = JSON.parse(descriptorString);
+
+        let payloadPos = pos;
+
+        //irradiance
+        switch (descriptor.irradiance.type) {
+            case 'irradiance_sh_coefficients_9':
+                //irradiance
+                let harmonics = <IrradianceSHCoefficients9>descriptor.irradiance;
+
+                EnvironmentDeserializer._ConvertSHIrradianceToLambertianRadiance(harmonics);
+
+                //harmonics now represent radiance
+                EnvironmentDeserializer._ConvertSHToSP(harmonics, environment.irradiancePolynomialCoefficients);
+                break;
+            default:
+                Tools.Error('Unhandled MapType descriptor.irradiance.type (' + descriptor.irradiance.type + ')');
+        }
+
+        //specular
+        switch (descriptor.specular.type) {
+            case 'cubemap_faces':
+
+                var specularDescriptor = <CubemapFaces>descriptor.specular;
+
+                let specularTexture = environment.specularTexture = new TextureCube(PixelFormat.RGBA, PixelType.UNSIGNED_BYTE);
+                environment.textureIntensityScale = specularDescriptor.multiplier != null ? specularDescriptor.multiplier : 1.0;
+
+                let mipmaps = specularDescriptor.mipmaps;
+                let imageType = specularDescriptor.imageType;
+
+                for (let l = 0; l < mipmaps.length; l++) {
+                    let faceRanges = mipmaps[l];
+
+                    specularTexture.source[l] = [];
+
+                    for (let i = 0; i < 6; i++) {
+
+                        let range = faceRanges[i];
+                        let bytes = new Uint8Array(arrayBuffer, payloadPos + range.pos, range.length);
+
+                        switch (imageType) {
+                            case 'png':
+
+                                //construct image element from bytes
+                                let image = new Image();
+                                let src = URL.createObjectURL(new Blob([bytes], { type: 'image/png' }));
+                                image.src = src;
+                                specularTexture.source[l][i] = image;
+
+                                break;
+                            default:
+                                Tools.Error('Unhandled ImageType descriptor.specular.imageType (' + imageType + ')');
+                        }
+                    }
+                }
+
+                break;
+            default:
+                Tools.Error('Unhandled MapType descriptor.specular.type (' + descriptor.specular.type + ')');
+        }
+
+        return environment;
+    }
+
+    /**
+     * Convert from irradiance to outgoing radiance for Lambertian BDRF, suitable for efficient shader evaluation.
+     *	  L = (1/pi) * E * rho
+     * 
+     * This is done by an additional scale by 1/pi, so is a fairly trivial operation but important conceptually.
+     * @param harmonics Spherical harmonic coefficients (9)
+     */
+    private static _ConvertSHIrradianceToLambertianRadiance(harmonics: any): void {
+        EnvironmentDeserializer._ScaleSH(harmonics, 1 / Math.PI);
+        // The resultant SH now represents outgoing radiance, so includes the Lambert 1/pi normalisation factor but without albedo (rho) applied
+        // (The pixel shader must apply albedo after texture fetches, etc).
+    }
+
+    /**
+     * Convert spherical harmonics to spherical polynomial coefficients
+     * @param harmonics Spherical harmonic coefficients (9)
+     * @param outPolynomialCoefficents Polynomial coefficients (9) object to store result
+     */
+    private static _ConvertSHToSP(harmonics: any, outPolynomialCoefficents: SphericalPolynomalCoefficients) {
+        const rPi = 1 / Math.PI;
+
+        //x
+        outPolynomialCoefficents.x.x = 1.02333 * harmonics.l11[0] * rPi;
+        outPolynomialCoefficents.x.y = 1.02333 * harmonics.l11[1] * rPi;
+        outPolynomialCoefficents.x.z = 1.02333 * harmonics.l11[2] * rPi;
+
+        outPolynomialCoefficents.y.x = 1.02333 * harmonics.l1_1[0] * rPi;
+        outPolynomialCoefficents.y.y = 1.02333 * harmonics.l1_1[1] * rPi;
+        outPolynomialCoefficents.y.z = 1.02333 * harmonics.l1_1[2] * rPi;
+
+        outPolynomialCoefficents.z.x = 1.02333 * harmonics.l10[0] * rPi;
+        outPolynomialCoefficents.z.y = 1.02333 * harmonics.l10[1] * rPi;
+        outPolynomialCoefficents.z.z = 1.02333 * harmonics.l10[2] * rPi;
+
+        //xx
+        outPolynomialCoefficents.xx.x = (0.886277 * harmonics.l00[0] - 0.247708 * harmonics.l20[0] + 0.429043 * harmonics.l22[0]) * rPi;
+        outPolynomialCoefficents.xx.y = (0.886277 * harmonics.l00[1] - 0.247708 * harmonics.l20[1] + 0.429043 * harmonics.l22[1]) * rPi;
+        outPolynomialCoefficents.xx.z = (0.886277 * harmonics.l00[2] - 0.247708 * harmonics.l20[2] + 0.429043 * harmonics.l22[2]) * rPi;
+
+        outPolynomialCoefficents.yy.x = (0.886277 * harmonics.l00[0] - 0.247708 * harmonics.l20[0] - 0.429043 * harmonics.l22[0]) * rPi;
+        outPolynomialCoefficents.yy.y = (0.886277 * harmonics.l00[1] - 0.247708 * harmonics.l20[1] - 0.429043 * harmonics.l22[1]) * rPi;
+        outPolynomialCoefficents.yy.z = (0.886277 * harmonics.l00[2] - 0.247708 * harmonics.l20[2] - 0.429043 * harmonics.l22[2]) * rPi;
+
+        outPolynomialCoefficents.zz.x = (0.886277 * harmonics.l00[0] + 0.495417 * harmonics.l20[0]) * rPi;
+        outPolynomialCoefficents.zz.y = (0.886277 * harmonics.l00[1] + 0.495417 * harmonics.l20[1]) * rPi;
+        outPolynomialCoefficents.zz.z = (0.886277 * harmonics.l00[2] + 0.495417 * harmonics.l20[2]) * rPi;
+
+        //yz
+        outPolynomialCoefficents.yz.x = 0.858086 * harmonics.l2_1[0] * rPi;
+        outPolynomialCoefficents.yz.y = 0.858086 * harmonics.l2_1[1] * rPi;
+        outPolynomialCoefficents.yz.z = 0.858086 * harmonics.l2_1[2] * rPi;
+
+        outPolynomialCoefficents.zx.x = 0.858086 * harmonics.l21[0] * rPi;
+        outPolynomialCoefficents.zx.y = 0.858086 * harmonics.l21[1] * rPi;
+        outPolynomialCoefficents.zx.z = 0.858086 * harmonics.l21[2] * rPi;
+
+        outPolynomialCoefficents.xy.x = 0.858086 * harmonics.l2_2[0] * rPi;
+        outPolynomialCoefficents.xy.y = 0.858086 * harmonics.l2_2[1] * rPi;
+        outPolynomialCoefficents.xy.z = 0.858086 * harmonics.l2_2[2] * rPi;
+    }
+
+    /**
+     * Multiplies harmonic coefficients in place
+     * @param harmonics Spherical harmonic coefficients (9)
+     * @param scaleFactor Value to multiply by
+     */
+    private static _ScaleSH(harmonics: any, scaleFactor: number) {
+        harmonics.l00[0] *= scaleFactor;
+        harmonics.l00[1] *= scaleFactor;
+        harmonics.l00[2] *= scaleFactor;
+        harmonics.l1_1[0] *= scaleFactor;
+        harmonics.l1_1[1] *= scaleFactor;
+        harmonics.l1_1[2] *= scaleFactor;
+        harmonics.l10[0] *= scaleFactor;
+        harmonics.l10[1] *= scaleFactor;
+        harmonics.l10[2] *= scaleFactor;
+        harmonics.l11[0] *= scaleFactor;
+        harmonics.l11[1] *= scaleFactor;
+        harmonics.l11[2] *= scaleFactor;
+        harmonics.l2_2[0] *= scaleFactor;
+        harmonics.l2_2[1] *= scaleFactor;
+        harmonics.l2_2[2] *= scaleFactor;
+        harmonics.l2_1[0] *= scaleFactor;
+        harmonics.l2_1[1] *= scaleFactor;
+        harmonics.l2_1[2] *= scaleFactor;
+        harmonics.l20[0] *= scaleFactor;
+        harmonics.l20[1] *= scaleFactor;
+        harmonics.l20[2] *= scaleFactor;
+        harmonics.l21[0] *= scaleFactor;
+        harmonics.l21[1] *= scaleFactor;
+        harmonics.l21[2] *= scaleFactor;
+        harmonics.l22[0] *= scaleFactor;
+        harmonics.l22[1] *= scaleFactor;
+        harmonics.l22[2] *= scaleFactor;
+    }
+}

+ 410 - 0
Viewer/src/labs/texture.ts

@@ -0,0 +1,410 @@
+/**
+ * WebGL Pixel Formats
+ */
+export const enum PixelFormat {
+    DEPTH_COMPONENT = 0x1902,
+    ALPHA = 0x1906,
+    RGB = 0x1907,
+    RGBA = 0x1908,
+    LUMINANCE = 0x1909,
+    LUMINANCE_ALPHA = 0x190a,
+}
+
+/**
+ * WebGL Pixel Types
+ */
+export const enum PixelType {
+    UNSIGNED_BYTE = 0x1401,
+    UNSIGNED_SHORT_4_4_4_4 = 0x8033,
+    UNSIGNED_SHORT_5_5_5_1 = 0x8034,
+    UNSIGNED_SHORT_5_6_5 = 0x8363,
+}
+
+/**
+ * WebGL Texture Magnification Filter
+ */
+export const enum TextureMagFilter {
+    NEAREST = 0x2600,
+    LINEAR = 0x2601,
+}
+
+/**
+ * WebGL Texture Minification Filter
+ */
+export const enum TextureMinFilter {
+    NEAREST = 0x2600,
+    LINEAR = 0x2601,
+    NEAREST_MIPMAP_NEAREST = 0x2700,
+    LINEAR_MIPMAP_NEAREST = 0x2701,
+    NEAREST_MIPMAP_LINEAR = 0x2702,
+    LINEAR_MIPMAP_LINEAR = 0x2703,
+}
+
+/**
+ * WebGL Texture Wrap Modes
+ */
+export const enum TextureWrapMode {
+    REPEAT = 0x2901,
+    CLAMP_TO_EDGE = 0x812f,
+    MIRRORED_REPEAT = 0x8370,
+}
+
+/**
+ * Raw texture data and descriptor sufficient for WebGL texture upload
+ */
+export interface TextureData {
+    /**
+     * Width of image
+     */
+    width: number;
+    /**
+     * Height of image
+     */
+    height: number;
+    /**
+     * Format of pixels in data
+     */
+    format: PixelFormat;
+    /**
+     * Row byte alignment of pixels in data
+     */
+    alignment: number;
+    /**
+     * Pixel data
+     */
+    data: ArrayBufferView;
+}
+
+/**
+ * Wraps sampling parameters for a WebGL texture
+ */
+export interface SamplingParameters {
+    /**
+     * Magnification mode when upsampling from a WebGL texture
+     */
+    magFilter?: TextureMagFilter;
+    /**
+     * Minification mode when upsampling from a WebGL texture
+     */
+    minFilter?: TextureMinFilter;
+    /**
+     * X axis wrapping mode when sampling out of a WebGL texture bounds
+     */
+    wrapS?: TextureWrapMode;
+    /**
+     * Y axis wrapping mode when sampling out of a WebGL texture bounds
+     */
+    wrapT?: TextureWrapMode;
+    /**
+    * Anisotropic filtering samples
+    */
+    maxAnisotropy?: number;
+}
+
+/**
+ * Represents a valid WebGL texture source for use in texImage2D
+ */
+export type TextureSource = TextureData | ImageData | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement;
+
+/**
+ * A generic set of texture mipmaps (where index 0 has the largest dimension)
+ */
+export type Mipmaps<T> = Array<T>;
+
+/**
+ * A set of 6 cubemap arranged in the order [+x, -x, +y, -y, +z, -z]
+ */
+export type Faces<T> = Array<T>;
+
+/**
+ * A set of texture mipmaps specifically for 2D textures in WebGL (where index 0 has the largest dimension)
+ */
+export type Mipmaps2D = Mipmaps<TextureSource>;
+
+/**
+ * A set of texture mipmaps specifically for cubemap textures in WebGL (where index 0 has the largest dimension)
+ */
+export type MipmapsCube = Mipmaps<Faces<TextureSource>>;
+
+/**
+ * A minimal WebGL cubemap descriptor
+ */
+export class TextureCube {
+
+    /**
+     * Returns the width of a face of the texture or 0 if not available
+     */
+    public get Width(): number {
+        return (this.source && this.source[0] && this.source[0][0]) ? this.source[0][0].width : 0;
+    }
+
+    /**
+     * Returns the height of a face of the texture or 0 if not available
+     */
+    public get Height(): number {
+        return (this.source && this.source[0] && this.source[0][0]) ? this.source[0][0].height : 0;
+    }
+
+    /**
+     * constructor
+     * @param internalFormat WebGL pixel format for the texture on the GPU
+     * @param type WebGL pixel type of the supplied data and texture on the GPU
+     * @param source An array containing mipmap levels of faces, where each mipmap level is an array of faces and each face is a TextureSource object
+     */
+    constructor(public internalFormat: PixelFormat, public type: PixelType, public source: MipmapsCube = []) { }
+}
+
+/**
+     * A static class providing methods to aid working with Bablyon textures.
+     */
+export class TextureUtils {
+
+    /**
+     * A prefix used when storing a babylon texture object reference on a Spectre texture object
+     */
+    public static BabylonTextureKeyPrefix = '__babylonTexture_';
+
+    /**
+     * Controls anisotropic filtering for deserialized textures.
+     */
+    public static MaxAnisotropy = 4;
+
+    /**
+     * Returns a BabylonCubeTexture instance from a Spectre texture cube, subject to sampling parameters.
+     * If such a texture has already been requested in the past, this texture will be returned, otherwise a new one will be created.
+     * The advantage of this is to enable working with texture objects without the need to initialize on the GPU until desired.
+     * @param scene A Babylon Scene instance
+     * @param textureCube A Spectre TextureCube object
+     * @param parameters WebGL texture sampling parameters
+     * @param automaticMipmaps Pass true to enable automatic mipmap generation where possible (requires power of images)
+     * @param environment Specifies that the texture will be used as an environment
+     * @param singleLod Specifies that the texture will be a singleLod (for environment)
+     * @return Babylon cube texture
+     */
+    public static GetBabylonCubeTexture(scene: BABYLON.Scene, textureCube: TextureCube, automaticMipmaps: boolean, environment = false, singleLod = false): BABYLON.CubeTexture {
+        if (!textureCube) throw new Error("no texture cube provided");
+
+        var parameters: SamplingParameters;
+        if (environment) {
+            parameters = singleLod ? TextureUtils._EnvironmentSingleMipSampling : TextureUtils._EnvironmentSampling;
+        }
+        else {
+            parameters = {
+                magFilter: TextureMagFilter.NEAREST,
+                minFilter: TextureMinFilter.NEAREST,
+                wrapS: TextureWrapMode.CLAMP_TO_EDGE,
+                wrapT: TextureWrapMode.CLAMP_TO_EDGE
+            };
+        }
+
+        let key = TextureUtils.BabylonTextureKeyPrefix + parameters.magFilter + '' + parameters.minFilter + '' + parameters.wrapS + '' + parameters.wrapT;
+
+        let babylonTexture: BABYLON.CubeTexture = (<any>textureCube)[key];
+
+        if (!babylonTexture) {
+
+            //initialize babylon texture
+            babylonTexture = new BABYLON.CubeTexture('', scene);
+            if (environment) {
+                babylonTexture.lodGenerationOffset = TextureUtils.EnvironmentLODOffset;
+                babylonTexture.lodGenerationScale = TextureUtils.EnvironmentLODScale;
+            }
+
+            babylonTexture.gammaSpace = false;
+
+            let internalTexture = new BABYLON.InternalTexture(scene.getEngine(), BABYLON.InternalTexture.DATASOURCE_CUBERAW);
+            let glTexture = internalTexture._webGLTexture;
+            //babylon properties
+            internalTexture.isCube = true;
+            internalTexture.generateMipMaps = false;
+
+            babylonTexture._texture = internalTexture;
+
+            TextureUtils.ApplySamplingParameters(babylonTexture, parameters);
+
+            let maxMipLevel = automaticMipmaps ? 0 : textureCube.source.length - 1;
+            let texturesUploaded = 0;
+
+            var textureComplete = function () {
+                return texturesUploaded === ((maxMipLevel + 1) * 6);
+            };
+
+            var uploadFace = function (i: number, level: number, face: TextureSource) {
+                if (!glTexture) return;
+
+                if (i === 0 && level === 0) {
+                    internalTexture.width = face.width;
+                    internalTexture.height = face.height;
+                }
+
+                let gl = (<any>(scene.getEngine()))._gl;
+                gl.bindTexture(gl.TEXTURE_CUBE_MAP, glTexture);
+                gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
+                if (face instanceof HTMLElement || face instanceof ImageData) {
+                    gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, level, textureCube.internalFormat, textureCube.internalFormat, textureCube.type, <any>face);
+                } else {
+                    let textureData = <TextureData>face;
+                    gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, level, textureCube.internalFormat, textureData.width, textureData.height, 0, textureData.format, textureCube.type, textureData.data);
+                }
+
+                texturesUploaded++;
+
+                if (textureComplete()) {
+                    //generate mipmaps
+                    if (automaticMipmaps) {
+                        let w = face.width;
+                        let h = face.height;
+                        let isPot = (((w !== 0) && (w & (w - 1))) === 0) && (((h !== 0) && (h & (h - 1))) === 0);
+                        if (isPot) {
+                            gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
+                        }
+                    }
+
+                    // Upload Separate lods in case there is no support for texture lod.
+                    if (environment && !scene.getEngine().getCaps().textureLOD && !singleLod) {
+                        const mipSlices = 3;
+                        for (let i = 0; i < mipSlices; i++) {
+                            let lodKey = TextureUtils.BabylonTextureKeyPrefix + 'lod' + i;
+                            let lod: BABYLON.CubeTexture = (<any>textureCube)[lodKey];
+
+                            //initialize lod texture if it doesn't already exist
+                            if (lod == null && textureCube.Width) {
+                                //compute LOD from even spacing in smoothness (matching shader calculation)
+                                let smoothness = i / (mipSlices - 1);
+                                let roughness = 1 - smoothness;
+                                const kMinimumVariance = 0.0005;
+                                let alphaG = roughness * roughness + kMinimumVariance;
+                                let microsurfaceAverageSlopeTexels = alphaG * textureCube.Width;
+
+                                let environmentSpecularLOD = TextureUtils.EnvironmentLODScale * (BABYLON.Scalar.Log2(microsurfaceAverageSlopeTexels)) + TextureUtils.EnvironmentLODOffset;
+
+                                let maxLODIndex = textureCube.source.length - 1;
+                                let mipmapIndex = Math.min(Math.max(Math.round(environmentSpecularLOD), 0), maxLODIndex);
+
+                                lod = TextureUtils.GetBabylonCubeTexture(scene, new TextureCube(PixelFormat.RGBA, PixelType.UNSIGNED_BYTE, [textureCube.source[mipmapIndex]]), false, true, true);
+
+                                if (i === 0) {
+                                    internalTexture._lodTextureLow = lod;
+                                }
+                                else if (i === 1) {
+                                    internalTexture._lodTextureMid = lod;
+                                }
+                                else {
+                                    internalTexture._lodTextureHigh = lod;
+                                }
+
+                                (<any>textureCube)[lodKey] = lod;
+                            }
+                        }
+                    }
+
+                    internalTexture.isReady = true;
+                }
+
+                gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
+                scene.getEngine().resetTextureCache();
+            };
+
+            for (let i = 0; i <= maxMipLevel; i++) {
+                let faces = textureCube.source[i];
+                for (let j = 0; j < faces.length; j++) {
+                    let face = faces[j];
+                    if (face instanceof HTMLImageElement && !face.complete) {
+                        face.addEventListener('load', () => {
+                            uploadFace(j, i, face);
+                        }, false);
+                    } else {
+                        uploadFace(j, i, face);
+                    }
+                }
+            }
+
+            scene.getEngine().resetTextureCache();
+
+            babylonTexture.isReady = () => {
+                return textureComplete();
+            };
+
+            (<any>textureCube)[key] = babylonTexture;
+        }
+
+        return babylonTexture;
+    }
+
+    /**
+     * Applies Spectre SamplingParameters to a Babylon texture by directly setting texture parameters on the internal WebGLTexture as well as setting Babylon fields
+     * @param babylonTexture Babylon texture to apply texture to (requires the Babylon texture has an initialize _texture field)
+     * @param parameters Spectre SamplingParameters to apply
+     */
+    public static ApplySamplingParameters(babylonTexture: BABYLON.BaseTexture, parameters: SamplingParameters) {
+        let scene = babylonTexture.getScene();
+        if (!scene) return;
+        let gl = (<any>(scene.getEngine()))._gl;
+
+        let target = babylonTexture.isCube ? gl.TEXTURE_CUBE_MAP : gl.TEXTURE_2D;
+
+        let internalTexture = babylonTexture._texture;
+        if (!internalTexture) return;
+        let glTexture = internalTexture._webGLTexture;
+        gl.bindTexture(target, glTexture);
+
+        if (parameters.magFilter != null) gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, parameters.magFilter);
+        if (parameters.minFilter != null) gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, parameters.minFilter);
+        if (parameters.wrapS != null) gl.texParameteri(target, gl.TEXTURE_WRAP_S, parameters.wrapS);
+        if (parameters.wrapT != null) gl.texParameteri(target, gl.TEXTURE_WRAP_T, parameters.wrapT);
+
+        //set babylon wrap modes from sampling parameter
+        switch (parameters.wrapS) {
+            case TextureWrapMode.REPEAT: babylonTexture.wrapU = BABYLON.Texture.WRAP_ADDRESSMODE; break;
+            case TextureWrapMode.CLAMP_TO_EDGE: babylonTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE; break;
+            case TextureWrapMode.MIRRORED_REPEAT: babylonTexture.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE; break;
+            default: babylonTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE;
+        }
+
+        switch (parameters.wrapT) {
+            case TextureWrapMode.REPEAT: babylonTexture.wrapV = BABYLON.Texture.WRAP_ADDRESSMODE; break;
+            case TextureWrapMode.CLAMP_TO_EDGE: babylonTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE; break;
+            case TextureWrapMode.MIRRORED_REPEAT: babylonTexture.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE; break;
+            default: babylonTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE;
+        }
+
+        if (parameters.maxAnisotropy != null && parameters.maxAnisotropy > 1) {
+            let anisotropicExt = gl.getExtension('EXT_texture_filter_anisotropic');
+            if (anisotropicExt) {
+                let maxAnisotropicSamples = gl.getParameter(anisotropicExt.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
+                let maxAnisotropy = Math.min(parameters.maxAnisotropy, maxAnisotropicSamples);
+                gl.texParameterf(target, anisotropicExt.TEXTURE_MAX_ANISOTROPY_EXT, maxAnisotropy);
+                babylonTexture.anisotropicFilteringLevel = maxAnisotropy;
+            }
+        }
+
+        gl.bindTexture(target, null);
+        scene.getEngine().resetTextureCache();
+    }
+
+    private static _EnvironmentSampling: SamplingParameters = {
+        magFilter: TextureMagFilter.LINEAR,
+        minFilter: TextureMinFilter.LINEAR_MIPMAP_LINEAR,
+        wrapS: TextureWrapMode.CLAMP_TO_EDGE,
+        wrapT: TextureWrapMode.CLAMP_TO_EDGE,
+        maxAnisotropy: 1
+    };
+
+    private static _EnvironmentSingleMipSampling: SamplingParameters = {
+        magFilter: TextureMagFilter.LINEAR,
+        minFilter: TextureMinFilter.LINEAR,
+        wrapS: TextureWrapMode.CLAMP_TO_EDGE,
+        wrapT: TextureWrapMode.CLAMP_TO_EDGE,
+        maxAnisotropy: 1
+    };
+
+    //from "/Internal/Lighting.EnvironmentFilterScale" in Engine/*/Configuration.cpp
+    /**
+     * Environment preprocessing dedicated value (Internal Use or Advanced only).
+     */
+    public static EnvironmentLODScale = 0.8;
+    /**
+     * Environment preprocessing dedicated value (Internal Use or Advanced only)..
+     */
+    public static EnvironmentLODOffset = 1.0;
+}

+ 141 - 0
Viewer/src/labs/viewerLabs.ts

@@ -0,0 +1,141 @@
+import { PBREnvironment, EnvironmentDeserializer } from "./environmentSerializer";
+import { SceneManager } from '../viewer/sceneManager';
+
+import { Tools, Quaternion } from 'babylonjs';
+import { ViewerConfiguration } from "../configuration/configuration";
+import { TextureUtils } from "./texture";
+
+export class ViewerLabs {
+
+    constructor(private _sceneManager: SceneManager) { }
+
+    public environmentAssetsRootURL: string;
+    public environment: PBREnvironment = {
+        //irradiance
+        irradiancePolynomialCoefficients: {
+            x: new BABYLON.Vector3(0, 0, 0),
+            y: new BABYLON.Vector3(0, 0, 0),
+            z: new BABYLON.Vector3(0, 0, 0),
+            xx: new BABYLON.Vector3(0, 0, 0),
+            yy: new BABYLON.Vector3(0, 0, 0),
+            zz: new BABYLON.Vector3(0, 0, 0),
+            yz: new BABYLON.Vector3(0, 0, 0),
+            zx: new BABYLON.Vector3(0, 0, 0),
+            xy: new BABYLON.Vector3(0, 0, 0)
+        },
+
+        textureIntensityScale: 1.0
+    };
+
+    /**
+         * Loads an environment map from a given URL
+         * @param url URL of environment map
+         * @param onSuccess Callback fired after environment successfully applied to the scene
+         * @param onProgress Callback fired at progress events while loading the environment map
+         * @param onError Callback fired when the load fails
+         */
+    public loadEnvironment(url: string, onSuccess?: (env: PBREnvironment) => void, onProgress?: (bytesLoaded: number, bytesTotal: number) => void, onError?: (e: any) => void): void;
+    /**
+     * Loads an environment map from a given URL
+     * @param buffer ArrayBuffer containing environment map
+     * @param onSuccess Callback fired after environment successfully applied to the scene
+     * @param onProgress Callback fired at progress events while loading the environment map
+     * @param onError Callback fired when the load fails
+     */
+    public loadEnvironment(buffer: ArrayBuffer, onSuccess?: (env: PBREnvironment) => void, onProgress?: (bytesLoaded: number, bytesTotal: number) => void, onError?: (e: any) => void): void;
+    /**
+     * Sets the environment to an already loaded environment
+     * @param env PBREnvironment instance
+     * @param onSuccess Callback fired after environment successfully applied to the scene
+     * @param onProgress Callback fired at progress events while loading the environment map
+     * @param onError Callback fired when the load fails
+     */
+    public loadEnvironment(env: PBREnvironment, onSuccess?: (env: PBREnvironment) => void, onProgress?: (bytesLoaded: number, bytesTotal: number) => void, onError?: (e: any) => void): void;
+    public loadEnvironment(data: string | ArrayBuffer | PBREnvironment, onSuccess?: (env: PBREnvironment) => void, onProgress?: (bytesLoaded: number, bytesTotal: number) => void, onError?: (e: any) => void): void {
+        //@! todo: should loadEnvironment cancel any currently loading environments?
+        if (data instanceof ArrayBuffer) {
+            this.environment = EnvironmentDeserializer.Parse(data);
+            if (onSuccess) onSuccess(this.environment);
+        } else if (typeof data === 'string') {
+            let url = this.getEnvironmentAssetUrl(data);
+            this._sceneManager.scene._loadFile(
+                url,
+                (arrayBuffer: ArrayBuffer) => {
+                    this.environment = EnvironmentDeserializer.Parse(arrayBuffer);
+                    if (onSuccess) onSuccess(this.environment);
+                },
+                (progressEvent) => { if (onProgress) onProgress(progressEvent.loaded, progressEvent.total); },
+                false,
+                true,
+                (r, e) => {
+                    if (onError) {
+                        onError(e);
+                    }
+                }
+            );
+        } else {
+            //data assumed to be PBREnvironment object
+            this.environment = data;
+            if (onSuccess) onSuccess(data);
+        }
+    }
+
+    /**
+     * Applies an `EnvironmentMapConfiguration` to the scene
+     * @param environmentMapConfiguration Environment map configuration to apply
+     */
+    public applyEnvironmentMapConfiguration(rotationY?: number) {
+        if (!this.environment) return;
+
+        //set orientation
+        let rotatquatRotationionY = Quaternion.RotationAxis(BABYLON.Axis.Y, rotationY || 0);
+
+        // Add env texture to the scene.
+        if (this.environment.specularTexture) {
+            // IE crashes when disposing the old texture and setting a new one
+            if (!this._sceneManager.scene.environmentTexture) {
+                this._sceneManager.scene.environmentTexture = TextureUtils.GetBabylonCubeTexture(this._sceneManager.scene, this.environment.specularTexture, false, true);
+            }
+            if (this._sceneManager.scene.environmentTexture) {
+                this._sceneManager.scene.environmentTexture.level = this.environment.textureIntensityScale;
+                this._sceneManager.scene.environmentTexture.invertZ = true;
+                this._sceneManager.scene.environmentTexture.lodLevelInAlpha = true;
+
+                var poly = this._sceneManager.scene.environmentTexture.sphericalPolynomial || new BABYLON.SphericalPolynomial();
+                poly.x = this.environment.irradiancePolynomialCoefficients.x;
+                poly.y = this.environment.irradiancePolynomialCoefficients.y;
+                poly.z = this.environment.irradiancePolynomialCoefficients.z;
+                poly.xx = this.environment.irradiancePolynomialCoefficients.xx;
+                poly.xy = this.environment.irradiancePolynomialCoefficients.xy;
+                poly.yy = this.environment.irradiancePolynomialCoefficients.yy;
+                poly.yz = this.environment.irradiancePolynomialCoefficients.yz;
+                poly.zx = this.environment.irradiancePolynomialCoefficients.zx;
+                poly.zz = this.environment.irradiancePolynomialCoefficients.zz;
+                this._sceneManager.scene.environmentTexture.sphericalPolynomial = poly;
+
+                //set orientation
+                BABYLON.Matrix.FromQuaternionToRef(rotatquatRotationionY, this._sceneManager.scene.environmentTexture.getReflectionTextureMatrix());
+            }
+        }
+    }
+
+    /**
+     * Get an environment asset url by using the configuration if the path is not absolute.
+     * @param url Asset url
+     * @returns The Asset url using the `environmentAssetsRootURL` if the url is not an absolute path.
+     */
+    public getEnvironmentAssetUrl(url: string): string {
+        let returnUrl = url;
+        if (url && url.toLowerCase().indexOf("//") === -1) {
+            if (!this.environmentAssetsRootURL) {
+                Tools.Warn("Please, specify the root url of your assets before loading the configuration (labs.environmentAssetsRootURL) or disable the background through the viewer options.");
+                return url;
+            }
+
+            returnUrl = this.environmentAssetsRootURL + returnUrl;
+        }
+
+        return returnUrl;
+    }
+
+}

+ 61 - 9
Viewer/src/model/modelLoader.ts

@@ -1,7 +1,11 @@
-import { AbstractViewer } from "..";
-import { ISceneLoaderPlugin, ISceneLoaderPluginAsync, Tools, SceneLoader, Tags, GLTFFileLoader } from "babylonjs";
+import { AbstractViewer } from "../viewer/viewer";
+import { ISceneLoaderPlugin, ISceneLoaderPluginAsync, Tools, SceneLoader, Tags } from "babylonjs";
+import { GLTFFileLoader, GLTFLoaderAnimationStartMode } from "babylonjs-loaders";
 import { IModelConfiguration } from "../configuration/configuration";
-import { ViewerModel, ModelState } from "./viewerModel";
+import { ViewerModel, ModelState } from "../model/viewerModel";
+import { ILoaderPlugin } from './plugins/loaderPlugin';
+import { TelemetryLoaderPlugin } from './plugins/telemetryLoaderPlugin';
+import { getLoaderPluginByName } from './plugins/';
 
 /**
  * An instance of the class is in charge of loading the model correctly.
@@ -16,6 +20,8 @@ export class ModelLoader {
 
     private _loaders: Array<ISceneLoaderPlugin | ISceneLoaderPluginAsync>;
 
+    private _plugins: Array<ILoaderPlugin>;
+
     /**
      * Create a new Model loader
      * @param _viewer the viewer using this model loader
@@ -23,6 +29,22 @@ export class ModelLoader {
     constructor(private _viewer: AbstractViewer) {
         this._loaders = [];
         this._loadId = 0;
+        this._plugins = [];
+    }
+
+    public addPlugin(plugin: ILoaderPlugin | string) {
+        let actualPlugin: ILoaderPlugin = {};
+        if (typeof plugin === 'string') {
+            let loadedPlugin = getLoaderPluginByName(plugin);
+            if (loadedPlugin) {
+                actualPlugin = loadedPlugin;
+            }
+        } else {
+            actualPlugin = plugin;
+        }
+        if (actualPlugin && this._plugins.indexOf(actualPlugin) === -1) {
+            this._plugins.push(actualPlugin);
+        }
     }
 
     /**
@@ -33,6 +55,8 @@ export class ModelLoader {
 
         const model = new ViewerModel(this._viewer, modelConfiguration);
 
+        model.loadId = this._loadId++;
+
         if (!modelConfiguration.url) {
             model.state = ModelState.ERROR;
             Tools.Error("No URL provided");
@@ -43,11 +67,11 @@ export class ModelLoader {
         let base = modelConfiguration.root || Tools.GetFolderPath(modelConfiguration.url);
         let plugin = modelConfiguration.loader;
 
-        model.loader = SceneLoader.ImportMesh(undefined, base, filename, this._viewer.scene, (meshes, particleSystems, skeletons, animationGroups) => {
+        model.loader = SceneLoader.ImportMesh(undefined, base, filename, this._viewer.sceneManager.scene, (meshes, particleSystems, skeletons, animationGroups) => {
             meshes.forEach(mesh => {
                 Tags.AddTagsTo(mesh, "viewerMesh");
+                model.addMesh(mesh);
             });
-            model.meshes = meshes;
             model.particleSystems = particleSystems;
             model.skeletons = skeletons;
 
@@ -55,22 +79,41 @@ export class ModelLoader {
                 model.addAnimationGroup(animationGroup);
             }
 
-            model.initAnimations();
+            this._checkAndRun("onLoaded", model);
             model.onLoadedObservable.notifyObserversWithPromise(model);
         }, (progressEvent) => {
+            this._checkAndRun("onProgress", progressEvent);
             model.onLoadProgressObservable.notifyObserversWithPromise(progressEvent);
-        }, (e, m, exception) => {
+        }, (scene, m, exception) => {
             model.state = ModelState.ERROR;
             Tools.Error("Load Error: There was an error loading the model. " + m);
+            this._checkAndRun("onError", m, exception);
             model.onLoadErrorObservable.notifyObserversWithPromise({ message: m, exception: exception });
         }, plugin)!;
 
         if (model.loader.name === "gltf") {
             let gltfLoader = (<GLTFFileLoader>model.loader);
-            gltfLoader.animationStartMode = BABYLON.GLTFLoaderAnimationStartMode.NONE;
+            gltfLoader.animationStartMode = GLTFLoaderAnimationStartMode.NONE;
+            gltfLoader.compileMaterials = true;
+            // if ground is set to "mirror":
+            if (this._viewer.configuration.ground && typeof this._viewer.configuration.ground === 'object' && this._viewer.configuration.ground.mirror) {
+                gltfLoader.useClipPlane = true;
+            }
+            Object.keys(gltfLoader).filter(name => name.indexOf('on') === 0 && name.indexOf('Observable') !== -1).forEach(functionName => {
+                gltfLoader[functionName].add((payload) => {
+                    this._checkAndRun(functionName.replace("Observable", ''), payload);
+                });
+            });
+
+            gltfLoader.onParsedObservable.add((data) => {
+                if (data && data.json && data.json['asset']) {
+                    model.loadInfo = data.json['asset'];
+                }
+            })
         }
 
-        model.loadId = this._loadId++;
+        this._checkAndRun("onInit", model.loader, model);
+
         this._loaders.push(model.loader);
 
         return model;
@@ -101,4 +144,13 @@ export class ModelLoader {
         this._loaders.length = 0;
         this._disposed = true;
     }
+
+    private _checkAndRun(functionName: string, ...payload: Array<any>) {
+        if (this._disposed) return;
+        this._plugins.filter(p => p[functionName]).forEach(plugin => {
+            try {
+                plugin[functionName].apply(this, payload);
+            } catch (e) { }
+        })
+    }
 }

+ 14 - 0
Viewer/src/loader/plugins/extendedMaterialLoaderPlugin.ts

@@ -0,0 +1,14 @@
+import { ILoaderPlugin } from "./loaderPlugin";
+import { telemetryManager } from "../../telemetryManager";
+import { ViewerModel } from "../..";
+import { Color3, Texture, BaseTexture, Tools, ISceneLoaderPlugin, ISceneLoaderPluginAsync, Material, PBRMaterial, Engine } from "babylonjs";
+
+export class ExtendedMaterialLoaderPlugin implements ILoaderPlugin {
+
+    private _model: ViewerModel;
+
+    public onMaterialLoaded(baseMaterial: Material) {
+        var material = baseMaterial as PBRMaterial;
+        material.alphaMode = Engine.ALPHA_PREMULTIPLIED_PORTERDUFF;
+    }
+}

+ 28 - 0
Viewer/src/loader/plugins/index.ts

@@ -0,0 +1,28 @@
+import { TelemetryLoaderPlugin } from "./telemetryLoaderPlugin";
+import { ILoaderPlugin } from "./loaderPlugin";
+import { MSFTLodLoaderPlugin } from './msftLodLoaderPlugin';
+import { MinecraftLoaderPlugin } from './minecraftLoaderPlugin';
+import { ExtendedMaterialLoaderPlugin } from './extendedMaterialLoaderPlugin';
+
+const pluginCache: { [key: string]: ILoaderPlugin } = {};
+
+export function getLoaderPluginByName(name: string) {
+    if (!pluginCache[name]) {
+        switch (name) {
+            case 'telemetry':
+                pluginCache[name] = new TelemetryLoaderPlugin();
+                break;
+            case 'msftLod':
+                pluginCache[name] = new MSFTLodLoaderPlugin();
+                break;
+            case 'minecraft':
+                pluginCache[name] = new MSFTLodLoaderPlugin();
+                break;
+            case 'extendedMaterial':
+                pluginCache[name] = new ExtendedMaterialLoaderPlugin();
+                break;
+        }
+    }
+
+    return pluginCache[name];
+}

+ 16 - 0
Viewer/src/loader/plugins/loaderPlugin.ts

@@ -0,0 +1,16 @@
+import { ViewerModel } from "../../model/viewerModel";
+import { IGLTFLoaderExtension, IGLTFLoaderData } from "babylonjs-loaders";
+import { AbstractMesh, ISceneLoaderPlugin, ISceneLoaderPluginAsync, SceneLoaderProgressEvent, BaseTexture, Material } from "babylonjs";
+
+export interface ILoaderPlugin {
+    onInit?: (loader: ISceneLoaderPlugin | ISceneLoaderPluginAsync, model: ViewerModel) => void;
+    onLoaded?: (model: ViewerModel) => void;
+    onError?: (message: string, exception?: any) => void;
+    onProgress?: (progressEvent: SceneLoaderProgressEvent) => void;
+    onExtensionLoaded?: (extension: IGLTFLoaderExtension) => void;
+    onParsed?: (parsedData: IGLTFLoaderData) => void;
+    onMeshLoaded?: (mesh: AbstractMesh) => void;
+    onTextureLoaded?: (texture: BaseTexture) => void;
+    onMaterialLoaded?: (material: Material) => void;
+    onComplete?: () => void;
+}

+ 39 - 0
Viewer/src/loader/plugins/minecraftLoaderPlugin.ts

@@ -0,0 +1,39 @@
+import { ILoaderPlugin } from "./loaderPlugin";
+import { telemetryManager } from "../../telemetryManager";
+import { ViewerModel } from "../..";
+import { Tools, ISceneLoaderPlugin, ISceneLoaderPluginAsync, Material } from "babylonjs";
+import { IGLTFLoaderData, GLTF2 } from "babylonjs-loaders";
+
+
+export class MinecraftLoaderPlugin implements ILoaderPlugin {
+
+    private _model: ViewerModel;
+
+    private _minecraftEnabled: boolean;
+
+    public onInit(loader: ISceneLoaderPlugin | ISceneLoaderPluginAsync, model: ViewerModel) {
+        this._model = model;
+        this._minecraftEnabled = false;
+    }
+
+    public inParsed(data: IGLTFLoaderData) {
+        if (data && data.json && data.json['meshes'] && data.json['meshes'].length) {
+            var meshes = data.json['meshes'] as GLTF2.IMesh[];
+            for (var i = 0; i < meshes.length; i++) {
+                var mesh = meshes[i];
+                if (mesh && mesh.extras && mesh.extras.MSFT_minecraftMesh) {
+                    this._minecraftEnabled = true;
+                    break;
+                }
+            }
+        }
+    }
+
+    public onMaterialLoaded(material: Material) {
+        if (this._minecraftEnabled && material.needAlphaBlending()) {
+            material.forceDepthWrite = true;
+            material.backFaceCulling = true;
+            material.separateCullingPass = true;
+        }
+    }
+}

+ 23 - 0
Viewer/src/loader/plugins/msftLodLoaderPlugin.ts

@@ -0,0 +1,23 @@
+import { ILoaderPlugin } from "./loaderPlugin";
+import { telemetryManager } from "../../telemetryManager";
+import { ViewerModel } from "../..";
+import { Tools, ISceneLoaderPlugin, ISceneLoaderPluginAsync } from "babylonjs";
+import { IGLTFLoaderExtension, GLTF2 } from "babylonjs-loaders";
+
+
+export class MSFTLodLoaderPlugin implements ILoaderPlugin {
+
+    private _model: ViewerModel;
+
+    public onInit(loader: ISceneLoaderPlugin | ISceneLoaderPluginAsync, model: ViewerModel) {
+        this._model = model;
+    }
+
+    public onExtensionLoaded(extension: IGLTFLoaderExtension) {
+        if (extension.name === "MSFT_lod" && this._model.configuration.loaderConfiguration) {
+            const MSFT_lod = extension as GLTF2.Extensions.MSFT_lod;
+            MSFT_lod.enabled = !!this._model.configuration.loaderConfiguration.progressiveLoading;
+            MSFT_lod.maxLODsToLoad = this._model.configuration.loaderConfiguration.maxLODsToLoad || Number.MAX_VALUE;
+        }
+    }
+}

+ 46 - 0
Viewer/src/loader/plugins/telemetryLoaderPlugin.ts

@@ -0,0 +1,46 @@
+import { ILoaderPlugin } from "./loaderPlugin";
+import { telemetryManager } from "../../telemetryManager";
+import { ViewerModel } from "../..";
+import { Tools, ISceneLoaderPlugin, ISceneLoaderPluginAsync } from "babylonjs";
+
+
+export class TelemetryLoaderPlugin implements ILoaderPlugin {
+
+    private _model: ViewerModel;
+
+    private _loadStart: number;
+    private _loadEnd: number;
+
+    public onInit(loader: ISceneLoaderPlugin | ISceneLoaderPluginAsync, model: ViewerModel) {
+        this._model = model;
+        this._loadStart = Tools.Now;
+    }
+
+    public onLoaded(model: ViewerModel) {
+        telemetryManager.broadcast("Load First LOD Complete", model.getViewer(), {
+            model: model,
+            loadTime: Tools.Now - this._loadStart
+        });
+        telemetryManager.flushWebGLErrors(this._model.getViewer());
+    }
+
+    public onError(message: string, exception: any) {
+        this._loadEnd = Tools.Now;
+        telemetryManager.broadcast("Load Error", this._model.getViewer(), {
+            model: this._model,
+            loadTime: this._loadEnd - this._loadStart
+        });
+
+        telemetryManager.flushWebGLErrors(this._model.getViewer());
+    }
+
+    public onComplete() {
+        this._loadEnd = Tools.Now;
+        telemetryManager.broadcast("Load Complete", this._model.getViewer(), {
+            model: this._model,
+            loadTime: this._loadEnd - this._loadStart
+        });
+
+        telemetryManager.flushWebGLErrors(this._model.getViewer());
+    }
+}

+ 180 - 48
Viewer/src/model/viewerModel.ts

@@ -1,9 +1,11 @@
-import { ISceneLoaderPlugin, ISceneLoaderPluginAsync, AnimationGroup, Animatable, AbstractMesh, Tools, Scene, SceneLoader, Observable, SceneLoaderProgressEvent, Tags, ParticleSystem, Skeleton, IDisposable, Nullable, Animation, GLTFFileLoader, Quaternion } from "babylonjs";
+import { ISceneLoaderPlugin, ISceneLoaderPluginAsync, AnimationGroup, Animatable, AbstractMesh, Tools, Scene, SceneLoader, Observable, SceneLoaderProgressEvent, Tags, ParticleSystem, Skeleton, IDisposable, Nullable, Animation, Quaternion, Material, Vector3, AnimationPropertiesOverride } from "babylonjs";
+import { GLTFFileLoader, GLTF2 } from "babylonjs-loaders";
 import { IModelConfiguration } from "../configuration/configuration";
 import { IModelAnimation, GroupModelAnimation, AnimationPlayMode } from "./modelAnimation";
 
 import * as deepmerge from '../../assets/deepmerge.min.js';
 import { AbstractViewer } from "..";
+import { extendClassWithConfig } from "../helper";
 
 
 export enum ModelState {
@@ -29,10 +31,10 @@ export class ViewerModel implements IDisposable {
     /**
      * the list of meshes that are a part of this model
      */
-    public meshes: Array<AbstractMesh> = [];
+    private _meshes: Array<AbstractMesh> = [];
     /**
      * This model's root mesh (the parent of all other meshes).
-     * This mesh also exist in the meshes array.
+     * This mesh does not(!) exist in the meshes array.
      */
     public rootMesh: AbstractMesh;
     /**
@@ -76,6 +78,8 @@ export class ViewerModel implements IDisposable {
      * A loadID provided by the modelLoader, unique to ths (Abstract)Viewer instance.
      */
     public loadId: number;
+
+    public loadInfo: GLTF2.IAsset;
     private _loadedUrl: string;
     private _modelConfiguration: IModelConfiguration;
 
@@ -87,14 +91,69 @@ export class ViewerModel implements IDisposable {
 
         this.state = ModelState.INIT;
 
+        this.rootMesh = new AbstractMesh("modelRootMesh", this._viewer.sceneManager.scene);
+
         this._animations = [];
         //create a copy of the configuration to make sure it doesn't change even after it is changed in the viewer
-        this._modelConfiguration = deepmerge({}, modelConfiguration);
+        this._modelConfiguration = deepmerge(this._viewer.configuration.model || {}, modelConfiguration);
+
+        this._viewer.sceneManager.models.push(this);
+        this._viewer.onModelAddedObservable.notifyObservers(this);
+        this.onLoadedObservable.add(() => {
+            this._viewer.onModelLoadedObservable.notifyObservers(this);
+            this._initAnimations();
+        });
+    }
 
-        this._viewer.models.push(this);
+    /**
+     * Is this model enabled?
+     */
+    public get enabled() {
+        return this.rootMesh.isEnabled();
+    }
+
+    /**
+     * Set whether this model is enabled or not.
+     */
+    public set enabled(enable: boolean) {
+        this.rootMesh.setEnabled(enable);
+    }
+
+    /**
+     * Get the viewer showing this model
+     */
+    public getViewer() {
+        return this._viewer;
     }
 
     /**
+     * Add a mesh to this model.
+     * Any mesh that has no parent will be provided with the root mesh as its new parent.
+     * 
+     * @param mesh the new mesh to add
+     * @param triggerLoaded should this mesh trigger the onLoaded observable. Used when adding meshes manually.
+     */
+    public addMesh(mesh: AbstractMesh, triggerLoaded?: boolean) {
+        if (!mesh.parent) {
+            mesh.parent = this.rootMesh;
+        }
+        mesh.receiveShadows = !!this.configuration.receiveShadows;
+        this._meshes.push(mesh);
+        if (triggerLoaded) {
+            return this.onLoadedObservable.notifyObserversWithPromise(this);
+        }
+    }
+
+    /**
+     * get the list of meshes (excluding the root mesh)
+     */
+    public get meshes() {
+        return this._meshes;
+    }
+
+    public get
+
+    /**
      * Get the model's configuration
      */
     public get configuration(): IModelConfiguration {
@@ -122,16 +181,11 @@ export class ViewerModel implements IDisposable {
     }
 
 
-    public initAnimations() {
-        this._animations.forEach(a => {
-            a.dispose();
-        });
-        this._animations.length = 0;
-
+    private _initAnimations() {
         // check if this is not a gltf loader and init the animations
-        if (this.loader.name !== 'gltf') {
+        if (this.skeletons.length) {
             this.skeletons.forEach((skeleton, idx) => {
-                let ag = new AnimationGroup("animation-" + idx, this._viewer.scene);
+                let ag = new AnimationGroup("animation-" + idx, this._viewer.sceneManager.scene);
                 skeleton.getAnimatables().forEach(a => {
                     if (a.animations[0]) {
                         ag.addTargetedAnimation(a.animations[0], a);
@@ -214,7 +268,8 @@ export class ViewerModel implements IDisposable {
     }
 
     private _configureModel() {
-        let meshesWithNoParent: Array<AbstractMesh> = this.meshes.filter(m => !m.parent);
+        // this can be changed to the meshes that have rootMesh a parent without breaking anything.
+        let meshesWithNoParent: Array<AbstractMesh> = [this.rootMesh] //this._meshes.filter(m => m.parent === this.rootMesh);
         let updateMeshesWithNoParent = (variable: string, value: any, param?: string) => {
             meshesWithNoParent.forEach(mesh => {
                 if (param) {
@@ -238,32 +293,6 @@ export class ViewerModel implements IDisposable {
                 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;
@@ -272,7 +301,6 @@ export class ViewerModel implements IDisposable {
             if (this._modelConfiguration.normalize === true) {
                 center = true;
                 unitSize = true;
-                parentIndex = 0;
             } else {
                 center = !!this._modelConfiguration.normalize.center;
                 unitSize = !!this._modelConfiguration.normalize.unitSize;
@@ -281,7 +309,7 @@ export class ViewerModel implements IDisposable {
 
             let meshesToNormalize: Array<AbstractMesh> = [];
             if (parentIndex !== undefined) {
-                meshesToNormalize.push(this.meshes[parentIndex]);
+                meshesToNormalize.push(this._meshes[parentIndex]);
             } else {
                 meshesToNormalize = meshesWithNoParent;
             }
@@ -300,21 +328,125 @@ export class ViewerModel implements IDisposable {
                     const center = boundingInfo.min.add(halfSizeVec);
                     mesh.position = center.scale(-1);
 
-                    // Set on ground.
-                    mesh.position.y += halfSizeVec.y;
-
                     // Recompute Info.
                     mesh.computeWorldMatrix(true);
                 });
             }
+        } else {
+            //center automatically
+            meshesWithNoParent.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);
+
+                // Recompute Info.
+                mesh.computeWorldMatrix(true);
+            });
         }
+
+        // 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.rotationOffsetAxis) {
+            let rotationAxis = new Vector3(0, 0, 0).copyFrom(this._modelConfiguration.rotationOffsetAxis as Vector3);
+
+            meshesWithNoParent.forEach(m => {
+                if (this._modelConfiguration.rotationOffsetAngle) {
+                    m.rotate(rotationAxis, this._modelConfiguration.rotationOffsetAngle);
+                }
+            })
+
+        }
+
+        if (this._modelConfiguration.scaling) {
+            updateXYZ('scaling', this._modelConfiguration.scaling);
+        }
+
+        if (this._modelConfiguration.castShadow) {
+            this._meshes.forEach(mesh => {
+                Tags.AddTagsTo(mesh, 'castShadow');
+            });
+        }
+
+        let meshes = this.rootMesh.getChildMeshes(false);
+        meshes.filter(m => m.material).forEach((mesh) => {
+            this._applyModelMaterialConfiguration(mesh.material!);
+        });
+
         this.onAfterConfigure.notifyObservers(this);
     }
 
     /**
+     * Apply a material configuration to a material
+     * @param material Material to apply configuration to
+     */
+    private _applyModelMaterialConfiguration(material: Material) {
+        if (!this._modelConfiguration.material) return;
+
+        extendClassWithConfig(material, this._modelConfiguration.material);
+
+        if (material instanceof BABYLON.PBRMaterial) {
+            if (this._modelConfiguration.material.directIntensity !== undefined) {
+                material.directIntensity = this._modelConfiguration.material.directIntensity;
+            }
+
+            if (this._modelConfiguration.material.emissiveIntensity !== undefined) {
+                material.emissiveIntensity = this._modelConfiguration.material.emissiveIntensity;
+            }
+
+            if (this._modelConfiguration.material.environmentIntensity !== undefined) {
+                material.environmentIntensity = this._modelConfiguration.material.environmentIntensity;
+            }
+
+            if (this._modelConfiguration.material.directEnabled !== undefined) {
+                material.disableLighting = !this._modelConfiguration.material.directEnabled;
+            }
+            if (this._viewer.sceneManager.mainColor) {
+                material.reflectionColor = this._viewer.sceneManager.mainColor;
+            }
+        }
+        else if (material instanceof BABYLON.MultiMaterial) {
+            for (let i = 0; i < material.subMaterials.length; i++) {
+                const subMaterial = material.subMaterials[i];
+                if (subMaterial) {
+                    this._applyModelMaterialConfiguration(subMaterial);
+                }
+            }
+        }
+    }
+
+    /**
+     * Will remove this model from the viewer (but NOT dispose it).
+     */
+    public remove() {
+        this._viewer.sceneManager.models.splice(this._viewer.sceneManager.models.indexOf(this), 1);
+        // hide it
+        this.rootMesh.isVisible = false;
+        this._viewer.onModelRemovedObservable.notifyObservers(this);
+    }
+
+    /**
      * Dispose this model, including all of its associated assets.
      */
     public dispose() {
+        this.remove();
         this.onAfterConfigure.clear();
         this.onLoadedObservable.clear();
         this.onLoadErrorObservable.clear();
@@ -328,8 +460,8 @@ export class ViewerModel implements IDisposable {
         this.skeletons.length = 0;
         this._animations.forEach(ag => ag.dispose());
         this._animations.length = 0;
-        this.meshes.forEach(m => m.dispose());
-        this.meshes.length = 0;
-        this._viewer.models.splice(this._viewer.models.indexOf(this), 1);
+        this._meshes.forEach(m => m.dispose());
+        this._meshes.length = 0;
+        this.rootMesh.dispose();
     }
 }

+ 128 - 0
Viewer/src/telemetryManager.ts

@@ -0,0 +1,128 @@
+import { Engine, Observable } from "babylonjs";
+import { AbstractViewer } from "./viewer/viewer";
+
+/**
+ * The data structure of a telemetry event.
+ */
+export interface TelemetryData {
+    event: string;
+    session: string;
+    date: Date;
+    now: number;
+    viewer?: AbstractViewer
+    detail: any;
+}
+
+/**
+ * Receives Telemetry events and raises events to the API
+ */
+export class TelemetryManager {
+
+    public onEventBroadcastedObservable: Observable<TelemetryData> = new Observable();
+
+    private _currentSessionId: string;
+
+    private _event: (event: string, viewer: AbstractViewer, details?: any) => void = this._eventEnabled;
+
+    /**
+     * Receives a telemetry event
+     * @param event The name of the Telemetry event
+     * @param details An additional value, or an object containing a list of property/value pairs
+     */
+    public get broadcast() {
+        return this._event;
+    }
+
+    /**
+     * Log a Telemetry event for errors raised on the WebGL context.
+     * @param engine The Babylon engine with the WebGL context.
+     */
+    public flushWebGLErrors(viewer: AbstractViewer) {
+        const engine = viewer.engine;
+        if (!engine) {
+            return;
+        }
+        let logErrors = true;
+
+        while (logErrors) {
+            let gl = (<any>engine)._gl;
+            if (gl && gl.getError) {
+                let error = gl.getError();
+                if (error === gl.NO_ERROR) {
+                    logErrors = false;
+                } else {
+                    this.broadcast("WebGL Error", viewer, { error: error });
+                }
+            } else {
+                logErrors = false;
+            }
+        }
+    }
+
+    /**
+     * Enable or disable telemetry events
+     * @param enabled Boolan, true if events are enabled 
+     */
+    public set enable(enabled: boolean) {
+        if (enabled) {
+            this._event = this._eventEnabled;
+        } else {
+            this._event = this._eventDisabled;
+        }
+    }
+
+    /**
+     * Called on event when disabled, typically do nothing here
+     */
+    private _eventDisabled(): void {
+        // nothing to do
+    }
+
+    /**
+     * Called on event when enabled
+     * @param event - The name of the Telemetry event
+     * @param details An additional value, or an object containing a list of property/value pairs
+     */
+    private _eventEnabled(event: string, viewer?: AbstractViewer, details?: any): void {
+        let telemetryData: TelemetryData = {
+            viewer,
+            event: event,
+            session: this.session,
+            date: new Date(),
+            now: window.performance ? window.performance.now() : Date.now(),
+            detail: null
+        };
+
+        if (typeof details === "object") {
+            for (var attr in details) {
+                if (details.hasOwnProperty(attr)) {
+                    telemetryData[attr] = details[attr];
+                }
+            }
+        } else if (details) {
+            telemetryData.detail = details;
+        }
+
+        this.onEventBroadcastedObservable.notifyObservers(telemetryData);
+    }
+
+    /**
+     * Returns the current session ID or creates one if it doesn't exixt
+     * @return The current session ID
+     */
+    public get session(): string {
+        if (!this._currentSessionId) {
+            //String + Timestamp + Random Integer
+            this._currentSessionId = "SESSION_" + Date.now() + Math.floor(Math.random() * 0x10000);
+        }
+        return this._currentSessionId;
+    }
+
+    public dispose() {
+        this.onEventBroadcastedObservable.clear();
+        delete this.onEventBroadcastedObservable;
+    }
+}
+
+export const telemetryManager = new TelemetryManager();
+

+ 51 - 14
Viewer/src/templateManager.ts

@@ -1,6 +1,7 @@
 
 import { Observable, IFileRequest, Tools } from 'babylonjs';
 import { isUrl, camelToKebab, kebabToCamel } from './helper';
+import * as deepmerge from '../assets/deepmerge.min.js';
 
 /**
  * A single template configuration object
@@ -293,8 +294,10 @@ export class Template {
      */
     public initPromise: Promise<Template>;
 
-    private _fragment: DocumentFragment;
+    private _fragment: DocumentFragment | Element;
+    private _addedFragment: DocumentFragment | Element;
     private _htmlTemplate: string;
+    private _rawHtml: string;
 
     private loadRequests: Array<IFileRequest>;
 
@@ -317,8 +320,14 @@ export class Template {
                 this._htmlTemplate = htmlTemplate;
                 let compiledTemplate = Handlebars.compile(htmlTemplate);
                 let config = this._configuration.params || {};
-                let rawHtml = compiledTemplate(config);
-                this._fragment = document.createRange().createContextualFragment(rawHtml);
+                this._rawHtml = compiledTemplate(config);
+                try {
+                    this._fragment = document.createRange().createContextualFragment(this._rawHtml);
+                } catch (e) {
+                    let test = document.createElement(this.name);
+                    test.innerHTML = this._rawHtml;
+                    this._fragment = test;
+                }
                 this.isLoaded = true;
                 this.isShown = true;
                 this.onLoaded.notifyObservers(this);
@@ -334,16 +343,26 @@ export class Template {
      * 
      * @param params the new template parameters
      */
-    public updateParams(params: { [key: string]: string | number | boolean | object }) {
-        this._configuration.params = params;
+    public updateParams(params: { [key: string]: string | number | boolean | object }, append: boolean = true) {
+        if (append) {
+            this._configuration.params = deepmerge(this._configuration.params, params);
+        } else {
+            this._configuration.params = params;
+        }
         // update the template
         if (this.isLoaded) {
-            this.dispose();
+            // this.dispose();
         }
         let compiledTemplate = Handlebars.compile(this._htmlTemplate);
         let config = this._configuration.params || {};
-        let rawHtml = compiledTemplate(config);
-        this._fragment = document.createRange().createContextualFragment(rawHtml);
+        this._rawHtml = compiledTemplate(config);
+        try {
+            this._fragment = document.createRange().createContextualFragment(this._rawHtml);
+        } catch (e) {
+            let test = document.createElement(this.name);
+            test.innerHTML = this._rawHtml;
+            this._fragment = test;
+        }
         if (this.parent) {
             this.appendTo(this.parent, true);
         }
@@ -363,10 +382,16 @@ export class Template {
     public getChildElements(): Array<string> {
         let childrenArray: string[] = [];
         //Edge and IE don't support frage,ent.children
-        let children = this._fragment.children;
+        let children: HTMLCollection | NodeListOf<Element> = this._fragment && this._fragment.children;
+        if (!this._fragment) {
+            let fragment = this.parent.querySelector(this.name);
+            if (fragment) {
+                children = fragment.querySelectorAll('*');
+            }
+        }
         if (!children) {
             // casting to HTMLCollection, as both NodeListOf and HTMLCollection have 'item()' and 'length'.
-            children = <HTMLCollection>this._fragment.querySelectorAll('*');
+            children = this._fragment.querySelectorAll('*');
         }
         for (let i = 0; i < children.length; ++i) {
             childrenArray.push(kebabToCamel(children.item(i).nodeName.toLowerCase()));
@@ -382,8 +407,11 @@ export class Template {
      */
     public appendTo(parent: HTMLElement, forceRemove?: boolean) {
         if (this.parent) {
-            if (forceRemove) {
-                this.parent.removeChild(this._fragment);
+            if (forceRemove && this._addedFragment) {
+                /*let fragement = this.parent.querySelector(this.name)
+                if (fragement)
+                    this.parent.removeChild(fragement);*/
+                this.parent.innerHTML = '';
             } else {
                 return;
             }
@@ -393,7 +421,12 @@ export class Template {
         if (this._configuration.id) {
             this.parent.id = this._configuration.id;
         }
-        this._fragment = this.parent.appendChild(this._fragment);
+        if (this._fragment) {
+            this.parent.appendChild(this._fragment);
+            this._addedFragment = this._fragment;
+        } else {
+            this.parent.insertAdjacentHTML("beforeend", this._rawHtml);
+        }
         // appended only one frame after.
         setTimeout(() => {
             this._registerEvents();
@@ -420,6 +453,10 @@ export class Template {
             } else {
                 // flex? box? should this be configurable easier than the visibilityFunction?
                 this.parent.style.display = 'flex';
+                // support old browsers with no flex:
+                if (this.parent.style.display !== 'flex') {
+                    this.parent.style.display = '';
+                }
                 return this;
             }
         }).then(() => {
@@ -445,7 +482,7 @@ export class Template {
                 return visibilityFunction(this);
             } else {
                 // flex? box? should this be configurable easier than the visibilityFunction?
-                this.parent.style.display = 'hide';
+                this.parent.style.display = 'none';
                 return this;
             }
         }).then(() => {

+ 65 - 33
Viewer/src/viewer/defaultViewer.ts

@@ -6,6 +6,7 @@ import { AbstractViewer } from './viewer';
 import { SpotLight, MirrorTexture, Plane, ShadowGenerator, Texture, BackgroundMaterial, Observable, ShadowLight, CubeTexture, BouncingBehavior, FramingBehavior, Behavior, Light, Engine, Scene, AutoRotationBehavior, AbstractMesh, Quaternion, StandardMaterial, ArcRotateCamera, ImageProcessingConfiguration, Color3, Vector3, SceneLoader, Mesh, HemisphericLight } from 'babylonjs';
 import { CameraBehavior } from '../interfaces';
 import { ViewerModel } from '../model/viewerModel';
+import { extendClassWithConfig } from '../helper';
 
 /**
  * The Default viewer is the default implementation of the AbstractViewer.
@@ -21,15 +22,13 @@ export class DefaultViewer extends AbstractViewer {
     constructor(public containerElement: HTMLElement, initialConfiguration: ViewerConfiguration = { extends: 'default' }) {
         super(containerElement, initialConfiguration);
         this.onModelLoadedObservable.add(this._onModelLoaded);
-    }
+        this.sceneManager.onSceneInitObservable.add(() => {
+            // extendClassWithConfig(this.sceneManager.scene, this._configuration.scene);
+            return this.sceneManager.scene;
+        });
 
-    /**
-     * Overriding the AbstractViewer's _initScene fcuntion
-     */
-    protected _initScene(): Promise<Scene> {
-        return super._initScene().then(() => {
-            this._extendClassWithConfig(this.scene, this._configuration.scene);
-            return this.scene;
+        this.sceneManager.onLightsConfiguredObservable.add((data) => {
+            this._configureLights(data.newConfiguration, data.model!);
         })
     }
 
@@ -92,24 +91,30 @@ export class DefaultViewer extends AbstractViewer {
             this.templateManager.eventManager.registerCallback('viewer', triggerNavbar.bind(this, false), 'pointerup');
             this.templateManager.eventManager.registerCallback('navBar', triggerNavbar.bind(this, true), 'pointerover');
 
-            // other events
-            let viewerTemplate = this.templateManager.getTemplate('viewer');
-            let viewerElement = viewerTemplate && viewerTemplate.parent;
-            // full screen
-            let triggerFullscren = (eventData: EventCallback) => {
-                if (viewerElement) {
-                    let fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || (<any>document).mozFullScreenElement || (<any>document).msFullscreenElement;
-                    if (!fullscreenElement) {
-                        let requestFullScreen = viewerElement.requestFullscreen || viewerElement.webkitRequestFullscreen || (<any>viewerElement).msRequestFullscreen || (<any>viewerElement).mozRequestFullScreen;
-                        requestFullScreen.call(viewerElement);
-                    } else {
-                        let exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen || (<any>document).msExitFullscreen || (<any>document).mozCancelFullScreen
-                        exitFullscreen.call(document);
-                    }
-                }
-            }
+            this.templateManager.eventManager.registerCallback('navBar', this.toggleFullscreen, 'pointerdown', '#fullscreen-button');
+            this.templateManager.eventManager.registerCallback('navBar', (data) => {
+                if (data && data.event && data.event.target)
+                    this.sceneManager.models[0].playAnimation(data.event.target['value']);
+            }, 'change', '#animation-selector');
+        }
+    }
 
-            this.templateManager.eventManager.registerCallback('navBar', triggerFullscren, 'pointerdown', '#fullscreen-button');
+    /**
+     * Toggle fullscreen of the entire viewer
+     */
+    public toggleFullscreen = () => {
+        let viewerTemplate = this.templateManager.getTemplate('viewer');
+        let viewerElement = viewerTemplate && viewerTemplate.parent;
+
+        if (viewerElement) {
+            let fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || (<any>document).mozFullScreenElement || (<any>document).msFullscreenElement;
+            if (!fullscreenElement) {
+                let requestFullScreen = viewerElement.requestFullscreen || viewerElement.webkitRequestFullscreen || (<any>viewerElement).msRequestFullscreen || (<any>viewerElement).mozRequestFullScreen;
+                requestFullScreen.call(viewerElement);
+            } else {
+                let exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen || (<any>document).msExitFullscreen || (<any>document).mozCancelFullScreen
+                exitFullscreen.call(document);
+            }
         }
     }
 
@@ -130,6 +135,10 @@ export class DefaultViewer extends AbstractViewer {
         let navbar = this.templateManager.getTemplate('navBar');
         if (!navbar) return;
 
+        if (model.getAnimationNames().length > 1) {
+            navbar.updateParams({ animations: model.getAnimationNames() });
+        }
+
         let modelConfiguration = model.configuration;
 
         let metadataContainer = navbar.parent.querySelector('#model-metadata');
@@ -241,6 +250,30 @@ export class DefaultViewer extends AbstractViewer {
     }
 
     /**
+     * show the viewer (in case it was hidden)
+     * 
+     * @param visibilityFunction an optional function to execute in order to show the container
+     */
+    public show(visibilityFunction?: ((template: Template) => Promise<Template>)): Promise<Template> {
+        let template = this.templateManager.getTemplate('main');
+        //not possible, but yet:
+        if (!template) return Promise.reject('Main template not found');
+        return template.show(visibilityFunction);
+    }
+
+    /**
+     * hide the viewer (in case it is visible)
+     * 
+     * @param visibilityFunction an optional function to execute in order to hide the container
+     */
+    public hide(visibilityFunction?: ((template: Template) => Promise<Template>)) {
+        let template = this.templateManager.getTemplate('main');
+        //not possible, but yet:
+        if (!template) return Promise.reject('Main template not found');
+        return template.hide(visibilityFunction);
+    }
+
+    /**
      * Show the loading screen.
      * The loading screen can be configured using the configuration object
      */
@@ -286,8 +319,7 @@ export class DefaultViewer extends AbstractViewer {
      * @param lightsConfiguration the light configuration to use
      * @param model the model that will be used to configure the lights (if the lights are model-dependant)
      */
-    protected _configureLights(lightsConfiguration: { [name: string]: ILightConfiguration | boolean } = {}, model: ViewerModel) {
-        super._configureLights(lightsConfiguration, model);
+    private _configureLights(lightsConfiguration: { [name: string]: ILightConfiguration | boolean } = {}, model?: ViewerModel) {
         // labs feature - flashlight
         if (this._configuration.lab && this._configuration.lab.flashlight) {
             let pointerPosition = Vector3.Zero();
@@ -299,7 +331,7 @@ export class DefaultViewer extends AbstractViewer {
                 angle = this._configuration.lab.flashlight.angle || angle;
             }
             var flashlight = new SpotLight("flashlight", Vector3.Zero(),
-                Vector3.Zero(), exponent, angle, this.scene);
+                Vector3.Zero(), exponent, angle, this.sceneManager.scene);
             if (typeof this._configuration.lab.flashlight === "object") {
                 flashlight.intensity = this._configuration.lab.flashlight.intensity || flashlight.intensity;
                 if (this._configuration.lab.flashlight.diffuse) {
@@ -314,8 +346,8 @@ export class DefaultViewer extends AbstractViewer {
                 }
 
             }
-            this.scene.constantlyUpdateMeshUnderPointer = true;
-            this.scene.onPointerObservable.add((eventData, eventState) => {
+            this.sceneManager.scene.constantlyUpdateMeshUnderPointer = true;
+            this.sceneManager.scene.onPointerObservable.add((eventData, eventState) => {
                 if (eventData.type === 4 && eventData.pickInfo) {
                     lightTarget = (eventData.pickInfo.pickedPoint);
                 } else {
@@ -323,14 +355,14 @@ export class DefaultViewer extends AbstractViewer {
                 }
             });
             let updateFlashlightFunction = () => {
-                if (this.camera && flashlight) {
-                    flashlight.position.copyFrom(this.camera.position);
+                if (this.sceneManager.camera && flashlight) {
+                    flashlight.position.copyFrom(this.sceneManager.camera.position);
                     if (lightTarget) {
                         lightTarget.subtractToRef(flashlight.position, flashlight.direction);
                     }
                 }
             }
-            this.scene.registerBeforeRender(updateFlashlightFunction);
+            this.sceneManager.scene.registerBeforeRender(updateFlashlightFunction);
             this._registeredOnBeforeRenderFunctions.push(updateFlashlightFunction);
         }
     }

Файловите разлики са ограничени, защото са твърде много
+ 1084 - 0
Viewer/src/viewer/sceneManager.ts


Файловите разлики са ограничени, защото са твърде много
+ 219 - 645
Viewer/src/viewer/viewer.ts


+ 78 - 0
Viewer/tests/commons/boot.ts

@@ -0,0 +1,78 @@
+import webglSupport from './mockWebGL';
+import { Helper, useNullEngine } from "./helper";
+import { viewerGlobals } from "../../src";
+
+export class Boot {
+    public static AppendResult = false;
+
+    public static main() {
+        //let babylonSource = Boot.loadSync('base/js/babylon.viewer.max.js');
+        //let spectreSource = Boot.loadSync('base/js/spectreonly.js');
+
+        document.body.innerHTML = `<div id="result-div"></div><div id="working-div"></div>`;
+
+        //register actions to occur before each test
+        beforeEach(function (done) {
+            // tslint:disable-next-line:no-console
+            //console.debug('> Executing "' + details.name + '"');
+
+            //clear DOM and create canvas and container
+            document.getElementById('working-div')!.innerHTML = `<div style="font-size:30px;">WORKING CANVASES.</div> 
+				<div id="viewer-testing" style="width:512px;height:512px;">
+                    <div id="renderCanvas" width="512" height="512" style="width:512px;height:512px;">
+                    <canvas width="512" height="512" style="width:512px;height:512px;"></canvas></div>
+					<canvas id="referenceCanvas" width="512" height="512" style="width:512px;height:512px;"></canvas>
+				</div>
+			`;
+
+            if (Boot.AppendResult) {
+                var newResult = document.createElement('div');
+                document.getElementById('result-div')!.appendChild(newResult);
+
+                newResult.innerHTML = `<div class="result">
+						<div class="resultDisplay"></div>
+						<img class="renderImg" width="512" height="512" style="width:512px;height:512px;"></img>
+						<img class="referenceImg" width="512" height="512" style="width:512px;height:512px;"></img>
+                    </div>`;
+
+                /*if (!(<any>window).BABYLON) {
+                    eval.call(null, babylonSource);
+                    eval.call(null, spectreSource);
+                }*/
+
+            }
+            else {
+                //reset global state before executing test
+                //delete (<any>window).BABYLON;
+                //delete (<any>window).BabylonViewer;
+                //delete (<any>window).SPECTRE;
+
+                /*eval.call(null, babylonSource);
+                eval.call(null, spectreSource);*/
+            }
+
+            viewerGlobals.disableInit = true;
+
+            var DOMContentLoaded_event = document.createEvent("Event")
+            DOMContentLoaded_event.initEvent("DOMContentLoaded", true, true)
+            window.document.dispatchEvent(DOMContentLoaded_event);
+
+            // Disable Webgl2 support in test mode for Phantom/IE compatibility.
+            viewerGlobals.disableWebGL2Support = true;
+            done();
+        });
+
+        afterEach(function (done) {
+            Helper.disposeViewer();
+            //(<any>window).BabylonViewer.disposeAll();
+            done();
+        });
+    }
+
+}
+
+if (!useNullEngine) {
+    console.log("mocking webgl");
+    webglSupport();
+}
+export var main = Boot.main;

Файловите разлики са ограничени, защото са твърде много
+ 238 - 0
Viewer/tests/commons/helper.ts


+ 583 - 0
Viewer/tests/commons/mockWebGL.ts

@@ -0,0 +1,583 @@
+/**
+ * This webGL Support shim will allow running tests using the normal engine in phantomjs.
+ */
+
+//Tests should be in the same condition no matter what environment they are running in.
+//!(<any>window).WebGLRenderingContext){
+export default function webglSupport() {
+
+    //mock webgl support
+    (<any>HTMLCanvasElement.prototype)._getContext = HTMLCanvasElement.prototype.getContext;
+    HTMLCanvasElement.prototype.getContext = function (ctxName, options) {
+        //patch in mock webgl
+        switch (ctxName) {
+            case 'webgl':
+            case 'experimental-webgl':
+                var ctx = new (<any>window).WebGLRenderingContext();
+                // tslint:disable-next-line:no-invalid-this
+                ctx.canvas = this;
+                // tslint:disable-next-line:no-invalid-this
+                ctx.drawingBufferWidth = this.clientWidth;
+                // tslint:disable-next-line:no-invalid-this
+                ctx.drawingBufferHeight = this.clientHeight;
+                return (<any>ctx);
+        }
+        //pass through to default behavior
+        // tslint:disable-next-line:no-invalid-this
+        return this._getContext.apply(this, arguments);
+    };
+
+    //define WebGLRenderingContext
+    (<any>window).WebGLRenderingContext = () => { };
+
+    (<any>window).WebGLRenderingContext.prototype = {
+        /* ClearBufferMask */
+        DEPTH_BUFFER_BIT: 0x00000100,
+        STENCIL_BUFFER_BIT: 0x00000400,
+        COLOR_BUFFER_BIT: 0x00004000,
+
+        /* BeginMode */
+        POINTS: 0x0000,
+        LINES: 0x0001,
+        LINE_LOOP: 0x0002,
+        LINE_STRIP: 0x0003,
+        TRIANGLES: 0x0004,
+        TRIANGLE_STRIP: 0x0005,
+        TRIANGLE_FAN: 0x0006,
+
+        /* AlphaFunction (not supported in ES20) */
+        /*      NEVER */
+        /*      LESS */
+        /*      EQUAL */
+        /*      LEQUAL */
+        /*      GREATER */
+        /*      NOTEQUAL */
+        /*      GEQUAL */
+        /*      ALWAYS */
+
+        /* BlendingFactorDest */
+        ZERO: 0,
+        ONE: 1,
+        SRC_COLOR: 0x0300,
+        ONE_MINUS_SRC_COLOR: 0x0301,
+        SRC_ALPHA: 0x0302,
+        ONE_MINUS_SRC_ALPHA: 0x0303,
+        DST_ALPHA: 0x0304,
+        ONE_MINUS_DST_ALPHA: 0x0305,
+
+        /* BlendingFactorSrc */
+        /*      ZERO */
+        /*      ONE */
+        DST_COLOR: 0x0306,
+        ONE_MINUS_DST_COLOR: 0x0307,
+        SRC_ALPHA_SATURATE: 0x0308,
+        /*      SRC_ALPHA */
+        /*      ONE_MINUS_SRC_ALPHA */
+        /*      DST_ALPHA */
+        /*      ONE_MINUS_DST_ALPHA */
+
+        /* BlendEquationSeparate */
+        FUNC_ADD: 0x8006,
+        BLEND_EQUATION: 0x8009,
+        BLEND_EQUATION_RGB: 0x8009,   /* same as BLEND_EQUATION */
+        BLEND_EQUATION_ALPHA: 0x883D,
+
+        /* BlendSubtract */
+        FUNC_SUBTRACT: 0x800A,
+        FUNC_REVERSE_SUBTRACT: 0x800B,
+
+        /* Separate Blend Functions */
+        BLEND_DST_RGB: 0x80C8,
+        BLEND_SRC_RGB: 0x80C9,
+        BLEND_DST_ALPHA: 0x80CA,
+        BLEND_SRC_ALPHA: 0x80CB,
+        CONSTANT_COLOR: 0x8001,
+        ONE_MINUS_CONSTANT_COLOR: 0x8002,
+        CONSTANT_ALPHA: 0x8003,
+        ONE_MINUS_CONSTANT_ALPHA: 0x8004,
+        BLEND_COLOR: 0x8005,
+
+        /* Buffer Objects */
+        ARRAY_BUFFER: 0x8892,
+        ELEMENT_ARRAY_BUFFER: 0x8893,
+        ARRAY_BUFFER_BINDING: 0x8894,
+        ELEMENT_ARRAY_BUFFER_BINDING: 0x8895,
+        STREAM_DRAW: 0x88E0,
+        STATIC_DRAW: 0x88E4,
+        DYNAMIC_DRAW: 0x88E8,
+        BUFFER_SIZE: 0x8764,
+        BUFFER_USAGE: 0x8765,
+        CURRENT_VERTEX_ATTRIB: 0x8626,
+
+        /* CullFaceMode */
+        FRONT: 0x0404,
+        BACK: 0x0405,
+        FRONT_AND_BACK: 0x0408,
+
+        /* DepthFunction */
+        /*      NEVER */
+        /*      LESS */
+        /*      EQUAL */
+        /*      LEQUAL */
+        /*      GREATER */
+        /*      NOTEQUAL */
+        /*      GEQUAL */
+        /*      ALWAYS */
+
+        /* EnableCap */
+        /* TEXTURE_2D */
+        CULL_FACE: 0x0B44,
+        BLEND: 0x0BE2,
+        DITHER: 0x0BD0,
+        STENCIL_TEST: 0x0B90,
+        DEPTH_TEST: 0x0B71,
+        SCISSOR_TEST: 0x0C11,
+        POLYGON_OFFSET_FILL: 0x8037,
+        SAMPLE_ALPHA_TO_COVERAGE: 0x809E,
+        SAMPLE_COVERAGE: 0x80A0,
+
+        /* ErrorCode */
+        NO_ERROR: 0,
+        INVALID_ENUM: 0x0500,
+        INVALID_VALUE: 0x0501,
+        INVALID_OPERATION: 0x0502,
+        OUT_OF_MEMORY: 0x0505,
+
+        /* FrontFaceDirection */
+        CW: 0x0900,
+        CCW: 0x0901,
+
+        /* GetPName */
+        LINE_WIDTH: 0x0B21,
+        ALIASED_POINT_SIZE_RANGE: 0x846D,
+        ALIASED_LINE_WIDTH_RANGE: 0x846E,
+        CULL_FACE_MODE: 0x0B45,
+        FRONT_FACE: 0x0B46,
+        DEPTH_RANGE: 0x0B70,
+        DEPTH_WRITEMASK: 0x0B72,
+        DEPTH_CLEAR_VALUE: 0x0B73,
+        DEPTH_FUNC: 0x0B74,
+        STENCIL_CLEAR_VALUE: 0x0B91,
+        STENCIL_FUNC: 0x0B92,
+        STENCIL_FAIL: 0x0B94,
+        STENCIL_PASS_DEPTH_FAIL: 0x0B95,
+        STENCIL_PASS_DEPTH_PASS: 0x0B96,
+        STENCIL_REF: 0x0B97,
+        STENCIL_VALUE_MASK: 0x0B93,
+        STENCIL_WRITEMASK: 0x0B98,
+        STENCIL_BACK_FUNC: 0x8800,
+        STENCIL_BACK_FAIL: 0x8801,
+        STENCIL_BACK_PASS_DEPTH_FAIL: 0x8802,
+        STENCIL_BACK_PASS_DEPTH_PASS: 0x8803,
+        STENCIL_BACK_REF: 0x8CA3,
+        STENCIL_BACK_VALUE_MASK: 0x8CA4,
+        STENCIL_BACK_WRITEMASK: 0x8CA5,
+        VIEWPORT: 0x0BA2,
+        SCISSOR_BOX: 0x0C10,
+        /*      SCISSOR_TEST */
+        COLOR_CLEAR_VALUE: 0x0C22,
+        COLOR_WRITEMASK: 0x0C23,
+        UNPACK_ALIGNMENT: 0x0CF5,
+        PACK_ALIGNMENT: 0x0D05,
+        MAX_TEXTURE_SIZE: 0x0D33,
+        MAX_VIEWPORT_DIMS: 0x0D3A,
+        SUBPIXEL_BITS: 0x0D50,
+        RED_BITS: 0x0D52,
+        GREEN_BITS: 0x0D53,
+        BLUE_BITS: 0x0D54,
+        ALPHA_BITS: 0x0D55,
+        DEPTH_BITS: 0x0D56,
+        STENCIL_BITS: 0x0D57,
+        POLYGON_OFFSET_UNITS: 0x2A00,
+        /*      POLYGON_OFFSET_FILL */
+        POLYGON_OFFSET_FACTOR: 0x8038,
+        TEXTURE_BINDING_2D: 0x8069,
+        SAMPLE_BUFFERS: 0x80A8,
+        SAMPLES: 0x80A9,
+        SAMPLE_COVERAGE_VALUE: 0x80AA,
+        SAMPLE_COVERAGE_INVERT: 0x80AB,
+
+        /* GetTextureParameter */
+        /*      TEXTURE_MAG_FILTER */
+        /*      TEXTURE_MIN_FILTER */
+        /*      TEXTURE_WRAP_S */
+        /*      TEXTURE_WRAP_T */
+        COMPRESSED_TEXTURE_FORMATS: 0x86A3,
+
+        /* HintMode */
+        DONT_CARE: 0x1100,
+        FASTEST: 0x1101,
+        NICEST: 0x1102,
+
+        /* HintTarget */
+        GENERATE_MIPMAP_HINT: 0x8192,
+
+        /* DataType */
+        BYTE: 0x1400,
+        UNSIGNED_BYTE: 0x1401,
+        SHORT: 0x1402,
+        UNSIGNED_SHORT: 0x1403,
+        INT: 0x1404,
+        UNSIGNED_INT: 0x1405,
+        FLOAT: 0x1406,
+
+        /* PixelFormat */
+        DEPTH_COMPONENT: 0x1902,
+        ALPHA: 0x1906,
+        RGB: 0x1907,
+        RGBA: 0x1908,
+        LUMINANCE: 0x1909,
+        LUMINANCE_ALPHA: 0x190A,
+
+        /* PixelType */
+        /*      UNSIGNED_BYTE */
+        UNSIGNED_SHORT_4_4_4_4: 0x8033,
+        UNSIGNED_SHORT_5_5_5_1: 0x8034,
+        UNSIGNED_SHORT_5_6_5: 0x8363,
+
+        /* Shaders */
+        FRAGMENT_SHADER: 0x8B30,
+        VERTEX_SHADER: 0x8B31,
+        MAX_VERTEX_ATTRIBS: 0x8869,
+        MAX_VERTEX_UNIFORM_VECTORS: 0x8DFB,
+        MAX_VARYING_VECTORS: 0x8DFC,
+        MAX_COMBINED_TEXTURE_IMAGE_UNITS: 0x8B4D,
+        MAX_VERTEX_TEXTURE_IMAGE_UNITS: 0x8B4C,
+        MAX_TEXTURE_IMAGE_UNITS: 0x8872,
+        MAX_FRAGMENT_UNIFORM_VECTORS: 0x8DFD,
+        SHADER_TYPE: 0x8B4F,
+        DELETE_STATUS: 0x8B80,
+        LINK_STATUS: 0x8B82,
+        VALIDATE_STATUS: 0x8B83,
+        ATTACHED_SHADERS: 0x8B85,
+        ACTIVE_UNIFORMS: 0x8B86,
+        ACTIVE_ATTRIBUTES: 0x8B89,
+        SHADING_LANGUAGE_VERSION: 0x8B8C,
+        CURRENT_PROGRAM: 0x8B8D,
+
+        /* StencilFunction */
+        NEVER: 0x0200,
+        LESS: 0x0201,
+        EQUAL: 0x0202,
+        LEQUAL: 0x0203,
+        GREATER: 0x0204,
+        NOTEQUAL: 0x0205,
+        GEQUAL: 0x0206,
+        ALWAYS: 0x0207,
+
+        /* StencilOp */
+        /*      ZERO */
+        KEEP: 0x1E00,
+        REPLACE: 0x1E01,
+        INCR: 0x1E02,
+        DECR: 0x1E03,
+        INVERT: 0x150A,
+        INCR_WRAP: 0x8507,
+        DECR_WRAP: 0x8508,
+
+        /* StringName */
+        VENDOR: 0x1F00,
+        RENDERER: 0x1F01,
+        VERSION: 0x1F02,
+
+        /* TextureMagFilter */
+        NEAREST: 0x2600,
+        LINEAR: 0x2601,
+
+        /* TextureMinFilter */
+        /*      NEAREST */
+        /*      LINEAR */
+        NEAREST_MIPMAP_NEAREST: 0x2700,
+        LINEAR_MIPMAP_NEAREST: 0x2701,
+        NEAREST_MIPMAP_LINEAR: 0x2702,
+        LINEAR_MIPMAP_LINEAR: 0x2703,
+
+        /* TextureParameterName */
+        TEXTURE_MAG_FILTER: 0x2800,
+        TEXTURE_MIN_FILTER: 0x2801,
+        TEXTURE_WRAP_S: 0x2802,
+        TEXTURE_WRAP_T: 0x2803,
+
+        /* TextureTarget */
+        TEXTURE_2D: 0x0DE1,
+        TEXTURE: 0x1702,
+        TEXTURE_CUBE_MAP: 0x8513,
+        TEXTURE_BINDING_CUBE_MAP: 0x8514,
+        TEXTURE_CUBE_MAP_POSITIVE_X: 0x8515,
+        TEXTURE_CUBE_MAP_NEGATIVE_X: 0x8516,
+        TEXTURE_CUBE_MAP_POSITIVE_Y: 0x8517,
+        TEXTURE_CUBE_MAP_NEGATIVE_Y: 0x8518,
+        TEXTURE_CUBE_MAP_POSITIVE_Z: 0x8519,
+        TEXTURE_CUBE_MAP_NEGATIVE_Z: 0x851A,
+        MAX_CUBE_MAP_TEXTURE_SIZE: 0x851C,
+
+        /* TextureUnit */
+        TEXTURE0: 0x84C0,
+        TEXTURE1: 0x84C1,
+        TEXTURE2: 0x84C2,
+        TEXTURE3: 0x84C3,
+        TEXTURE4: 0x84C4,
+        TEXTURE5: 0x84C5,
+        TEXTURE6: 0x84C6,
+        TEXTURE7: 0x84C7,
+        TEXTURE8: 0x84C8,
+        TEXTURE9: 0x84C9,
+        TEXTURE10: 0x84CA,
+        TEXTURE11: 0x84CB,
+        TEXTURE12: 0x84CC,
+        TEXTURE13: 0x84CD,
+        TEXTURE14: 0x84CE,
+        TEXTURE15: 0x84CF,
+        TEXTURE16: 0x84D0,
+        TEXTURE17: 0x84D1,
+        TEXTURE18: 0x84D2,
+        TEXTURE19: 0x84D3,
+        TEXTURE20: 0x84D4,
+        TEXTURE21: 0x84D5,
+        TEXTURE22: 0x84D6,
+        TEXTURE23: 0x84D7,
+        TEXTURE24: 0x84D8,
+        TEXTURE25: 0x84D9,
+        TEXTURE26: 0x84DA,
+        TEXTURE27: 0x84DB,
+        TEXTURE28: 0x84DC,
+        TEXTURE29: 0x84DD,
+        TEXTURE30: 0x84DE,
+        TEXTURE31: 0x84DF,
+        ACTIVE_TEXTURE: 0x84E0,
+
+        /* TextureWrapMode */
+        REPEAT: 0x2901,
+        CLAMP_TO_EDGE: 0x812F,
+        MIRRORED_REPEAT: 0x8370,
+
+        /* Uniform Types */
+        FLOAT_VEC2: 0x8B50,
+        FLOAT_VEC3: 0x8B51,
+        FLOAT_VEC4: 0x8B52,
+        INT_VEC2: 0x8B53,
+        INT_VEC3: 0x8B54,
+        INT_VEC4: 0x8B55,
+        BOOL: 0x8B56,
+        BOOL_VEC2: 0x8B57,
+        BOOL_VEC3: 0x8B58,
+        BOOL_VEC4: 0x8B59,
+        FLOAT_MAT2: 0x8B5A,
+        FLOAT_MAT3: 0x8B5B,
+        FLOAT_MAT4: 0x8B5C,
+        SAMPLER_2D: 0x8B5E,
+        SAMPLER_CUBE: 0x8B60,
+
+        /* Vertex Arrays */
+        VERTEX_ATTRIB_ARRAY_ENABLED: 0x8622,
+        VERTEX_ATTRIB_ARRAY_SIZE: 0x8623,
+        VERTEX_ATTRIB_ARRAY_STRIDE: 0x8624,
+        VERTEX_ATTRIB_ARRAY_TYPE: 0x8625,
+        VERTEX_ATTRIB_ARRAY_NORMALIZED: 0x886A,
+        VERTEX_ATTRIB_ARRAY_POINTER: 0x8645,
+        VERTEX_ATTRIB_ARRAY_BUFFER_BINDING: 0x889F,
+
+        /* Read Format */
+        IMPLEMENTATION_COLOR_READ_TYPE: 0x8B9A,
+        IMPLEMENTATION_COLOR_READ_FORMAT: 0x8B9B,
+
+        /* Shader Source */
+        COMPILE_STATUS: 0x8B81,
+
+        /* Shader Precision-Specified Types */
+        LOW_FLOAT: 0x8DF0,
+        MEDIUM_FLOAT: 0x8DF1,
+        HIGH_FLOAT: 0x8DF2,
+        LOW_INT: 0x8DF3,
+        MEDIUM_INT: 0x8DF4,
+        HIGH_INT: 0x8DF5,
+
+        /* Framebuffer Object. */
+        FRAMEBUFFER: 0x8D40,
+        RENDERBUFFER: 0x8D41,
+        RGBA4: 0x8056,
+        RGB5_A1: 0x8057,
+        RGB565: 0x8D62,
+        DEPTH_COMPONENT16: 0x81A5,
+        STENCIL_INDEX: 0x1901,
+        STENCIL_INDEX8: 0x8D48,
+        DEPTH_STENCIL: 0x84F9,
+        RENDERBUFFER_WIDTH: 0x8D42,
+        RENDERBUFFER_HEIGHT: 0x8D43,
+        RENDERBUFFER_INTERNAL_FORMAT: 0x8D44,
+        RENDERBUFFER_RED_SIZE: 0x8D50,
+        RENDERBUFFER_GREEN_SIZE: 0x8D51,
+        RENDERBUFFER_BLUE_SIZE: 0x8D52,
+        RENDERBUFFER_ALPHA_SIZE: 0x8D53,
+        RENDERBUFFER_DEPTH_SIZE: 0x8D54,
+        RENDERBUFFER_STENCIL_SIZE: 0x8D55,
+        FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE: 0x8CD0,
+        FRAMEBUFFER_ATTACHMENT_OBJECT_NAME: 0x8CD1,
+        FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL: 0x8CD2,
+        FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE: 0x8CD3,
+        COLOR_ATTACHMENT0: 0x8CE0,
+        DEPTH_ATTACHMENT: 0x8D00,
+        STENCIL_ATTACHMENT: 0x8D20,
+        DEPTH_STENCIL_ATTACHMENT: 0x821A,
+        NONE: 0,
+        FRAMEBUFFER_COMPLETE: 0x8CD5,
+        FRAMEBUFFER_INCOMPLETE_ATTACHMENT: 0x8CD6,
+        FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: 0x8CD7,
+        FRAMEBUFFER_INCOMPLETE_DIMENSIONS: 0x8CD9,
+        FRAMEBUFFER_UNSUPPORTED: 0x8CDD,
+        FRAMEBUFFER_BINDING: 0x8CA6,
+        RENDERBUFFER_BINDING: 0x8CA7,
+        MAX_RENDERBUFFER_SIZE: 0x84E8,
+        INVALID_FRAMEBUFFER_OPERATION: 0x0506,
+
+        /* WebGL-specific enums */
+        UNPACK_FLIP_Y_WEBGL: 0x9240,
+        UNPACK_PREMULTIPLY_ALPHA_WEBGL: 0x9241,
+        CONTEXT_LOST_WEBGL: 0x9242,
+        UNPACK_COLORSPACE_CONVERSION_WEBGL: 0x9243,
+        BROWSER_DEFAULT_WEBGL: 0x9244,
+
+        canvas: null,
+        drawingBufferWidth: 0,
+        drawingBufferHeight: 0,
+
+        getContextAttributes: function () { return {}; },
+        isContextLost: function () { return {}; },
+        getSupportedExtensions: function () { return {}; },
+        getExtension: function (name) { return name === "EXT_texture_filter_anisotropic" ? {} : null; },
+        activeTexture: function () { return {}; },
+        attachShader: function () { return {}; },
+        bindAttribLocation: function () { return {}; },
+        bindBuffer: function () { return {}; },
+        bindFramebuffer: function () { return {}; },
+        bindRenderbuffer: function () { return {}; },
+        bindTexture: function () { return {}; },
+        blendColor: function () { return {}; },
+        blendEquation: function () { return {}; },
+        blendEquationSeparate: function () { return {}; },
+        blendFunc: function () { return {}; },
+        blendFuncSeparate: function () { return {}; },
+        bufferData: function () { return {}; },
+        bufferSubData: function () { return {}; },
+        checkFramebufferStatus: function () { return {}; },
+        clear: function () { return {}; },
+        clearColor: function () { return {}; },
+        clearDepth: function () { return {}; },
+        clearStencil: function () { return {}; },
+        colorMask: function () { return {}; },
+        compileShader: function () { return {}; },
+        compressedTexImage2D: function () { return {}; },
+        compressedTexSubImage2D: function () { return {}; },
+        copyTexImage2D: function () { return {}; },
+        copyTexSubImage2D: function () { return {}; },
+        createBuffer: function () { return {}; },
+        createFramebuffer: function () { return {}; },
+        createProgram: function () { return {}; },
+        createRenderbuffer: function () { return {}; },
+        createShader: function () { return {}; },
+        createTexture: function () { return {}; },
+        cullFace: function () { return {}; },
+        deleteBuffer: function () { return {}; },
+        deleteFramebuffer: function () { return {}; },
+        deleteProgram: function () { return {}; },
+        deleteRenderbuffer: function () { return {}; },
+        deleteShader: function () { return {}; },
+        deleteTexture: function () { return {}; },
+        depthFunc: function () { return {}; },
+        depthMask: function () { return {}; },
+        depthRange: function () { return {}; },
+        detachShader: function () { return {}; },
+        disable: function () { return {}; },
+        disableVertexAttribArray: function () { return {}; },
+        drawArrays: function () { return {}; },
+        drawElements: function () { return {}; },
+        enable: function () { return {}; },
+        enableVertexAttribArray: function () { return {}; },
+        finish: function () { return {}; },
+        flush: function () { return {}; },
+        framebufferRenderbuffer: function () { return {}; },
+        framebufferTexture2D: function () { return {}; },
+        frontFace: function () { return {}; },
+        generateMipmap: function () { return {}; },
+        getActiveAttrib: function () { return {}; },
+        getActiveUniform: function () { return {}; },
+        getAttachedShaders: function () { return {}; },
+        getAttribLocation: function () { return {}; },
+        getBufferParameter: function () { return {}; },
+        getParameter: function () { return 256; },
+        getError: function () { return 0; },
+        getFramebufferAttachmentParameter: function () { return {}; },
+        getProgramParameter: function () { return {}; },
+        getProgramInfoLog: function () { return {}; },
+        getRenderbufferParameter: function () { return {}; },
+        getShaderParameter: function () { return {}; },
+        getShaderPrecisionFormat: function () { return {}; },
+        getShaderInfoLog: function () { return {}; },
+        getShaderSource: function () { return {}; },
+        getTexParameter: function () { return {}; },
+        getUniform: function () { return {}; },
+        getUniformLocation: function () { return {}; },
+        getVertexAttrib: function () { return {}; },
+        getVertexAttribOffset: function () { return {}; },
+        hint: function () { return {}; },
+        isBuffer: function () { return {}; },
+        isEnabled: function () { return {}; },
+        isFramebuffer: function () { return {}; },
+        isProgram: function () { return {}; },
+        isRenderbuffer: function () { return {}; },
+        isShader: function () { return {}; },
+        isTexture: function () { return {}; },
+        lineWidth: function () { return {}; },
+        linkProgram: function () { return {}; },
+        pixelStorei: function () { return {}; },
+        polygonOffset: function () { return {}; },
+        readPixels: function () { return {}; },
+        renderbufferStorage: function () { return {}; },
+        sampleCoverage: function () { return {}; },
+        scissor: function () { return {}; },
+        shaderSource: function () { return {}; },
+        stencilFunc: function () { return {}; },
+        stencilFuncSeparate: function () { return {}; },
+        stencilMask: function () { return {}; },
+        stencilMaskSeparate: function () { return {}; },
+        stencilOp: function () { return {}; },
+        stencilOpSeparate: function () { return {}; },
+        texImage2D: function () { return {}; },
+        texImage3D: function () { return {}; },
+        texParameterf: function () { return {}; },
+        texParameteri: function () { return {}; },
+        texSubImage2D: function () { return {}; },
+        uniform1f: function () { return {}; },
+        uniform1fv: function () { return {}; },
+        uniform1i: function () { return {}; },
+        uniform1iv: function () { return {}; },
+        uniform2f: function () { return {}; },
+        uniform2fv: function () { return {}; },
+        uniform2i: function () { return {}; },
+        uniform2iv: function () { return {}; },
+        uniform3f: function () { return {}; },
+        uniform3fv: function () { return {}; },
+        uniform3i: function () { return {}; },
+        uniform3iv: function () { return {}; },
+        uniform4f: function () { return {}; },
+        uniform4fv: function () { return {}; },
+        uniform4i: function () { return {}; },
+        uniform4iv: function () { return {}; },
+        uniformMatrix2fv: function () { return {}; },
+        uniformMatrix3fv: function () { return {}; },
+        uniformMatrix4fv: function () { return {}; },
+        useProgram: function () { return {}; },
+        validateProgram: function () { return {}; },
+        vertexAttrib1f: function () { return {}; },
+        vertexAttrib1fv: function () { return {}; },
+        vertexAttrib2f: function () { return {}; },
+        vertexAttrib2fv: function () { return {}; },
+        vertexAttrib3f: function () { return {}; },
+        vertexAttrib3fv: function () { return {}; },
+        vertexAttrib4f: function () { return {}; },
+        vertexAttrib4fv: function () { return {}; },
+        vertexAttribPointer: function () { return {}; },
+        viewport: function () { return {}; }
+    }
+
+}

+ 39 - 0
Viewer/tests/karma.conf.js

@@ -0,0 +1,39 @@
+module.exports = function (config) {
+    config.set({
+        basePath: '../',
+        captureTimeout: 3e5,
+        browserNoActivityTimeout: 3e5,
+        browserDisconnectTimeout: 3e5,
+        browserDisconnectTolerance: 3,
+        concurrency: 1,
+
+        urlRoot: '/karma/',
+
+        frameworks: ['mocha', 'chai', 'sinon'],
+
+        files: [
+            './tests/build/*.js',
+            { pattern: './tests/**/*', watched: false, included: false, served: true },
+        ],
+        proxies: {
+            '/tests/': '/base/tests/'
+        },
+        client: {
+            mocha: {
+                timeout: 10000
+            }
+        },
+
+        port: 3000,
+        colors: true,
+        autoWatch: false,
+        singleRun: true,
+        browserNoActivityTimeout: 20000,
+
+        // level of logging
+        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+        logLevel: config.LOG_INFO,
+
+        browsers: ['PhantomJS']
+    })
+}

+ 16 - 0
Viewer/tests/package.json

@@ -0,0 +1,16 @@
+{
+    "name": "babylonjsviewerunittest",
+    "version": "7.7.7",
+    "description": "Unit Tests Suite For Babylon.js' viewer",
+    "main": "",
+    "repository": {
+        "url": "https://github.com/BabylonJS/Babylon.js/"
+    },
+    "readme": "https://github.com/BabylonJS/Babylon.js/edit/master/readme.md",
+    "license": "(Apache-2.0)",
+    "devDependencies": {
+        "@types/mocha": "2.2.46",
+        "@types/chai": "^4.1.0",
+        "@types/sinon": "^4.1.3"
+    }
+}

+ 6 - 0
Viewer/tests/unit/src/index.ts

@@ -0,0 +1,6 @@
+import { main } from '../../commons/boot';
+if (window && !window['validation']) {
+    main();
+}
+export * from './viewer/viewer';
+export * from '../../../src'

+ 433 - 0
Viewer/tests/unit/src/viewer/viewer.ts

@@ -0,0 +1,433 @@
+import { Helper } from "../../../commons/helper";
+import { assert, expect, should } from "../viewerReference";
+import { DefaultViewer, AbstractViewer, Version, viewerManager } from "../../../../src";
+
+export let name = "viewer Tests";
+
+/**
+ * To prevent test-state-leakage ensure that there is a viewer.dispose() for every new DefaultViewer
+ */
+
+describe('Viewer', function () {
+    it('should initialize a new viewer and its internal variables', (done) => {
+        let viewer = Helper.getNewViewerInstance();
+        assert.isDefined(viewer.baseId, "base id should be defined");
+        assert.isDefined(viewer.templateManager, "template manager should be defined");
+        assert.isDefined(viewer.sceneManager, "scene manager should be defined");
+        assert.isDefined(viewer.modelLoader, "model loader should be defined");
+        viewer.onInitDoneObservable.add(() => {
+            assert.isDefined(viewer, "Viewer can not be instantiated.");
+            viewer.dispose();
+            done();
+        });
+    });
+
+    it('should be added to the viewer manager', (done) => {
+        let viewer = Helper.getNewViewerInstance();
+        viewer.onInitDoneObservable.add(() => {
+            assert.isDefined(viewerManager.getViewerById(viewer.baseId), "Viewer was not added to the viewer manager.");
+            viewer.dispose();
+            done();
+        });
+    });
+
+    it('should have a defined canvas', (done) => {
+        let viewer = Helper.getNewViewerInstance();
+        viewer.onInitDoneObservable.add(() => {
+            assert.isDefined(viewer.canvas, "Canvas is not defined");
+            assert.isTrue(viewer.canvas instanceof HTMLCanvasElement, "Canvas is not a canvas");
+            viewer.dispose();
+            done();
+        });
+    });
+
+    it('should not initialize if element is undefined', (done) => {
+        try {
+            // force typescript to "think" that the element exist with "!"
+            let viewer = Helper.getNewViewerInstance(document.getElementById('doesntexist')!);
+            expect(viewer).not.to.exist;
+            if (viewer) viewer.dispose();
+        } catch (e) {
+            // exception was thrown, we are happy
+            assert.isTrue(true);
+        }
+        done();
+    });
+
+    it('should be shown and hidden', (done) => {
+        let viewer: DefaultViewer = <DefaultViewer>Helper.getNewViewerInstance();
+        viewer.onInitDoneObservable.add(() => {
+            // default visibility is not none
+            expect(viewer.containerElement.style.display).not.to.equal('none');
+            viewer.hide().then(() => {
+                // element is hidden
+                assert.equal(viewer.containerElement.style.display, 'none', "Viewer is still visible");
+                viewer.show().then(() => {
+                    //element is shown
+                    assert.notEqual(viewer.containerElement.style.display, 'none', "Viewer is not visible");
+                    viewer.dispose();
+                    done();
+                });
+            });
+        });
+    });
+
+    it('should execute registered functions on every rendered frame', (done) => {
+        let viewer: DefaultViewer = <DefaultViewer>Helper.getNewViewerInstance();
+        let renderCount = 0;
+        let sceneRenderCount = 0;
+        viewer.onSceneInitObservable.add(() => {
+            viewer.sceneManager.scene.registerBeforeRender(() => {
+                sceneRenderCount++;
+            });
+            viewer.onFrameRenderedObservable.add(() => {
+                renderCount++;
+                assert.equal(renderCount, sceneRenderCount, "function was not executed with each frame");
+                if (renderCount === 20) {
+                    viewer.dispose();
+                    done();
+                }
+            });
+        });
+    });
+
+    it('should disable and enable rendering', (done) => {
+
+        let viewer: DefaultViewer = <DefaultViewer>Helper.getNewViewerInstance();
+        let renderCount = 0;
+
+        viewer.onInitDoneObservable.add(() => {
+            viewer.onFrameRenderedObservable.add(() => {
+                renderCount++;
+            });
+            assert.equal(renderCount, 0);
+            window.requestAnimationFrame(function () {
+                assert.equal(renderCount, 1, "render loop should have been executed");
+                viewer.runRenderLoop = false;
+                window.requestAnimationFrame(function () {
+                    assert.equal(renderCount, 1, "Render loop should not have been executed");
+                    viewer.runRenderLoop = true;
+                    window.requestAnimationFrame(function () {
+                        assert.equal(renderCount, 2, "render loop should have been executed again");
+                        viewer.dispose();
+                        done();
+                    });
+                });
+            });
+        });
+    });
+
+    it('should have a version', (done) => {
+        assert.exists(Version, "Viewer should have a version");
+        assert.equal(Version, BABYLON.Engine.Version, "Viewer version should equal to Babylon's engine version");
+        done();
+    });
+
+    it('should resize the viewer correctly', (done) => {
+
+        let viewer: DefaultViewer = <DefaultViewer>Helper.getNewViewerInstance();
+        let resizeCount = 0;
+        //wait for the engine to init
+        viewer.onEngineInitObservable.add((engine) => {
+            // mock the resize function
+            engine.resize = () => {
+                resizeCount++;
+            }
+        });
+
+        viewer.onInitDoneObservable.add(() => {
+            assert.equal(resizeCount, 0);
+            viewer.forceResize();
+            assert.equal(resizeCount, 1, "Engine should resize when Viewer.forceResize() is called.");
+
+            viewer.updateConfiguration({
+                engine: {
+                    disableResize: true
+                }
+            });
+
+            viewer.forceResize();
+
+            assert.equal(resizeCount, 1, "Engine should not resize when disableResize is enabled");
+
+            viewer.updateConfiguration({
+                engine: {
+                    disableResize: false
+                }
+            });
+
+            viewer.canvas.style.width = '0px';
+            viewer.canvas.style.height = '0px';
+            viewer.forceResize();
+
+            assert.equal(resizeCount, 1, "Engine should not resize when the canvas has width/height 0.");
+
+            viewer.dispose();
+            // any since it is protected
+            viewer.forceResize();
+
+            assert.equal(resizeCount, 1, "Engine should not resize when if Viewer has been disposed.");
+            done();
+        });
+    });
+
+    it('should render in background if set to true', (done) => {
+        let viewer = Helper.getNewViewerInstance();
+        viewer.onInitDoneObservable.add(() => {
+            assert.isFalse(viewer.engine.renderEvenInBackground, "Engine is rendering in background");
+            viewer.updateConfiguration({
+                scene: {
+                    renderInBackground: true
+                }
+            });
+            assert.isTrue(viewer.engine.renderEvenInBackground, "Engine is not rendering in background");
+            viewer.dispose();
+            done();
+        });
+    });
+
+    it('should attach and detach camera control correctly', (done) => {
+        let viewer = Helper.getNewViewerInstance();
+        viewer.onInitDoneObservable.add(() => {
+            assert.isDefined(viewer.sceneManager.camera.inputs.attachedElement, "Camera is not attached per default");
+            viewer.updateConfiguration({
+                scene: {
+                    disableCameraControl: true
+                }
+            });
+            assert.isNull(viewer.sceneManager.camera.inputs.attachedElement, "Camera is still attached");
+            viewer.updateConfiguration({
+                scene: {
+                    disableCameraControl: false
+                }
+            });
+            assert.isDefined(viewer.sceneManager.camera.inputs.attachedElement, "Camera not attached");
+            viewer.dispose();
+            done();
+        });
+    });
+
+    it('should take screenshot when called', (done) => {
+        let viewer = Helper.getNewViewerInstance();
+        viewer.onInitDoneObservable.add(() => {
+            Helper.MockScreenCapture(viewer, Helper.mockScreenCaptureData());
+
+            viewer.takeScreenshot(function (data) {
+                assert.equal(data, Helper.mockScreenCaptureData(), "Screenshot failed.");
+
+                viewer.dispose();
+                done();
+            });
+        });
+    });
+
+    it('should notify observers correctly during init', (done) => {
+        let viewer = Helper.getNewViewerInstance();
+
+        let shouldBeRendering = false;
+
+        viewer.onFrameRenderedObservable.add(() => {
+            assert.isTrue(shouldBeRendering, "rendered before init done");
+            viewer.dispose();
+            done();
+        });
+
+        viewer.onEngineInitObservable.add((engine) => {
+            assert.equal(engine, viewer.engine, "engine instance is not the same");
+            assert.isUndefined(viewer.sceneManager.scene, "scene exists before initScene");
+        });
+
+        viewer.onSceneInitObservable.add((scene) => {
+            assert.equal(scene, viewer.sceneManager.scene, "scene instance is not the same");
+        })
+
+        viewer.onInitDoneObservable.add((viewerInstance) => {
+            assert.isDefined(viewerInstance.sceneManager.scene, "scene is not defined");
+            //scene exists, it should now start rendering
+            shouldBeRendering = true;
+        });
+    });
+
+    it('should render if forceRender was called', (done) => {
+        let viewer = Helper.getNewViewerInstance();
+        viewer.runRenderLoop = false;
+        viewer.onInitDoneObservable.add(() => {
+            viewer.onFrameRenderedObservable.add(() => {
+                assert.isTrue(true, "not rendered");
+                viewer.dispose();
+                done();
+            });
+            viewer.forceRender();
+        });
+    });
+});
+
+//}
+/*
+
+
+QUnit.test('Viewer disable ctrl for panning', function (assert) {
+    let viewer = new DefaultViewer(Helper.getCanvas());
+
+    QUnit.assert.ok(viewer.Scene.Camera._useCtrlForPanning, "Viewer should use CTRL for panning by default.");
+
+    viewer.dispose();
+    viewer = null;
+
+    viewer = new DefaultViewer(Helper.getCanvas(), {
+        disableCtrlForPanning: true
+    });
+
+    QUnit.assert.ok(viewer.Scene.Camera._useCtrlForPanning === false, "Viewer should not use CTRL for panning with disableCameraControl set to true.");
+    viewer.dispose();
+});
+
+QUnit.test('Viewer get models', function (assert) {
+    let viewer = new DefaultViewer(Helper.getCanvas());
+
+    let mesh1 = Helper.createMockMesh(viewer);
+    let mesh2 = Helper.createMockMesh(viewer);
+    let model1 = new SPECTRE.Model(viewer, "Model 1");
+    let model2 = new SPECTRE.Model(viewer, "Model 2");
+    model1.setMesh(mesh1);
+    model2.setMesh(mesh2);
+
+    viewer.Scene.addModel(model1, false);
+    viewer.Scene.addModel(model2, false);
+
+    QUnit.assert.equal(viewer.Scene.Models.length, 2, "Viewer.getModels should return all models in the scene by default.");
+
+    // Further tests fail unless this viewer is disposed
+    // TODO fully isolate tests
+    viewer.dispose();
+});
+
+QUnit.test('Viewer model add/remove', function (assert) {
+    let modelsInScene = 0;
+
+    let viewer = new DefaultViewer(Helper.getCanvas(), {
+        onModelAdd: function () {
+            modelsInScene += 1;
+        },
+        onModelRemove: function () {
+            modelsInScene -= 1;
+        }
+    });
+
+    let mesh1 = Helper.createMockMesh(viewer);
+    let model = new SPECTRE.Model(viewer, "Model");
+    model.setMesh(mesh1);
+
+    viewer.Scene.addModel(model, false);
+
+    QUnit.assert.equal(modelsInScene, 1, "onModelAdd should be called when a model is registered");
+
+    viewer.Scene.removeModel(model, false);
+
+    QUnit.assert.equal(modelsInScene, 0, "onModelRemove should be called when a model is unregistered");
+
+    viewer.dispose();
+});
+
+QUnit.test('Viewer typical case with dispose', function (assert) {
+    let done = assert.async();
+
+    let viewer = new DefaultViewer(Helper.getCanvas(), {
+        environmentAssetsRootURL: 'base/assets/environment/',
+        environmentMap: 'legacy/joa-256.env',
+        unifiedConfiguration: 'base/assets/UnifiedConfiguration.json'
+    });
+
+    //load different models sequentially to simulate typical use
+    viewer.loadGLTF('base/assets/Modok/Modok.FBX.gltf', {
+        completeCallback: (model) => {
+            model.EngineModel.translate(new BABYLON.Vector3(1, 0, 0), 0.1);
+
+            setTimeout(() => {
+                viewer.Scene.removeModel(model, true, () => {
+                    viewer.loadGLTF('base/assets/Modok/Modok.FBX.gltf', {
+                        readyCallback: () => {
+                            //starting loading a few assets and ensure there's no failure when disposing
+                            viewer.loadEnvironment('legacy/joa-256.env', () => {
+                                assert.ok(false, 'Viewer should have been disposed! Load should not complete.');
+                            });
+                            viewer.loadGLTF('base/assets/Modok/Modok.FBX.gltf', {
+                                readyCallback: () => {
+                                    assert.ok(false, 'Viewer should have been disposed! Load should not complete.');
+                                },
+                            });
+
+                            try {
+                                console.log('Disposing viewer');
+                                viewer.dispose();
+                                viewer = null;
+                                console.log('Viewer disposed');
+                            } catch (e) {
+                                assert.ok(false, `Viewer failed to dispose without exception ${e}`);
+                            }
+
+                            setTimeout(() => {
+                                //wait some time to verify there were no exceptions no complete callbacks fire unexpectedly
+                                assert.strictEqual(viewer, null, 'Viewer should be set to null');
+                                done();
+                            }, 2000);
+                        }
+                    });
+                });
+            }, 3000);
+        }
+    });
+});
+
+QUnit.test('Test getEnvironmentAssetUrl relative no root', function (assert) {
+    var viewer = Helper.createViewer();
+    assert.ok(viewer.getEnvironmentAssetUrl("foo.png") === "foo.png", "Relative url should be return unmodified without configuration.");
+});
+
+QUnit.test('Test getEnvironmentAssetUrl absolute no root', function (assert) {
+    var viewer = Helper.createViewer();
+    assert.ok(viewer.getEnvironmentAssetUrl("http://foo.png") === "http://foo.png", "Absolute url should not be undefined without configuration.");
+});
+
+QUnit.test('Test getEnvironmentAssetUrl relative root', function (assert) {
+    var viewer = Helper.createViewer({ environmentAssetsRootURL: "https://foo/" });
+    assert.ok(viewer.getEnvironmentAssetUrl("foo.png") === "https://foo/foo.png", "Relative url should not be be undefined with configuration.");
+});
+
+QUnit.test('Test getEnvironmentAssetUrl absolute root', function (assert) {
+    var viewer = Helper.createViewer({ environmentAssetsRootURL: "https://foo/" });
+    assert.ok(viewer.getEnvironmentAssetUrl("http://foo.png") === "http://foo.png", "Absolute url should not be undefined with configuration.");
+});
+
+QUnit.test('unlockBabylonFeatures', function () {
+    let viewer = Helper.createViewer();
+
+    QUnit.assert.ok(viewer.Scene.EngineScene.shadowsEnabled, "shadowsEnabled");
+    QUnit.assert.ok(!viewer.Scene.EngineScene.particlesEnabled, "particlesEnabled");
+    QUnit.assert.ok(!viewer.Scene.EngineScene.collisionsEnabled, "collisionsEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.lightsEnabled, "lightsEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.texturesEnabled, "texturesEnabled");
+    QUnit.assert.ok(!viewer.Scene.EngineScene.lensFlaresEnabled, "lensFlaresEnabled");
+    QUnit.assert.ok(!viewer.Scene.EngineScene.proceduralTexturesEnabled, "proceduralTexturesEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.renderTargetsEnabled, "renderTargetsEnabled");
+    QUnit.assert.ok(!viewer.Scene.EngineScene.spritesEnabled, "spritesEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.skeletonsEnabled, "skeletonsEnabled");
+    QUnit.assert.ok(!viewer.Scene.EngineScene.audioEnabled, "audioEnabled");
+
+    viewer.unlockBabylonFeatures();
+
+    QUnit.assert.ok(viewer.Scene.EngineScene.shadowsEnabled, "shadowsEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.particlesEnabled, "particlesEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.postProcessesEnabled, "postProcessesEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.collisionsEnabled, "collisionsEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.lightsEnabled, "lightsEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.texturesEnabled, "texturesEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.lensFlaresEnabled, "lensFlaresEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.proceduralTexturesEnabled, "proceduralTexturesEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.renderTargetsEnabled, "renderTargetsEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.spritesEnabled, "spritesEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.skeletonsEnabled, "skeletonsEnabled");
+    QUnit.assert.ok(viewer.Scene.EngineScene.audioEnabled, "audioEnabled");
+});
+
+*/

+ 9 - 0
Viewer/tests/unit/src/viewerReference.ts

@@ -0,0 +1,9 @@
+/// <reference path="../../node_modules/@types/chai/index.d.ts" />
+/// <reference path="../../node_modules/@types/mocha/index.d.ts" />
+/// <reference path="../../node_modules/@types/sinon/index.d.ts" />
+/*
+ * Create a constant with the ChaiJS' expect module just to make the code more readable.
+ */
+export const should = chai.should();
+export const expect = chai.expect;
+export const assert = chai.assert;

+ 38 - 0
Viewer/tests/unit/tsconfig.json

@@ -0,0 +1,38 @@
+{
+    "compilerOptions": {
+        "target": "es5",
+        "module": "commonjs",
+        "noResolve": false,
+        "noImplicitAny": false,
+        "strictNullChecks": true,
+        "removeComments": true,
+        "preserveConstEnums": true,
+        "sourceMap": true,
+        "experimentalDecorators": true,
+        "isolatedModules": false,
+        "declaration": false,
+        "lib": [
+            "dom",
+            "es2015.promise",
+            "es5"
+        ],
+        "types": [
+            "node"
+        ],
+        "baseUrl": "./src/",
+        "paths": {
+            "babylonjs": [
+                "../../../../dist/preview release/babylon.d.ts"
+            ],
+            "babylonjs-loaders": [
+                "../../../src/externalModules.d.ts"
+            ],
+            "babylonjs-gltf2interface": [
+                "../../../../dist/babylon.glTF2Interface.d.ts"
+            ]
+        },
+    },
+    "include": [
+        "./src/**/*.ts"
+    ]
+}

+ 55 - 0
Viewer/tests/unit/webpack.config.js

@@ -0,0 +1,55 @@
+const path = require('path');
+
+module.exports = {
+    entry: {
+        'test': __dirname + '/src/index.ts'
+    },
+    output: {
+        libraryTarget: 'umd',
+        library: 'BabylonViewer',
+        umdNamedDefine: true
+    },
+    resolve: {
+        extensions: ['.ts', '.js'],
+        alias: {
+            "babylonjs": __dirname + '/../../../dist/preview release/babylon.max.js',
+            "babylonjs-materials": __dirname + '/../../../dist/preview release/materialsLibrary/babylonjs.materials.js',
+            "babylonjs-loaders": __dirname + '/../../../dist/preview release/loaders/babylonjs.loaders.js',
+            "pep": __dirname + '/../../assets/pep.min.js'
+        }
+    },
+    externals: {
+        // until physics will be integrated in the viewer, ignore cannon
+        cannon: 'CANNON',
+        oimo: 'OIMO',
+        './Oimo': 'OIMO',
+        "earcut": true
+    },
+    devtool: 'source-map',
+    module: {
+        loaders: [{
+            test: /\.tsx?$/,
+            loader: 'ts-loader',
+            exclude: /node_modules/
+        },
+        {
+            test: /\.(html)$/,
+            use: {
+                loader: 'html-loader',
+                options: {
+                    minimize: true
+                }
+            }
+        },
+        {
+            test: /\.(jpe?g|png|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
+            use: 'base64-image-loader?limit=1000&name=[name].[ext]'
+        }]
+    },
+    devServer: {
+        contentBase: path.join(__dirname, "dist"),
+        compress: false,
+        //open: true,
+        port: 9000
+    }
+}

BIN
Viewer/tests/validation/LogoV3.png


BIN
Viewer/tests/validation/ReferenceImages/BrainStem.png


BIN
Viewer/tests/validation/ReferenceImages/BrainStemTransformation.png


BIN
Viewer/tests/validation/ReferenceImages/Control.png


BIN
Viewer/tests/validation/ReferenceImages/Diffuse.png


+ 92 - 0
Viewer/tests/validation/config.json

@@ -0,0 +1,92 @@
+{
+    "root": "",
+    "tests": [
+        {
+            "title": "Control",
+            "createMesh": true,
+            "configuration": {
+                "extends": "extended, shadowDirectionalLight"
+            },
+            "referenceImage": "Control.png"
+        },
+        {
+            "title": "Diffuse",
+            "createMesh": true,
+            "createMaterial": true,
+            "configuration": {
+                "extends": "extended, shadowDirectionalLight",
+                "castShadow": true,
+                "model": {
+                    "material": {
+                        "albedoColor": {
+                            "r": 0,
+                            "g": 0,
+                            "b": 1
+                        },
+                        "reflectivityColor": {
+                            "r": 0,
+                            "g": 0,
+                            "b": 0
+                        },
+                        "microSurface": 0
+                    }
+                }
+            },
+            "referenceImage": "Diffuse.png"
+        },
+        {
+            "title": "Specular {{glos * 20}} - {{spec * 20}}",
+            "createMesh": true,
+            "createMaterial": true,
+            "repeatVariables": "glos,spec",
+            "repeatTimes": "6,6",
+            "configuration": {
+                "extends": "extended, shadowDirectionalLight",
+                "castShadow": true,
+                "model": {
+                    "material": {
+                        "albedoColor": {
+                            "r": 0,
+                            "g": 0,
+                            "b": 1
+                        },
+                        "reflectivityColor": {
+                            "r": "{{spec / 5}}",
+                            "g": "{{spec / 5}}",
+                            "b": "{{spec / 5}}"
+                        },
+                        "microSurface": "{{glos / 5}}"
+                    }
+                }
+            },
+            "referenceImage": "Specular{{glos * 20}}-{{spec * 20}}.png"
+        },
+        {
+            "title": "BrainStem",
+            "configuration": {
+                "extends": "extended, shadowDirectionalLight"
+            },
+            "model": "/dist/assets/BrainStem/BrainStem.gltf",
+            "referenceImage": "BrainStem.png"
+        },
+        {
+            "title": "BrainStem transformation",
+            "configuration": {
+                "extends": "extended, shadowDirectionalLight",
+                "camera": {
+                    "disableAutoFocus": true
+                },
+                "model": {
+                    "position": {
+                        "x": 0.2
+                    },
+                    "rotation": {
+                        "y": 0.9
+                    }
+                }
+            },
+            "model": "/dist/assets/BrainStem/BrainStem.gltf",
+            "referenceImage": "BrainStemTransformation.png"
+        }
+    ]
+}

+ 88 - 0
Viewer/tests/validation/index.css

@@ -0,0 +1,88 @@
+html,
+body {
+    width: 100%;
+    height: 100%;
+    padding: 0;
+    margin: 0;
+    background-color: white;
+}
+
+body {
+    background: url("LogoV3.png");
+    background-position: center center;
+    background-repeat: no-repeat;
+}
+
+.container {
+    width: calc(100% - 50px);
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: center;
+    align-items: center;
+    background: rgba(255, 255, 255, 0.6);
+    margin: auto;
+    margin-top: 10px;
+    margin-bottom: 10px;
+    outline: 2px solid black;
+    max-width: 1000px;
+}
+
+.containerTitle {
+    width: 100%;
+    order: 1;
+    display: flex;
+    background-color: rgba(0, 0, 0, 0.8);
+    height: 50px;
+}
+
+.title {
+    font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
+    font-size: 40px;
+    color: white;
+    text-align: center;
+    margin-left: 20px;
+    order: 1;
+}
+
+.result {
+    color: green;
+    font-size: 30px;
+    order: 2;
+    text-align: center;
+    margin-top: 5px;
+    margin-left: 20px;
+}
+
+.result.failed {
+    color: red;
+}
+
+.waitRing {
+    order: 3;
+    height: 40px;
+    margin-top: 5px;
+    margin-bottom: 5px;
+}
+
+.waitRing.hidden {
+    display: none;
+}
+
+babylon {
+    position: absolute !important;
+    transform: translateX(-600px);
+    width: 600px;
+    height: 400px;
+}
+
+.renderImage {
+    order: 1;
+    flex-basis: 50%;
+    width: 50%;
+}
+
+.resultImage {
+    width: 50%;
+    flex-basis: 50%;
+    order: 2;
+}

+ 55 - 0
Viewer/tests/validation/index.html

@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+
+    <head>
+        <title>BabylonJS - Build validation page</title>
+        <link href="index.css" rel="stylesheet" />
+        <script src="https://preview.babylonjs.com/draco_decoder.js"></script>
+    </head>
+
+    <body>
+        <script>
+            /*BABYLONDEVTOOLS.Loader.require('validation.js')
+                .load(function() {*/
+            // Loading tests
+            var xhr = new XMLHttpRequest();
+
+            xhr.open("GET", "config.json", true);
+
+            xhr.addEventListener("load", function () {
+                if (xhr.status === 200) {
+
+                    config = JSON.parse(xhr.responseText);
+
+                    // Run tests
+                    var index = 0;
+                    if (window.location.search) {
+                        justOnce = true;
+                        var title = window.location.search.replace("?", "").replace(/%20/g, " ");
+                        for (var index = 0; index < config.tests.length; index++) {
+                            if (config.tests[index].title === title) {
+                                break;
+                            }
+                        }
+                    }
+
+                    var recursiveRunTest = function (i) {
+                        runTest(i, function () {
+                            i++;
+                            if (justOnce || i >= config.tests.length) {
+                                return;
+                            }
+                            recursiveRunTest(i);
+                        });
+                    }
+
+                    recursiveRunTest(index);
+                }
+            }, false);
+
+            xhr.send();
+			//});
+        </script>
+    </body>
+
+</html>

+ 125 - 0
Viewer/tests/validation/integration.js

@@ -0,0 +1,125 @@
+window.__karma__.loaded = function () { };
+
+window.validation = true;
+
+window.onload = function () {
+    // Loading tests
+    var xhr = new XMLHttpRequest();
+
+    xhr.open("GET", "/tests/validation/config.json", true);
+
+    xhr.addEventListener("load", function () {
+        if (xhr.status === 200) {
+
+            config = JSON.parse(xhr.responseText);
+
+            config.tests.forEach(function (test, index) {
+                if (test.repeatVariables) {
+                    let paramsArray = [];
+                    var variables = test.repeatVariables.split(",");
+                    var repeatTimes = test.repeatTimes.split(",").map(s => +s);
+
+                    for (var i = 0; i < repeatTimes[0]; ++i) {
+                        if (repeatTimes[1]) {
+                            for (var j = 0; j < repeatTimes[1]; ++j) {
+                                var obj = {};
+                                obj[variables[0]] = i;
+                                obj[variables[1]] = j;
+                                paramsArray.push(obj);
+                            }
+                        } else {
+                            var obj = {};
+                            obj[variables[0]] = i;
+                            paramsArray.push(obj);
+                        }
+                    }
+                    paramsArray.forEach(function (params) {
+
+                        let newTest = processTest(test, "", params);
+                        delete newTest.repeatVariables;
+                        config.tests.push(newTest);
+                    });
+                }
+            });
+
+            describe("Validation Tests", function () {
+                // Run tests
+                config.tests.forEach(function (test, index) {
+                    if (test.repeatVariables || test.onlyVisual || test.excludeFromAutomaticTesting) {
+                        return;
+                    }
+
+                    it(test.title, function (done) {
+                        this.timeout(60000);
+                        var self = this;
+
+                        var deferredDone = function (err) {
+                            setTimeout(function () {
+                                done(err);
+                            }, 1000);
+                        }
+
+                        try {
+                            runTest(index, function (result, screenshot) {
+                                try {
+                                    expect(result).to.be.true;
+                                    deferredDone();
+                                }
+                                catch (e) {
+                                    if (screenshot) {
+                                        console.error(screenshot);
+                                    }
+                                    deferredDone(e);
+                                }
+                            });
+
+                        }
+                        catch (e) {
+                            deferredDone(e);
+                        }
+                    });
+                });
+            });
+
+            window.__karma__.start();
+        }
+    }, false);
+
+
+    xhr.send();
+}
+
+function processTest(test, key, params) {
+    if (!key) {
+        let testCopy = Object.assign({}, test);
+        Object.keys(testCopy).forEach(testKey => {
+            testCopy[testKey] = processTest(testCopy, testKey, params);
+        });
+        return testCopy;
+    } else {
+        if (typeof test[key] === "object") {
+            let testCopy = Object.assign({}, test[key]);
+            Object.keys(testCopy).forEach(testKey => {
+                testCopy[testKey] = processTest(testCopy, testKey, params);
+            });
+            return testCopy;
+        } else if (typeof test[key] === "string") {
+            let evals = test[key].match(/{{\s*([^{}]+)\s*}}/g);
+            if (evals) {
+                let clean = evals.map(function (x) { var s = x.replace(/}/g, "").replace(/{/g, ""); return s; });
+                evals.forEach((ev, idx) => {
+                    var valuated = clean[idx];
+                    Object.keys(params).forEach(p => {
+                        valuated = valuated.replace(p, "" + params[p]);
+                    });
+                    valuated = eval(valuated);
+                    test[key] = test[key].replace(ev, valuated);
+                });
+                test[key] = parseFloat(test[key]) || test[key];
+            }
+
+            return test[key];
+        }
+        else return test[key];
+    }
+}

+ 94 - 0
Viewer/tests/validation/karma.conf.browserstack.js

@@ -0,0 +1,94 @@
+module.exports = function (config) {
+    'use strict';
+    config.set({
+
+        basePath: '../../',
+        captureTimeout: 3e5,
+        browserNoActivityTimeout: 3e5,
+        browserDisconnectTimeout: 3e5,
+        browserDisconnectTolerance: 3,
+        concurrency: 1,
+
+        urlRoot: '/karma',
+
+        frameworks: ['mocha', 'chai', 'sinon'],
+
+        files: [
+            './dist/preview release/earcut.min.js',
+            './Tools/DevLoader/BabylonLoader.js',
+            './tests/validation/index.css',
+            './tests/validation/integration.js',
+            './favicon.ico',
+            { pattern: 'dist/**/*', watched: false, included: false, served: true },
+            { pattern: 'assets/**/*', watched: false, included: false, served: true },
+            { pattern: 'tests/**/*', watched: false, included: false, served: true },
+            { pattern: 'Playground/scenes/**/*', watched: false, included: false, served: true },
+            { pattern: 'Playground/textures/**/*', watched: false, included: false, served: true },
+            { pattern: 'Playground/sounds/**/*', watched: false, included: false, served: true },
+            { pattern: 'Tools/DevLoader/**/*', watched: false, included: false, served: true },            
+            { pattern: 'Tools/Gulp/config.json', watched: false, included: false, served: true },
+        ],
+        proxies: {
+            '/': '/base/'
+        },
+
+        port: 1338,
+        colors: true,
+        autoWatch: false,
+        singleRun: false,
+
+        // level of logging
+        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+        logLevel: config.LOG_INFO,
+
+        browserStack: {
+            project: 'Babylon JS Validation Tests',
+            video: false,
+            debug : 'true',
+            timeout: 1200,
+            build: process.env.TRAVIS_BUILD_NUMBER,
+            username: process.env.BROWSER_STACK_USERNAME,
+            accessKey: process.env.BROWSER_STACK_ACCESS_KEY
+        },
+        customLaunchers: {
+            bs_chrome_win: {
+                base: 'BrowserStack',
+                browser: 'Chrome',
+                browser_version: '63.0',
+                os: 'Windows',
+                os_version: '10'
+            },
+            bs_edge_win: {
+                base: 'BrowserStack',
+                browser: 'Edge',
+                browser_version: '16.0',
+                os: 'Windows',
+                os_version: '10'
+            },
+            bs_firefox_win: {
+                base: 'BrowserStack',
+                browser: 'Firefox',
+                browser_version: '57.0',
+                os: 'Windows',
+                os_version: '10'
+            },
+            bs_chrome_android: {
+                base: 'BrowserStack',
+                os: 'Android',
+                os_version : '8.0',
+                device : 'Google Pixel',
+                real_mobile : 'true'
+            },
+            bs_safari_ios: {
+                base: 'BrowserStack',
+                os: 'ios',
+                os_version : '10.3',
+                device : 'iPhone 7',
+                real_mobile : 'true'
+            }
+        },
+        browsers: ['bs_chrome_android'],
+        reporters: ['dots', 'BrowserStack'],
+        singleRun: true
+    });
+};

+ 40 - 0
Viewer/tests/validation/karma.conf.js

@@ -0,0 +1,40 @@
+module.exports = function (config) {
+    'use strict';
+    config.set({
+        basePath: '../../',
+        captureTimeout: 3e5,
+        browserNoActivityTimeout: 3e5,
+        browserDisconnectTimeout: 3e5,
+        browserDisconnectTolerance: 3,
+        concurrency: 1,
+
+        urlRoot: '/karma/',
+
+        frameworks: ['mocha', 'chai', 'sinon'],
+
+        files: [
+            './tests/validation/index.css',
+            './tests/validation/integration.js',
+            './tests/build/test.js',
+            './tests/validation/validation.js',
+            { pattern: './tests/**/*', watched: false, included: false, served: true },
+            { pattern: './dist/assets/**/*', watched: false, included: false, served: true },
+        ],
+        proxies: {
+            '/tests/': '/base/tests/',
+            '/dist/assets/': '/base//dist/assets/'
+        },
+
+        port: 3000,
+        colors: true,
+        autoWatch: false,
+        singleRun: false,
+
+        // level of logging
+        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+        logLevel: config.LOG_INFO,
+
+        browsers: ['Chrome']
+
+    });
+};

BIN
Viewer/tests/validation/loading.gif


+ 60 - 0
Viewer/tests/validation/validate.html

@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>BabylonJS - Build validation page</title>
+	<link href="index.css" rel="stylesheet" />
+    <script src="https://preview.babylonjs.com/draco_decoder.js"></script>
+    <script src="https://preview.babylonjs.com/cannon.js"></script>
+    <script src="https://preview.babylonjs.com/Oimo.js"></script>
+    <script src="https://preview.babylonjs.com/babylon.js"></script>
+    <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>
+
+    <script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
+	<script src="https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script>
+    <script src="https://preview.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>
+	<script src="https://preview.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script>
+    <script src="https://preview.babylonjs.com/gui/babylon.gui.min.js"></script>	
+</head>
+<body>
+	<script src="validation.js"></script>
+	<script>
+		// Loading tests
+		var xhr = new XMLHttpRequest();
+
+		xhr.open("GET", "config.json", true);
+
+		xhr.addEventListener("load", function () {
+			if (xhr.status === 200) {
+
+				config = JSON.parse(xhr.responseText);
+
+				// Run tests
+				var index = 0;
+				if (window.location.search) {
+					justOnce = true;
+					var title = window.location.search.replace("?", "").replace(/%20/g, " ");
+					for (var index = 0; index < config.tests.length; index++) {
+						if (config.tests[index].title === title) {
+							break;
+						}
+					}
+				}
+
+				var recursiveRunTest = function(i) {
+					runTest(i, function() {
+						i++;
+						if (justOnce || i >= config.tests.length) {
+							return;
+						}
+						recursiveRunTest(i);
+					});
+				}
+
+				recursiveRunTest(index);
+			}
+		}, false);
+
+		xhr.send();
+    </script>	
+</body>
+</html>

+ 288 - 0
Viewer/tests/validation/validation.js

@@ -0,0 +1,288 @@
+"use strict";
+
+var currentViewer;
+var viewerElement;
+var currentScene;
+var config;
+var justOnce;
+
+var threshold = 25;
+var errorRatio = 1.5;
+
+// Overload the random to make it deterministic
+var seed = 100000,
+    constant = Math.pow(2, 13) + 1,
+    prime = 37,
+    maximum = Math.pow(2, 50);
+
+Math.random = function () {
+    seed *= constant;
+    seed += prime;
+    seed %= maximum;
+
+    return seed / maximum;
+}
+
+function compare(renderData, referenceCanvas) {
+    var width = referenceCanvas.width;
+    var height = referenceCanvas.height;
+    var size = width * height * 4;
+
+    var referenceContext = referenceCanvas.getContext("2d");
+
+    var referenceData = referenceContext.getImageData(0, 0, width, height);
+
+    var differencesCount = 0;
+    for (var index = 0; index < size; index += 4) {
+        if (Math.abs(renderData[index] - referenceData.data[index]) < threshold &&
+            Math.abs(renderData[index + 1] - referenceData.data[index + 1]) < threshold &&
+            Math.abs(renderData[index + 2] - referenceData.data[index + 2]) < threshold) {
+            continue;
+        }
+
+        referenceData.data[index] = 255;
+        referenceData.data[index + 1] *= 0.5;
+        referenceData.data[index + 2] *= 0.5;
+        differencesCount++;
+    }
+
+    referenceContext.putImageData(referenceData, 0, 0);
+
+    return (differencesCount * 100) / (width * height) > errorRatio;
+}
+
+function getRenderData(canvas, engine) {
+    var width = canvas.width;
+    var height = canvas.height;
+
+    var renderData = engine.readPixels(0, 0, width, height);
+    var numberOfChannelsByLine = width * 4;
+    var halfHeight = height / 2;
+
+    for (var i = 0; i < halfHeight; i++) {
+        for (var j = 0; j < numberOfChannelsByLine; j++) {
+            var currentCell = j + i * numberOfChannelsByLine;
+            var targetLine = height - i - 1;
+            var targetCell = j + targetLine * numberOfChannelsByLine;
+
+            var temp = renderData[currentCell];
+            renderData[currentCell] = renderData[targetCell];
+            renderData[targetCell] = temp;
+        }
+    }
+
+    return renderData;
+}
+
+function saveRenderImage(data, canvas) {
+    var width = canvas.width;
+    var height = canvas.height;
+    var screenshotCanvas = document.createElement('canvas');
+    screenshotCanvas.width = width;
+    screenshotCanvas.height = height;
+    var context = screenshotCanvas.getContext('2d');
+
+    var imageData = context.createImageData(width, height);
+    var castData = imageData.data;
+    castData.set(data);
+    context.putImageData(imageData, 0, 0);
+
+    return screenshotCanvas.toDataURL();
+}
+
+function evaluate(test, resultCanvas, result, renderImage, index, waitRing, done) {
+    seed = 100000;
+    var renderData = getRenderData(currentViewer.canvas, currentViewer.engine);
+    var testRes = true;
+
+    // gl check
+    var gl = currentViewer.engine._gl;
+    if (gl.getError() !== 0) {
+        result.classList.add("failed");
+        result.innerHTML = "×";
+        testRes = false;
+        console.log('%c failed (gl error)', 'color: red');
+    } else {
+
+        // Visual check
+        if (!test.onlyVisual) {
+            if (compare(renderData, resultCanvas)) {
+                result.classList.add("failed");
+                result.innerHTML = "×";
+                testRes = false;
+                console.log('%c failed', 'color: red');
+            } else {
+                result.innerHTML = "✔";
+                testRes = true;
+                console.log('%c validated', 'color: green');
+            }
+        }
+    }
+    waitRing.classList.add("hidden");
+
+    var renderB64 = saveRenderImage(renderData, currentViewer.canvas);
+    renderImage.src = renderB64;
+
+    done(testRes, renderB64);
+}
+
+function runTest(index, done) {
+    if (index >= config.tests.length) {
+        done(false);
+    }
+
+
+    var test = Object.assign({}, config.tests[index]);
+
+    //process params
+    /*if (params) {
+        processTest(test, "", params);
+    }*/
+
+    var container = document.createElement("div");
+    container.id = "container#" + index;
+    container.className = "container";
+    document.body.appendChild(container);
+
+    var titleContainer = document.createElement("div");
+    titleContainer.className = "containerTitle";
+    container.appendChild(titleContainer);
+
+    var title = document.createElement("div");
+    title.className = "title";
+    titleContainer.appendChild(title);
+
+    var result = document.createElement("div");
+    result.className = "result";
+    titleContainer.appendChild(result);
+
+    var waitRing = document.createElement("img");
+    waitRing.className = "waitRing";
+    titleContainer.appendChild(waitRing);
+    waitRing.src = "/tests/validation/loading.gif";
+
+    var resultCanvas = document.createElement("canvas");
+    resultCanvas.className = "resultImage";
+    container.appendChild(resultCanvas);
+
+    title.innerHTML = test.title;
+
+    console.log("Running " + test.title);
+
+    var resultContext = resultCanvas.getContext("2d");
+    var img = new Image();
+    img.onload = function () {
+        resultCanvas.width = img.width;
+        resultCanvas.height = img.height;
+        resultContext.drawImage(img, 0, 0);
+    }
+
+    img.src = "/tests/validation/ReferenceImages/" + test.referenceImage;
+
+    var renderImage = new Image();
+    renderImage.className = "renderImage";
+    container.appendChild(renderImage);
+
+    location.href = "#" + container.id;
+
+    //run a single test
+    var configuration = test.configuration || {};
+
+    configuration.engine = configuration.engine || {};
+    configuration.engine.engineOptions = configuration.engine.engineOptions || {};
+    configuration.engine.engineOptions.preserveDrawingBuffer = true;
+
+    //cancel camera behvaviors for the tests
+    configuration.camera = configuration.camera || {};
+    configuration.camera.behaviors = null;
+
+    // make sure we use only local assets
+    configuration.skybox = {
+        cubeTexture: {
+            url: "/dist/assets/environment/DefaultSkybox_cube_radiance_256.dds"
+        }
+    }
+
+    //envirnonment directory
+    configuration.lab = configuration.lab || {};
+    configuration.lab.environmentAssetsRootURL = "/dist/assets/environment/";
+    configuration.lab.environmentMap = false;
+
+    //model config
+    configuration.model = configuration.model || {};
+    configuration.model.castShadow = !test.createMesh
+
+    // create a new viewer
+    currentViewer && currentViewer.dispose();
+    currentViewer = null;
+    currentScene = null;
+    viewerElement.innerHTML = '';
+    currentViewer = new BabylonViewer.DefaultViewer(viewerElement, configuration);
+
+    currentViewer.onInitDoneObservable.add(() => {
+
+        var currentFrame = 0;
+        var waitForFrame = test.waitForFrame || 4;
+
+        if (test.model) {
+            currentViewer.initModel(test.model);
+        } else if (test.createMesh) {
+            prepareMeshForViewer(currentViewer, configuration, test);
+        }
+
+        currentViewer.onModelLoadedObservable.add((model) => {
+            currentViewer.onFrameRenderedObservable.add(() => {
+                if (test.animationTest && !currentFrame) {
+                    model.playAnimation(model.getAnimationNames()[0]);
+                }
+                if (currentFrame === waitForFrame) {
+                    currentViewer.sceneManager.scene.executeWhenReady(() => {
+                        evaluate(test, resultCanvas, result, renderImage, index, waitRing, done);
+                    });
+                }
+                currentFrame++;
+            });
+
+        })
+    });
+}
+
+function prepareMeshForViewer(viewer, configuration, test) {
+    let meshModel = new BabylonViewer.ViewerModel(viewer, configuration.model || {});
+
+    let sphereMesh = BABYLON.Mesh.CreateSphere('sphere-' + test.title, 20, 1.0, viewer.sceneManager.scene);
+    if (test.createMaterial) {
+        let material = new BABYLON.PBRMaterial("sphereMat", viewer.sceneManager.scene);
+        material.environmentBRDFTexture = null;
+        material.useAlphaFresnel = material.needAlphaBlending();
+        material.backFaceCulling = material.forceDepthWrite;
+        material.twoSidedLighting = true;
+        material.useSpecularOverAlpha = false;
+        material.useRadianceOverAlpha = false;
+        material.usePhysicalLightFalloff = true;
+        material.forceNormalForward = true;
+        sphereMesh.material = material;
+    }
+    meshModel.addMesh(sphereMesh, true);
+}
+
+function init() {
+    BABYLON.SceneLoader.ShowLoadingScreen = false;
+    BABYLON.SceneLoader.ForceFullSceneLoadingForIncremental = true;
+
+    // Draco configuration
+    BABYLON.DracoCompression.Configuration.decoder = {
+        wasmUrl: "../../dist/preview%20release/draco_wasm_wrapper_gltf.js",
+        wasmBinaryUrl: "../../dist/preview%20release/draco_decoder_gltf.wasm",
+        fallbackUrl: "../../dist/preview%20release/draco_decoder_gltf.js"
+    };
+
+    viewerElement = document.createElement("babylon");
+    document.body.appendChild(viewerElement);
+
+    // disable init
+    BabylonViewer.viewerGlobals.disableInit = true;
+}
+
+init();
+

+ 3 - 2
Viewer/tsconfig-gulp.json

@@ -25,12 +25,13 @@
                 "../dist/preview release/babylon.d.ts"
             ],
             "babylonjs-loaders": [
-                "../dist/preview release/loaders/babylonjs.loaders.d.ts"
+                "externalModules.d.ts"
             ]
         }
     },
     "exclude": [
         "node_modules",
-        "dist"
+        "dist",
+        "tests"
     ]
 }

+ 1 - 1
Viewer/tsconfig.json

@@ -25,7 +25,7 @@
                 "../../dist/preview release/babylon.d.ts"
             ],
             "babylonjs-loaders": [
-                "../../dist/preview release/loaders/babylonjs.loaders.d.ts"
+                "externalModules.d.ts"
             ],
             "babylonjs-gltf2interface": [
                 "../../dist/babylon.glTF2Interface.d.ts"

Файловите разлики са ограничени, защото са твърде много
+ 2100 - 0
dist/preview release/viewer/babylon.viewer.d.ts


Файловите разлики са ограничени, защото са твърде много
+ 11 - 10
dist/preview release/viewer/babylon.viewer.js


Файловите разлики са ограничени, защото са твърде много
+ 5303 - 3467
dist/preview release/viewer/babylon.viewer.max.js


Файловите разлики са ограничени, защото са твърде много
+ 1163 - 404
dist/preview release/viewer/babylon.viewer.module.d.ts


+ 2 - 0
dist/preview release/viewer/package.json

@@ -13,6 +13,8 @@
     "files": [
         "babylon.viewer.js",
         "babylon.viewer.module.d.ts",
+        "babylon.glTF2Interface.d.ts",
+        "babylonjs.loaders.d.ts",
         "babylon.d.ts",
         "readme.md",
         "package.json"

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

@@ -119,6 +119,7 @@
 - Add support for sparse accessors to glTF 2.0 loader. ([bghgary](https://github.com/bghgary))
 - Add support for cameras to glTF 2.0 loader. ([bghgary](https://github.com/bghgary)]
 - Add support for preprocessing urls to glTF 2.0 loader. ([bghgary](https://github.com/bghgary)]
+- Viewer upgrade - see [PR](https://github.com/BabylonJS/Babylon.js/pull/4160) ([RaananW](https://github.com/RaananW))
 
 ## Bug fixes
 

+ 18 - 18
src/Mesh/babylon.mesh.ts

@@ -1558,7 +1558,7 @@
          * Modifies the mesh geometry according to the passed transformation matrix.  
          * This method returns nothing but it really modifies the mesh even if it's originally not set as updatable. 
          * The mesh normals are modified accordingly the same transformation.  
-         * tuto : http://doc.babylonjs.com/tutorials/How_Rotations_and_Translations_Work#baking-transform  
+         * tuto : http://doc.babylonjs.com/resources/baking_transformations  
          * Note that, under the hood, this method sets a new VertexBuffer each call.  
          * Returns the Mesh.  
          */
@@ -2565,7 +2565,7 @@
          * It's the offset to join together the points from the same path. Ex : offset = 10 means the point 1 is joined to the point 11.    
          * The optional parameter `instance` is an instance of an existing Ribbon object to be updated with the passed `pathArray` parameter : http://doc.babylonjs.com/tutorials/How_to_dynamically_morph_a_mesh#ribbon   
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static CreateRibbon(name: string, pathArray: Vector3[][], closeArray: boolean = false, closePath: boolean, offset: number, scene?: Scene, updatable: boolean = false, sideOrientation?: number, instance?: Mesh): Mesh {
@@ -2585,7 +2585,7 @@
          * The parameter `radius` sets the radius size (float) of the polygon (default 0.5).  
          * The parameter `tessellation` sets the number of polygon sides (positive integer, default 64). So a tessellation valued to 3 will build a triangle, to 4 a square, etc.  
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static CreateDisc(name: string, radius: number, tessellation: number, scene: Nullable<Scene> = null, updatable?: boolean, sideOrientation?: number): Mesh {
@@ -2603,7 +2603,7 @@
          * Please consider using the same method from the MeshBuilder class instead.   
          * The parameter `size` sets the size (float) of each box side (default 1).  
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static CreateBox(name: string, size: number, scene: Nullable<Scene> = null, updatable?: boolean, sideOrientation?: number): Mesh {
@@ -2621,7 +2621,7 @@
          * The parameter `diameter` sets the diameter size (float) of the sphere (default 1).  
          * The parameter `segments` sets the sphere number of horizontal stripes (positive integer, default 32).  
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static CreateSphere(name: string, segments: number, diameter: number, scene?: Scene, updatable?: boolean, sideOrientation?: number): Mesh {
@@ -2646,7 +2646,7 @@
          * The parameter `tessellation` sets the number of cylinder sides (positive integer, default 24). Set it to 3 to get a prism for instance.
          * The parameter `subdivisions` sets the number of rings along the cylinder height (positive integer, default 1).   
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static CreateCylinder(name: string, height: number, diameterTop: number, diameterBottom: number, tessellation: number, subdivisions: any, scene?: Scene, updatable?: any, sideOrientation?: number): Mesh {
@@ -2680,7 +2680,7 @@
          * The parameter `thickness` sets the diameter size of the tube of the torus (float, default 0.5).  
          * The parameter `tessellation` sets the number of torus sides (postive integer, default 16).  
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static CreateTorus(name: string, diameter: number, thickness: number, tessellation: number, scene?: Scene, updatable?: boolean, sideOrientation?: number): Mesh {
@@ -2702,7 +2702,7 @@
          * The parameter `tubularSegments` sets the number of tubes to decompose the knot into (positive integer, default 32).  
          * The parameters `p` and `q` are the number of windings on each axis (positive integers, default 2 and 3).    
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static CreateTorusKnot(name: string, radius: number, tube: number, radialSegments: number, tubularSegments: number, p: number, q: number, scene?: Scene, updatable?: boolean, sideOrientation?: number): Mesh {
@@ -2803,7 +2803,7 @@
          * The extrusion is a parametric shape :  http://doc.babylonjs.com/tutorials/Parametric_Shapes.  It has no predefined shape. Its final shape will depend on the input parameters.  
          * Please consider using the same method from the MeshBuilder class instead.    
          *
-         * Please read this full tutorial to understand how to design an extruded shape : http://doc.babylonjs.com/tutorials/Parametric_Shapes#extrusion     
+         * Please read this full tutorial to understand how to design an extruded shape : http://doc.babylonjs.com/how_to/parametric_shapes#extruded-shapes     
          * The parameter `shape` is a required array of successive Vector3. This array depicts the shape to be extruded in its local space : the shape must be designed in the xOy plane and will be
          * extruded along the Z axis.    
          * The parameter `path` is a required array of successive Vector3. This is the axis curve the shape is extruded along.      
@@ -2813,7 +2813,7 @@
          * The optional parameter `instance` is an instance of an existing ExtrudedShape object to be updated with the passed `shape`, `path`, `scale` or `rotation` parameters : http://doc.babylonjs.com/tutorials/How_to_dynamically_morph_a_mesh#extruded-shape  
          * Remember you can only change the shape or path point positions, not their number when updating an extruded shape.       
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static ExtrudeShape(name: string, shape: Vector3[], path: Vector3[], scale: number, rotation: number, cap: number, scene: Nullable<Scene> = null, updatable?: boolean, sideOrientation?: number, instance?: Mesh): Mesh {
@@ -2835,7 +2835,7 @@
          * The custom extrusion is a parametric shape :  http://doc.babylonjs.com/tutorials/Parametric_Shapes.  It has no predefined shape. Its final shape will depend on the input parameters.  
          * Please consider using the same method from the MeshBuilder class instead.    
          *
-         * Please read this full tutorial to understand how to design a custom extruded shape : http://doc.babylonjs.com/tutorials/Parametric_Shapes#extrusion     
+         * Please read this full tutorial to understand how to design a custom extruded shape : http://doc.babylonjs.com/how_to/parametric_shapes#extruded-shapes     
          * The parameter `shape` is a required array of successive Vector3. This array depicts the shape to be extruded in its local space : the shape must be designed in the xOy plane and will be
          * extruded along the Z axis.    
          * The parameter `path` is a required array of successive Vector3. This is the axis curve the shape is extruded along.      
@@ -2861,7 +2861,7 @@
          * The optional parameter `instance` is an instance of an existing ExtrudedShape object to be updated with the passed `shape`, `path`, `scale` or `rotation` parameters : http://doc.babylonjs.com/tutorials/How_to_dynamically_morph_a_mesh#extruded-shape  
          * Remember you can only change the shape or path point positions, not their number when updating an extruded shape.       
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static ExtrudeShapeCustom(name: string, shape: Vector3[], path: Vector3[], scaleFunction: Function, rotationFunction: Function, ribbonCloseArray: boolean, ribbonClosePath: boolean, cap: number, scene: Scene, updatable?: boolean, sideOrientation?: number, instance?: Mesh): Mesh {
@@ -2890,7 +2890,7 @@
          * The parameter `radius` (positive float, default 1) is the radius value of the lathe.        
          * The parameter `tessellation` (positive integer, default 64) is the side number of the lathe.      
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static CreateLathe(name: string, shape: Vector3[], radius: number, tessellation: number, scene: Scene, updatable?: boolean, sideOrientation?: number): Mesh {
@@ -2910,7 +2910,7 @@
          * Please consider using the same method from the MeshBuilder class instead.    
          * The parameter `size` sets the size (float) of both sides of the plane at once (default 1).  
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static CreatePlane(name: string, size: number, scene: Scene, updatable?: boolean, sideOrientation?: number): Mesh {
@@ -2967,7 +2967,7 @@
         }
         /**
          * Creates a ground mesh from a height map.    
-         * tuto : http://doc.babylonjs.com/tutorials/14._Height_Map   
+         * tuto : http://doc.babylonjs.com/babylon101/height_map   
          * Please consider using the same method from the MeshBuilder class instead.    
          * The parameter `url` sets the URL of the height map image resource.  
          * The parameters `width` and `height` (positive floats, default 10) set the ground width and height sizes.     
@@ -3013,7 +3013,7 @@
          * The parameter `cap` sets the way the extruded shape is capped. Possible values : BABYLON.Mesh.NO_CAP (default), BABYLON.Mesh.CAP_START, BABYLON.Mesh.CAP_END, BABYLON.Mesh.CAP_ALL         
          * The optional parameter `instance` is an instance of an existing Tube object to be updated with the passed `pathArray` parameter : http://doc.babylonjs.com/tutorials/How_to_dynamically_morph_a_mesh#tube    
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static CreateTube(name: string, path: Vector3[], radius: number, tessellation: number, radiusFunction: { (i: number, distance: number): number; }, cap: number, scene: Scene, updatable?: boolean, sideOrientation?: number, instance?: Mesh): Mesh {
@@ -3043,7 +3043,7 @@
          * To understand how to set `faceUV` or `faceColors`, please read this by considering the right number of faces of your polyhedron, instead of only 6 for the box : http://doc.babylonjs.com/tutorials/CreateBox_Per_Face_Textures_And_Colors  
          * The parameter `flat` (boolean, default true). If set to false, it gives the polyhedron a single global face, so less vertices and shared normals. In this case, `faceColors` and `faceUV` are ignored.    
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.   
          */
         public static CreatePolyhedron(name: string, options: { type?: number, size?: number, sizeX?: number, sizeY?: number, sizeZ?: number, custom?: any, faceUV?: Vector4[], faceColors?: Color4[], updatable?: boolean, sideOrientation?: number }, scene: Scene): Mesh {
@@ -3057,7 +3057,7 @@
          * The parameter `subdivisions` sets the number of subdivisions (postive integer, default 4). The more subdivisions, the more faces on the icosphere whatever its size.    
          * The parameter `flat` (boolean, default true) gives each side its own normals. Set it to false to get a smooth continuous light reflection on the surface.  
          * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          */
         public static CreateIcoSphere(name: string, options: { radius?: number, flat?: boolean, subdivisions?: number, sideOrientation?: number, updatable?: boolean }, scene: Scene): Mesh {

+ 27 - 27
src/Mesh/babylon.meshBuilder.ts

@@ -22,7 +22,7 @@
          * * You can set different colors and different images to each box side by using the parameters `faceColors` (an array of 6 Color3 elements) and `faceUV` (an array of 6 Vector4 elements)
          * * Please read this tutorial : http://doc.babylonjs.com/tutorials/CreateBox_Per_Face_Textures_And_Colors  
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
          * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#box  
          * @param name defines the name of the mesh
@@ -51,7 +51,7 @@
          * * You can create an unclosed sphere with the parameter `arc` (positive float, default 1), valued between 0 and 1, what is the ratio of the circumference (latitude) : 2 x PI x ratio  
          * * You can create an unclosed sphere on its height with the parameter `slice` (positive float, default1), valued between 0 and 1, what is the height ratio (longitude)
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation  
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation  
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
          * @param name defines the name of the mesh
          * @param options defines the options used to create the mesh
@@ -78,13 +78,13 @@
          * * The parameter `tessellation` sets the number of polygon sides (positive integer, default 64). So a tessellation valued to 3 will build a triangle, to 4 a square, etc
          * * You can create an unclosed polygon with the parameter `arc` (positive float, default 1), valued between 0 and 1, what is the ratio of the circumference : 2 x PI x ratio  
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
          * @param name defines the name of the mesh
          * @param options defines the options used to create the mesh
          * @param scene defines the hosting scene
          * @returns the plane polygonal mesh
-         * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#disc 
+         * @see http://doc.babylonjs.com/how_to/set_shapes#disc-or-regular-polygon 
          */
         public static CreateDisc(name: string, options: { radius?: number, tessellation?: number, arc?: number, updatable?: boolean, sideOrientation?: number, frontUVs?: Vector4, backUVs?: Vector4 }, scene: Nullable<Scene> = null): Mesh {
             var disc = new Mesh(name, scene);
@@ -106,13 +106,13 @@
          * * The parameter `subdivisions` sets the number of subdivisions (postive integer, default 4). The more subdivisions, the more faces on the icosphere whatever its size
          * * The parameter `flat` (boolean, default true) gives each side its own normals. Set it to false to get a smooth continuous light reflection on the surface
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
          * @param name defines the name of the mesh
          * @param options defines the options used to create the mesh
          * @param scene defines the hosting scene
          * @returns the icosahedron mesh
-         * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#icosphere   
+         * @see http://doc.babylonjs.com/how_to/polyhedra_shapes#icosphere   
          */
         public static CreateIcoSphere(name: string, options: { radius?: number, radiusX?: number, radiusY?: number, radiusZ?: number, flat?: boolean, subdivisions?: number, sideOrientation?: number, frontUVs?: Vector4, backUVs?: Vector4, updatable?: boolean }, scene: Scene): Mesh {
             var sphere = new Mesh(name, scene);
@@ -136,7 +136,7 @@
          * * It's the offset to join the points from the same path. Ex : offset = 10 means the point 1 is joined to the point 11
          * * The optional parameter `instance` is an instance of an existing Ribbon object to be updated with the passed `pathArray` parameter : http://doc.babylonjs.com/tutorials/How_to_dynamically_morph_a_mesh#ribbon   
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * * The optional parameter `invertUV` (boolean, default false) swaps in the geometry the U and V coordinates to apply a texture
          * * The parameter `uvs` is an optional flat array of `Vector2` to update/set each ribbon vertex with its own custom UV values instead of the computed ones
          * * The parameters `colors` is an optional flat array of `Color4` to set/update each ribbon vertex with its own custom color values
@@ -299,7 +299,7 @@
          * * If `enclose` is true, a ring surface is 3 successive elements in the array : the tubular surface, then the two closing faces.    
          * * Example how to set colors and textures on a sliced cylinder : http://www.html5gamedevs.com/topic/17945-creating-a-closed-slice-of-a-cylinder/#comment-106379  
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          * @param name defines the name of the mesh
          * @param options defines the options used to create the mesh
@@ -326,7 +326,7 @@
          * * The parameter `thickness` sets the diameter size of the tube of the torus (float, default 0.5)
          * * The parameter `tessellation` sets the number of torus sides (postive integer, default 16)
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          * @param name defines the name of the mesh
          * @param options defines the options used to create the mesh
@@ -354,7 +354,7 @@
          * * The parameter `tubularSegments` sets the number of tubes to decompose the knot into (positive integer, default 32)
          * * The parameters `p` and `q` are the number of windings on each axis (positive integers, default 2 and 3)
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          * @param name defines the name of the mesh
          * @param options defines the options used to create the mesh
@@ -386,7 +386,7 @@
          * * Updating a simple Line mesh, you just need to update every line in the `lines` array : http://doc.babylonjs.com/tutorials/How_to_dynamically_morph_a_mesh#lines-and-dashedlines
          * * When updating an instance, remember that only line point positions can change, not the number of points, neither the number of lines
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
-         * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#linesystem  
+         * @see http://doc.babylonjs.com/how_to/parametric_shapes#line-system  
          * @param name defines the name of the new line system
          * @param options defines the options used to create the line system
          * @param scene defines the hosting scene
@@ -448,7 +448,7 @@
          * * The optional parameter `useVertexAlpha` is to be set to `false` (default `true`) when you don't need alpha blending (faster)
          * * When updating an instance, remember that only point positions can change, not the number of points
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
-         * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#lines
+         * @see http://doc.babylonjs.com/how_to/parametric_shapes#lines
          * @param name defines the name of the new line system
          * @param options defines the options used to create the line system
          * @param scene defines the hosting scene
@@ -475,7 +475,7 @@
          * @param options defines the options used to create the mesh
          * @param scene defines the hosting scene
          * @returns the dashed line mesh
-         * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#dashed-lines
+         * @see http://doc.babylonjs.com/how_to/parametric_shapes#dashed-lines
          */
         public static CreateDashedLines(name: string, options: { points: Vector3[], dashSize?: number, gapSize?: number, dashNb?: number, updatable?: boolean, instance?: LinesMesh }, scene: Nullable<Scene> = null): LinesMesh {
             var points = options.points;
@@ -547,7 +547,7 @@
          * * The optional parameter `instance` is an instance of an existing ExtrudedShape object to be updated with the passed `shape`, `path`, `scale` or `rotation` parameters : http://doc.babylonjs.com/tutorials/How_to_dynamically_morph_a_mesh#extruded-shape  
          * * Remember you can only change the shape or path point positions, not their number when updating an extruded shape.       
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * * The optional parameter `invertUV` (boolean, default false) swaps in the geometry the U and V coordinates to apply a texture.  
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.  
          * @param name defines the name of the mesh
@@ -555,8 +555,8 @@
          * @param scene defines the hosting scene
          * @returns the extruded shape mesh
          * @see http://doc.babylonjs.com/tutorials/Parametric_Shapes
-         * @see http://doc.babylonjs.com/tutorials/Parametric_Shapes#extrusion     
-         * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#extruded-shapes
+         * @see http://doc.babylonjs.com/how_to/parametric_shapes#extruded-shapes     
+         * @see http://doc.babylonjs.com/how_to/parametric_shapes#extruded-shapes
          */
         public static ExtrudeShape(name: string, options: { shape: Vector3[], path: Vector3[], scale?: number, rotation?: number, cap?: number, updatable?: boolean, sideOrientation?: number, frontUVs?: Vector4, backUVs?: Vector4, instance?: Mesh, invertUV?: boolean }, scene: Nullable<Scene> = null): Mesh {
             var path = options.path;
@@ -587,16 +587,16 @@
          * * The optional parameter `instance` is an instance of an existing ExtrudedShape object to be updated with the passed `shape`, `path`, `scale` or `rotation` parameters : http://doc.babylonjs.com/tutorials/How_to_dynamically_morph_a_mesh#extruded-shape  
          * * Remember you can only change the shape or path point positions, not their number when updating an extruded shape
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation   
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation   
          * * The optional parameter `invertUV` (boolean, default false) swaps in the geometry the U and V coordinates to apply a texture
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
          * @param name defines the name of the mesh
          * @param options defines the options used to create the mesh
          * @param scene defines the hosting scene
          * @returns the custom extruded shape mesh
-         * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#custom-extruded-shapes
+         * @see http://doc.babylonjs.com/how_to/parametric_shapes#custom-extruded-shapes
          * @see http://doc.babylonjs.com/tutorials/Parametric_Shapes
-         * @see http://doc.babylonjs.com/tutorials/Parametric_Shapes#extrusion
+         * @see http://doc.babylonjs.com/how_to/parametric_shapes#extruded-shapes
          */
         public static ExtrudeShapeCustom(name: string, options: { shape: Vector3[], path: Vector3[], scaleFunction?: any, rotationFunction?: any, ribbonCloseArray?: boolean, ribbonClosePath?: boolean, cap?: number, updatable?: boolean, sideOrientation?: number, frontUVs?: Vector4, backUVs?: Vector4, instance?: Mesh, invertUV?: boolean }, scene: Scene): Mesh {
             var path = options.path;
@@ -623,14 +623,14 @@
          * * The parameter `closed` (boolean, default true) opens/closes the lathe circumference. This should be set to false when used with the parameter "arc"
          * * The parameter `cap` sets the way the extruded shape is capped. Possible values : BABYLON.Mesh.NO_CAP (default), BABYLON.Mesh.CAP_START, BABYLON.Mesh.CAP_END, BABYLON.Mesh.CAP_ALL          
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * * The optional parameter `invertUV` (boolean, default false) swaps in the geometry the U and V coordinates to apply a texture
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
          * @param name defines the name of the mesh
          * @param options defines the options used to create the mesh
          * @param scene defines the hosting scene
          * @returns the lathe mesh
-         * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#lathe 
+         * @see http://doc.babylonjs.com/how_to/parametric_shapes#lathe 
          */
         public static CreateLathe(name: string, options: { shape: Vector3[], radius?: number, tessellation?: number, arc?: number, closed?: boolean, updatable?: boolean, sideOrientation?: number, frontUVs?: Vector4, backUVs?: Vector4, cap?: number, invertUV?: boolean }, scene: Scene): Mesh {
             var arc: number = options.arc ? ((options.arc <= 0 || options.arc > 1) ? 1.0 : options.arc) : 1.0;
@@ -678,7 +678,7 @@
          * * You can set some different plane dimensions by using the parameters `width` and `height` (both by default have the same value than `size`)
          * * The parameter `sourcePlane` is a Plane instance. It builds a mesh plane from a Math plane
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
          * @param name defines the name of the mesh
          * @param options defines the options used to create the mesh
@@ -778,7 +778,7 @@
          * @param options defines the options used to create the mesh
          * @param scene defines the hosting scene
          * @returns the ground mesh
-         * @see http://doc.babylonjs.com/tutorials/14._Height_Map   
+         * @see http://doc.babylonjs.com/babylon101/height_map   
          * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#ground-from-a-height-map
          */
         public static CreateGroundFromHeightMap(name: string, url: string, options: { width?: number, height?: number, subdivisions?: number, minHeight?: number, maxHeight?: number, colorFilter?: Color3, updatable?: boolean, onReady?: (mesh: GroundMesh) => void }, scene: Scene): GroundMesh {
@@ -919,7 +919,7 @@
          * * The parameter `cap` sets the way the extruded shape is capped. Possible values : BABYLON.Mesh.NO_CAP (default), BABYLON.Mesh.CAP_START, BABYLON.Mesh.CAP_END, BABYLON.Mesh.CAP_ALL         
          * * The optional parameter `instance` is an instance of an existing Tube object to be updated with the passed `pathArray` parameter : http://doc.babylonjs.com/tutorials/How_to_dynamically_morph_a_mesh#tube    
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation  
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation  
          * * The optional parameter `invertUV` (boolean, default false) swaps in the geometry the U and V coordinates to apply a texture
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
          * @param name defines the name of the mesh
@@ -1051,13 +1051,13 @@
          * * To understand how to set `faceUV` or `faceColors`, please read this by considering the right number of faces of your polyhedron, instead of only 6 for the box : http://doc.babylonjs.com/tutorials/CreateBox_Per_Face_Textures_And_Colors
          * * The parameter `flat` (boolean, default true). If set to false, it gives the polyhedron a single global face, so less vertices and shared normals. In this case, `faceColors` and `faceUV` are ignored
          * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE  
-         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/tutorials/02._Discover_Basic_Elements#side-orientation    
+         * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : http://doc.babylonjs.com/babylon101/discover_basic_elements#side-orientation    
          * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
          * @param name defines the name of the mesh
          * @param options defines the options used to create the mesh
          * @param scene defines the hosting scene
          * @returns the polyhedron mesh
-         * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#polyhedron
+         * @see http://doc.babylonjs.com/how_to/polyhedra_shapes
          */
         public static CreatePolyhedron(name: string, options: { type?: number, size?: number, sizeX?: number, sizeY?: number, sizeZ?: number, custom?: any, faceUV?: Vector4[], faceColors?: Color4[], flat?: boolean, updatable?: boolean, sideOrientation?: number, frontUVs?: Vector4, backUVs?: Vector4 }, scene: Scene): Mesh {
             var polyhedron = new Mesh(name, scene);
@@ -1084,7 +1084,7 @@
          * @param options defines the options used to create the mesh
          * @param scene defines the hosting scene
          * @returns the decal mesh
-         * @see http://doc.babylonjs.com/tutorials/Mesh_CreateXXX_Methods_With_Options_Parameter#decals
+         * @see http://doc.babylonjs.com/how_to/decals
          */
         public static CreateDecal(name: string, sourceMesh: AbstractMesh, options: { position?: Vector3, normal?: Vector3, size?: Vector3, angle?: number }): Mesh {
             var indices = <IndicesArray>sourceMesh.getIndices();

+ 4 - 4
what's new.md

@@ -5,7 +5,7 @@
 - Added VRExperienceHelper to create WebVR experience with 2 lines of code. [Documentation](http://doc.babylonjs.com/how_to/webvr_helper) ([davrous](https://github.com/davrous), [TrevorDev](https://github.com/TrevorDev))
 - Added BackgroundMaterial. [Documentation](https://doc.babylonjs.com/how_to/backgroundmaterial) ([sebavan](https://github.com/sebavan))
 - Added EnvironmentHelper. [Documentation](https://doc.babylonjs.com/babylon101/environment#skybox-and-ground) ([sebavan](https://github.com/sebavan))
-- Added support for webgl context lost and restored events. [Documentation](http://doc.babylonjs.com/tutorials/optimizing_your_scene#handling-webgl-context-lost) ([deltakosh](https://github.com/deltakosh))
+- Added support for webgl context lost and restored events. [Documentation](http://doc.babylonjs.com/how_to/optimizing_your_scene#handling-webgl-context-lost) ([deltakosh](https://github.com/deltakosh))
 - Added support for non-pow2 textures when in WebGL2 mode ([deltakosh](https://github.com/deltakosh))
 - Engine can now be initialized with an existing webgl context ([deltakosh](https://github.com/deltakosh))
 - Introduced behaviors. [Documentation](http://doc.babylonjs.com/overviews/behaviors) ([deltakosh](https://github.com/deltakosh))
@@ -16,10 +16,10 @@
   - Bouncing ([deltakosh](https://github.com/deltakosh))
 - New InputText for Babylon.GUI. [Documentation](http://doc.babylonjs.com/overviews/gui#inputtext) ([deltakosh](https://github.com/deltakosh))
 - New VirtualKeyboard for Babylon.GUI. [Documentation](http://doc.babylonjs.com/overviews/gui#virtualkeyboard) ([deltakosh](https://github.com/deltakosh) / [adam](https://github.com/abow))
-- Added support for depth pre-pass rendering. [Documentation](http://doc.babylonjs.com/tutorials/transparency_and_how_meshes_are_rendered#depth-pre-pass-meshes) ([deltakosh](https://github.com/deltakosh))
-- Added support for `material.separateCullingPass`. [Documentation](http://doc.babylonjs.com/tutorials/transparency_and_how_meshes_are_rendered#things-to-do-and-not-to-do) ([sebavan](https://github.com/sebavan))
+- Added support for depth pre-pass rendering. [Documentation](http://doc.babylonjs.com/resources/transparency_and_how_meshes_are_rendered#depth-pre-pass-meshes) ([deltakosh](https://github.com/deltakosh))
+- Added support for `material.separateCullingPass`. [Documentation](http://doc.babylonjs.com/resources/transparency_and_how_meshes_are_rendered#things-to-do-and-not-to-do) ([sebavan](https://github.com/sebavan))
 - Added support for Windows Motion Controllers ([Lewis Weaver](https://github.com/leweaver))
-- Added support for Particle animation in ParticleSystem. [Documentation](http://doc.babylonjs.com/tutorials/particles#particle-animation) ([Ibraheem Osama](https://github.com/IbraheemOsama))
+- Added support for Particle animation in ParticleSystem. [Documentation](http://doc.babylonjs.com/how_to/animate) ([Ibraheem Osama](https://github.com/IbraheemOsama))
 - More robust TypeScript code with *strictNullChecks*, *noImplicitAny*, *noImplicitThis* and *noImplicitReturns* compiler options ([deltakosh](https://github.com/deltakosh) and [RaananW](https://github.com/RaananW))
 - Introduced `NullEngine` which can be used to use Babylon.js in headless mode. [Documentation](http://doc.babylonjs.com/generals/nullengine) ([deltakosh](https://github.com/deltakosh))
 - New instrumentations tools. [Documentation](http://doc.babylonjs.com/how_to/optimizing_your_scene#instrumentation) ([deltakosh](https://github.com/deltakosh))