@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 />
{#snippet fallback()}
<HTML transform>
<p>
It seems your browser doesn't support WASM.<br />
I'm sorry.
</p>
</HTML>
{/snippet}
</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 { Quaternion, type Vector3Tuple, type QuaternionTuple } from 'three'
import Particle from './Particle.svelte'
import { SvelteSet } from 'svelte/reactivity'
type Body = {
mounted: number
position: Vector3Tuple
quaternion: QuaternionTuple
}
let bodies = new SvelteSet<Body>()
let lastBodyMounted = 0
let bodyEveryMilliseconds = 800
let longevityMilliseconds = 8000
const quaternion = new Quaternion()
useTask(() => {
const now = performance.now()
if (lastBodyMounted + bodyEveryMilliseconds < now) {
const body: Body = {
mounted: now,
position: [0, 15, 0],
quaternion: quaternion.random().toArray()
}
bodies.add(body)
lastBodyMounted = now
}
bodies.forEach((body) => {
if (body.mounted + longevityMilliseconds < now) {
bodies.delete(body)
}
})
})
</script>
{#each bodies as body (body)}
<Particle {...body} />
{/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"
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 { QuaternionTuple, Vector3Tuple } from 'three'
import { BoxGeometry, MeshStandardMaterial, MathUtils } from 'three'
interface Props {
position: Vector3Tuple
quaternion: QuaternionTuple
}
let { position, quaternion }: Props = $props()
const audios: {
threshold: number
volume: number
ref: PositionalAudio | undefined
source: string
}[] = $state(
new Array(9).fill(0).map((_, i) => {
return {
threshold: i / 10,
ref: undefined,
volume: (i + 2) / 10,
source: `/audio/ball_bounce_${i + 1}.mp3`
}
})
)
const fireSound: ContactEvent = (event) => {
if ($muted) return
const volume = MathUtils.clamp((event.totalForceMagnitude - 30) / 1100, 0.1, 1)
const audio = audios.find((a) => a.volume >= volume)
audio?.ref?.stop?.()
audio?.ref?.play?.()
}
</script>
<T.Group
{position}
{quaternion}
>
<RigidBody
type="dynamic"
oncontact={fireSound}
>
{#each audios as audio}
<PositionalAudio
bind:this={audio.ref}
autoplay={false}
detune={600 - Math.random() * 1200}
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 />