@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<br />
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 { 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: ContactEvent = (event) => {
if ($muted) return
const volume = clamp((event.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.x={position?.x}
position.y={position?.y}
position.z={position?.z}
rotation={rotationCasted}
>
<RigidBody
type={'dynamic'}
oncontact={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 />