# Rapier ImpulseJoint Motors

## Description

In this lesson we will,

• Implement a follow cam to chase the car,
• Setup revolute ImpulseJoint constraints to join the front wheels to steering axels, and then to the car body,
• Use `configureMotorVelocity` to rotate the rear wheels of the car,
• Use `configureMotorPosition` to steer the front wheels.
• Use collision/interaction groups.

## Impulsejoint

We are using the revolute type of `ImpulseJoint`,

``````world.createImpulseJoint(RAPIER.JointData.revolute(...
``````

The revolute joint prevents any relative movement between two rigid-bodies, except for relative rotations along one axis.

Types of `ImpulseJoints` we can use in Rapier `v0.12.0`,

Type Description
`Revolute` Removes all degrees of freedom between the affected bodies except for the rotation along one axis.
`Fixed` Removes all relative degrees of freedom between the affected bodies. Good for hard locking bodies together.
`Prismatic` Removes all degrees of freedom between the affected bodies except for the translation along one axis.
`Spherical` Removes all relative linear degrees of freedom between the affected bodies.

Note that when spawning rigid bodies linked with joints, it is important to position the joint relative to the parent body, otherwise the physics engine will need to apply an opposite force to correct its position for the joint.

For example, the steering axels are added to the car body, but translated forward left and forward right, so they are positioned where you would expect the front wheels to be. When adding the front wheels to the axels, they can be joined at `0,0,0`, since the axels themselves have already been offset the car bodies centre.

It is also important that when shapes are added to the world, that they aren't positioned the same place as any existing shapes. This will cause unpredictable collisions and the body will appear to jump, spin or be thrown in some random way. The only time you can position a shape, in the same place as any other shape, is if you have set its collision group to ignore the other shape.

## Joint motors

Impulse joints can be controlled using the `configureMotorVelocity` and `configureMotorPosition` methods.

In this example, `configureMotorVelocity` controls continuous rotations for the rear wheels.

``````;(this.wheelBLMotor as RAPIER.PrismaticImpulseJoint).configureMotorVelocity(targetVelocity, factor)
``````

The `configureMotorPosition` controls how far left and right the steering moves.

``````;(this.wheelFLAxel as RAPIER.PrismaticImpulseJoint).configureMotorPosition(targetSteer, stiffness, damping)
``````

The `configureMotorPosition` method limits the amount of rotation, so that it doesn't continue to revolve like the `configureMotorVelocity` method. However, If using the `configureMotorPosition` method, you should also configure your `ImpulseJoint` `MotorModel`, to use the `ForceBased` model. The default `MotorModel` = `AccelerationBased`, which is useful if calling the `configureMotorVelocity` method.

``````;(this.wheelFRAxel as RAPIER.PrismaticImpulseJoint).configureMotorModel(RAPIER.MotorModel.ForceBased)
``````

## Rapier Collision Group Calculator

This example also demonstrates setting collision/interaction groups between each rigid body.

Without the collision group settings, the wheels and steering axels conflict with the car body and shake unpredictably.

So, each shape is added to its own membership group, and then a filter is applied.

If the group memberships are,

``````floor = 0
car = 1
wheel = 2
axel = 3
``````

Then, the collision group calculations are,

1. The floor collides with the wheels and car. `membership=0, filter=[1,2]` = `65542`

2. The car collides with the floor only. `membership=1, filter=[0]` = `131073`

3. The wheels collide with the floor only. `membership=2, filter=[0]` = `262145`

4. The axels collide with nothing. `membership=3` = `589823`

Use collision groups to limit which shapes/colliders can collide with each other.

• Membership : A number or array. E.g., `0` or `1` or `[0,2,3]`
• Filter : A number, array or nothing. E.g., `0` or `1` or `[0,2,3]`

With the calculated value, when creating a rigid body, you can set the collision group.

``````const someShape = RAPIER.ColliderDesc.cuboid().setCollisionGroups(65542)
``````

## Lesson Scripts

### ./index.html

 ``` 1 2 3 4 5 6 7 8 9 10 11 12``` `````` Three.js TypeScript Tutorials by Sean Bradley : https://sbcode.net/threejs ``````

### ./src/style.css

 ```1 2 3 4``` ``````body { overflow: hidden; margin: 0px; } ``````

### ./src/Box.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``` ``````import { Scene, Object3D, Mesh, BoxGeometry, MeshStandardMaterial } from 'three' import { World, RigidBody, RigidBodyDesc, ColliderDesc } from '@dimforge/rapier3d-compat' export default class Box { dynamicBody: [Object3D, RigidBody] constructor(scene: Scene, world: World, position: [number, number, number]) { const boxMesh = new Mesh(new BoxGeometry(), new MeshStandardMaterial()) boxMesh.castShadow = true scene.add(boxMesh) const boxBody = world.createRigidBody(RigidBodyDesc.dynamic().setTranslation(...position)) const boxShape = ColliderDesc.cuboid(0.5, 0.5, 0.5).setRestitution(0.5).setMass(0.1) world.createCollider(boxShape, boxBody) this.dynamicBody = [boxMesh, boxBody] } update() { this.dynamicBody[0].position.copy(this.dynamicBody[1].translation()) this.dynamicBody[0].quaternion.copy(this.dynamicBody[1].rotation()) } } ``````

### ./src/Car.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 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252``` ``````import { Group, Mesh, Object3D, Quaternion, Scene, SpotLight, TextureLoader, Vector3 } from 'three' import { RigidBody, ImpulseJoint, World, RigidBodyDesc, ColliderDesc, JointData, MotorModel, PrismaticImpulseJoint } from '@dimforge/rapier3d-compat' import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js' // collision groups // floorShape = 0 // carShape = 1 // wheelShape = 2 // axelShape = 3 export default class Car { dynamicBodies: [Object3D, RigidBody][] = [] followTarget = new Object3D() lightLeftTarget = new Object3D() lightRightTarget = new Object3D() carBody?: RigidBody wheelBLMotor?: ImpulseJoint wheelBRMotor?: ImpulseJoint wheelFLAxel?: ImpulseJoint wheelFRAxel?: ImpulseJoint v = new Vector3() keyMap: { [key: string]: boolean } pivot: Object3D constructor(keyMap: { [key: string]: boolean }, pivot: Object3D) { this.followTarget.position.set(0, 1, 0) this.lightLeftTarget.position.set(-0.35, 1, -10) this.lightRightTarget.position.set(0.35, 1, -10) this.keyMap = keyMap this.pivot = pivot } async init(scene: Scene, world: World, position: [number, number, number]) { await new GLTFLoader().loadAsync('models/sedanSports.glb').then((gltf) => { const carMesh = gltf.scene.getObjectByName('body') as Group carMesh.position.set(0, 0, 0) carMesh.traverse((o) => { o.castShadow = true }) carMesh.add(this.followTarget) const textureLoader = new TextureLoader() const textureFlare0 = textureLoader.load('img/lensflare0.png') const textureFlare3 = textureLoader.load('img/lensflare3.png') const lensflareLeft = new Lensflare() lensflareLeft.addElement(new LensflareElement(textureFlare0, 1000, 0)) lensflareLeft.addElement(new LensflareElement(textureFlare3, 500, 0.2)) lensflareLeft.addElement(new LensflareElement(textureFlare3, 250, 0.8)) lensflareLeft.addElement(new LensflareElement(textureFlare3, 125, 0.6)) lensflareLeft.addElement(new LensflareElement(textureFlare3, 62.5, 0.4)) const lensflareRight = new Lensflare() lensflareRight.addElement(new LensflareElement(textureFlare0, 1000, 0)) lensflareRight.addElement(new LensflareElement(textureFlare3, 500, 0.2)) lensflareRight.addElement(new LensflareElement(textureFlare3, 250, 0.8)) lensflareRight.addElement(new LensflareElement(textureFlare3, 125, 0.6)) lensflareRight.addElement(new LensflareElement(textureFlare3, 62.5, 0.4)) const headLightLeft = new SpotLight(undefined, Math.PI * 20) headLightLeft.position.set(-0.4, 0.5, -1.01) headLightLeft.angle = Math.PI / 4 headLightLeft.penumbra = 0.5 headLightLeft.castShadow = true headLightLeft.shadow.blurSamples = 10 headLightLeft.shadow.radius = 5 const headLightRight = headLightLeft.clone() headLightRight.position.set(0.4, 0.5, -1.01) carMesh.add(headLightLeft) headLightLeft.target = this.lightLeftTarget headLightLeft.add(lensflareLeft) carMesh.add(this.lightLeftTarget) carMesh.add(headLightRight) headLightRight.target = this.lightRightTarget headLightRight.add(lensflareRight) carMesh.add(this.lightRightTarget) const wheelBLMesh = gltf.scene.getObjectByName('wheel_backLeft') as Group const wheelBRMesh = gltf.scene.getObjectByName('wheel_backRight') as Group const wheelFLMesh = gltf.scene.getObjectByName('wheel_frontLeft') as Group const wheelFRMesh = gltf.scene.getObjectByName('wheel_frontRight') as Group scene.add(carMesh, wheelBLMesh, wheelBRMesh, wheelFLMesh, wheelFRMesh) // create bodies for car, wheels and axels this.carBody = world.createRigidBody( RigidBodyDesc.dynamic() .setTranslation(...position) .setCanSleep(false) ) const wheelBLBody = world.createRigidBody( RigidBodyDesc.dynamic() .setTranslation(position[0] - 0.55, position[1], position[2] + 0.63) .setCanSleep(false) ) const wheelBRBody = world.createRigidBody( RigidBodyDesc.dynamic() .setTranslation(position[0] + 0.55, position[1], position[2] + 0.63) .setCanSleep(false) ) const wheelFLBody = world.createRigidBody( RigidBodyDesc.dynamic() .setTranslation(position[0] - 0.55, position[1], position[2] - 0.63) .setCanSleep(false) ) const wheelFRBody = world.createRigidBody( RigidBodyDesc.dynamic() .setTranslation(position[0] + 0.55, position[1], position[2] - 0.63) .setCanSleep(false) ) // const axelFLBody = world.createRigidBody( // RigidBodyDesc.dynamic() // .setTranslation(position[0] - 0.55, position[1], position[2] - 0.63) // .setCanSleep(false) // ) // const axelFRBody = world.createRigidBody( // RigidBodyDesc.dynamic() // .setTranslation(position[0] + 0.55, position[1], position[2] - 0.63) // .setCanSleep(false) // ) // create a convexhull from all meshes in the carMesh group const v = new Vector3() let positions: number[] = [] carMesh.updateMatrixWorld(true) // ensure world matrix is up to date carMesh.traverse((o) => { if (o.type === 'Mesh') { const positionAttribute = (o as Mesh).geometry.getAttribute('position') for (let i = 0, l = positionAttribute.count; i < l; i++) { v.fromBufferAttribute(positionAttribute, i) v.applyMatrix4((o.parent as Object3D).matrixWorld) positions.push(...v) } } }) // create shapes for carBody, wheelBodies and axelBodies const carShape = (ColliderDesc.convexMesh(new Float32Array(positions)) as ColliderDesc).setMass(1).setRestitution(0.5).setFriction(3) //.setCollisionGroups(131073) const wheelBLShape = ColliderDesc.cylinder(0.1, 0.3) .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), -Math.PI / 2)) .setTranslation(-0.2, 0, 0) .setRestitution(0.5) .setFriction(2) //.setCollisionGroups(262145) const wheelBRShape = ColliderDesc.cylinder(0.1, 0.3) .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2)) .setTranslation(0.2, 0, 0) .setRestitution(0.5) .setFriction(2) //.setCollisionGroups(262145) const wheelFLShape = ColliderDesc.cylinder(0.1, 0.3) .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2)) .setTranslation(-0.2, 0, 0) .setRestitution(0.5) .setFriction(2.5) //.setCollisionGroups(262145) const wheelFRShape = ColliderDesc.cylinder(0.1, 0.3) .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2)) .setTranslation(0.2, 0, 0) .setRestitution(0.5) .setFriction(2.5) //.setCollisionGroups(262145) // const axelFLShape = ColliderDesc.cuboid(0.1, 0.1, 0.1) // .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2)) // .setMass(0.1) // // .setCollisionGroups(589823) // const axelFRShape = ColliderDesc.cuboid(0.1, 0.1, 0.1) // .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2)) // .setMass(0.1) // // .setCollisionGroups(589823) //joins wheels to car body world.createImpulseJoint(JointData.revolute(new Vector3(-0.55, 0, 0.63), new Vector3(0, 0, 0), new Vector3(-1, 0, 0)), this.carBody, wheelBLBody, true) world.createImpulseJoint(JointData.revolute(new Vector3(0.55, 0, 0.63), new Vector3(0, 0, 0), new Vector3(-1, 0, 0)), this.carBody, wheelBRBody, true) world.createImpulseJoint(JointData.revolute(new Vector3(-0.55, 0, -0.63), new Vector3(0, 0, 0), new Vector3(1, 0, 0)), this.carBody, wheelFLBody, true) world.createImpulseJoint(JointData.revolute(new Vector3(0.55, 0, -0.63), new Vector3(0, 0, 0), new Vector3(1, 0, 0)), this.carBody, wheelFRBody, true) // // attach back wheel to cars. These will be configurable motors. // this.wheelBLMotor = world.createImpulseJoint(JointData.revolute(new Vector3(-0.55, 0, 0.63), new Vector3(0, 0, 0), new Vector3(-1, 0, 0)), this.carBody, wheelBLBody, true) // this.wheelBRMotor = world.createImpulseJoint(JointData.revolute(new Vector3(0.55, 0, 0.63), new Vector3(0, 0, 0), new Vector3(-1, 0, 0)), this.carBody, wheelBRBody, true) // // attach steering axels to car. These will be configurable motors. // this.wheelFLAxel = world.createImpulseJoint(JointData.revolute(new Vector3(-0.55, 0, -0.63), new Vector3(0, 0, 0), new Vector3(0, 1, 0)), this.carBody, axelFLBody, true) // ;(this.wheelFLAxel as PrismaticImpulseJoint).configureMotorModel(MotorModel.ForceBased) // this.wheelFRAxel = world.createImpulseJoint(JointData.revolute(new Vector3(0.55, 0, -0.63), new Vector3(0, 0, 0), new Vector3(0, 1, 0)), this.carBody, axelFRBody, true) // ;(this.wheelFRAxel as PrismaticImpulseJoint).configureMotorModel(MotorModel.ForceBased) // // // attach front wheel to steering axels // world.createImpulseJoint(JointData.revolute(new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(1, 0, 0)), axelFLBody, wheelFLBody, true) // world.createImpulseJoint(JointData.revolute(new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(1, 0, 0)), axelFRBody, wheelFRBody, true) // create world collider world.createCollider(carShape, this.carBody) world.createCollider(wheelBLShape, wheelBLBody) world.createCollider(wheelBRShape, wheelBRBody) world.createCollider(wheelFLShape, wheelFLBody) world.createCollider(wheelFRShape, wheelFRBody) //world.createCollider(axelFLShape, axelFLBody) //world.createCollider(axelFRShape, axelFRBody) // update local dynamicBodies so mesh positions and quaternions are updated with the physics world info this.dynamicBodies.push([carMesh, this.carBody]) this.dynamicBodies.push([wheelBLMesh, wheelBLBody]) this.dynamicBodies.push([wheelBRMesh, wheelBRBody]) this.dynamicBodies.push([wheelFLMesh, wheelFLBody]) this.dynamicBodies.push([wheelFRMesh, wheelFRBody]) //this.dynamicBodies.push([new Object3D(), axelFRBody]) //this.dynamicBodies.push([new Object3D(), axelFLBody]) }) } update(delta: number) { for (let i = 0, n = this.dynamicBodies.length; i < n; i++) { this.dynamicBodies[i][0].position.copy(this.dynamicBodies[i][1].translation()) this.dynamicBodies[i][0].quaternion.copy(this.dynamicBodies[i][1].rotation()) } this.followTarget.getWorldPosition(this.v) this.pivot.position.lerp(this.v, delta * 5) // frame rate independent //this.pivot.position.copy(this.v) // let targetVelocity = 0 // if (this.keyMap['KeyW']) { // targetVelocity = 500 // } // if (this.keyMap['KeyS']) { // targetVelocity = -200 // } // ;(this.wheelBLMotor as PrismaticImpulseJoint).configureMotorVelocity(targetVelocity, 2.0) // ;(this.wheelBRMotor as PrismaticImpulseJoint).configureMotorVelocity(targetVelocity, 2.0) // let targetSteer = 0 // if (this.keyMap['KeyA']) { // targetSteer += 0.6 // } // if (this.keyMap['KeyD']) { // targetSteer -= 0.6 // } // ;(this.wheelFLAxel as PrismaticImpulseJoint).configureMotorPosition(targetSteer, 100, 10) // ;(this.wheelFRAxel as PrismaticImpulseJoint).configureMotorPosition(targetSteer, 100, 10) } } ``````

### ./src/RapierDebugRenderer.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``` ``````import { Scene, LineSegments, BufferGeometry, LineBasicMaterial, BufferAttribute } from 'three' import { World } from '@dimforge/rapier3d-compat' export default class RapierDebugRenderer { mesh world enabled = true constructor(scene: Scene, world: World) { this.world = world this.mesh = new LineSegments(new BufferGeometry(), new LineBasicMaterial({ color: 0xffffff, vertexColors: true })) this.mesh.frustumCulled = false scene.add(this.mesh) } update() { if (this.enabled) { const { vertices, colors } = this.world.debugRender() this.mesh.geometry.setAttribute('position', new BufferAttribute(vertices, 3)) this.mesh.geometry.setAttribute('color', new BufferAttribute(colors, 4)) this.mesh.visible = true } else { this.mesh.visible = false } } } ``````

### ./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``` ``````import './style.css' import * as THREE from 'three' import { RGBELoader } from 'three/addons/loaders/RGBELoader.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' import RapierDebugRenderer from './RapierDebugRenderer' import Car from './Car' //import Box from './Box' 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 scene = new THREE.Scene() const rapierDebugRenderer = new RapierDebugRenderer(scene, world) const gridHelper = new THREE.GridHelper(200, 100, 0x222222, 0x222222) gridHelper.position.y = -0.5 scene.add(gridHelper) await new RGBELoader().loadAsync('img/venice_sunset_1k.hdr').then((texture) => { texture.mapping = THREE.EquirectangularReflectionMapping scene.environment = texture scene.environmentIntensity = 0.1 // new in Three r163. https://threejs.org/docs/#api/en/scenes/Scene.environmentIntensity }) const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100) camera.position.set(0, 0, 4) const renderer = new THREE.WebGLRenderer({ antialias: true }) renderer.setSize(window.innerWidth, window.innerHeight) renderer.toneMapping = THREE.ACESFilmicToneMapping renderer.shadowMap.enabled = true document.body.appendChild(renderer.domElement) window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) }) /* A follow cam implementation. A followTarget is added to the car mesh. A reference to the pivot is given to the car. The cars update method lerps the pivot towards to followTarget. */ const pivot = new THREE.Object3D() const yaw = new THREE.Object3D() const pitch = new THREE.Object3D() scene.add(pivot) pivot.add(yaw) yaw.add(pitch) pitch.add(camera) // adding the perspective camera to the hierarchy function onDocumentMouseMove(e: MouseEvent) { yaw.rotation.y -= e.movementX * 0.002 const v = pitch.rotation.x - e.movementY * 0.002 // limit range if (v > -1 && v < 0.1) { pitch.rotation.x = v } } function onDocumentMouseWheel(e: WheelEvent) { e.preventDefault() const v = camera.position.z + e.deltaY * 0.005 // limit range if (v >= 1 && v <= 10) { camera.position.z = v } } // end follow cam. const keyMap: { [key: string]: boolean } = {} const onDocumentKey = (e: KeyboardEvent) => { keyMap[e.code] = e.type === 'keydown' } document.addEventListener('click', () => { renderer.domElement.requestPointerLock() }) document.addEventListener('pointerlockchange', () => { if (document.pointerLockElement === renderer.domElement) { document.addEventListener('keydown', onDocumentKey) document.addEventListener('keyup', onDocumentKey) renderer.domElement.addEventListener('mousemove', onDocumentMouseMove) renderer.domElement.addEventListener('wheel', onDocumentMouseWheel) } else { document.removeEventListener('keydown', onDocumentKey) document.removeEventListener('keyup', onDocumentKey) renderer.domElement.removeEventListener('mousemove', onDocumentMouseMove) renderer.domElement.removeEventListener('wheel', onDocumentMouseWheel) } }) const floorMesh = new THREE.Mesh(new THREE.BoxGeometry(200, 1, 200), 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(100, 0.5, 100) //.setCollisionGroups(65542) world.createCollider(floorShape, floorBody) const car = new Car(keyMap, pivot) await car.init(scene, world, [0, 1, 0]) // const boxes: Box[] = [] // for (let x = 0; x < 8; x += 1) { // for (let y = 0; y < 8; y += 1) { // boxes.push(new Box(scene, world, [(x - 4) * 1.2, y + 1, -20])) // } // } const stats = new Stats() document.body.appendChild(stats.dom) const gui = new GUI() gui.add(rapierDebugRenderer, 'enabled').name('Rapier Degug Renderer') 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() car.update(delta) //boxes.forEach((b) => b.update()) rapierDebugRenderer.update() renderer.render(scene, camera) stats.update() } animate() ``````

Collider Collision Groups

Revolute joint

Joint motors