Skip to content

Physics with Rapier

Video Lecture

Physics with Rapier Physics with Rapier

Description

Rapier is a physics engine that we can use to calculate Rigid body forces, velocities, contacts, constraints and more.

Calculating physics is useful in video games, animations, robotics and simulations.

We can use a physics engine independently of Threejs, or any rendering engine in fact, if we only wanted to calculate physics properties and events over time.

Instead, we are going to use that information, from the physics calculations, to continually update a THREE.Object3D position and quaternion between each frame render.

When synchronizing the Rapier physics engine with a Threejs scene, it is useful to understand that a THREE.Object3D and the RAPIER.RigidBody are independent objects.

The THREE.Object3D could be a mesh with a complicated geometry and colourful material, however the RAPIER.RigidBody can still be a simple shape, or collection of shapes, so that the physics calculations are worked out fast enough between each frame, to unsure a smooth frame rate and appearance of animation.

<>

Install,

npm install @dimforge/rapier3d-compat --save-dev

Note

We could instead install and use @dimforge/rapier3d. But at the time of creating this video, version 0.12.0 of @dimforge/rapier3d did not work in a Vite production build. See Rapier WASM Notes below.

Lesson Script

./src/main.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import Stats from 'three/addons/libs/stats.module.js'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import RAPIER from '@dimforge/rapier3d-compat'

await RAPIER.init() // This line is only needed if using the compat version
const gravity = new RAPIER.Vector3(0.0, -9.81, 0.0)
const world = new RAPIER.World(gravity)
const dynamicBodies: [THREE.Object3D, RAPIER.RigidBody][] = []

const scene = new THREE.Scene()

const light1 = new THREE.SpotLight(undefined, Math.PI * 10)
light1.position.set(2.5, 5, 5)
light1.angle = Math.PI / 3
light1.penumbra = 0.5
light1.castShadow = true
light1.shadow.blurSamples = 10
light1.shadow.radius = 5
scene.add(light1)

const light2 = light1.clone()
light2.position.set(-2.5, 5, 5)
scene.add(light2)

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100)
camera.position.set(0, 2, 5)

const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.VSMShadowMap
document.body.appendChild(renderer.domElement)

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
})

const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.target.y = 1

// Cuboid Collider
const cubeMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshNormalMaterial())
cubeMesh.castShadow = true
scene.add(cubeMesh)
// const cubeBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 5, 0).setCanSleep(false))
// const cubeShape = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5).setMass(1).setRestitution(1.1)
// world.createCollider(cubeShape, cubeBody)
// dynamicBodies.push([cubeMesh, cubeBody])

// // Ball Collider
// const sphereMesh = new THREE.Mesh(new THREE.SphereGeometry(), new THREE.MeshNormalMaterial())
// sphereMesh.castShadow = true
// scene.add(sphereMesh)
// const sphereBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(-2, 5, 0).setCanSleep(false))
// const sphereShape = RAPIER.ColliderDesc.ball(1).setMass(1).setRestitution(1.1)
// world.createCollider(sphereShape, sphereBody)
// dynamicBodies.push([sphereMesh, sphereBody])

// // Cylinder Collider
// const cylinderMesh = new THREE.Mesh(new THREE.CylinderGeometry(1, 1, 2, 16), new THREE.MeshNormalMaterial())
// cylinderMesh.castShadow = true
// scene.add(cylinderMesh)
// const cylinderBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 5, 0).setCanSleep(false))
// const cylinderShape = RAPIER.ColliderDesc.cylinder(1, 1).setMass(1).setRestitution(1.1)
// world.createCollider(cylinderShape, cylinderBody)
// dynamicBodies.push([cylinderMesh, cylinderBody])

// // ConvexHull Collider
// const icosahedronMesh = new THREE.Mesh(new THREE.IcosahedronGeometry(1, 0), new THREE.MeshNormalMaterial())
// icosahedronMesh.castShadow = true
// scene.add(icosahedronMesh)
// const icosahedronBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(2, 5, 0).setCanSleep(false))
// const points = new Float32Array(icosahedronMesh.geometry.attributes.position.array)
// const icosahedronShape = (RAPIER.ColliderDesc.convexHull(points) as RAPIER.ColliderDesc).setMass(1).setRestitution(1.1)
// world.createCollider(icosahedronShape, icosahedronBody)
// dynamicBodies.push([icosahedronMesh, icosahedronBody])

