threlte logo
@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 to ForceBased (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