Skip to content

Commit

Permalink
Merge pull request #127 from pmndrs/fixedstep
Browse files Browse the repository at this point in the history
Add `world.fixedStep()`
  • Loading branch information
marcofugaro committed Jan 6, 2022
2 parents cf4e6e4 + b56a4b3 commit fbba069
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 69 deletions.
20 changes: 2 additions & 18 deletions examples/threejs.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@
// cannon.js variables
let world
let body
const timeStep = 1 / 60
let lastCallTime

initThree()
initCannon()
Expand Down Expand Up @@ -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)
}
</script>
Expand Down
32 changes: 11 additions & 21 deletions examples/threejs_cloth.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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++) {
Expand All @@ -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)
}
</script>
</body>
Expand Down
17 changes: 3 additions & 14 deletions examples/threejs_mousepick.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@

// cannon.js variables
let world
const timeStep = 1 / 60
let lastCallTime
let jointBody
let jointConstraint
let cubeBody
Expand Down Expand Up @@ -296,27 +294,18 @@
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++) {
meshes[i].position.copy(bodies[i].position)
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()
}
</script>
</body>
Expand Down
2 changes: 1 addition & 1 deletion examples/worker.html
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down
2 changes: 1 addition & 1 deletion examples/worker_sharedarraybuffer.html
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down
35 changes: 22 additions & 13 deletions getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`**.

<details>
<summary>See advanced world stepping example</summary>

```js
const timeStep = 1 / 60 // seconds
Expand All @@ -36,6 +52,8 @@ function animate() {
animate()
```

</details>

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.
Expand Down Expand Up @@ -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}`)
Expand Down
26 changes: 25 additions & 1 deletion src/world/World.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ export class World extends EventTarget {

idToBodyMap: { [id: number]: Body }

lastCallTime?: number

constructor(
options: {
/**
Expand Down Expand Up @@ -441,14 +443,36 @@ 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.
*
* There are two modes. The simple mode is fixed timestepping without interpolation. In this case you only use the first argument. The second case uses interpolation. In that you also provide the time since the function was last used, as well as the maximum fixed timesteps to take.
*
* @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
Expand Down

0 comments on commit fbba069

Please sign in to comment.