@threlte/rapier
<BasicVehicleController>
Basic Vehicle Controller
This recipe helps you get started with a basic vehicle controller.
The car has a <RigidBody>
component for the body to which four axles are attached with either a RevoluteImpulseJoint
for the steered wheels or a FixedImpulseJoint
for the unsteered back wheels.
Each wheel is attached to an axle with a RevoluteImpulseJoint
and the back wheels are configured to be a motor.
To increase the decoupling of joint rigid bodies, the solver iterations are increased by a factor of 100.
The car can be controlled with the WASD keys. The spacebar activates the handbreak.
The property dominance
on <RigidBody>
components can be used to make objects more or less vulnerable to impacts of the car.
<script lang="ts">
import { useTweakpane } from '$lib/useTweakpane'
import { Canvas } from '@threlte/core'
import { HTML } from '@threlte/extras'
import { Debug, World } from '@threlte/rapier'
import Scene from './Scene.svelte'
const { action, pane } = useTweakpane()
pane.addBlade({
view: 'text',
text: "Use the 'wasd' keys to drive",
lineCount: 3
})
</script>
<div use:action />
<Canvas>
<World>
<Debug
depthTest={false}
depthWrite={false}
/>
<Scene />
<HTML
slot="fallback"
transform
>
<p>
It seems your browser<br />
doesn't support WASM.<br />
I'm sorry.
</p>
</HTML>
</World>
</Canvas>
<style>
p {
font-size: 0.75rem;
line-height: 1rem;
}
</style>
<script lang="ts">
import type {
RevoluteImpulseJoint,
RigidBody as RapierRigidBody
} from '@dimforge/rapier3d-compat'
import { T } from '@threlte/core'
import { Collider, RigidBody, useFixedJoint, useRevoluteJoint } from '@threlte/rapier'
import { spring } from 'svelte/motion'
import { clamp, DEG2RAD, mapLinear } from 'three/src/math/MathUtils'
import type { AxleProps } from './Axle.svelte'
import { useCar } from './useCar'
import { useWasd } from './useWasd'
import Wheel from './Wheel.svelte'
type $$Props = AxleProps
export let side: $$Props['side']
export let anchor: $$Props['anchor']
export let parentRigidBody: $$Props['parentRigidBody'] = undefined
export let isSteered: $$Props['isSteered'] = false
export let isDriven: $$Props['isDriven'] = false
let axleRigidBody: RapierRigidBody
const wasd = useWasd()
const { speed } = useCar()
const steeringAngle = spring(mapLinear(clamp($speed / 12, 0, 1), 0, 1, 1, 0.5) * $wasd.x * 15)
$: steeringAngle.set(mapLinear(clamp($speed / 12, 0, 1), 0, 1, 1, 0.5) * $wasd.x * 15)
const { joint, rigidBodyA, rigidBodyB } = isSteered
? useRevoluteJoint(anchor, [0, 0, 0], [0, 1, 0])
: useFixedJoint(anchor, [0, 0, 0], [0, 0, 0], [0, 0, 0])
$: if (parentRigidBody) rigidBodyA.set(parentRigidBody)
$: if (axleRigidBody) rigidBodyB.set(axleRigidBody)
$: $joint?.setContactsEnabled(false)
$: if (isSteered) {
;($joint as RevoluteImpulseJoint)?.configureMotorPosition(
$steeringAngle * -1 * DEG2RAD,
1000000,
0
)
}
</script>
<T.Group {...$$restProps}>
<RigidBody bind:rigidBody={axleRigidBody}>
<Collider mass={1} shape={'cuboid'} args={[0.03, 0.03, 0.03]} />
</RigidBody>
<Wheel
{isDriven}
anchor={[0, 0, side === 'left' ? 0.2 : -0.2]}
position={[0, 0, side === 'left' ? 0.2 : -0.2]}
parentRigidBody={axleRigidBody}
/>
</T.Group>
import type { Events, Props, Slots } from '@threlte/core'
import { SvelteComponentTyped } from 'svelte'
import type { Group, Vector3 } from 'three'
import type { RigidBody } from '@dimforge/rapier3d-compat'
export type AxleProps = Props<Group> & {
side: 'left' | 'right'
parentRigidBody: RigidBody | undefined
anchor: Parameters<Vector3['set']>
isSteered?: boolean
isDriven?: boolean
}
export default class Axle extends SvelteComponentTyped<AxleProps, Events<Group>, Slots<Group>> {}
<script lang="ts">
import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
import { T, useFrame } from '@threlte/core'
import { HTML } from '@threlte/extras'
import { Collider, RigidBody, useRapier } from '@threlte/rapier'
import { onDestroy, setContext } from 'svelte'
import { writable } from 'svelte/store'
import { BoxGeometry, MeshStandardMaterial, Vector3 } from 'three'
import { DEG2RAD } from 'three/src/math/MathUtils'
import Axle from './Axle.svelte'
import type { CarProps } from './Car.svelte'
type $$Props = CarProps
let parentRigidBody: RapierRigidBody
const carContext = {
speed: writable(0)
}
const { speed } = carContext
setContext<typeof carContext>('threlte-car-context', carContext)
const { world } = useRapier()
const v3 = new Vector3()
useFrame(() => {
const s = parentRigidBody.linvel()
v3.set(s.x, s.y, s.z)
carContext.speed.set(v3.length())
})
const initialIterations = {
maxStabilizationIterations: world.maxStabilizationIterations,
maxVelocityFrictionIterations: world.maxVelocityFrictionIterations,
maxVelocityIterations: world.maxVelocityIterations
}
world.maxStabilizationIterations *= 100
world.maxVelocityFrictionIterations *= 100
world.maxVelocityIterations *= 100
onDestroy(() => {
world.maxStabilizationIterations = initialIterations.maxStabilizationIterations
world.maxVelocityFrictionIterations = initialIterations.maxVelocityFrictionIterations
world.maxVelocityIterations = initialIterations.maxVelocityIterations
})
</script>
<T.Group {...$$restProps}>
<RigidBody bind:rigidBody={parentRigidBody} canSleep={false}>
<Collider mass={1} shape={'cuboid'} args={[1.25, 0.4, 0.5]} />
<!-- CAR BODY MESH -->
<T.Mesh
castShadow
geometry={new BoxGeometry(2.5, 0.8, 1)}
material={new MeshStandardMaterial()}
/>
<slot />
<HTML rotation={{ y: 90 * DEG2RAD }} transform position={{ x: 3 }}>
<p class="text-xs text-black">
{($speed * 3.6).toFixed(0)} km/h
</p>
</HTML>
</RigidBody>
<!-- FRONT AXLES -->
<Axle
side={'left'}
isSteered
{parentRigidBody}
position={[-1.2, -0.4, 0.8]}
anchor={[-1.2, -0.4, 0.8]}
/>
<Axle
side={'right'}
isSteered
{parentRigidBody}
position={[-1.2, -0.4, -0.8]}
anchor={[-1.2, -0.4, -0.8]}
/>
<!-- BACK AXLES -->
<Axle
isDriven
side={'left'}
{parentRigidBody}
position={[1.2, -0.4, 0.8]}
anchor={[1.2, -0.4, 0.8]}
/>
<Axle
isDriven
side={'right'}
{parentRigidBody}
position={[1.2, -0.4, -0.8]}
anchor={[1.2, -0.4, -0.8]}
/>
</T.Group>
import type { Events, Props, Slots } from '@threlte/core'
import { SvelteComponentTyped } from 'svelte'
import type { Group } from 'three'
export type CarProps = Props<Group>
export default class Car extends SvelteComponentTyped<CarProps, Events<Group>, Slots<Group>> {}
<script lang="ts">
import { T } from '@threlte/core'
import { AutoColliders } from '@threlte/rapier'
import { BoxGeometry, MeshStandardMaterial } from 'three'
</script>
<T.GridHelper args={[150, 15]} position.y={0.001} />
<AutoColliders shape={'cuboid'} position={[0, -0.5, 0]}>
<T.Mesh
receiveShadow
geometry={new BoxGeometry(150, 1, 150)}
material={new MeshStandardMaterial()}
/>
</AutoColliders>
<script lang="ts">
import { T } from '@threlte/core'
import { Environment, HTML, useGltf } from '@threlte/extras'
import { AutoColliders, RigidBody } from '@threlte/rapier'
import { BoxGeometry, MeshStandardMaterial } from 'three'
import { DEG2RAD } from 'three/src/math/MathUtils'
import Car from './Car.svelte'
import Ground from './Ground.svelte'
const gltf = useGltf('/models/loop/loop.glb')
</script>
<Environment
path="/hdr/"
files="shanghai_riverside_1k.hdr"
/>
<T.DirectionalLight position={[8, 20, -3]} />
<Ground />
<RigidBody
dominance={1}
position={[-10, 3, -12]}
>
<HTML
transform
sprite
pointerEvents={'none'}
position={{ y: 1 }}
>
<p>Dominance: 1</p>
</HTML>
<AutoColliders shape={'cuboid'}>
<T.Mesh
geometry={new BoxGeometry(1, 1, 1)}
material={new MeshStandardMaterial()}
/>
</AutoColliders>
</RigidBody>
<RigidBody
dominance={-1}
position={[-15, 3, -14]}
>
<HTML
transform
sprite
pointerEvents={'none'}
position={{ y: 3 }}
>
<p>Dominance: -1</p>
</HTML>
<AutoColliders shape={'cuboid'}>
<T.Mesh
geometry={new BoxGeometry(3, 3, 3)}
material={new MeshStandardMaterial()}
/>
</AutoColliders>
</RigidBody>
<RigidBody
dominance={0}
position={[-13, 3, -10]}
>
<HTML
transform
sprite
pointerEvents={'none'}
position={{ y: 2 }}
>
<p>Dominance: 0</p>
</HTML>
<AutoColliders shape={'cuboid'}>
<T.Mesh
geometry={new BoxGeometry(2, 2, 2)}
material={new MeshStandardMaterial()}
/>
</AutoColliders>
</RigidBody>
{#if $gltf}
<AutoColliders shape={'trimesh'}>
<T
is={$gltf.scene}
rotation.y={90 * DEG2RAD}
position={[-50, -0.3, -3]}
/>
</AutoColliders>
{/if}
<Car
position.x={70}
position.y={5}
>
<T.PerspectiveCamera
rotation={[-90 * DEG2RAD, 70 * DEG2RAD, 90 * DEG2RAD]}
position.x={10}
position.y={5}
fov={60}
makeDefault
/>
</Car>
<script lang="ts">
import {
MotorModel,
type Collider as RapierCollider,
type RigidBody as RapierRigidBody
} from '@dimforge/rapier3d-compat'
import { T } from '@threlte/core'
import { Collider, RigidBody, useRevoluteJoint } from '@threlte/rapier'
import type { Vector3 } from 'three'
import { CylinderGeometry, MeshStandardMaterial } from 'three'
import { DEG2RAD } from 'three/src/math/MathUtils'
import { useWasd } from './useWasd'
export let position: Parameters<Vector3['set']>
export let parentRigidBody: RapierRigidBody | undefined = undefined
export let anchor: Parameters<Vector3['set']>
let collider: RapierCollider
export let isDriven = false
const wasd = useWasd()
let isSpaceDown = false
const { rigidBodyA, rigidBodyB, joint } = useRevoluteJoint(anchor, [0, 0, 0], [0, 0, 1])
$: if (parentRigidBody) rigidBodyA.set(parentRigidBody)
$: $joint?.configureMotorModel(MotorModel.AccelerationBased)
$: $joint?.configureMotorModel(
isDriven && isSpaceDown ? MotorModel.ForceBased : MotorModel.AccelerationBased
)
$: if (isDriven) $joint?.configureMotorVelocity(isSpaceDown ? 0 : $wasd.y * 1000, 10)
$: $joint?.setContactsEnabled(false)
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === ' ') {
e.preventDefault()
isSpaceDown = true
}
}
const onKeyUp = (e: KeyboardEvent) => {
if (e.key === ' ') {
e.preventDefault()
isSpaceDown = false
}
}
</script>
<svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} />
<RigidBody canSleep={false} {position} bind:rigidBody={$rigidBodyB}>
<Collider
mass={1}
friction={1.5}
shape={'cylinder'}
args={[0.12, 0.3]}
bind:collider
rotation={[90 * DEG2RAD, 0, 0]}
/>
<!-- WHEEL MESH -->
<T.Mesh
castShadow
rotation.x={90 * DEG2RAD}
geometry={new CylinderGeometry(0.3, 0.3, 0.24)}
material={new MeshStandardMaterial()}
/>
</RigidBody>
import { getContext } from 'svelte'
import type { Writable } from 'svelte/store'
type CarContext = {
speed: Writable<number>
}
export const useCar = () => {
return getContext<CarContext>('threlte-car-context')
}
import { onDestroy } from 'svelte'
import { derived, get, writable } from 'svelte/store'
export const useWasd = () => {
const wasdKeys = writable({
w: false,
a: false,
s: false,
d: false
})
const onKeyDown = (e: KeyboardEvent) => {
if (!Object.keys(get(wasdKeys)).includes(e.key)) return
wasdKeys.update((keys) => {
keys[e.key as keyof typeof keys] = true
return keys
})
}
const onKeyUp = (e: KeyboardEvent) => {
if (!Object.keys(get(wasdKeys)).includes(e.key)) return
wasdKeys.update((keys) => {
keys[e.key as keyof typeof keys] = false
return keys
})
}
const wasd = derived(wasdKeys, (wasdKeys) => {
return {
x: 0 + (wasdKeys.d ? 1 : 0) - (wasdKeys.a ? 1 : 0),
y: 0 + (wasdKeys.w ? 1 : 0) - (wasdKeys.s ? 1 : 0)
}
})
window.addEventListener('keydown', onKeyDown)
window.addEventListener('keyup', onKeyUp)
onDestroy(() => {
window.removeEventListener('keydown', onKeyDown)
window.removeEventListener('keyup', onKeyUp)
})
return wasd
}
Tips:
- Experiment with front wheel drive or all wheel drive (property
isDriven
on<Axle>
component) - Play around with the
mass
properties of the colliders - Make an all wheel steered vehicle
- Increase or decrease the power output of the wheel motors
- Change the motor model from
AccelerationBased
toForceBased
(you will need to adapt the power output) - Experiment with different wheel collider shapes
- Increase or decrease the scale of the car
- Move the axles and observe the maneuverability