瀏覽代碼

Merge remote-tracking branch 'origin/master' into range-request

Gary Hsu 5 年之前
父節點
當前提交
7fac99f75c
共有 55 個文件被更改,包括 1345 次插入576 次删除
  1. 13 0
      .vscode/launch.json
  2. 45 21
      Playground/babylon.d.txt
  3. 3 1
      Tools/Config/config.json
  4. 25 3
      Tools/DevLoader/BabylonLoader.js
  5. 58 0
      Tools/Gulp/helpers/gulp-processConstants.js
  6. 1 1
      Tools/Gulp/helpers/gulp-processImportsToEs6.js
  7. 12 2
      Tools/Gulp/tasks/gulpTasks-libraries.js
  8. 18 3
      Tools/Gulp/tasks/gulpTasks-librariesES6.js
  9. 45 21
      dist/preview release/babylon.d.ts
  10. 2 2
      dist/preview release/babylon.js
  11. 313 171
      dist/preview release/babylon.max.js
  12. 1 1
      dist/preview release/babylon.max.js.map
  13. 91 42
      dist/preview release/babylon.module.d.ts
  14. 45 21
      dist/preview release/documentation.d.ts
  15. 1 1
      dist/preview release/packagesSizeBaseLine.json
  16. 91 42
      dist/preview release/viewer/babylon.module.d.ts
  17. 48 44
      dist/preview release/viewer/babylon.viewer.js
  18. 1 1
      dist/preview release/viewer/babylon.viewer.max.js
  19. 2 0
      dist/preview release/what's new.md
  20. 56 0
      localDev/index-worker.html
  21. 60 0
      localDev/worker.js
  22. 2 2
      package.json
  23. 13 6
      src/Cameras/Inputs/BaseCameraPointersInput.ts
  24. 3 1
      src/Cameras/Inputs/arcRotateCameraVRDeviceOrientationInput.ts
  25. 4 2
      src/Cameras/Inputs/freeCameraDeviceOrientationInput.ts
  26. 3 0
      src/Cameras/VR/vrExperienceHelper.ts
  27. 4 1
      src/Cameras/VR/webVRCamera.ts
  28. 8 6
      src/Engines/Extensions/engine.cubeTexture.ts
  29. 1 1
      src/Engines/Extensions/engine.dynamicTexture.ts
  30. 2 1
      src/Engines/Extensions/engine.videoTexture.ts
  31. 6 5
      src/Engines/Extensions/engine.webVR.ts
  32. 0 4
      src/Engines/constants.ts
  33. 43 43
      src/Engines/engine.ts
  34. 30 13
      src/Engines/thinEngine.ts
  35. 4 2
      src/Gamepads/gamepadManager.ts
  36. 15 7
      src/Inputs/scene.inputManager.ts
  37. 6 3
      src/Materials/Textures/dynamicTexture.ts
  38. 3 3
      src/Materials/Textures/internalTexture.ts
  39. 2 2
      src/Materials/Textures/texture.ts
  40. 6 7
      src/Meshes/Builders/groundBuilder.ts
  41. 12 6
      src/Meshes/buffer.ts
  42. 2 2
      src/Meshes/instancedMesh.ts
  43. 4 5
      src/Meshes/mesh.ts
  44. 22 0
      src/Misc/canvasGenerator.ts
  45. 66 44
      src/Misc/environmentTextureTools.ts
  46. 23 2
      src/Misc/fileTools.ts
  47. 1 0
      src/Misc/index.ts
  48. 1 1
      src/Misc/tools.ts
  49. 1 1
      src/Offline/database.ts
  50. 1 2
      src/Particles/particleHelper.ts
  51. 6 2
      src/Particles/particleSystemSet.ts
  52. 20 2
      src/Particles/solidParticle.ts
  53. 90 19
      src/Particles/solidParticleSystem.ts
  54. 6 4
      src/Sprites/spriteSceneComponent.ts
  55. 4 0
      src/scene.ts

+ 13 - 0
.vscode/launch.json

@@ -132,6 +132,19 @@
             ]
         },
         {
+            "name": "Launch Local Dev - Worker mode (Chrome)",
+            "type": "chrome",
+            "request": "launch",
+            "url": "http://localhost:1338/localDev/index-worker.html",
+            "webRoot": "${workspaceRoot}/",
+            "sourceMaps": true,
+            "preLaunchTask": "run",
+            "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
+            "runtimeArgs": [
+                "--enable-unsafe-es3-apis"
+            ]
+        },
+        {
             "name": "Launch Local Dev (Experimental Firefox)",
             "type": "firefox",
             "request": "launch",

文件差異過大導致無法顯示
+ 45 - 21
Playground/babylon.d.txt


+ 3 - 1
Tools/Config/config.json

@@ -534,6 +534,7 @@
             }
         ],
         "build": {
+            "ignoreInWorkerMode": true,
             "ignoreInTestMode": true,
             "mainFolder": "./inspector/",
             "uncheckedLintImports": [
@@ -593,7 +594,8 @@
                 "entry": "./legacy/legacy.ts"
             }
         ],
