Преглед изворни кода

Deterministic lockstep for physics and animations for frame-rate independence in game logic.

Time is quantized according to PhysicsEngine timeStep, performing a maximum of lockstepMaxSteps in case of late-frames.

Two new observable events are added to plug game-logic code, via registerBeforeStep and registerAfterStep, which allow to get current stepId via scene.getStepId().
Gianni пре 8 година
родитељ
комит
ae86779435

Разлика између датотеке није приказан због своје велике величине
+ 8003 - 7967
dist/preview release/babylon.d.ts


Разлика између датотеке није приказан због своје велике величине
+ 38 - 38
dist/preview release/babylon.js


+ 137 - 22
dist/preview release/babylon.max.js

@@ -7478,6 +7478,9 @@ var BABYLON;
             this._drawCalls = new BABYLON.PerfCounter();
             this._renderingQueueLaunched = false;
             this._activeRenderLoops = [];
+            // Deterministic lockstepMaxSteps
+            this._deterministicLockstep = false;
+            this._lockstepMaxSteps = 4;
             // FPS
             this._performanceMonitor = new BABYLON.PerformanceMonitor();
             this._fps = 60;
@@ -7530,6 +7533,12 @@ var BABYLON;
                 if (antialias != null) {
                     options.antialias = antialias;
                 }
+                if (options.deterministicLockstep === undefined) {
+                    options.deterministicLockstep = false;
+                }
+                if (options.lockstepMaxSteps === undefined) {
+                    options.lockstepMaxSteps = 4;
+                }
                 if (options.preserveDrawingBuffer === undefined) {
                     options.preserveDrawingBuffer = false;
                 }
@@ -7539,6 +7548,8 @@ var BABYLON;
                 if (options.stencil === undefined) {
                     options.stencil = true;
                 }
+                this._deterministicLockstep = options.deterministicLockstep;
+                this._lockstepMaxSteps = options.lockstepMaxSteps;
                 // GL
                 if (!options.disableWebGL2Support) {
                     try {
@@ -7620,7 +7631,7 @@ var BABYLON;
             // Constants
             this._gl.HALF_FLOAT_OES = 0x8D61; // Half floating-point type (16-bit).
             this._gl.RGBA16F = 0x881A; // RGBA 16-bit floating-point color-renderable internal sized format.
-            this._gl.RGBA32F = 0x8814; // RGBA 32-bit floating-point color-renderable internal sized format.         
+            this._gl.RGBA32F = 0x8814; // RGBA 32-bit floating-point color-renderable internal sized format.
             this._gl.DEPTH24_STENCIL8 = 35056;
             // Extensions
             this._caps.standardDerivatives = this._webGLVersion > 1 || (this._gl.getExtension('OES_standard_derivatives') !== null);
@@ -7648,7 +7659,7 @@ var BABYLON;
             }
             this._caps.textureHalfFloatRender = this._caps.textureHalfFloat && this._canRenderToHalfFloatFramebuffer();
             this._caps.textureLOD = this._webGLVersion > 1 || this._gl.getExtension('EXT_shader_texture_lod');
-            // Vertex array object 
+            // Vertex array object
             if (this._webGLVersion > 1) {
                 this._caps.vertexArrayObject = true;
             }
@@ -7664,7 +7675,7 @@ var BABYLON;
                     this._caps.vertexArrayObject = false;
                 }
             }
-            // Instances count            
+            // Instances count
             if (this._webGLVersion > 1) {
                 this._caps.instancedArrays = true;
             }
@@ -7682,7 +7693,7 @@ var BABYLON;
             }
             // Intelligently add supported compressed formats in order to check for.
             // Check for ASTC support first as it is most powerful and to be very cross platform.
-            // Next PVRTC & DXT, which are probably superior to ETC1/2.  
+            // Next PVRTC & DXT, which are probably superior to ETC1/2.
             // Likely no hardware which supports both PVR & DXT, so order matters little.
             // ETC2 is newer and handles ETC1 (no alpha capability), so check for first.
             if (this._caps.astc)
@@ -8186,6 +8197,12 @@ var BABYLON;
                 this._activeTexturesCache[index] = null;
             }
         };
