threlte logo
@threlte/rapier

<RigidBody>

The real-time simulation of rigid bodies subjected to forces and contacts is the main feature of a physics engine for videogames, robotics, or animation. Rigid bodies are typically used to simulate the dynamics of non-deformable solids as well as to integrate the trajectory of solids which velocities are controlled by the user (e.g. moving platforms).

Note that rigid-bodies are only responsible for the dynamics and kinematics of the solid. Colliders can be attached to a rigid-body to specify its shape and enable collision-detection. A rigid-body without collider attached to it will not be affected by contacts (because there is no shape to compute contact against).

<script lang="ts">
	import { Canvas } from '@threlte/core'
	import { HTML } from '@threlte/extras'
	import { World } from '@threlte/rapier'
	import { muted } from './Particle.svelte'
	import Scene from './Scene.svelte'
	import { useTweakpane } from '$lib/useTweakpane'

	const { addButton, action } = useTweakpane()

	addButton({
		title: 'toggle sound',
		onClick: () => ($muted = !$muted)
	})
</script>

<div use:action />

<Canvas>
	<World>
		<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 { useFrame } from '@threlte/core'
	import { Euler, Vector3 } from 'three'
	import Particle from './Particle.svelte'

	const getId = () => {
		return Math.random().toString(16).slice(2)
	}

	const getRandomPosition = () => {
		return new Vector3(0.5 - Math.random() * 1, 5 - Math.random() * 1 + 10, 0.5 - Math.random() * 1)
	}

	const getRandomRotation = () => {
		return new Euler(Math.random() * 10, Math.random() * 10, Math.random() * 10)
	}

	type Body = {
		id: string
		mounted: number
		position: Vector3
		rotation: Euler
	}

	let bodies: Body[] = []

	let lastBodyMounted: number = 0
	let bodyEveryMilliseconds = 2000
	let longevityMilliseconds = 8000

	useFrame(() => {
		if (lastBodyMounted + bodyEveryMilliseconds < Date.now()) {
			const body: Body = {
				id: getId(),
				mounted: Date.now(),
				position: getRandomPosition(),
				rotation: getRandomRotation()
			}
			bodies.unshift(body)
			lastBodyMounted = Date.now()
			bodies = bodies
		}
		const deleteIds: string[] = []
		bodies.forEach((body) => {
			if (body.mounted + longevityMilliseconds < Date.now()) {
				deleteIds.push(body.id)
			}
		})

		if (deleteIds.length) {
			deleteIds.forEach((id) => {
				const index = bodies.findIndex((body) => body.id === id)
				if (index !== -1) bodies.splice(index, 1)
			})
			bodies = bodies
		}
	})
</script>

{#each bodies as body (body.id)}
	<Particle position={body.position} rotation={body.rotation} />
{/each}
<script lang="ts">
  import { T } from '@threlte/core'
  import { AutoColliders } from '@threlte/rapier'
</script>

<T.Group position={[0, -0.5, 0]}>
  <AutoColliders shape={'cuboid'}>
    <T.Mesh receiveShadow>
      <T.BoxGeometry args={[10, 1, 10]} />
      <T.MeshStandardMaterial />
    </T.Mesh>
  </AutoColliders>
</T.Group>
<script
  lang="ts"
  context="module"
>
  const geometry = new BoxGeometry(1, 1, 1)
  const material = new MeshStandardMaterial()
  export const muted = writable(true)
</script>

<script lang="ts">
  import { T } from '@threlte/core'
  import { PositionalAudio } from '@threlte/extras'
  import { Collider, RigidBody, type ContactEvent } from '@threlte/rapier'
  import { writable } from 'svelte/store'
  import type { Euler, Vector3 } from 'three'
  import { BoxGeometry, MeshStandardMaterial } from 'three'
  import { clamp } from 'three/src/math/MathUtils'

  export let position: Vector3 | undefined = undefined
  export let rotation: Euler | undefined = undefined

  const audios: {
    threshold: number
    volume: number
    stop: (() => any) | undefined
    play: ((...args: any[]) => any) | undefined
    source: string
  }[] = new Array(9).fill(0).map((_, i) => {
    return {
      threshold: i / 10,
      play: undefined,
      stop: undefined,
      volume: (i + 2) / 10,
      source: `/audio/ball_bounce_${i + 1}.mp3`
    }
  })

  const fireSound = (e: ContactEvent) => {
    if ($muted) return
    const volume = clamp((e.detail.totalForceMagnitude - 30) / 1100, 0.1, 1)
    const audio = audios.find((a) => a.volume >= volume)
    audio?.stop?.()
    audio?.play?.()
  }

  $: rotationCasted = rotation?.toArray() as [x: number, y: number, z: number]
</script>

<T.Group
  position={position?.toArray()}
  rotation={rotationCasted}
>
  <RigidBody
    type={'dynamic'}
    on:contact={fireSound}
  >
    {#each audios as audio}
      <PositionalAudio
        autoplay={false}
        detune={600 - Math.random() * 1200}
        bind:stop={audio.stop}
        bind:play={audio.play}
        src={audio.source}
        volume={audio.volume}
      />
    {/each}

    <Collider
      contactForceEventThreshold={30}
      restitution={0.4}
      shape={'cuboid'}
      args={[0.5, 0.5, 0.5]}
    />
    <T.Mesh
      castShadow
      receiveShadow
      {geometry}
      {material}
    />
  </RigidBody>
</T.Group>
<script lang="ts">
  import { T } from '@threlte/core'
  import { OrbitControls, AudioListener } from '@threlte/extras'
  import { Debug } from '@threlte/rapier'
  import Emitter from './Emitter.svelte'
  import Ground from './Ground.svelte'
</script>

<T.PerspectiveCamera
  makeDefault
  position={[10, 10, 10]}
>
  <OrbitControls enableZoom={false} />
  <AudioListener />
</T.PerspectiveCamera>

<T.DirectionalLight
  castShadow
  position={[8, 20, -3]}
/>

<T.GridHelper args={[50]} />

<Ground />

<Debug />

<Emitter />

Component Signature

Props

name
type
required
default

angularDamping
number
no
0

angularVelocity
Rotation
no
{}

canSleep
boolean
no
true

ccd
boolean
no
false

dominance
number
no
0

enabledRotations
Boolean3Array
no
[true, true, true]

enabledTranslations
Boolean3Array
no
[true, true, true]

gravityScale
number
no
1

linearDamping
number
no
0

linearVelocity
Position
no
{}

lockRotations
boolean
no
false

lockTranslations
boolean
no
false

type
'fixed' | 'dynamic' | 'kinematicPosition' | 'kinematicVelocity'
no
'dynamic'

userData
Record<string, any>
no
{}

Events

name
payload

sleep
void

wake
void

collisionenter
CustomEvent<{ targetCollider: Collider targetRigidBody: RigidBody | null manifold: TempContactManifold flipped: boolean }>

collisionexit
CustomEvent<{ targetCollider: Collider targetRigidBody: RigidBody | null }>

sensorenter
CustomEvent<{ targetCollider: Collider targetRigidBody: RigidBody | null }>

sensorexit
CustomEvent<{ targetCollider: Collider targetRigidBody: RigidBody | null }>

contact
CustomEvent<{ targetCollider: Collider targetRigidBody: RigidBody | null manifold: TempContactManifold flipped: boolean maxForceDirection: Vector maxForceMagnitude: number totalForce: Vector totalForceMagnitude: number }>

Bindings

name
type

rigidBody
RigidBody