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 { Pane, Button } from 'svelte-tweakpane-ui'
</script>

<Pane
  title="Rigid Body"
  position="fixed"
>
  <Button
    title="toggle sound"
    on:click={() => {
      $muted = !$muted
    }}
  />
</Pane>

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

<style>
  div {
    height: 100%;
  }
  p {
    font-size: 0.75rem;
    line-height: 1rem;
  }
</style>
<script lang="ts">
  import { useTask } 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

  useTask(() => {
    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.js'

  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

create
{ ref: RigidBody, cleanup: (callback: () => void) => void }

sleep
void

wake
void

collisionenter
{ targetCollider: Collider, targetRigidBody: RigidBody | null, manifold: TempContactManifold, flipped: boolean }

collisionexit
{ targetCollider: Collider, targetRigidBody: RigidBody | null }

sensorenter
{ targetCollider: Collider, targetRigidBody: RigidBody | null }

sensorexit
{ targetCollider: Collider, targetRigidBody: RigidBody | null }

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

Bindings

name
type

rigidBody
RigidBody