+        Engine.prototype.isDeterministicLockStep = function () {
+            return this._deterministicLockstep;
+        };
+        Engine.prototype.getLockstepMaxSteps = function () {
+            return this._lockstepMaxSteps;
+        };
         Engine.prototype.getGlInfo = function () {
             return {
                 vendor: this._glVendor,
@@ -9277,7 +9294,7 @@ var BABYLON;
         Engine.prototype.setState = function (culling, zOffset, force, reverseSide) {
             if (zOffset === void 0) { zOffset = 0; }
             if (reverseSide === void 0) { reverseSide = false; }
-            // Culling        
+            // Culling
             var showSide = reverseSide ? this._gl.FRONT : this._gl.BACK;
             var hideSide = reverseSide ? this._gl.BACK : this._gl.FRONT;
             var cullFace = this.cullBackFaces ? showSide : hideSide;
@@ -9385,7 +9402,7 @@ var BABYLON;
             }
             this.resetTextureCache();
             this._currentEffect = null;
-            // 6/8/2017: deltakosh: Should not be required anymore. 
+            // 6/8/2017: deltakosh: Should not be required anymore.
             // This message is then mostly for the future myself which will scream out loud when seeing that actually it was required :)
             if (bruteForce) {
                 this._currentProgram = null;
@@ -15958,6 +15975,16 @@ var BABYLON;
             */
             this.onMeshRemovedObservable = new BABYLON.Observable();
             /**
+            * An event triggered before calculating deterministic simulation step
+            * @type {BABYLON.Observable}
+            */
+            this.onBeforeStepObservable = new BABYLON.Observable();
+            /**
+            * An event triggered after calculating deterministic simulation step
+            * @type {BABYLON.Observable}
+            */
+            this.onAfterStepObservable = new BABYLON.Observable();
+            /**
              * This Observable will be triggered for each stage of each renderingGroup of each rendered camera.
              * The RenderinGroupInfo class contains all the information about the context in which the observable is called
              * If you wish to register an Observer only for a given set of renderingGroup, use the mask with a combination of the renderingGroup index elevated to the power of two (1 for renderingGroup 0, 2 for renderingrOup1, 4 for 2 and 8 for 3)
@@ -15986,6 +16013,10 @@ var BABYLON;
             this._previousStartingPointerPosition = new BABYLON.Vector2(0, 0);
             this._startingPointerTime = 0;
             this._previousStartingPointerTime = 0;
+            // Deterministic lockstep
+            this._timeAccumulator = 0;
+            this._currentStepId = 0;
+            this._currentInternalStep = 0;
             // Coordinate system
             /**
             * use right-handed coordinate system on this scene.
@@ -16267,6 +16298,26 @@ var BABYLON;
             enumerable: true,
             configurable: true
         });
+        Object.defineProperty(Scene.prototype, "beforeStep", {
+            set: function (callback) {
+                if (this.onBeforeStepObservable) {
+                    this.onBeforeStepObservable.remove(this._onBeforeStepObserver);
+                }
+                this._onBeforeStepObserver = this.onBeforeStepObservable.add(callback);
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Scene.prototype, "afterStep", {
+            set: function (callback) {
+                if (this.onAfterStepObservable) {
+                    this.onAfterStepObservable.remove(this._onAfterStepObserver);
+                }
+                this._onAfterStepObserver = this.onAfterStepObservable.add(callback);
+            },
+            enumerable: true,
+            configurable: true
+        });
         Object.defineProperty(Scene.prototype, "unTranslatedPointer", {
             get: function () {
                 return new BABYLON.Vector2(this._unTranslatedPointerX, this._unTranslatedPointerY);
@@ -16288,6 +16339,14 @@ var BABYLON;
             enumerable: true,
             configurable: true
         });
+        Scene.prototype.getStepId = function () {
+            return this._currentStepId;
+        };
+        ;
+        Scene.prototype.getInternalStep = function () {
+            return this._currentInternalStep;
+        };
+        ;
         Object.defineProperty(Scene.prototype, "fogEnabled", {
             get: function () {
                 return this._fogEnabled;
@@ -17134,6 +17193,18 @@ var BABYLON;
         Scene.prototype.unregisterAfterRender = function (func) {
             this.onAfterRenderObservable.removeCallback(func);
         };
+        Scene.prototype.registerBeforeStep = function (func) {
+            this.onBeforeStepObservable.add(func);
+        };
+        Scene.prototype.unregisterBeforeStep = function (func) {
+            this.onBeforeStepObservable.removeCallback(func);
+        };
+        Scene.prototype.registerAfterStep = function (func) {
+            this.onAfterStepObservable.add(func);
+        };
+        Scene.prototype.unregisterAfterStep = function (func) {
+            this.onAfterStepObservable.removeCallback(func);
+        };
         Scene.prototype._addPendingData = function (data) {
             this._pendingData.push(data);
         };
@@ -17314,7 +17385,7 @@ var BABYLON;
         Scene.prototype.removeMesh = function (toRemove) {
             var index = this.meshes.indexOf(toRemove);
             if (index !== -1) {
-                // Remove from the scene if mesh found 
+                // Remove from the scene if mesh found
                 this.meshes.splice(index, 1);
             }
             //notify the collision coordinator
@@ -17327,7 +17398,7 @@ var BABYLON;
         Scene.prototype.removeSkeleton = function (toRemove) {
             var index = this.skeletons.indexOf(toRemove);
             if (index !== -1) {
-                // Remove from the scene if found 
+                // Remove from the scene if found
                 this.skeletons.splice(index, 1);
             }
             return index;
@@ -17335,7 +17406,7 @@ var BABYLON;
         Scene.prototype.removeMorphTargetManager = function (toRemove) {
             var index = this.morphTargetManagers.indexOf(toRemove);
             if (index !== -1) {
-                // Remove from the scene if found 
+                // Remove from the scene if found
                 this.morphTargetManagers.splice(index, 1);
             }
             return index;
@@ -17343,7 +17414,7 @@ var BABYLON;
         Scene.prototype.removeLight = function (toRemove) {
             var index = this.lights.indexOf(toRemove);
             if (index !== -1) {
-                // Remove from the scene if mesh found 
+                // Remove from the scene if mesh found
                 this.lights.splice(index, 1);
                 this.sortLightsByPriority();
             }
@@ -17353,7 +17424,7 @@ var BABYLON;
         Scene.prototype.removeCamera = function (toRemove) {
             var index = this.cameras.indexOf(toRemove);
             if (index !== -1) {
-                // Remove from the scene if mesh found 
+                // Remove from the scene if mesh found
                 this.cameras.splice(index, 1);
             }
             // Remove from activeCameras
@@ -18222,15 +18293,47 @@ var BABYLON;
             if (this.simplificationQueue && !this.simplificationQueue.running) {
                 this.simplificationQueue.executeNext();
             }
-            // Animations
-            var deltaTime = Math.max(Scene.MinDeltaTime, Math.min(this._engine.getDeltaTime(), Scene.MaxDeltaTime));
-            this._animationRatio = deltaTime * (60.0 / 1000.0);
-            this._animate();
-            // Physics
-            if (this._physicsEngine) {
-                BABYLON.Tools.StartPerformanceCounter("Physics");
-                this._physicsEngine._step(deltaTime / 1000.0);
-                BABYLON.Tools.EndPerformanceCounter("Physics");
+            if (this._engine.isDeterministicLockStep()) {
+                var deltaTime = Math.max(Scene.MinDeltaTime, Math.min(this._engine.getDeltaTime(), Scene.MaxDeltaTime)) / 1000;
+                var defaultTimeStep = this._physicsEngine.getTimeStep();
+                var maxSubSteps = this._engine.getLockstepMaxSteps();
+                this._timeAccumulator += deltaTime;
+                // compute the amount of fixed steps we should have taken since the last step
+                var internalSteps = Math.floor(this._timeAccumulator / defaultTimeStep);
+                internalSteps = Math.min(internalSteps, maxSubSteps);
+                for (this._currentInternalStep = 0; this._currentInternalStep < internalSteps; this._currentInternalStep++) {
+                    this.onBeforeStepObservable.notifyObservers(this);
+                    // Animations
+                    this._animationRatio = defaultTimeStep * (60.0 / 1000.0);
+                    this._animate();
+                    // Physics
+                    if (this._physicsEngine) {
+                        BABYLON.Tools.StartPerformanceCounter("Physics");
+                        this._physicsEngine._step(defaultTimeStep);
+                        BABYLON.Tools.EndPerformanceCounter("Physics");
+                    }
+                    this._timeAccumulator -= defaultTimeStep;
+                    this.onAfterStepObservable.notifyObservers(this);
+                    this._currentStepId++;
+                    if ((internalSteps > 1) && (this._currentInternalStep != internalSteps - 1)) {
+                        // Q: can this be optimized by putting some code in the afterStep callback?
+                        // I had to put this code here, otherwise mesh attached to bones of another mesh skeleton,
+                        // would return incorrect positions for internal stepIds (non-rendered steps)
+                        this._evaluateActiveMeshes();
+                    }
+                }
+            }
+            else {
+                // Animations
+                var deltaTime = Math.max(Scene.MinDeltaTime, Math.min(this._engine.getDeltaTime(), Scene.MaxDeltaTime));
+                this._animationRatio = deltaTime * (60.0 / 1000.0);
+                this._animate();
+                // Physics
+                if (this._physicsEngine) {
+                    BABYLON.Tools.StartPerformanceCounter("Physics");
+                    this._physicsEngine._step(deltaTime / 1000.0);
+                    BABYLON.Tools.EndPerformanceCounter("Physics");
+                }
             }
             // Before render
             this.onBeforeRenderObservable.notifyObservers(this);
@@ -18518,7 +18621,7 @@ var BABYLON;
             if (this._depthRenderer) {
                 this._depthRenderer.dispose();
             }
-            // Smart arrays            
+            // Smart arrays
             if (this.activeCamera) {
                 this.activeCamera._activeMeshes.dispose();
                 this.activeCamera = null;
@@ -60144,6 +60247,12 @@ var BABYLON;
             if (newTimeStep === void 0) { newTimeStep = 1 / 60; }
             this._physicsPlugin.setTimeStep(newTimeStep);
         };
+        /**
+         * Get the time step of the physics engine.
+         */
+        PhysicsEngine.prototype.getTimeStep = function () {
+            return this._physicsPlugin.getTimeStep();
+        };
         PhysicsEngine.prototype.dispose = function () {
             this._impostors.forEach(function (impostor) {
                 impostor.dispose();
@@ -60286,6 +60395,9 @@ var BABYLON;
         CannonJSPlugin.prototype.setTimeStep = function (timeStep) {
             this._fixedTimeStep = timeStep;
         };
+        CannonJSPlugin.prototype.getTimeStep = function () {
+            return this._fixedTimeStep;
+        };
         CannonJSPlugin.prototype.executeStep = function (delta, impostors) {
             // Delta is in seconds, should be provided in milliseconds
             this.world.step(this._fixedTimeStep, this._useDeltaForWorldStep ? delta * 1000 : 0, 3);
@@ -60581,7 +60693,7 @@ var BABYLON;
                 //calculate the translation
                 var translation = mesh.getBoundingInfo().boundingBox.centerWorld.subtract(center).subtract(mesh.position).negate();
                 this._tmpPosition.copyFromFloats(translation.x, translation.y - mesh.getBoundingInfo().boundingBox.extendSizeWorld.y, translation.z);
-                //add it inverted to the delta 
+                //add it inverted to the delta
                 this._tmpDeltaPosition.copyFrom(mesh.getBoundingInfo().boundingBox.centerWorld.subtract(c));
                 this._tmpDeltaPosition.y += mesh.getBoundingInfo().boundingBox.extendSizeWorld.y;
                 mesh.setPivotMatrix(oldPivot);
@@ -60724,6 +60836,9 @@ var BABYLON;
         OimoJSPlugin.prototype.setTimeStep = function (timeStep) {
             this.world.timeStep = timeStep;
         };
+        OimoJSPlugin.prototype.getTimeStep = function () {
+            return this.world.timeStep;
+        };
         OimoJSPlugin.prototype.executeStep = function (delta, impostors) {
             var _this = this;
             impostors.forEach(function (impostor) {

Разлика између датотеке није приказан због своје велике величине
+ 8003 - 7967
dist/preview release/babylon.module.d.ts


Разлика између датотеке није приказан због своје велике величине
+ 41 - 41
dist/preview release/babylon.worker.js


+ 5 - 1
src/Physics/Plugins/babylon.cannonJSPlugin.ts

@@ -28,6 +28,10 @@
             this._fixedTimeStep = timeStep;
         }
 
+        public getTimeStep(): number {
+          return this._fixedTimeStep;
+        }
+
         public executeStep(delta: number, impostors: Array<PhysicsImpostor>): void {
             // Delta is in seconds, should be provided in milliseconds
             this.world.step(this._fixedTimeStep, this._useDeltaForWorldStep ? delta * 1000 : 0, 3);
@@ -376,7 +380,7 @@
                 var translation = mesh.getBoundingInfo().boundingBox.centerWorld.subtract(center).subtract(mesh.position).negate();
 
                 this._tmpPosition.copyFromFloats(translation.x, translation.y - mesh.getBoundingInfo().boundingBox.extendSizeWorld.y, translation.z);
-                //add it inverted to the delta 
+                //add it inverted to the delta
                 this._tmpDeltaPosition.copyFrom(mesh.getBoundingInfo().boundingBox.centerWorld.subtract(c));
                 this._tmpDeltaPosition.y += mesh.getBoundingInfo().boundingBox.extendSizeWorld.y;
 

+ 6 - 2
src/Physics/Plugins/babylon.oimoJSPlugin.ts

@@ -22,6 +22,10 @@ module BABYLON {
             this.world.timeStep = timeStep;
         }
 
+        public getTimeStep(): number {
+          return this.world.timeStep;
+        }
+
         private _tmpImpostorsArray: Array<PhysicsImpostor> = [];
 
         public executeStep(delta: number, impostors: Array<PhysicsImpostor>) {
@@ -414,7 +418,7 @@ module BABYLON {
             mesh.position.x = body.position.x;
             mesh.position.y = body.position.y;
             mesh.position.z = body.position.z;
-            
+
             mesh.rotationQuaternion.x = body.orientation.x;
             mesh.rotationQuaternion.y = body.orientation.y;
             mesh.rotationQuaternion.z = body.orientation.z;
@@ -436,4 +440,4 @@ module BABYLON {
             this.world.clear();
         }
     }
-}
+}

+ 15 - 7
src/Physics/babylon.physicsEngine.ts

@@ -24,10 +24,10 @@
             this.gravity = gravity;
             this._physicsPlugin.setGravity(this.gravity);
         }
-        
+
         /**
          * Set the time step of the physics engine.
-         * default is 1/60. 
+         * default is 1/60.
          * To slow it down, enter 1/600 for example.
          * To speed it up, 1/30
          * @param {number} newTimeStep the new timestep to apply to this world.
@@ -36,6 +36,13 @@
             this._physicsPlugin.setTimeStep(newTimeStep);
         }
 
+        /**
+         * Get the time step of the physics engine.
+         */
+        public getTimeStep(): number {
+            return this._physicsPlugin.getTimeStep();
+        }
+
         public dispose(): void {
             this._impostors.forEach(function (impostor) {
                 impostor.dispose();
@@ -51,7 +58,7 @@
         public static Epsilon = 0.001;
 
         //new methods and parameters
-        
+
         private _impostors: Array<PhysicsImpostor> = [];
         private _joints: Array<PhysicsImpostorJoint> = [];
 
@@ -84,7 +91,7 @@
                 }
             }
         }
-        
+
         /**
          * Add a joint to the physics engine
          * @param {PhysicsImpostor} mainImpostor the main impostor to which the joint is added.
@@ -111,7 +118,7 @@
             if (matchingJoints.length) {
                 this._physicsPlugin.removeJoint(matchingJoints[0]);
                 //TODO remove it from the list as well
-                
+
             }
         }
 
@@ -139,7 +146,7 @@
         public getPhysicsPlugin(): IPhysicsEnginePlugin {
             return this._physicsPlugin;
         }
-        
+
         public getImpostorForPhysicsObject(object: IPhysicsEnabledObject) {
             for (var i = 0; i < this._impostors.length; ++i) {
                 if (this._impostors[i].object === object) {
@@ -162,6 +169,7 @@
         name: string;
         setGravity(gravity: Vector3);
         setTimeStep(timeStep: number);
+        getTimeStep(): number;
         executeStep(delta: number, impostors: Array<PhysicsImpostor>): void; //not forgetting pre and post events
         applyImpulse(impostor: PhysicsImpostor, force: Vector3, contactPoint: Vector3);
         applyForce(impostor: PhysicsImpostor, force: Vector3, contactPoint: Vector3);
@@ -193,4 +201,4 @@
         syncMeshWithImpostor(mesh:AbstractMesh, impostor:PhysicsImpostor);
         dispose();
     }
-}
+}

+ 41 - 16
src/babylon.engine.ts

@@ -272,6 +272,8 @@
         autoEnableWebVR?: boolean;
         disableWebGL2Support?: boolean;
         audioEngine?: boolean;
+        deterministicLockstep?: boolean;
+        lockstepMaxSteps?: number;
     }
 
     /**
@@ -563,7 +565,7 @@
          */
         public onCanvasBlurObservable = new Observable<Engine>();
 
-        //WebVR 
+        //WebVR
 
         //The new WebVR uses promises.
         //this promise resolves with the current devices available.
@@ -622,6 +624,10 @@
         private _renderingQueueLaunched = false;
         private _activeRenderLoops = [];
 
+        // Deterministic lockstepMaxSteps
+        private _deterministicLockstep: boolean = false;
+        private _lockstepMaxSteps: number = 4;
+
         // FPS
         private _performanceMonitor = new PerformanceMonitor();
         private _fps = 60;
@@ -723,6 +729,14 @@
                     options.antialias = antialias;
                 }
 
+                if (options.deterministicLockstep === undefined) {
+                    options.deterministicLockstep = false;
+                }
+
+                if (options.lockstepMaxSteps === undefined) {
+                    options.lockstepMaxSteps = 4;
+                }
+
                 if (options.preserveDrawingBuffer === undefined) {
                     options.preserveDrawingBuffer = false;
                 }
@@ -735,6 +749,9 @@
                     options.stencil = true;
                 }
 
+                this._deterministicLockstep = options.deterministicLockstep;
+                this._lockstepMaxSteps = options.lockstepMaxSteps;
+
                 // GL
                 if (!options.disableWebGL2Support) {
                     try {
@@ -829,7 +846,7 @@
             // Constants
             this._gl.HALF_FLOAT_OES = 0x8D61; // Half floating-point type (16-bit).
             this._gl.RGBA16F = 0x881A; // RGBA 16-bit floating-point color-renderable internal sized format.
-            this._gl.RGBA32F = 0x8814; // RGBA 32-bit floating-point color-renderable internal sized format.         
+            this._gl.RGBA32F = 0x8814; // RGBA 32-bit floating-point color-renderable internal sized format.
             this._gl.DEPTH24_STENCIL8 = 35056;
 
             // Extensions
@@ -865,7 +882,7 @@
 
             this._caps.textureLOD = this._webGLVersion > 1 || this._gl.getExtension('EXT_shader_texture_lod');
 
-            // Vertex array object 
+            // Vertex array object
             if (this._webGLVersion > 1) {
                 this._caps.vertexArrayObject = true;
             } else {
@@ -880,7 +897,7 @@
                     this._caps.vertexArrayObject = false;
                 }
             }
-            // Instances count            
+            // Instances count
             if (this._webGLVersion > 1) {
                 this._caps.instancedArrays = true;
             } else {
@@ -898,7 +915,7 @@
 
             // Intelligently add supported compressed formats in order to check for.
             // Check for ASTC support first as it is most powerful and to be very cross platform.
-            // Next PVRTC & DXT, which are probably superior to ETC1/2.  
+            // Next PVRTC & DXT, which are probably superior to ETC1/2.
             // Likely no hardware which supports both PVR & DXT, so order matters little.
             // ETC2 is newer and handles ETC1 (no alpha capability), so check for first.
             if (this._caps.astc) this.texturesSupported.push('-astc.ktx');
@@ -1014,6 +1031,14 @@
             }
         }
 
+        public isDeterministicLockStep(): boolean {
+          return this._deterministicLockstep;
+        }
+
+        public getLockstepMaxSteps(): number {
+          return this._lockstepMaxSteps;
+        }
+
         public getGlInfo() {
             return {
                 vendor: this._glVendor,
@@ -1496,7 +1521,7 @@
             }
 
             if (this._cachedViewport && !forceFullscreenViewport) {
-                this.setViewport(this._cachedViewport, requiredWidth, requiredHeight);            
+                this.setViewport(this._cachedViewport, requiredWidth, requiredHeight);
             } else {
                 gl.viewport(0, 0, requiredWidth || texture._width, requiredHeight || texture._height);
             }
@@ -2340,7 +2365,7 @@
 
         // States
         public setState(culling: boolean, zOffset: number = 0, force?: boolean, reverseSide = false): void {
-            // Culling        
+            // Culling
             var showSide = reverseSide ? this._gl.FRONT : this._gl.BACK;
             var hideSide = reverseSide ? this._gl.BACK : this._gl.FRONT;
             var cullFace = this.cullBackFaces ? showSide : hideSide;
@@ -2462,7 +2487,7 @@
             this.resetTextureCache();
             this._currentEffect = null;
 
-            // 6/8/2017: deltakosh: Should not be required anymore. 
+            // 6/8/2017: deltakosh: Should not be required anymore.
             // This message is then mostly for the future myself which will scream out loud when seeing that actually it was required :)
             if (bruteForce) {
                 this._currentProgram = null;
@@ -2484,20 +2509,20 @@
         /**
          * Set the compressed texture format to use, based on the formats you have, and the formats
          * supported by the hardware / browser.
-         * 
+         *
          * Khronos Texture Container (.ktx) files are used to support this.  This format has the
          * advantage of being specifically designed for OpenGL.  Header elements directly correspond
          * to API arguments needed to compressed textures.  This puts the burden on the container
          * generator to house the arcane code for determining these for current & future formats.
-         * 
+         *
          * for description see https://www.khronos.org/opengles/sdk/tools/KTX/
          * for file layout see https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/
-         * 
+         *
          * Note: The result of this call is not taken into account when a texture is base64.
-         * 
+         *
          * @param {Array<string>} formatsAvailable- The list of those format families you have created
          * on your server.  Syntax: '-' + format family + '.ktx'.  (Case and order do not matter.)
-         * 
+         *
          * Current families are astc, dxt, pvrtc, etc2, & etc1.
          * @returns The extension selected.
          */
@@ -2530,7 +2555,7 @@
          * @param {ArrayBuffer | HTMLImageElement} buffer- A source of a file previously fetched as either an ArrayBuffer (compressed or image format) or HTMLImageElement (image format)
          * @param {WebGLTexture} fallback- An internal argument in case the function must be called again, due to etc1 not having alpha capabilities.
          * @param {number} format-  Internal format.  Default: RGB when extension is '.jpg' else RGBA.  Ignored for compressed textures.
-         * 
+         *
          * @returns {WebGLTexture} for assignment back into BABYLON.Texture
          */
         public createTexture(urlArg: string, noMipmap: boolean, invertY: boolean, scene: Scene, samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE, onLoad: () => void = null, onError: () => void = null, buffer: ArrayBuffer | HTMLImageElement = null, fallBack?: WebGLTexture, format?: number): WebGLTexture {
@@ -3909,7 +3934,7 @@
 
             var anisotropicFilterExtension = this._caps.textureAnisotropicFilterExtension;
             var value = texture.anisotropicFilteringLevel;
-            
+
 
             if (internalTexture.samplingMode === Texture.NEAREST_SAMPLINGMODE) {
                 value = 1;
@@ -4317,4 +4342,4 @@
             }
         }
     }
-}
+}

+ 120 - 21
src/babylon.scene.ts

@@ -221,7 +221,7 @@
          * Forward main pass or through the imageProcessingPostProcess if present.
          * As in the majority of the scene they are the same (exception for multi camera),
          * this is easier to reference from here than from all the materials and post process.
-         * 
+         *
          * No setter as we it is a shared configuration, you can set the values instead.
          */
         public get imageProcessingConfiguration(): ImageProcessingConfiguration {
@@ -383,6 +383,34 @@
         public onMeshRemovedObservable = new Observable<AbstractMesh>();
 
         /**
+        * An event triggered before calculating deterministic simulation step
+        * @type {BABYLON.Observable}
+        */
+        public onBeforeStepObservable = new Observable<Scene>();
+
+        private _onBeforeStepObserver: Observer<Scene>;
+        public set beforeStep(callback: (data:Scene, state: EventState) => void) {
+            if (this.onBeforeStepObservable) {
+                this.onBeforeStepObservable.remove(this._onBeforeStepObserver);
+            }
+            this._onBeforeStepObserver = this.onBeforeStepObservable.add(callback);
+        }
+
+        /**
+        * An event triggered after calculating deterministic simulation step
+        * @type {BABYLON.Observable}
+        */
+        public onAfterStepObservable = new Observable<Scene>();
+
+        private _onAfterStepObserver: Observer<Scene>;
+        public set afterStep(callback: (data:Scene, state: EventState) => void) {
+            if (this.onAfterStepObservable) {
+                this.onAfterStepObservable.remove(this._onAfterStepObserver);
+            }
+            this._onAfterStepObserver = this.onAfterStepObservable.add(callback);
+        }
+
+        /**
          * This Observable will be triggered for each stage of each renderingGroup of each rendered camera.
          * The RenderinGroupInfo class contains all the information about the context in which the observable is called
          * If you wish to register an Observer only for a given set of renderingGroup, use the mask with a combination of the renderingGroup index elevated to the power of two (1 for renderingGroup 0, 2 for renderingrOup1, 4 for 2 and 8 for 3)
@@ -458,6 +486,11 @@
         private _startingPointerTime = 0;
         private _previousStartingPointerTime = 0;
 
+        // Deterministic lockstep
+        private _timeAccumulator: number = 0;
+        private _currentStepId: number = 0;
+        private _currentInternalStep: number = 0;
+
         // Mirror
         public _mirroredCameraPosition: Vector3;
 
@@ -482,6 +515,14 @@
             return this._useRightHandedSystem;
         }
 
+        public getStepId(): number {
+            return this._currentStepId;
+        };
+
+        public getInternalStep(): number {
+            return this._currentInternalStep;
+        };
+
         // Fog
 
         private _fogEnabled = true;
@@ -1598,6 +1639,8 @@
             this._cachedVisibility = null;
         }
 
+
+
         public registerBeforeRender(func: () => void): void {
             this.onBeforeRenderObservable.add(func);
         }
@@ -1614,6 +1657,22 @@
             this.onAfterRenderObservable.removeCallback(func);
         }
 
+        public registerBeforeStep(func: () => void): void {
+            this.onBeforeStepObservable.add(func);
+        }
+
+        public unregisterBeforeStep(func: () => void): void {
+            this.onBeforeStepObservable.removeCallback(func);
+        }
+
+        public registerAfterStep(func: () => void): void {
+            this.onAfterStepObservable.add(func);
+        }
+
+        public unregisterAfterStep(func: () => void): void {
+            this.onAfterStepObservable.removeCallback(func);
+        }
+
         public _addPendingData(data): void {
             this._pendingData.push(data);
         }
@@ -1663,7 +1722,7 @@
         // Animations
         /**
          * Will start the animation sequence of a given target
-         * @param target - the target 
+         * @param target - the target
          * @param {number} from - from which frame should animation start
          * @param {number} to - till which frame should animation run.
          * @param {boolean} [loop] - should the animation loop
@@ -1729,7 +1788,7 @@
 
         /**
          * Will stop the animation of the given target
-         * @param target - the target 
+         * @param target - the target
          * @param animationName - the name of the animation to stop (all animations will be stopped is empty)
          * @see beginAnimation
          */
@@ -1828,7 +1887,7 @@
         public removeMesh(toRemove: AbstractMesh): number {
             var index = this.meshes.indexOf(toRemove);
             if (index !== -1) {
-                // Remove from the scene if mesh found 
+                // Remove from the scene if mesh found
                 this.meshes.splice(index, 1);
             }
             //notify the collision coordinator
@@ -1844,7 +1903,7 @@
         public removeSkeleton(toRemove: Skeleton): number {
             var index = this.skeletons.indexOf(toRemove);
             if (index !== -1) {
-                // Remove from the scene if found 
+                // Remove from the scene if found
                 this.skeletons.splice(index, 1);
             }
 
@@ -1854,7 +1913,7 @@
         public removeMorphTargetManager(toRemove: MorphTargetManager): number {
             var index = this.morphTargetManagers.indexOf(toRemove);
             if (index !== -1) {
-                // Remove from the scene if found 
+                // Remove from the scene if found
                 this.morphTargetManagers.splice(index, 1);
             }
 
@@ -1864,7 +1923,7 @@
         public removeLight(toRemove: Light): number {
             var index = this.lights.indexOf(toRemove);
             if (index !== -1) {
-                // Remove from the scene if mesh found 
+                // Remove from the scene if mesh found
                 this.lights.splice(index, 1);
                 this.sortLightsByPriority();
             }
@@ -1875,7 +1934,7 @@
         public removeCamera(toRemove: Camera): number {
             var index = this.cameras.indexOf(toRemove);
             if (index !== -1) {
-                // Remove from the scene if mesh found 
+                // Remove from the scene if mesh found
                 this.cameras.splice(index, 1);
             }
             // Remove from activeCameras
@@ -2415,7 +2474,7 @@
         /**
          * Return a the first highlight layer of the scene with a given name.
          * @param name The name of the highlight layer to look for.
-         * @return The highlight layer if found otherwise null. 
+         * @return The highlight layer if found otherwise null.
          */
         public getHighlightLayerByName(name: string): HighlightLayer {
             for (var index = 0; index < this.highlightLayers.length; index++) {
@@ -2923,16 +2982,56 @@
                 this.simplificationQueue.executeNext();
             }
 
-            // Animations
-            var deltaTime = Math.max(Scene.MinDeltaTime, Math.min(this._engine.getDeltaTime(), Scene.MaxDeltaTime));
-            this._animationRatio = deltaTime * (60.0 / 1000.0);
-            this._animate();
+            if(this._engine.isDeterministicLockStep()){
+              var deltaTime = Math.max(Scene.MinDeltaTime, Math.min(this._engine.getDeltaTime(), Scene.MaxDeltaTime)) / 1000;
+              var defaultTimeStep = this._physicsEngine.getTimeStep();
+              var maxSubSteps = this._engine.getLockstepMaxSteps();
 
-            // Physics
-            if (this._physicsEngine) {
-                Tools.StartPerformanceCounter("Physics");
-                this._physicsEngine._step(deltaTime / 1000.0);
-                Tools.EndPerformanceCounter("Physics");
+              this._timeAccumulator += deltaTime;
+
+              // compute the amount of fixed steps we should have taken since the last step
+              var internalSteps = Math.floor(this._timeAccumulator / defaultTimeStep);
+              internalSteps = Math.min(internalSteps, maxSubSteps);
+
+              for(this._currentInternalStep=0; this._currentInternalStep<internalSteps; this._currentInternalStep++){
+
+                this.onBeforeStepObservable.notifyObservers(this);
+
+                // Animations
+                this._animationRatio = defaultTimeStep * (60.0 / 1000.0);
+                this._animate();
+
+                // Physics
+                if (this._physicsEngine) {
+                   Tools.StartPerformanceCounter("Physics");
+                   this._physicsEngine._step(defaultTimeStep);
+                   Tools.EndPerformanceCounter("Physics");
+                }
+                this._timeAccumulator -= defaultTimeStep;
+
+                this.onAfterStepObservable.notifyObservers(this);
+                this._currentStepId++;
+
+                if((internalSteps>1) && (this._currentInternalStep != internalSteps-1)) {
+                    // Q: can this be optimized by putting some code in the afterStep callback?
+                    // I had to put this code here, otherwise mesh attached to bones of another mesh skeleton,
+                    // would return incorrect positions for internal stepIds (non-rendered steps)
+                    this._evaluateActiveMeshes();
+                }
+              }
+            }
+            else {
+              // Animations
+              var deltaTime = Math.max(Scene.MinDeltaTime, Math.min(this._engine.getDeltaTime(), Scene.MaxDeltaTime));
+              this._animationRatio = deltaTime * (60.0 / 1000.0);
+              this._animate();
+
+              // Physics
+              if (this._physicsEngine) {
+                 Tools.StartPerformanceCounter("Physics");
+                 this._physicsEngine._step(deltaTime / 1000.0);
+                 Tools.EndPerformanceCounter("Physics");
+              }
             }
 
             // Before render
@@ -3269,7 +3368,7 @@
                 this._depthRenderer.dispose();
             }
 
-            // Smart arrays            
+            // Smart arrays
             if (this.activeCamera) {
                 this.activeCamera._activeMeshes.dispose();
                 this.activeCamera = null;
@@ -3864,7 +3963,7 @@
         /**
          * Overrides the default sort function applied in the renderging group to prepare the meshes.
          * This allowed control for front to back rendering or reversly depending of the special needs.
-         * 
+         *
          * @param renderingGroupId The rendering group id corresponding to its index
          * @param opaqueSortCompareFn The opaque queue comparison function use to sort.
          * @param alphaTestSortCompareFn The alpha test queue comparison function use to sort.
@@ -3883,7 +3982,7 @@
 
         /**
          * Specifies whether or not the stencil and depth buffer are cleared between two rendering groups.
-         * 
+         *
          * @param renderingGroupId The rendering group id corresponding to its index
          * @param autoClearDepthStencil Automatically clears depth and stencil between groups if true.
          * @param depth Automatically clears depth between groups if true and autoClear is true.