-        "build": {
+        "build": {            
+            "ignoreInWorkerMode": true,
             "ignoreInTestMode": true,
             "mainFolder": "./nodeEditor/",
             "uncheckedLintImports": [

+ 25 - 3
Tools/DevLoader/BabylonLoader.js

@@ -33,6 +33,7 @@ var BABYLONDEVTOOLS;
         var dependencies;
         var useDist;
         var testMode;
+        var workerMode;
         var min;
         var babylonJSPath;
 
@@ -44,8 +45,14 @@ var BABYLONDEVTOOLS;
             esmQueue = [];
             dependencies = [];
             callback = null;
-            min = (document.location.href.toLowerCase().indexOf('dist=min') > 0);
-            useDist = (min || useDist || document.location.href.toLowerCase().indexOf('dist=true') > 0);
+            if (typeof document !== "undefined") {
+                min = document.location.href.toLowerCase().indexOf('dist=min') > 0;
+                useDist = (min || useDist || document.location.href.toLowerCase().indexOf('dist=true') > 0);
+            } else {
+                min = false;
+                useDist = false;
+                workerMode = true;
+            }
             babylonJSPath = '';
         }
 
@@ -108,7 +115,7 @@ var BABYLONDEVTOOLS;
         }
 
         Loader.prototype.dequeue = function() {
-            if (queue.length + esmQueue.length == 0) {
+            if (queue.length + esmQueue.length === 0) {
                 console.log('Scripts loaded');
                 BABYLON.Engine.ShadersRepository = "/src/Shaders/";
                 if (callback) {
@@ -117,6 +124,14 @@ var BABYLONDEVTOOLS;
                 return;
             }
 
+            if (typeof document === "undefined") {
+                let url = esmQueue.length ? esmQueue.shift() : queue.shift();
+                console.log(url);
+                importScripts(url);
+                this.dequeue();    
+                return;
+            } 
+
             var head = document.getElementsByTagName('head')[0];
             var script = document.createElement('script');
 
@@ -169,6 +184,9 @@ var BABYLONDEVTOOLS;
             distFolder += "/";
             
             if (!useDist) {
+                if (workerMode && module.build.ignoreInWorkerMode) {
+                    return;
+                }
                 var tempDirectory = '/.temp/' + localDevUMDFolderName + distFolder;
                 this.loadScript((babylonJSPath + tempDirectory + library.output)
                     .replace(".min.", ".")
@@ -191,6 +209,10 @@ var BABYLONDEVTOOLS;
         }
 
         Loader.prototype.loadCoreDev = function() {
+            if (typeof document === "undefined") {                
+                this.loadScript(babylonJSPath + "/dist/preview release/babylon.max.js");
+                return;
+            }
             // Es6 core import
             this.loadESMScript(babylonJSPath + "/.temp/" + localDevES6FolderName + "/core/Legacy/legacy.js");
         }

+ 58 - 0
Tools/Gulp/helpers/gulp-processConstants.js

@@ -0,0 +1,58 @@
+// Dependencies.
+var through = require('through2');
+var PluginError = require('plugin-error');
+const fs = require('fs');
+const babylonConstants = require(__dirname + '/../../../dist/preview release/babylon.max').Constants;
+
+
+/**
+ * Replace all constants by their inlined values.
+ */
+function processConstants(sourceCode) {
+    var regexImport = /import { Constants } from .*;/g;
+    sourceCode = sourceCode.replace(regexImport, "");
+
+    var regexConstant = /(?<![_0-9a-zA-Z])Constants\.([_0-9a-zA-Z]*)/g;
+    var match = regexConstant.exec(sourceCode);
+    var constantList = [];
+    while (match) {
+        var constantName = match[1];
+        if (constantName && constantName.length > 1) {
+            constantList.push(constantName);
+        }
+        match = regexConstant.exec(sourceCode);
+    }
+
+    for (var constant of constantList) {
+        var value = babylonConstants[constant];
+        var regex = new RegExp(`(?<![_0-9a-zA-Z])Constants\.${constant}(?![_0-9a-zA-Z])`, "g");
+        sourceCode = sourceCode.replace(regex, value);
+    }
+
+    return sourceCode;
+}
+
+/**
+ * Replaces all constants by their inlined values.
+ */
+function main() {
+    return through.obj(function (file, enc, cb) {
+            if (file.isNull()) {
+                cb(null, file);
+                return;
+            }
+            if (file.isStream()) {
+                cb(new PluginError("Process Constants", "Streaming not supported."));
+            }
+
+            let data = file.contents.toString();
+            data = processConstants(data);
+
+            // Go to disk.
+            fs.writeFileSync(file.path, data);
+
+            return cb();
+        });
+}
+
+module.exports = main;

+ 1 - 1
Tools/Gulp/helpers/gulp-processImportsToEs6.js

@@ -26,7 +26,7 @@ function main(replacements) {
                 return;
             }
             if (file.isStream()) {
-                cb(new PluginError("Process Shader", "Streaming not supported."));
+                cb(new PluginError("Process Imports", "Streaming not supported."));
             }
 
             let data = file.contents.toString();

+ 12 - 2
Tools/Gulp/tasks/gulpTasks-libraries.js

@@ -166,7 +166,7 @@ var processDTSFiles = function(libraries, settings, cb) {
 /**
  * Dynamic module creation In Serie for WebPack leaks.
  */
-function buildExternalLibraries(settings) {
+function buildExternalLibraries(settings, fast) {
     // Creates the required tasks.
     var tasks = [];
 
@@ -182,7 +182,11 @@ function buildExternalLibraries(settings) {
         appendLoseDTS.push(function() { return appendLoseDTSFiles(settings, false) });
     }
 
-    tasks.push(cleanup, shaders, buildMin, buildMax, buildAMDDTS, processDTS, ...appendLoseDTS);
+    if (fast) {
+        tasks.push(buildMax);
+    } else {
+        tasks.push(cleanup, shaders, buildMin, buildMax, buildAMDDTS, processDTS, ...appendLoseDTS);
+    }
 
     return gulp.series.apply(this, tasks);
 }
@@ -202,6 +206,12 @@ config.modules.map(function(module) {
 gulp.task("typescript", gulp.series("core"));
 
 /**
+ * Build the releasable files.
+ * Back Compat Only, now name core as it is a lib
+ */
+gulp.task("core-workers", buildExternalLibraries(config["core"], true));
+
+/**
  * Build all libs.
  */
 gulp.task("typescript-libraries", gulp.series(config.modules, config.viewerModules));

+ 18 - 3
Tools/Gulp/tasks/gulpTasks-librariesES6.js

@@ -8,6 +8,7 @@ var concat = require('gulp-concat');
 // Gulp Helpers
 var rmDir = require("../../NodeHelpers/rmDir");
 var processImports = require("../helpers/gulp-processImportsToEs6");
+var processConstants = require("../helpers/gulp-processConstants");
 var processLooseDeclarations = require("../helpers/gulp-processLooseDeclarationsEs6");
 var uncommentShaders = require('../helpers/gulp-removeShaderComments');
 var processShaders = require("../helpers/gulp-processShaders");
@@ -55,6 +56,7 @@ var source = function(settings) {
 var dep = function(settings) {
     const copyPaths = []
     // Add tsconfig rules.
+    copyPaths.push(path.join(config.computed.rootFolder, "/dist/preview release/babylon.max.js"));
     copyPaths.push(path.join(config.computed.rootFolder, "tsconfigRules.json"));
 
     const tsconfig = require(settings.computed.tsConfigPath);
@@ -82,7 +84,7 @@ var dep = function(settings) {
 /**
  * Adapt Sources import paths.
  */
-var modifySources = function(settings) {
+var modifySourcesImports = function(settings) {
     const tsconfig = require(settings.computed.tsConfigPath);
 
     var replacements = [];
@@ -113,6 +115,18 @@ var modifySources = function(settings) {
 }
 
 /**
+ * Inline Constants in sources.
+ */
+var modifySourcesConstants = function(settings) {
+    if (settings.isCore) {
+        return gulp.src([settings.computed.sourceES6Directory + "/**/*.ts", 
+            settings.computed.sourceES6Directory + "/**/*.tsx"])
+            .pipe(processConstants());
+    }
+    return Promise.resolve();
+}
+
+/**
  * Adapt TS Config Paths.
  */
 var modifyTsConfig = function(settings, cb) {
@@ -261,7 +275,8 @@ function buildES6Library(settings, module) {
     }
     var copySource = function() { return source(settings); };
     var dependencies = function() { return dep(settings); };
-    var adaptSourceImportPaths = function() { return modifySources(settings); };
+    var adaptSourceImportPaths = function() { return modifySourcesImports(settings); };
+    var adaptSourceConstants = function() { return modifySourcesConstants(settings); };
     var adaptTsConfigImportPaths = function(cb) { return modifyTsConfig(settings, cb); };
 
     // Build with ts or webpack
@@ -280,7 +295,7 @@ function buildES6Library(settings, module) {
         ];
     }
 
-    tasks.push(...cleanAndShaderTasks, copySource, dependencies, adaptSourceImportPaths, adaptTsConfigImportPaths, ...buildSteps);
+    tasks.push(...cleanAndShaderTasks, copySource, dependencies, adaptSourceImportPaths, adaptSourceConstants, adaptTsConfigImportPaths, ...buildSteps);
 
     return gulp.series.apply(this, tasks);
 }

文件差異過大導致無法顯示
+ 45 - 21
dist/preview release/babylon.d.ts


文件差異過大導致無法顯示
+ 2 - 2
dist/preview release/babylon.js


文件差異過大導致無法顯示
+ 313 - 171
dist/preview release/babylon.max.js


文件差異過大導致無法顯示
+ 1 - 1
dist/preview release/babylon.max.js.map


文件差異過大導致無法顯示
+ 91 - 42
dist/preview release/babylon.module.d.ts


文件差異過大導致無法顯示
+ 45 - 21
dist/preview release/documentation.d.ts


+ 1 - 1
dist/preview release/packagesSizeBaseLine.json

@@ -1 +1 @@
-{"thinEngineOnly":126491,"engineOnly":163368,"sceneOnly":508417,"minGridMaterial":638916,"minStandardMaterial":763703}
+{"thinEngineOnly":127291,"engineOnly":164204,"sceneOnly":509429,"minGridMaterial":639900,"minStandardMaterial":764687}

文件差異過大導致無法顯示
+ 91 - 42
dist/preview release/viewer/babylon.module.d.ts


文件差異過大導致無法顯示
+ 48 - 44
dist/preview release/viewer/babylon.viewer.js


文件差異過大導致無法顯示
+ 1 - 1
dist/preview release/viewer/babylon.viewer.max.js


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

@@ -12,6 +12,7 @@
 - WebXR updates:
   - WebXR updated to spec as of July 10th ([TrevorDev](https://github.com/TrevorDev))
   - WebXR webVR parity helpers (Vive, WMR, Oculus Rift) ([TrevorDev](https://github.com/TrevorDev))
+- Added support for Offscreen canvas [Doc](https://doc.babylonjs.com/how_to/using_offscreen_canvas) ([Deltakosh](https://github.com/deltakosh/)
 
 ## Updates
 
@@ -116,6 +117,7 @@
 
 ### Particles
 - Added the feature `expandable` to the Solid Particle System ([jerome](https://github.com/jbousquie/))
+- Added the feature `removeParticles()` to the Solid Particle System ([jerome](https://github.com/jbousquie/))
 
 ### Navigation Mesh
 - Added moveAlong function to cast a segment on mavmesh ([CedricGuillemet](https://github.com/CedricGuillemet/))

+ 56 - 0
localDev/index-worker.html

@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+    <title>Local Development - Worker mode</title>
+
+    <style>
+        html,
+        body {
+            width: 100%;
+            height: 100%;
+            padding: 0;
+            margin: 0;
+            overflow: hidden;
+        }
+
+        #renderCanvas {
+            width: 100%;
+            height: 100%;
+            display: block;
+            font-size: 0;
+        }
+
+        #fps {
+            position: absolute;
+            background-color: black;
+            border: 2px solid red;
+            text-align: center;
+            font-size: 16px;
+            color: white;
+            top: 15px;
+            right: 10px;
+            width: 60px;
+            height: 20px;
+        }
+    </style>
+</head>
+
+<body>
+    <div id="fps">0</div>
+    <canvas id="renderCanvas" touch-action="none"></canvas>
+
+    <script>
+        var canvas = document.getElementById("renderCanvas");
+
+        canvas.width = canvas.clientWidth;
+        canvas.height = canvas.clientHeight;
+
+        var offscreen = canvas.transferControlToOffscreen();
+
+        var worker = new Worker("worker.js"); 
+        worker.postMessage({canvas: offscreen}, [offscreen]);
+    </script>
+</body>
+
+</html>

+ 60 - 0
localDev/worker.js

@@ -0,0 +1,60 @@
+importScripts('../Tools/DevLoader/BabylonLoader.js');
+
+// Global to simulate PG.
+var engine = null;
+var canvas = null;
+
+onmessage = function(evt) {
+    canvas = evt.data.canvas;
+    
+    // Load the scripts + map file to allow vscode debug.
+    BABYLONDEVTOOLS.Loader
+        .require("src/index.js")
+        .load(function() {
+            if (typeof createEngine !== "undefined") {
+                engine = createEngine();
+            } else {
+                engine = new BABYLON.Engine(canvas, true, { premultipliedAlpha: false, stencil: true, disableWebGL2Support: false, preserveDrawingBuffer: true });
+            }
+
+            // call the scene creation from the js.
+            if (typeof delayCreateScene !== "undefined") {
+                var scene = delayCreateScene();
+
+                if (scene) {
+                    // Register a render loop to repeatedly render the scene
+
+                    engine.runRenderLoop(function() {
+                        if (scene.activeCamera) {
+                            scene.render();
+                        }
+                    });
+                }
+            }
+            else {
+                var scene = createScene();
+
+                if (scene) {
+
+                    var processCurrentScene = function(scene) {
+                        engine.runRenderLoop(function() {
+                            scene.render();
+                        });
+                    }
+
+                    if (scene.then) {
+                        // Handle if createScene returns a promise
+                        scene.then(function(currentScene) {
+                            processCurrentScene(currentScene);
+                        }).catch(function(e) {
+                            console.error(e);
+                            onError();
+                        });
+                    } else {
+                        // Register a render loop to repeatedly render the scene
+                        processCurrentScene(scene);
+                    }
+                }
+            }
+        });
+    }

+ 2 - 2
package.json

@@ -98,7 +98,7 @@
         "tslib": "^1.9.3",
         "tslint": "^5.11.0",
         "typedoc": "^0.15.0",
-        "typescript": "~3.5.1",
+        "typescript": "~3.6.3",
         "webpack": "^4.29.3",
         "webpack-bundle-analyzer": "^3.1.0",
         "webpack-cli": "^3.3.9",
@@ -107,4 +107,4 @@
         "xhr2": "^0.1.4",
         "xmlbuilder": "8.2.2"
     }
-}
+}

+ 13 - 6
src/Cameras/Inputs/BaseCameraPointersInput.ts

@@ -228,9 +228,13 @@ export abstract class BaseCameraPointersInput implements ICameraInput<Camera> {
         element.addEventListener("contextmenu",
             <EventListener>this.onContextMenu.bind(this), false);
 
-        Tools.RegisterTopRootEvents(this.camera.getScene().getEngine().getHostWindow(), [
-            { name: "blur", handler: this._onLostFocus }
-        ]);
+        let hostWindow = this.camera.getScene().getEngine().getHostWindow();
+
+        if (hostWindow) {
+            Tools.RegisterTopRootEvents(hostWindow, [
+                { name: "blur", handler: this._onLostFocus }
+            ]);
+        }
     }
 
     /**
@@ -239,9 +243,12 @@ export abstract class BaseCameraPointersInput implements ICameraInput<Camera> {
      */
     public detachControl(element: Nullable<HTMLElement>): void {
         if (this._onLostFocus) {
-            Tools.UnregisterTopRootEvents(this.camera.getScene().getEngine().getHostWindow(), [
-                { name: "blur", handler: this._onLostFocus }
-            ]);
+            let hostWindow = this.camera.getScene().getEngine().getHostWindow();
+            if (hostWindow) {
+                Tools.UnregisterTopRootEvents(hostWindow, [
+                    { name: "blur", handler: this._onLostFocus }
+                ]);
+            }
         }
 
         if (element && this._observer) {

+ 3 - 1
src/Cameras/Inputs/arcRotateCameraVRDeviceOrientationInput.ts

@@ -66,7 +66,9 @@ export class ArcRotateCameraVRDeviceOrientationInput implements ICameraInput<Arc
 
         let hostWindow = this.camera.getScene().getEngine().getHostWindow();
 
-        hostWindow.addEventListener("deviceorientation", this._deviceOrientationHandler);
+        if (hostWindow) {
+            hostWindow.addEventListener("deviceorientation", this._deviceOrientationHandler);
+        }
     }
 
     /** @hidden */

+ 4 - 2
src/Cameras/Inputs/freeCameraDeviceOrientationInput.ts

@@ -120,8 +120,10 @@ export class FreeCameraDeviceOrientationInput implements ICameraInput<FreeCamera
 
         let hostWindow = this.camera.getScene().getEngine().getHostWindow();
 
-        hostWindow.addEventListener("orientationchange", this._orientationChanged);
-        hostWindow.addEventListener("deviceorientation", this._deviceOrientation);
+        if (hostWindow) {
+            hostWindow.addEventListener("orientationchange", this._orientationChanged);
+            hostWindow.addEventListener("deviceorientation", this._deviceOrientation);
+        }
 
         //In certain cases, the attach control is called AFTER orientation was changed,
         //So this is needed.

+ 3 - 0
src/Cameras/VR/vrExperienceHelper.ts

@@ -784,6 +784,9 @@ export class VRExperienceHelper {
         // Window events
 
         let hostWindow = this._scene.getEngine().getHostWindow();
+        if (!hostWindow) {
+            return;
+        }
 
         hostWindow.addEventListener("resize", this._onResize);
         document.addEventListener("fullscreenchange", this._onFullscreenChange, false);

+ 4 - 1
src/Cameras/VR/webVRCamera.ts

@@ -510,7 +510,10 @@ export class WebVRFreeCamera extends FreeCamera implements PoseControlled {
         }
 
         let hostWindow = this._scene.getEngine().getHostWindow();
-        hostWindow.addEventListener('vrdisplaypresentchange', this._detachIfAttached);
+
+        if (hostWindow) {
+            hostWindow.addEventListener('vrdisplaypresentchange', this._detachIfAttached);
+        }
     }
 
     /**

+ 8 - 6
src/Engines/Extensions/engine.cubeTexture.ts

@@ -168,14 +168,16 @@ ThinEngine.prototype._cascadeLoadImgs = function(scene: Nullable<Scene>,
 ThinEngine.prototype._partialLoadImg = function(url: string, index: number, loadedImages: HTMLImageElement[], scene: Nullable<Scene>,
     onfinish: (images: HTMLImageElement[]) => void, onErrorCallBack: Nullable<(message?: string, exception?: any) => void> = null, mimeType?: string) {
 
-    var img: HTMLImageElement;
+    var img: Nullable<HTMLImageElement>;
 
     var onload = () => {
-        loadedImages[index] = img;
-        (<any>loadedImages)._internalCount++;
+        if (img) {
+            loadedImages[index] = img;
+            (<any>loadedImages)._internalCount++;
 
-        if (scene) {
-            scene._removePendingData(img);
+            if (scene) {
+                scene._removePendingData(img);
+            }
         }
 
         if ((<any>loadedImages)._internalCount === 6) {
@@ -194,7 +196,7 @@ ThinEngine.prototype._partialLoadImg = function(url: string, index: number, load
     };
 
     img = FileTools.LoadImage(url, onload, onerror, scene ? scene.offlineProvider : null, mimeType);
-    if (scene) {
+    if (scene && img) {
         scene._addPendingData(img);
     }
 };

+ 1 - 1
src/Engines/Extensions/engine.dynamicTexture.ts

@@ -23,7 +23,7 @@ declare module "../../Engines/thinEngine" {
          * @param format defines the format of the data
          * @param forceBindTexture if the texture should be forced to be bound eg. after a graphics context loss (Default: false)
          */
-        updateDynamicTexture(texture: Nullable<InternalTexture>, canvas: HTMLCanvasElement, invertY: boolean, premulAlpha?: boolean, format?: number, forceBindTexture?: boolean): void;
+        updateDynamicTexture(texture: Nullable<InternalTexture>, canvas: HTMLCanvasElement | OffscreenCanvas, invertY: boolean, premulAlpha?: boolean, format?: number, forceBindTexture?: boolean): void;
     }
 }
 

+ 2 - 1
src/Engines/Extensions/engine.videoTexture.ts

@@ -1,6 +1,7 @@
 import { ThinEngine } from "../../Engines/thinEngine";
 import { InternalTexture } from '../../Materials/Textures/internalTexture';
 import { Nullable } from '../../types';
+import { CanvasGenerator } from '../../Misc/canvasGenerator';
 
 declare module "../../Engines/thinEngine" {
     export interface ThinEngine {
@@ -37,7 +38,7 @@ ThinEngine.prototype.updateVideoTexture = function(texture: Nullable<InternalTex
         // Copy video through the current working canvas if video texture is not supported
         if (!this._videoTextureSupported) {
             if (!texture._workingCanvas) {
-                texture._workingCanvas = document.createElement("canvas");
+                texture._workingCanvas = CanvasGenerator.CreateCanvas(texture.width, texture.height);
                 let context = texture._workingCanvas.getContext("2d");
 
                 if (!context) {

+ 6 - 5
src/Engines/Extensions/engine.webVR.ts

@@ -140,9 +140,11 @@ Engine.prototype.initWebVRAsync = function(): Promise<IDisplayChangedEventArgs>
             this._vrExclusivePointerMode = this._vrDisplay && this._vrDisplay.isPresenting;
         };
         let hostWindow = this.getHostWindow();
-        hostWindow.addEventListener('vrdisplayconnect', this._onVrDisplayConnect);
-        hostWindow.addEventListener('vrdisplaydisconnect', this._onVrDisplayDisconnect);
-        hostWindow.addEventListener('vrdisplaypresentchange', this._onVrDisplayPresentChange);
+        if (hostWindow) {
+            hostWindow.addEventListener('vrdisplayconnect', this._onVrDisplayConnect);
+            hostWindow.addEventListener('vrdisplaydisconnect', this._onVrDisplayDisconnect);
+            hostWindow.addEventListener('vrdisplaypresentchange', this._onVrDisplayPresentChange);
+        }
     }
     this._webVRInitPromise = this._webVRInitPromise || this._getVRDisplaysAsync();
     this._webVRInitPromise.then(notifyObservers);
@@ -245,8 +247,7 @@ Engine.prototype._connectVREvents = function(canvas?: HTMLCanvasElement, documen
     };
 
     if (DomManagement.IsWindowObjectExist()) {
-        let hostWindow = this.getHostWindow();
-
+        let hostWindow = this.getHostWindow()!;
         hostWindow.addEventListener('vrdisplaypointerrestricted', this._onVRDisplayPointerRestricted, false);
         hostWindow.addEventListener('vrdisplaypointerunrestricted', this._onVRDisplayPointerUnrestricted, false);
     }

+ 0 - 4
src/Engines/constants.ts

@@ -412,10 +412,6 @@ export class Constants {
      * Special billboard mode where the particle will be biilboard to the camera but rotated to align with direction
      */
     public static readonly PARTICLES_BILLBOARDMODE_STRETCHED = 8;
-    /**
-     * Gets or sets base Assets URL
-     */
-    public static PARTICLES_BaseAssetsUrl = "https://assets.babylonjs.com/particles";
 
     /** Default culling strategy : this is an exclusion test and it's the more accurate.
      *  Test order :

+ 43 - 43
src/Engines/engine.ts

@@ -511,61 +511,61 @@ export class Engine extends ThinEngine {
                 this.onCanvasPointerOutObservable.notifyObservers(ev);
             };
 
+            canvas.addEventListener("pointerout", this._onCanvasPointerOut);
+
             if (DomManagement.IsWindowObjectExist()) {
-                let hostWindow = this.getHostWindow();
+                let hostWindow = this.getHostWindow()!;
                 hostWindow.addEventListener("blur", this._onBlur);
                 hostWindow.addEventListener("focus", this._onFocus);
-            }
 
-            canvas.addEventListener("pointerout", this._onCanvasPointerOut);
+                let anyDoc = document as any;
 
-            let anyDoc = document as any;
+                // Fullscreen
+                this._onFullscreenChange = () => {
 
-            // Fullscreen
-            this._onFullscreenChange = () => {
+                    if (anyDoc.fullscreen !== undefined) {
+                        this.isFullscreen = anyDoc.fullscreen;
+                    } else if (anyDoc.mozFullScreen !== undefined) {
+                        this.isFullscreen = anyDoc.mozFullScreen;
+                    } else if (anyDoc.webkitIsFullScreen !== undefined) {
+                        this.isFullscreen = anyDoc.webkitIsFullScreen;
+                    } else if (anyDoc.msIsFullScreen !== undefined) {
+                        this.isFullscreen = anyDoc.msIsFullScreen;
+                    }
 
-                if (anyDoc.fullscreen !== undefined) {
-                    this.isFullscreen = anyDoc.fullscreen;
-                } else if (anyDoc.mozFullScreen !== undefined) {
-                    this.isFullscreen = anyDoc.mozFullScreen;
-                } else if (anyDoc.webkitIsFullScreen !== undefined) {
-                    this.isFullscreen = anyDoc.webkitIsFullScreen;
-                } else if (anyDoc.msIsFullScreen !== undefined) {
-                    this.isFullscreen = anyDoc.msIsFullScreen;
-                }
+                    // Pointer lock
+                    if (this.isFullscreen && this._pointerLockRequested && canvas) {
+                        Engine._RequestPointerlock(canvas);
+                    }
+                };
+
+                document.addEventListener("fullscreenchange", this._onFullscreenChange, false);
+                document.addEventListener("mozfullscreenchange", this._onFullscreenChange, false);
+                document.addEventListener("webkitfullscreenchange", this._onFullscreenChange, false);
+                document.addEventListener("msfullscreenchange", this._onFullscreenChange, false);
 
                 // Pointer lock
-                if (this.isFullscreen && this._pointerLockRequested && canvas) {
-                    Engine._RequestPointerlock(canvas);
+                this._onPointerLockChange = () => {
+                    this.isPointerLock = (anyDoc.mozPointerLockElement === canvas ||
+                        anyDoc.webkitPointerLockElement === canvas ||
+                        anyDoc.msPointerLockElement === canvas ||
+                        anyDoc.pointerLockElement === canvas
+                    );
+                };
+
+                document.addEventListener("pointerlockchange", this._onPointerLockChange, false);
+                document.addEventListener("mspointerlockchange", this._onPointerLockChange, false);
+                document.addEventListener("mozpointerlockchange", this._onPointerLockChange, false);
+                document.addEventListener("webkitpointerlockchange", this._onPointerLockChange, false);
+
+                // Create Audio Engine if needed.
+                if (!Engine.audioEngine && options.audioEngine && Engine.AudioEngineFactory) {
+                    Engine.audioEngine = Engine.AudioEngineFactory(this.getRenderingCanvas());
                 }
-            };
-
-            document.addEventListener("fullscreenchange", this._onFullscreenChange, false);
-            document.addEventListener("mozfullscreenchange", this._onFullscreenChange, false);
-            document.addEventListener("webkitfullscreenchange", this._onFullscreenChange, false);
-            document.addEventListener("msfullscreenchange", this._onFullscreenChange, false);
-
-            // Pointer lock
-            this._onPointerLockChange = () => {
-                this.isPointerLock = (anyDoc.mozPointerLockElement === canvas ||
-                    anyDoc.webkitPointerLockElement === canvas ||
-                    anyDoc.msPointerLockElement === canvas ||
-                    anyDoc.pointerLockElement === canvas
-                );
-            };
-
-            document.addEventListener("pointerlockchange", this._onPointerLockChange, false);
-            document.addEventListener("mspointerlockchange", this._onPointerLockChange, false);
-            document.addEventListener("mozpointerlockchange", this._onPointerLockChange, false);
-            document.addEventListener("webkitpointerlockchange", this._onPointerLockChange, false);
+            }
 
             this._connectVREvents();
 
-            // Create Audio Engine if needed.
-            if (!Engine.audioEngine && options.audioEngine && Engine.AudioEngineFactory) {
-                Engine.audioEngine = Engine.AudioEngineFactory(this.getRenderingCanvas());
-            }
-
             this.enableOfflineSupport = Engine.OfflineProviderFactory !== undefined;
 
             if (!options.doNotHandleTouchAction) {
@@ -1936,7 +1936,7 @@ export class Engine extends ThinEngine {
     }
 
     private _disableTouchAction(): void {
-        if (!this._renderingCanvas) {
+        if (!this._renderingCanvas || !this._renderingCanvas.setAttribute) {
             return;
         }
 

+ 30 - 13
src/Engines/thinEngine.ts

@@ -29,6 +29,7 @@ import { BaseTexture } from '../Materials/Textures/baseTexture';
 import { IOfflineProvider } from '../Offline/IOfflineProvider';
 import { IEffectFallbacks } from '../Materials/iEffectFallbacks';
 import { IWebRequest } from '../Misc/interfaces/iWebRequest';
+import { CanvasGenerator } from '../Misc/canvasGenerator';
 
 declare type Observer<T> = import("../Misc/observable").Observer<T>;
 declare type VideoTexture = import("../Materials/Textures/videoTexture").VideoTexture;
@@ -339,9 +340,9 @@ export class ThinEngine {
     private _textureUnits: Int32Array;
 
     /** @hidden */
-    public _workingCanvas: Nullable<HTMLCanvasElement>;
+    public _workingCanvas: Nullable<HTMLCanvasElement | OffscreenCanvas>;
     /** @hidden */
-    public _workingContext: Nullable<CanvasRenderingContext2D>;
+    public _workingContext: Nullable<CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D>;
 
     /** @hidden */
     public _bindedRenderFunction: any;
@@ -910,7 +911,7 @@ export class ThinEngine {
             return;
         }
 
-        this._workingCanvas = document.createElement("canvas");
+        this._workingCanvas = CanvasGenerator.CreateCanvas(1, 1);
         let context = this._workingCanvas.getContext("2d");
 
         if (context) {
@@ -1040,7 +1041,11 @@ export class ThinEngine {
      * Gets host window
      * @returns the host window object
      */
-    public getHostWindow(): Window {
+    public getHostWindow(): Nullable<Window> {
+        if (!DomManagement.IsWindowObjectExist()) {
+            return null;
+        }
+
         if (this._renderingCanvas && this._renderingCanvas.ownerDocument && this._renderingCanvas.ownerDocument.defaultView) {
             return this._renderingCanvas.ownerDocument.defaultView;
         }
@@ -1180,8 +1185,16 @@ export class ThinEngine {
      * Resize the view according to the canvas' size
      */
     public resize(): void {
-        var width = this._renderingCanvas ? this._renderingCanvas.clientWidth : window.innerWidth;
-        var height = this._renderingCanvas ? this._renderingCanvas.clientHeight : window.innerHeight;
+        let width: number;
+        let height: number;
+
+        if (DomManagement.IsWindowObjectExist()) {
+            width = this._renderingCanvas ? this._renderingCanvas.clientWidth : window.innerWidth;
+            height = this._renderingCanvas ? this._renderingCanvas.clientHeight : window.innerHeight;
+        } else {
+            width = this._renderingCanvas ? this._renderingCanvas.width : 100;
+            height = this._renderingCanvas ? this._renderingCanvas.height : 100;
+        }
 
         this.setSize(width / this._hardwareScalingLevel, height / this._hardwareScalingLevel);
     }
@@ -2743,7 +2756,7 @@ export class ThinEngine {
      */
     public createTexture(urlArg: Nullable<string>, noMipmap: boolean, invertY: boolean, scene: Nullable<ISceneLike>, samplingMode: number = Constants.TEXTURE_TRILINEAR_SAMPLINGMODE,
         onLoad: Nullable<() => void> = null, onError: Nullable<(message: string, exception: any) => void> = null,
-        buffer: Nullable<string | ArrayBuffer | ArrayBufferView | HTMLImageElement | Blob> = null, fallback: Nullable<InternalTexture> = null, format: Nullable<number> = null,
+        buffer: Nullable<string | ArrayBuffer | ArrayBufferView | HTMLImageElement | Blob | ImageBitmap> = null, fallback: Nullable<InternalTexture> = null, format: Nullable<number> = null,
         forcedExtension: Nullable<string> = null, excludeLoaders: Array<IInternalTextureLoader> = [], mimeType?: string): InternalTexture {
         var url = String(urlArg); // assign a new string, so that the original is still available in case of fallback
         var fromData = url.substr(0, 5) === "data:";
@@ -2851,7 +2864,7 @@ export class ThinEngine {
                 }
             }
         } else {
-            var onload = (img: HTMLImageElement) => {
+            var onload = (img: HTMLImageElement | ImageBitmap) => {
                 if (fromBlob && !this._doNotHandleContextLost) {
                     // We need to store the image if we need to rebuild the texture
                     // in case of a webgl context lost
@@ -2905,8 +2918,8 @@ export class ThinEngine {
             };
 
             if (!fromData || isBase64) {
-                if (buffer instanceof HTMLImageElement) {
-                    onload(buffer);
+                if (buffer && ((<HTMLImageElement>buffer).decoding || (<ImageBitmap>buffer).close)) {
+                    onload(<HTMLImageElement>buffer);
                 } else {
                     FileTools.LoadImage(url, onload, onInternalError, scene ? scene.offlineProvider : null, mimeType);
                 }
@@ -3226,7 +3239,7 @@ export class ThinEngine {
     }
 
     /** @hidden */
-    public _uploadImageToTexture(texture: InternalTexture, image: HTMLImageElement, faceIndex: number = 0, lod: number = 0) {
+    public _uploadImageToTexture(texture: InternalTexture, image: HTMLImageElement | ImageBitmap, faceIndex: number = 0, lod: number = 0) {
         var gl = this._gl;
 
         var textureType = this._getWebGLTextureType(texture.type);
@@ -4225,8 +4238,8 @@ export class ThinEngine {
      */
     public static isSupported(): boolean {
         try {
-            var tempcanvas = document.createElement("canvas");
-            var gl = tempcanvas.getContext("webgl") || tempcanvas.getContext("experimental-webgl");
+            var tempcanvas = CanvasGenerator.CreateCanvas(1, 1);
+            var gl = tempcanvas.getContext("webgl") || (tempcanvas as any).getContext("experimental-webgl");
 
             return gl != null && !!window.WebGLRenderingContext;
         } catch (e) {
@@ -4309,6 +4322,10 @@ export class ThinEngine {
      */
     public static QueueNewFrame(func: () => void, requester?: any): number {
         if (!DomManagement.IsWindowObjectExist()) {
+            if (typeof requestAnimationFrame !== "undefined") {
+                return requestAnimationFrame(func);
+            }
+
             return setTimeout(func, 16);
         }
 

+ 4 - 2
src/Gamepads/gamepadManager.ts

@@ -104,8 +104,10 @@ export class GamepadManager {
             if (this._gamepadEventSupported) {
                 let hostWindow = this._scene ? this._scene.getEngine().getHostWindow() : window;
 
-                hostWindow.addEventListener('gamepadconnected', this._onGamepadConnectedEvent, false);
-                hostWindow.addEventListener('gamepaddisconnected', this._onGamepadDisconnectedEvent, false);
+                if (hostWindow) {
+                    hostWindow.addEventListener('gamepadconnected', this._onGamepadConnectedEvent, false);
+                    hostWindow.addEventListener('gamepaddisconnected', this._onGamepadDisconnectedEvent, false);
+                }
             }
             else {
                 this._startMonitoringGamepads();

+ 15 - 7
src/Inputs/scene.inputManager.ts

@@ -171,17 +171,21 @@ export class InputManager {
         canvas.tabIndex = 1;
 
         // Restore pointer
-        canvas.style.cursor = scene.defaultCursor;
+        if (!scene.doNotHandleCursors) {
+            canvas.style.cursor = scene.defaultCursor;
+        }
 
         var isMeshPicked = (pickResult && pickResult.hit && pickResult.pickedMesh) ? true : false;
         if (isMeshPicked) {
             scene.setPointerOverMesh(pickResult!.pickedMesh);
 
             if (this._pointerOverMesh && this._pointerOverMesh.actionManager && this._pointerOverMesh.actionManager.hasPointerTriggers) {
-                if (this._pointerOverMesh.actionManager.hoverCursor) {
-                    canvas.style.cursor = this._pointerOverMesh.actionManager.hoverCursor;
-                } else {
-                    canvas.style.cursor = scene.hoverCursor;
+                if (!scene.doNotHandleCursors) {
+                    if (this._pointerOverMesh.actionManager.hoverCursor) {
+                        canvas.style.cursor = this._pointerOverMesh.actionManager.hoverCursor;
+                    } else {
+                        canvas.style.cursor = scene.hoverCursor;
+                    }
                 }
             }
         } else {
@@ -804,7 +808,9 @@ export class InputManager {
 
         if (attachUp) {
             let hostWindow = scene.getEngine().getHostWindow();
-            hostWindow.addEventListener(eventPrefix + "up", <any>this._onPointerUp, false);
+            if (hostWindow) {
+                hostWindow.addEventListener(eventPrefix + "up", <any>this._onPointerUp, false);
+            }
         }
     }
 
@@ -840,7 +846,9 @@ export class InputManager {
         canvas.removeEventListener("keyup", this._onKeyUp);
 
         // Cursor
-        canvas.style.cursor = this._scene.defaultCursor;
+        if (!this._scene.doNotHandleCursors) {
+            canvas.style.cursor = this._scene.defaultCursor;
+        }
     }
 
     /**

+ 6 - 3
src/Materials/Textures/dynamicTexture.ts

@@ -7,6 +7,7 @@ import { Texture } from "../../Materials/Textures/texture";
 import { _TimeToken } from "../../Instrumentation/timeToken";
 import { Constants } from "../../Engines/constants";
 import "../../Engines/Extensions/engine.dynamicTexture";
+import { CanvasGenerator } from '../../Misc/canvasGenerator';
 
 /**
  * A class extending Texture allowing drawing on a texture
@@ -14,7 +15,7 @@ import "../../Engines/Extensions/engine.dynamicTexture";
  */
 export class DynamicTexture extends Texture {
     private _generateMipMaps: boolean;
-    private _canvas: HTMLCanvasElement;
+    private _canvas: HTMLCanvasElement | OffscreenCanvas;
     private _context: CanvasRenderingContext2D;
     private _engine: Engine;
 
@@ -42,7 +43,7 @@ export class DynamicTexture extends Texture {
             this._canvas = options;
             this._texture = this._engine.createDynamicTexture(options.width, options.height, generateMipMaps, samplingMode);
         } else {
-            this._canvas = document.createElement("canvas");
+            this._canvas = CanvasGenerator.CreateCanvas(1, 1);
 
             if (options.width || options.width === 0) {
                 this._texture = this._engine.createDynamicTexture(options.width, options.height, generateMipMaps, samplingMode);
@@ -206,7 +207,9 @@ export class DynamicTexture extends Texture {
         }
 
         const serializationObject = super.serialize();
-        serializationObject.base64String = this._canvas.toDataURL();
+        if ((this._canvas as HTMLCanvasElement).toDataURL) {
+            serializationObject.base64String = (this._canvas as HTMLCanvasElement).toDataURL();
+        }
 
         serializationObject.invertY = this._invertY;
         serializationObject.samplingMode = this.samplingMode;

+ 3 - 3
src/Materials/Textures/internalTexture.ts

@@ -160,7 +160,7 @@ export class InternalTexture {
     /** @hidden */
     public _source = InternalTextureSource.Unknown;
     /** @hidden */
-    public _buffer: Nullable<string | ArrayBuffer | ArrayBufferView | HTMLImageElement | Blob> = null;
+    public _buffer: Nullable<string | ArrayBuffer | ArrayBufferView | HTMLImageElement | Blob | ImageBitmap> = null;
     /** @hidden */
     public _bufferView: Nullable<ArrayBufferView> = null;
     /** @hidden */
@@ -174,9 +174,9 @@ export class InternalTexture {
     /** @hidden */
     public _files: Nullable<string[]> = null;
     /** @hidden */
-    public _workingCanvas: Nullable<HTMLCanvasElement> = null;
+    public _workingCanvas: Nullable<HTMLCanvasElement | OffscreenCanvas> = null;
     /** @hidden */
-    public _workingContext: Nullable<CanvasRenderingContext2D> = null;
+    public _workingContext: Nullable<CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D> = null;
     /** @hidden */
     public _framebuffer: Nullable<WebGLFramebuffer> = null;
     /** @hidden */

+ 2 - 2
src/Materials/Textures/texture.ts

@@ -218,7 +218,7 @@ export class Texture extends BaseTexture {
     protected _initialSamplingMode = Texture.BILINEAR_SAMPLINGMODE;
 
     /** @hidden */
-    public _buffer: Nullable<string | ArrayBuffer | ArrayBufferView | HTMLImageElement | Blob> = null;
+    public _buffer: Nullable<string | ArrayBuffer | ArrayBufferView | HTMLImageElement | Blob | ImageBitmap> = null;
     private _deleteBuffer: boolean = false;
     protected _format: Nullable<number> = null;
     private _delayedOnLoad: Nullable<() => void> = null;
@@ -277,7 +277,7 @@ export class Texture extends BaseTexture {
      * @param format defines the format of the texture we are trying to load (Engine.TEXTUREFORMAT_RGBA...)
      * @param mimeType defines an optional mime type information
      */
-    constructor(url: Nullable<string>, sceneOrEngine: Nullable<Scene | ThinEngine>, noMipmap: boolean = false, invertY: boolean = true, samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE, onLoad: Nullable<() => void> = null, onError: Nullable<(message?: string, exception?: any) => void> = null, buffer: Nullable<string | ArrayBuffer | ArrayBufferView | HTMLImageElement | Blob> = null, deleteBuffer: boolean = false, format?: number, mimeType?: string) {
+    constructor(url: Nullable<string>, sceneOrEngine: Nullable<Scene | ThinEngine>, noMipmap: boolean = false, invertY: boolean = true, samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE, onLoad: Nullable<() => void> = null, onError: Nullable<(message?: string, exception?: any) => void> = null, buffer: Nullable<string | ArrayBuffer | ArrayBufferView | HTMLImageElement | Blob | ImageBitmap> = null, deleteBuffer: boolean = false, format?: number, mimeType?: string) {
         super((sceneOrEngine && sceneOrEngine.getClassName() === "Scene") ? (sceneOrEngine as Scene) : null);
 
         this.name = url || "";

+ 6 - 7
src/Meshes/Builders/groundBuilder.ts

@@ -8,6 +8,7 @@ import { Tools } from "../../Misc/tools";
 import { Nullable } from '../../types';
 import { EngineStore } from '../../Engines/engineStore';
 import { Epsilon } from '../../Maths/math.constants';
+import { CanvasGenerator } from '../../Misc/canvasGenerator';
 
 VertexData.CreateGround = function(options: { width?: number, height?: number, subdivisions?: number, subdivisionsX?: number, subdivisionsY?: number }): VertexData {
     var indices = [];
@@ -382,9 +383,12 @@ export class GroundBuilder {
 
         ground._setReady(false);
 
-        var onload = (img: HTMLImageElement) => {
+        var onload = (img: HTMLImageElement | ImageBitmap) => {
+            var bufferWidth = img.width;
+            var bufferHeight = img.height;
+
             // Getting height map data
-            var canvas = document.createElement("canvas");
+            var canvas = CanvasGenerator.CreateCanvas(bufferWidth, bufferHeight);
             var context = canvas.getContext("2d");
 
             if (!context) {
@@ -395,11 +399,6 @@ export class GroundBuilder {
                 return;
             }
 
-            var bufferWidth = img.width;
-            var bufferHeight = img.height;
-            canvas.width = bufferWidth;
-            canvas.height = bufferHeight;
-
             context.drawImage(img, 0, 0);
 
             // Create VertexData from map data

+ 12 - 6
src/Meshes/buffer.ts

@@ -12,6 +12,7 @@ export class Buffer {
     public _data: Nullable<DataArray>;
     private _updatable: boolean;
     private _instanced: boolean;
+    private _divisor: number;
 
     /**
      * Gets the byte stride.
@@ -27,8 +28,9 @@ export class Buffer {
      * @param postponeInternalCreation whether to postpone creating the internal WebGL buffer (optional)
      * @param instanced whether the buffer is instanced (optional)
      * @param useBytes set to true if the stride in in bytes (optional)
+     * @param divisor sets an optional divisor for instances (1 by default)
      */
-    constructor(engine: any, data: DataArray, updatable: boolean, stride = 0, postponeInternalCreation = false, instanced = false, useBytes = false) {
+    constructor(engine: any, data: DataArray, updatable: boolean, stride = 0, postponeInternalCreation = false, instanced = false, useBytes = false, divisor?: number) {
         if (engine.getScene) { // old versions of VertexBuffer accepted 'mesh' instead of 'engine'
             this._engine = engine.getScene().getEngine();
         }
@@ -38,6 +40,7 @@ export class Buffer {
 
         this._updatable = updatable;
         this._instanced = instanced;
+        this._divisor = divisor || 1;
 
         this._data = data;
 
@@ -55,15 +58,16 @@ export class Buffer {
      * @param size defines the size in floats of attributes (position is 3 for instance)
      * @param stride defines the stride size in floats in the buffer (the offset to apply to reach next value when data is interleaved)
      * @param instanced defines if the vertex buffer contains indexed data
-     * @param useBytes defines if the offset and stride are in bytes
+     * @param useBytes defines if the offset and stride are in bytes     *
+     * @param divisor sets an optional divisor for instances (1 by default)
      * @returns the new vertex buffer
      */
-    public createVertexBuffer(kind: string, offset: number, size: number, stride?: number, instanced?: boolean, useBytes = false): VertexBuffer {
+    public createVertexBuffer(kind: string, offset: number, size: number, stride?: number, instanced?: boolean, useBytes = false, divisor?: number): VertexBuffer {
         const byteOffset = useBytes ? offset : offset * Float32Array.BYTES_PER_ELEMENT;
         const byteStride = stride ? (useBytes ? stride : stride * Float32Array.BYTES_PER_ELEMENT) : this.byteStride;
 
         // a lot of these parameters are ignored as they are overriden by the buffer
-        return new VertexBuffer(this._engine, this, kind, this._updatable, true, byteStride, instanced === undefined ? this._instanced : instanced, byteOffset, size, undefined, undefined, true);
+        return new VertexBuffer(this._engine, this, kind, this._updatable, true, byteStride, instanced === undefined ? this._instanced : instanced, byteOffset, size, undefined, undefined, true, this._divisor || divisor);
     }
 
     // Properties
@@ -274,8 +278,10 @@ export class VertexBuffer {
      * @param type the type of the component (optional)
      * @param normalized whether the data contains normalized data (optional)
      * @param useBytes set to true if stride and offset are in bytes (optional)
+     * @param divisor defines the instance divisor to use (1 by default)
      */
-    constructor(engine: any, data: DataArray | Buffer, kind: string, updatable: boolean, postponeInternalCreation?: boolean, stride?: number, instanced?: boolean, offset?: number, size?: number, type?: number, normalized = false, useBytes = false) {
+    constructor(engine: any, data: DataArray | Buffer, kind: string, updatable: boolean, postponeInternalCreation?: boolean, stride?: number,
+        instanced?: boolean, offset?: number, size?: number, type?: number, normalized = false, useBytes = false, divisor = 1) {
         if (data instanceof Buffer) {
             this._buffer = data;
             this._ownsBuffer = false;
@@ -316,7 +322,7 @@ export class VertexBuffer {
         this.normalized = normalized;
 
         this._instanced = instanced !== undefined ? instanced : false;
-        this._instanceDivisor = instanced ? 1 : 0;
+        this._instanceDivisor = instanced ? divisor : 0;
     }
 
     /** @hidden */

+ 2 - 2
src/Meshes/instancedMesh.ts

@@ -286,8 +286,8 @@ export class InstancedMesh extends AbstractMesh {
         }
 
         if (this._currentLOD) {
-
-            if (this._currentLOD._getWorldMatrixDeterminant() !== this._getWorldMatrixDeterminant()) {
+            let differentSign = (this._currentLOD._getWorldMatrixDeterminant() > 0) !== (this._getWorldMatrixDeterminant() > 0);
+            if (differentSign) {
                 this._internalAbstractMeshDataInfo._actAsRegularMesh = true;
                 return true;
             }

+ 4 - 5
src/Meshes/mesh.ts

@@ -34,6 +34,7 @@ import { MeshLODLevel } from './meshLODLevel';
 import { Path3D } from '../Maths/math.path';
 import { Plane } from '../Maths/math.plane';
 import { TransformNode } from './transformNode';
+import { CanvasGenerator } from '../Misc/canvasGenerator';
 
 declare type LinesMesh = import("./linesMesh").LinesMesh;
 declare type InstancedMesh = import("./instancedMesh").InstancedMesh;
@@ -2218,14 +2219,12 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
     public applyDisplacementMap(url: string, minHeight: number, maxHeight: number, onSuccess?: (mesh: Mesh) => void, uvOffset?: Vector2, uvScale?: Vector2, forceUpdate = false): Mesh {
         var scene = this.getScene();
 
-        var onload = (img: HTMLImageElement) => {
+        var onload = (img: HTMLImageElement | ImageBitmap) => {
             // Getting height map data
-            var canvas = document.createElement("canvas");
-            var context = <CanvasRenderingContext2D>canvas.getContext("2d");
             var heightMapWidth = img.width;
             var heightMapHeight = img.height;
-            canvas.width = heightMapWidth;
-            canvas.height = heightMapHeight;
+            var canvas = CanvasGenerator.CreateCanvas(heightMapWidth, heightMapHeight);
+            var context = <CanvasRenderingContext2D>canvas.getContext("2d");
 
             context.drawImage(img, 0, 0);
 

+ 22 - 0
src/Misc/canvasGenerator.ts

@@ -0,0 +1,22 @@
+/**
+ * Helper class used to generate a canvas to manipulate images
+ */
+export class CanvasGenerator {
+    /**
+     * Create a new canvas (or offscreen canvas depending on the context)
+     * @param width defines the expected width
+     * @param height defines the expected height
+     * @return a new canvas or offscreen canvas
+     */
+    public static CreateCanvas(width: number, height: number): HTMLCanvasElement | OffscreenCanvas {
+        if (typeof document === "undefined") {
+            return new OffscreenCanvas(width, height);
+        }
+
+        let canvas = document.createElement("canvas");
+        canvas.width = width;
+        canvas.height = height;
+
+        return canvas;
+    }
+}

+ 66 - 44
src/Misc/environmentTextureTools.ts

@@ -381,6 +381,50 @@ export class EnvironmentTextureTools {
         return EnvironmentTextureTools.UploadLevelsAsync(texture, imageData);
     }
 
+    private static _OnImageReadyAsync(image: HTMLImageElement | ImageBitmap, engine: Engine, expandTexture: boolean,
+        rgbdPostProcess:  Nullable<PostProcess>, url: string, face: number, i: number, generateNonLODTextures: boolean,
+        lodTextures: Nullable<{ [lod: number]: BaseTexture }>, cubeRtt: Nullable<InternalTexture>, texture: InternalTexture
+        ): Promise<void> {
+            return new Promise((resolve, reject) => {
+                if (expandTexture) {
+                    let tempTexture = engine.createTexture(null, true, true, null, Constants.TEXTURE_NEAREST_SAMPLINGMODE, null,
+                        (message) => {
+                            reject(message);
+                        },
+                        image);
+
+                    rgbdPostProcess!.getEffect().executeWhenCompiled(() => {
+                        // Uncompress the data to a RTT
+                        rgbdPostProcess!.onApply = (effect) => {
+                            effect._bindTexture("textureSampler", tempTexture);
+                            effect.setFloat2("scale", 1, 1);
+                        };
+
+                        engine.scenes[0].postProcessManager.directRender([rgbdPostProcess!], cubeRtt, true, face, i);
+
+                        // Cleanup
+                        engine.restoreDefaultFramebuffer();
+                        tempTexture.dispose();
+                        URL.revokeObjectURL(url);
+                        resolve();
+                    });
+                }
+                else {
+                    engine._uploadImageToTexture(texture, image, face, i);
+
+                    // Upload the face to the non lod texture support
+                    if (generateNonLODTextures) {
+                        let lodTexture = lodTextures![i];
+                        if (lodTexture) {
+                            engine._uploadImageToTexture(lodTexture._texture!, image, face, 0);
+                        }
+                    }
+                    resolve();
+                }
+            }
+        );
+    }
+
     /**
      * Uploads the levels of image data to the GPU.
      * @param texture defines the internal texture to upload to
@@ -502,52 +546,30 @@ export class EnvironmentTextureTools {
                 let bytes = imageData[i][face];
                 let blob = new Blob([bytes], { type: 'image/png' });
                 let url = URL.createObjectURL(blob);
-                let image = new Image();
-                image.src = url;
+                let promise: Promise<void>;
 
-                // Enqueue promise to upload to the texture.
-                let promise = new Promise<void>((resolve, reject) => {
-                    image.onload = () => {
-                        if (expandTexture) {
-                            let tempTexture = engine.createTexture(null, true, true, null, Constants.TEXTURE_NEAREST_SAMPLINGMODE, null,
-                                (message) => {
-                                    reject(message);
-                                },
-                                image);
-
-                            rgbdPostProcess!.getEffect().executeWhenCompiled(() => {
-                                // Uncompress the data to a RTT
-                                rgbdPostProcess!.onApply = (effect) => {
-                                    effect._bindTexture("textureSampler", tempTexture);
-                                    effect.setFloat2("scale", 1, 1);
-                                };
-
-                                engine.scenes[0].postProcessManager.directRender([rgbdPostProcess!], cubeRtt, true, face, i);
-
-                                // Cleanup
-                                engine.restoreDefaultFramebuffer();
-                                tempTexture.dispose();
-                                window.URL.revokeObjectURL(url);
-                                resolve();
+                if (typeof Image === "undefined") {
+                    promise = createImageBitmap(blob).then((img) => {
+                        return this._OnImageReadyAsync(img, engine, expandTexture, rgbdPostProcess, url, face, i, generateNonLODTextures, lodTextures, cubeRtt, texture);
+                    });
+                } else {
+                    let image = new Image();
+                    image.src = url;
+
+                    // Enqueue promise to upload to the texture.
+                    promise = new Promise<void>((resolve, reject) => {
+                        image.onload = () => {
+                            this._OnImageReadyAsync(image, engine, expandTexture, rgbdPostProcess, url, face, i, generateNonLODTextures, lodTextures, cubeRtt, texture)
+                            .then(() => resolve())
+                            .catch((reason) => {
+                                reject(reason);
                             });
-                        }
-                        else {
-                            engine._uploadImageToTexture(texture, image, face, i);
-
-                            // Upload the face to the non lod texture support
-                            if (generateNonLODTextures) {
-                                let lodTexture = lodTextures![i];
-                                if (lodTexture) {
-                                    engine._uploadImageToTexture(lodTexture._texture!, image, face, 0);
-                                }
-                            }
-                            resolve();
-                        }
-                    };
-                    image.onerror = (error) => {
-                        reject(error);
-                    };
-                });
+                        };
+                        image.onerror = (error) => {
+                            reject(error);
+                        };
+                    });
+                }
                 promises.push(promise);
             }
         }

+ 23 - 2
src/Misc/fileTools.ts

@@ -141,13 +141,13 @@ export class FileTools {
      * @param mimeType optional mime type
      * @returns the HTMLImageElement of the loaded image
      */
-    public static LoadImage(input: string | ArrayBuffer | ArrayBufferView | Blob, onLoad: (img: HTMLImageElement) => void, onError: (message?: string, exception?: any) => void, offlineProvider: Nullable<IOfflineProvider>, mimeType?: string): HTMLImageElement {
+    public static LoadImage(input: string | ArrayBuffer | ArrayBufferView | Blob, onLoad: (img: HTMLImageElement | ImageBitmap) => void, onError: (message?: string, exception?: any) => void, offlineProvider: Nullable<IOfflineProvider>, mimeType?: string): Nullable<HTMLImageElement> {
         let url: string;
         let usingObjectURL = false;
 
         if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
             if (typeof Blob !== 'undefined') {
-               url = URL.createObjectURL(new Blob([input]));
+                url = URL.createObjectURL(new Blob([input]));
                 usingObjectURL = true;
             } else {
                 url = `data:${mimeType || "image/jpg"};base64,` + this._ArrayBufferToBase64(input);
@@ -162,6 +162,27 @@ export class FileTools {
             url = this.PreprocessUrl(input);
         }
 
+        if (typeof Image === "undefined") {
+            this.LoadFile(url, (data) => {
+                createImageBitmap(new Blob([data])).then((imgBmp) => {
+                    onLoad(imgBmp);
+                    if (usingObjectURL) {
+                        URL.revokeObjectURL(url);
+                    }
+                }).catch((reason) => {
+                    if (onError) {
+                        onError("Error while trying to load image: " + input, reason);
+                    }
+                });
+            }, undefined, offlineProvider || undefined, true, (request, exception) => {
+                if (onError) {
+                    onError("Error while trying to load image: " + input, exception);
+                }
+            });
+
+            return null;
+        }
+
         var img = new Image();
         this.SetCorsBehavior(url, img);
 

+ 1 - 0
src/Misc/index.ts

@@ -40,5 +40,6 @@ export * from "./fileRequest";
 export * from "./customAnimationFrameRequester";
 export * from "./retryStrategy";
 export * from "./interfaces/screenshotSize";
+export * from "./canvasGenerator";
 export * from "./fileTools";
 export * from "./stringTools";

+ 1 - 1
src/Misc/tools.ts

@@ -363,7 +363,7 @@ export class Tools {
     * @param mimeType optional mime type
     * @returns the HTMLImageElement of the loaded image
     */
-    public static LoadImage(input: string | ArrayBuffer | Blob, onLoad: (img: HTMLImageElement) => void, onError: (message?: string, exception?: any) => void, offlineProvider: Nullable<IOfflineProvider>, mimeType?: string): HTMLImageElement {
+    public static LoadImage(input: string | ArrayBuffer | Blob, onLoad: (img: HTMLImageElement | ImageBitmap) => void, onError: (message?: string, exception?: any) => void, offlineProvider: Nullable<IOfflineProvider>, mimeType?: string): Nullable<HTMLImageElement> {
         return FileTools.LoadImage(input, onLoad, onError, offlineProvider, mimeType);
     }
 

+ 1 - 1
src/Offline/database.ts

@@ -25,7 +25,7 @@ export class Database implements IOfflineProvider {
     private _isSupported: boolean;
 
     // Handling various flavors of prefixed version of IndexedDB
-    private _idbFactory = <IDBFactory>(window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB);
+    private _idbFactory = <IDBFactory>(typeof window !== "undefined" ? window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB : indexedDB);
 
     /** Gets a boolean indicating if the user agent supports blob storage (this value will be updated after creating the first Database object) */
     private static IsUASupportingBlobStorage = true;

+ 1 - 2
src/Particles/particleHelper.ts

@@ -10,7 +10,6 @@ import { IParticleSystem } from "./IParticleSystem";
 import { GPUParticleSystem } from "./gpuParticleSystem";
 import { ParticleSystemSet } from "./particleSystemSet";
 import { ParticleSystem } from "./particleSystem";
-import { Constants } from "../Engines/constants";
 /**
  * This class is made for on one-liner static method to help creating particle system set.
  */
@@ -18,7 +17,7 @@ export class ParticleHelper {
     /**
      * Gets or sets base Assets URL
      */
-    public static BaseAssetsUrl = Constants.PARTICLES_BaseAssetsUrl;
+    public static BaseAssetsUrl = ParticleSystemSet.BaseAssetsUrl;
 
     /**
      * Create a default particle system that you can tweak

+ 6 - 2
src/Particles/particleSystemSet.ts

@@ -9,7 +9,6 @@ import { EngineStore } from "../Engines/engineStore";
 import { ParticleSystem } from "../Particles/particleSystem";
 import { Scene, IDisposable } from "../scene";
 import { StandardMaterial } from "../Materials/standardMaterial";
-import { Constants } from "../Engines/constants";
 
 /** Internal class used to store shapes for emitters */
 class ParticleSystemSetEmitterCreationOptions {
@@ -22,6 +21,11 @@ class ParticleSystemSetEmitterCreationOptions {
  * Represents a set of particle systems working together to create a specific effect
  */
 export class ParticleSystemSet implements IDisposable {
+    /**
+     * Gets or sets base Assets URL
+     */
+    public static BaseAssetsUrl = "https://assets.babylonjs.com/particles";
+
     private _emitterCreationOptions: ParticleSystemSetEmitterCreationOptions;
     private _emitterNode: Nullable<TransformNode>;
 
@@ -125,7 +129,7 @@ export class ParticleSystemSet implements IDisposable {
      */
     public static Parse(data: any, scene: Scene, gpu = false): ParticleSystemSet {
         var result = new ParticleSystemSet();
-        var rootUrl = Constants.PARTICLES_BaseAssetsUrl + "/textures/";
+        var rootUrl = this.BaseAssetsUrl + "/textures/";
 
         scene = scene || EngineStore.LastCreatedScene;
 

+ 20 - 2
src/Particles/solidParticle.ts

@@ -244,6 +244,21 @@ export class ModelShape {
      */
     public _shapeUV: number[];
     /**
+     * color array of the model
+     * @hidden
+     */
+    public _shapeColors: number[];
+    /**
+     * indices array of the model
+     * @hidden
+     */
+    public _indices: number[];
+    /**
+     * normals array of the model
+     * @hidden
+     */
+    public _normals: number[];
+    /**
      * length of the shape in the model indices array (internal use)
      * @hidden
      */
@@ -264,12 +279,15 @@ export class ModelShape {
      * SPS internal tool, don't use it manually.
      * @hidden
      */
-    constructor(id: number, shape: Vector3[], indicesLength: number, shapeUV: number[],
+    constructor(id: number, shape: Vector3[], indices: number[], normals: number[], colors: number[], shapeUV: number[],
         posFunction: Nullable<(particle: SolidParticle, i: number, s: number) => void>, vtxFunction: Nullable<(particle: SolidParticle, vertex: Vector3, i: number) => void>) {
         this.shapeID = id;
         this._shape = shape;
-        this._indicesLength = indicesLength;
+        this._indices = indices;
+        this._indicesLength = indices.length;
         this._shapeUV = shapeUV;
+        this._shapeColors = colors;
+        this._normals = normals;
         this._positionFunction = posFunction;
         this._vertexFunction = vtxFunction;
     }

+ 90 - 19
src/Particles/solidParticleSystem.ts

@@ -116,6 +116,7 @@ export class SolidParticleSystem implements IDisposable {
     private _mustUnrotateFixedNormals = false;
     private _particlesIntersect: boolean = false;
     private _needs32Bits: boolean = false;
+    private _isNotBuilt: boolean = true;
 
     /**
      * Creates a SPS (Solid Particle System) object.
@@ -160,7 +161,10 @@ export class SolidParticleSystem implements IDisposable {
      * @returns the created mesh
      */
     public buildMesh(): Mesh {
-        if (this.nbParticles === 0) {
+        if (!this._isNotBuilt && this.mesh) {
+            return this.mesh;
+        }
+        if (this.nbParticles === 0 && !this.mesh) {
             var triangle = DiscBuilder.CreateDisc("", { radius: 1, tessellation: 3 }, this._scene);
             this.addShape(triangle, 1);
             triangle.dispose();
@@ -210,6 +214,7 @@ export class SolidParticleSystem implements IDisposable {
                 this.particles.length = 0;
             }
         }
+        this._isNotBuilt = false;
         return this.mesh;
     }
 
@@ -245,6 +250,7 @@ export class SolidParticleSystem implements IDisposable {
         }
 
         var facetPos: number[] = [];      // submesh positions
+        var facetNor: number[] = [];
         var facetInd: number[] = [];      // submesh indices
         var facetUV: number[] = [];       // submesh UV
         var facetCol: number[] = [];      // submesh colors
@@ -258,6 +264,7 @@ export class SolidParticleSystem implements IDisposable {
             }
             // reset temp arrays
             facetPos.length = 0;
+            facetNor.length = 0;
             facetInd.length = 0;
             facetUV.length = 0;
             facetCol.length = 0;
@@ -267,12 +274,16 @@ export class SolidParticleSystem implements IDisposable {
             for (var j = f * 3; j < (f + size) * 3; j++) {
                 facetInd.push(fi);
                 var i: number = meshInd[j];
-                facetPos.push(meshPos[i * 3], meshPos[i * 3 + 1], meshPos[i * 3 + 2]);
+                var i3: number = i * 3;
+                facetPos.push(meshPos[i3], meshPos[i3 + 1], meshPos[i3 + 2]);
+                facetNor.push(meshNor[i3], meshNor[i3 + 1], meshNor[i3 + 2]);
                 if (meshUV) {
-                    facetUV.push(meshUV[i * 2], meshUV[i * 2 + 1]);
+                    var i2: number = i * 2;
+                    facetUV.push(meshUV[i2], meshUV[i2 + 1]);
                 }
                 if (meshCol) {
-                    facetCol.push(meshCol[i * 4], meshCol[i * 4 + 1], meshCol[i * 4 + 2], meshCol[i * 4 + 3]);
+                    var i4: number = i * 4;
+                    facetCol.push(meshCol[i4], meshCol[i4 + 1], meshCol[i4 + 2], meshCol[i4 + 3]);
                 }
                 fi++;
             }
@@ -281,6 +292,9 @@ export class SolidParticleSystem implements IDisposable {
             var idx: number = this.nbParticles;
             var shape: Vector3[] = this._posToShape(facetPos);
             var shapeUV: number[] = this._uvsToShapeUV(facetUV);
+            var shapeInd = Array.from(facetInd);
+            var shapeCol = Array.from(facetCol);
+            var shapeNor = Array.from(facetNor);
 
             // compute the barycenter of the shape
             barycenter.copyFromFloats(0, 0, 0);
@@ -303,12 +317,12 @@ export class SolidParticleSystem implements IDisposable {
             if (this._particlesIntersect) {
                 bInfo = new BoundingInfo(minimum, maximum);
             }
-            var modelShape = new ModelShape(this._shapeCounter, shape, size * 3, shapeUV, null, null);
+            var modelShape = new ModelShape(this._shapeCounter, shape, shapeInd, shapeNor, shapeCol, shapeUV, null, null);
 
             // add the particle in the SPS
             var currentPos = this._positions.length;
             var currentInd = this._indices.length;
-            this._meshBuilder(this._index, shape, this._positions, facetInd, this._indices, facetUV, this._uvs, facetCol, this._colors, meshNor, this._normals, idx, 0, null);
+            this._meshBuilder(this._index, shape, this._positions, shapeInd, this._indices, facetUV, this._uvs, shapeCol, this._colors, shapeNor, this._normals, idx, 0, null);
             this._addParticle(idx, currentPos, currentInd, modelShape, this._shapeCounter, 0, bInfo);
             // initialize the particle position
             this.particles[this.nbParticles].position.addInPlace(barycenter);
@@ -319,6 +333,7 @@ export class SolidParticleSystem implements IDisposable {
             this._shapeCounter++;
             f += size;
         }
+        this._isNotBuilt = true;        // buildMesh() is now expected for setParticles() to work
         return this;
     }
 
@@ -508,6 +523,9 @@ export class SolidParticleSystem implements IDisposable {
         var meshUV = <FloatArray>mesh.getVerticesData(VertexBuffer.UVKind);
         var meshCol = <FloatArray>mesh.getVerticesData(VertexBuffer.ColorKind);
         var meshNor = <FloatArray>mesh.getVerticesData(VertexBuffer.NormalKind);
+        var indices = Array.from(meshInd);
+        var shapeNormals = Array.from(meshNor);
+        var shapeColors = (meshCol) ? Array.from(meshCol) : [];
         var bbInfo;
         if (this._particlesIntersect) {
             bbInfo = mesh.getBoundingInfo();
@@ -519,7 +537,7 @@ export class SolidParticleSystem implements IDisposable {
         var posfunc = options ? options.positionFunction : null;
         var vtxfunc = options ? options.vertexFunction : null;
 
-        var modelShape = new ModelShape(this._shapeCounter, shape, meshInd.length, shapeUV, posfunc, vtxfunc);
+        var modelShape = new ModelShape(this._shapeCounter, shape, indices, shapeNormals, shapeColors, shapeUV, posfunc, vtxfunc);
 
         // particles
         var sp;
@@ -547,11 +565,12 @@ export class SolidParticleSystem implements IDisposable {
         }
         this.nbParticles += nb;
         this._shapeCounter++;
+        this._isNotBuilt = true;        // buildMesh() is now expected for setParticles() to work
         return this._shapeCounter - 1;
     }
 
     // rebuilds a particle back to its just built status : if needed, recomputes the custom positions and vertices
-    private _rebuildParticle(particle: SolidParticle): void {
+    private _rebuildParticle(particle: SolidParticle, reset: boolean = false): void {
         this._resetCopy();
         const copy = this._copy;
         if (particle._model._positionFunction) {        // recall to stored custom positionFunction
@@ -587,39 +606,91 @@ export class SolidParticleSystem implements IDisposable {
             Vector3.TransformCoordinatesToRef(tmpVertex, rotMatrix, tmpRotated);
             tmpRotated.addInPlace(pivotBackTranslation).addInPlace(copy.position).toArray(this._positions32, particle._pos + pt * 3);
         }
-        particle.position.setAll(0.0);
-        particle.rotation.setAll(0.0);
-        particle.rotationQuaternion = null;
-        particle.scaling.setAll(1.0);
-        particle.uvs.setAll(0.0);
-        particle.pivot.setAll(0.0);
-        particle.translateFromPivot = false;
-        particle.parentId = null;
+        if (reset) {
+            particle.position.setAll(0.0);
+            particle.rotation.setAll(0.0);
+            particle.rotationQuaternion = null;
+            particle.scaling.setAll(1.0);
+            particle.uvs.setAll(0.0);
+            particle.pivot.setAll(0.0);
+            particle.translateFromPivot = false;
+            particle.parentId = null;
+        }
     }
 
     /**
      * Rebuilds the whole mesh and updates the VBO : custom positions and vertices are recomputed if needed.
+     * @param reset boolean, default false : if the particles must be reset at position and rotation zero, scaling 1, color white, initial UVs and not parented.
      * @returns the SPS.
      */
-    public rebuildMesh(): SolidParticleSystem {
+    public rebuildMesh(reset: boolean = false): SolidParticleSystem {
         for (var p = 0; p < this.particles.length; p++) {
-            this._rebuildParticle(this.particles[p]);
+            this._rebuildParticle(this.particles[p], reset);
         }
         this.mesh.updateVerticesData(VertexBuffer.PositionKind, this._positions32, false, false);
         return this;
     }
+    /** Removes the particles from the start-th to the end-th included from an expandable SPS (required).
+     *  Returns an array with the removed particles.
+     *  If the number of particles to remove is lower than zero or greater than the global remaining particle number, then an empty array is returned.
+     *  The SPS can't be empty so at least one particle needs to remain in place.
+     *  Under the hood, the VertexData array, so the VBO buffer, is recreated each call.
+     * @param start index of the first particle to remove
+     * @param end index of the last particle to remove (included)
+     * @returns an array populated with the removed particles
+     */
+    public removeParticles(start: number, end: number): SolidParticle[] {
+        var nb = end - start + 1;
+        if (!this._expandable || nb <= 0 || nb >= this.nbParticles) {
+            return [];
+        }
+        const particles = this.particles;
+        const currentNb = this.nbParticles;
+        if (end < currentNb - 1) {              // update the particle indexes in the positions array in case they're remaining particles after the last removed
+            var startPositionIndex = particles[start]._pos;
+            var firstRemaining = end + 1;
+            var shift = particles[firstRemaining]._pos - startPositionIndex;
+            for (var i = firstRemaining; i < currentNb; i++) {
+                particles[i]._pos -= shift;
+            }
+        }
+        var removed = particles.splice(start, nb);
+        this._positions.length = 0;
+        this._indices.length = 0;
+        this._colors.length = 0;
+        this._uvs.length = 0;
+        this._normals.length = 0;
+        this._index = 0;
+        const particlesLength = particles.length;
+        for (var p = 0; p < particlesLength; p++) {
+            var particle = particles[p];
+            var model = particle._model;
+            var shape = model._shape;
+            var modelIndices = model._indices;
+            var modelNormals = model._normals;
+            var modelColors = model._shapeColors;
+            var modelUVs = model._shapeUV;
+            this._meshBuilder(this._index, shape, this._positions, modelIndices, this._indices, modelUVs, this._uvs, modelColors, this._colors, modelNormals, this._normals, particle.idx, particle.idxInShape, null);
+            this._index += shape.length;
+        }
+
+        this.nbParticles -= nb;
+        this._isNotBuilt = true;        // buildMesh() is now expected for setParticles() to work
+        return removed;
+    }
 
     /**
      *  Sets all the particles : this method actually really updates the mesh according to the particle positions, rotations, colors, textures, etc.
      *  This method calls `updateParticle()` for each particle of the SPS.
      *  For an animated SPS, it is usually called within the render loop.
+     * This methods does nothing if called on a non updatable or not yet built SPS. Example : buildMesh() not called after having added or removed particles from an expandable SPS.
      * @param start The particle index in the particle array where to start to compute the particle property values _(default 0)_
      * @param end The particle index in the particle array where to stop to compute the particle property values _(default nbParticle - 1)_
      * @param update If the mesh must be finally updated on this call after all the particle computations _(default true)_
      * @returns the SPS.
      */
     public setParticles(start: number = 0, end: number = this.nbParticles - 1, update: boolean = true): SolidParticleSystem {
-        if (!this._updatable) {
+        if (!this._updatable || this._isNotBuilt) {
             return this;
         }
 

+ 6 - 4
src/Sprites/spriteSceneComponent.ts

@@ -318,10 +318,12 @@ export class SpriteSceneComponent implements ISceneComponent {
 
             if (pickResult && pickResult.hit && pickResult.pickedSprite) {
                 scene.setPointerOverSprite(pickResult.pickedSprite);
-                if (scene._pointerOverSprite && scene._pointerOverSprite.actionManager && scene._pointerOverSprite.actionManager.hoverCursor) {
-                    canvas.style.cursor = scene._pointerOverSprite.actionManager.hoverCursor;
-                } else {
-                    canvas.style.cursor = scene.hoverCursor;
+                if (!scene.doNotHandleCursors) {
+                    if (scene._pointerOverSprite && scene._pointerOverSprite.actionManager && scene._pointerOverSprite.actionManager.hoverCursor) {
+                        canvas.style.cursor = scene._pointerOverSprite.actionManager.hoverCursor;
+                    } else {
+                        canvas.style.cursor = scene.hoverCursor;
+                    }
                 }
             } else {
                 scene.setPointerOverSprite(null);

+ 4 - 0
src/scene.ts

@@ -331,6 +331,10 @@ export class Scene extends AbstractScene implements IAnimatable {
      */
     public defaultCursor: string = "";
     /**
+     * Defines wether cursors are handled by the scene.
+     */
+    public doNotHandleCursors = false;
+    /**
      * This is used to call preventDefault() on pointer down
      * in order to block unwanted artifacts like system double clicks
      */