diff --git a/examples/threejs.html b/examples/threejs.html index 00e35477..a8a6bec3 100644 --- a/examples/threejs.html +++ b/examples/threejs.html @@ -32,8 +32,6 @@ // cannon.js variables let world let body - const timeStep = 1 / 60 - let lastCallTime initThree() initCannon() @@ -87,27 +85,13 @@ requestAnimationFrame(animate) // Step the physics world - updatePhysics() + world.fixedStep() // Copy coordinates from cannon.js to three.js mesh.position.copy(body.position) mesh.quaternion.copy(body.quaternion) - render() - } - - function updatePhysics() { - const time = performance.now() / 1000 - if (!lastCallTime) { - world.step(timeStep) - } else { - const dt = time - lastCallTime - world.step(timeStep, dt) - } - lastCallTime = time - } - - function render() { + // Render three.js renderer.render(scene, camera) } diff --git a/examples/threejs_cloth.html b/examples/threejs_cloth.html index d34c9685..156654e7 100644 --- a/examples/threejs_cloth.html +++ b/examples/threejs_cloth.html @@ -27,10 +27,6 @@ * https://viscomp.alexandra.dk/?p=147 */ - // Specify the simulation constants - const timeStep = 1 / 60 - let lastCallTime - const clothMass = 1 // 1 kg in total const clothSize = 1 // 1 meter const Nx = 12 // number of horizontal particles in the cloth @@ -210,24 +206,20 @@ function animate() { requestAnimationFrame(animate) controls.update() - updatePhysics() - render() - stats.update() - } - // Step the physics world - function updatePhysics() { - const time = performance.now() / 1000 - if (!lastCallTime) { - world.step(timeStep) - } else { - const dt = time - lastCallTime - world.step(timeStep, dt) - } - lastCallTime = time + // Step the physics world + world.fixedStep() + + // Sync the three.js meshes with the bodies + updateMeshes() + + // Render three.js + renderer.render(scene, camera) + + stats.update() } - function render() { + function updateMeshes() { // Make the three.js cloth follow the cannon.js particles for (let i = 0; i < Nx + 1; i++) { for (let j = 0; j < Ny + 1; j++) { @@ -246,8 +238,6 @@ // Make the three.js ball follow the cannon.js one // Copying quaternion is not needed since it's a sphere sphereMesh.position.copy(sphereBody.position) - - renderer.render(scene, camera) } diff --git a/examples/threejs_mousepick.html b/examples/threejs_mousepick.html index 1431de1a..c9f64157 100644 --- a/examples/threejs_mousepick.html +++ b/examples/threejs_mousepick.html @@ -31,8 +31,6 @@ // cannon.js variables let world - const timeStep = 1 / 60 - let lastCallTime let jointBody let jointConstraint let cubeBody @@ -296,7 +294,7 @@ requestAnimationFrame(animate) // Step the physics world - updatePhysics() + world.fixedStep() // Sync the three.js meshes with the bodies for (let i = 0; i !== meshes.length; i++) { @@ -304,19 +302,10 @@ meshes[i].quaternion.copy(bodies[i].quaternion) } + // Render three.js renderer.render(scene, camera) - stats.update() - } - function updatePhysics() { - const time = performance.now() / 1000 - if (!lastCallTime) { - world.step(timeStep) - } else { - const dt = time - lastCallTime - world.step(timeStep, dt) - } - lastCallTime = time + stats.update() } diff --git a/examples/worker.html b/examples/worker.html index a0ae2db1..14a09d42 100644 --- a/examples/worker.html +++ b/examples/worker.html @@ -50,7 +50,7 @@ const { positions, quaternions, timeStep } = event.data // Step the world - world.step(timeStep) + world.fixedStep(timeStep) // Copy the cannon.js data into the buffers for (let i = 0; i < bodies.length; i++) { diff --git a/examples/worker_sharedarraybuffer.html b/examples/worker_sharedarraybuffer.html index 362b7e34..08ed65f1 100644 --- a/examples/worker_sharedarraybuffer.html +++ b/examples/worker_sharedarraybuffer.html @@ -79,7 +79,7 @@ function update() { // Step the world - world.step(timeStep) + world.fixedStep(timeStep) // Copy the cannon.js data into the buffers for (let i = 0; i < bodies.length; i++) { diff --git a/getting-started.md b/getting-started.md index 6687f5d6..7ace9852 100644 --- a/getting-started.md +++ b/getting-started.md @@ -13,9 +13,25 @@ const world = new CANNON.World({ }) ``` -To step the simulation forward, we have to call **`world.step()`** each frame. -As a first argument we pass the fixed timestep at which we want the simulation to run, `1 / 60` means 60fps. -As a second argument, we pass the elapsed time since the last `.step()` call. This is used to keep the simulation at the same speed independently of the framerate, since `requestAnimationFrame` calls may vary on different devices or there might be performance issues. [Read more about fixed simulation stepping here](https://gafferongames.com/post/fix_your_timestep/). +To step the simulation forward, we have to call **`world.fixedStep()`** each frame. +As a first argument, we can pass the fixed timestep at which we want the simulation to run, the default value is `1 / 60` meaning `60fps`. +**`world.fixedStep()`** keeps track of the last time it was called to keep the simulation at the same speed independently of the framerate, since `requestAnimationFrame` calls may vary on different devices or if there are performance issues. [Read more about fixed simulation stepping here](https://gafferongames.com/post/fix_your_timestep/). + +```js +function animate() { + requestAnimationFrame(animate) + + // Run the simulation independently of framerate every 1 / 60 ms + world.fixedStep() +} +// Start the simulation loop +animate() +``` + +If you wish to pass the time since last call by hand (`dt` in the game world) you can use the more advanced **`world.step()`**. + +
+See advanced world stepping example ```js const timeStep = 1 / 60 // seconds @@ -36,6 +52,8 @@ function animate() { animate() ``` +
+ Rigid Bodies are the entities which will be simulated in the world, they can be simple shapes such as [Sphere](classes/sphere), [Box](classes/box), [Plane](classes/plane), [Cylinder](classes/cylinder), or more complex shapes such as [ConvexPolyhedron](classes/convexpolyhedron), [Particle](classes/particle), [Heightfield](classes/heightfield), [Trimesh](classes/trimesh). Let's create a basic sphere body. @@ -93,19 +111,10 @@ groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) // make it face up world.addBody(groundBody) // Start the simulation loop -const timeStep = 1 / 60 // seconds -let lastCallTime function animate() { requestAnimationFrame(animate) - const time = performance.now() / 1000 // seconds - if (!lastCallTime) { - world.step(timeStep) - } else { - const dt = time - lastCallTime - world.step(timeStep, dt) - } - lastCallTime = time + world.fixedStep() // the sphere y position shows the sphere falling console.log(`Sphere y position: ${sphereBody.position.y}`) diff --git a/src/world/World.ts b/src/world/World.ts index a7829199..1b4d907f 100644 --- a/src/world/World.ts +++ b/src/world/World.ts @@ -168,6 +168,8 @@ export class World extends EventTarget { idToBodyMap: { [id: number]: Body } + lastCallTime?: number + constructor( options: { /** @@ -441,6 +443,28 @@ export class World extends EventTarget { this.contactMaterialTable.set(cmat.materials[0].id, cmat.materials[1].id, cmat) } + /** + * Step the simulation forward keeping track of last called time + * to be able to step the world at a fixed rate, independently of framerate. + * + * @param dt The fixed time step size to use (default: 1 / 60). + * @param maxSubSteps Maximum number of fixed steps to take per function call (default: 10). + * @see https://gafferongames.com/post/fix_your_timestep/ + * @example + * // Run the simulation independently of framerate every 1 / 60 ms + * world.fixedStep() + */ + fixedStep(dt = 1 / 60, maxSubSteps = 10): void { + const time = performance.now() / 1000 // seconds + if (!this.lastCallTime) { + this.step(dt, undefined, maxSubSteps) + } else { + const timeSinceLastCalled = time - this.lastCallTime + this.step(dt, timeSinceLastCalled, maxSubSteps) + } + this.lastCallTime = time + } + /** * Step the physics world forward in time. * @@ -448,7 +472,7 @@ export class World extends EventTarget { * * @param dt The fixed time step size to use. * @param timeSinceLastCalled The time elapsed since the function was last called. - * @param maxSubSteps Maximum number of fixed steps to take per function call. + * @param maxSubSteps Maximum number of fixed steps to take per function call (default: 10). * @see https://web.archive.org/web/20180426154531/http://bulletphysics.org/mediawiki-1.5.8/index.php/Stepping_The_World#What_do_the_parameters_to_btDynamicsWorld::stepSimulation_mean.3F * @example * // fixed timestepping without interpolation