// // Trimesh Collider
// const torusKnotMesh = new THREE.Mesh(new THREE.TorusKnotGeometry(), new THREE.MeshNormalMaterial())
// torusKnotMesh.castShadow = true
// scene.add(torusKnotMesh)
// const torusKnotBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(4, 5, 0))
// const vertices = new Float32Array(torusKnotMesh.geometry.attributes.position.array)
// let indices = new Uint32Array((torusKnotMesh.geometry.index as THREE.BufferAttribute).array)
// const torusKnotShape = (RAPIER.ColliderDesc.trimesh(vertices, indices) as RAPIER.ColliderDesc)
//   .setMass(1)
//   .setRestitution(1.1)
// world.createCollider(torusKnotShape, torusKnotBody)
// dynamicBodies.push([torusKnotMesh, torusKnotBody])

const floorMesh = new THREE.Mesh(new THREE.BoxGeometry(100, 1, 100), new THREE.MeshPhongMaterial())
floorMesh.receiveShadow = true
floorMesh.position.y = -1
scene.add(floorMesh)
const floorBody = world.createRigidBody(RAPIER.RigidBodyDesc.fixed().setTranslation(0, -1, 0))
const floorShape = RAPIER.ColliderDesc.cuboid(50, 0.5, 50)
world.createCollider(floorShape, floorBody)

// const raycaster = new THREE.Raycaster()
// const mouse = new THREE.Vector2()

// renderer.domElement.addEventListener('click', (e) => {
//   mouse.set(
//     (e.clientX / renderer.domElement.clientWidth) * 2 - 1,
//     -(e.clientY / renderer.domElement.clientHeight) * 2 + 1
//   )

//   raycaster.setFromCamera(mouse, camera)

//   const intersects = raycaster.intersectObjects(
//     [cubeMesh, sphereMesh, cylinderMesh, icosahedronMesh, torusKnotMesh],
//     false
//   )

//   if (intersects.length) {
//     dynamicBodies.forEach((b) => {
//       b[0] === intersects[0].object && b[1].applyImpulse(new RAPIER.Vector3(0, 10, 0), true)
//     })
//   }
// })

const stats = new Stats()
document.body.appendChild(stats.dom)

const gui = new GUI()

const physicsFolder = gui.addFolder('Physics')
physicsFolder.add(world.gravity, 'x', -10.0, 10.0, 0.1)
physicsFolder.add(world.gravity, 'y', -10.0, 10.0, 0.1)
physicsFolder.add(world.gravity, 'z', -10.0, 10.0, 0.1)

const clock = new THREE.Clock()
let delta

function animate() {
  requestAnimationFrame(animate)

  delta = clock.getDelta()
  world.timestep = Math.min(delta, 0.1)
  world.step()

  for (let i = 0, n = dynamicBodies.length; i < n; i++) {
    dynamicBodies[i][0].position.copy(dynamicBodies[i][1].translation())
    dynamicBodies[i][0].quaternion.copy(dynamicBodies[i][1].rotation())
  }

  controls.update()

  renderer.render(scene, camera)

  stats.update()
}

animate()

Rapier WASM Notes

Instead of using the compat version of Rapier, we can install the version that downloads the binary WASM at run time.

npm install @dimforge/rapier3d --save-dev

Version 0.12.0 did not work in a Vite production build, so the video demonstrates the compat version.

The compat version embeds the WASM binary as a base64 encoded string, which results in a larger file size.

However, most webservers will likely gzip the bundle, and it will end up roughly the same size in bytes as it travels across the network anyway.

If using the non compat version, then you will need to install some plugins into Vite to allow importing the Rapier WASM binary.

npm install vite-plugin-wasm --save-dev
npm install vite-plugin-top-level-await --save-dev

We also need to create a vite.config.js indicating the usage of these plugins.

./vite.config.js

import { defineConfig } from 'vite'
import wasm from 'vite-plugin-wasm'
import topLevelAwait from 'vite-plugin-top-level-await'

export default defineConfig({
  plugins: [wasm(), topLevelAwait()],
})

When using the non compat version, it is not necessary to run this line as it is in the compat version.

await RAPIER.init()

Everything else about using the Rapier API is the same.

Rapier Javascript Docs (rapier.rs)

Rapier.js Repository (GitHub)

Rapier ImpulseJoint Motors Example

Comments