Skip to content

Rapier ImpulseJoint Motors

Video Lecture

Rapier ImpulseJoint Motors 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
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Three.js TypeScript Tutorials by Sean Bradley : https://sbcode.net/threejs</title>
  </head>

  <body>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

./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

Comments