@threlte/rapier
<Collider>
Colliders represent the geometric shapes that generate contacts and collision events when they touch. Attaching one or multiple colliders to a rigid body allow the rigid-body to be affected by contact forces.
<script lang="ts">
import { Canvas } from '@threlte/core'
import { HTML } from '@threlte/extras'
import { World } from '@threlte/rapier'
import Scene from './Scene.svelte'
import { Pane, Button } from 'svelte-tweakpane-ui'
let testIndex = $state(0)
</script>
<Pane
title="Colliders"
position="fixed"
>
<Button
label="type"
title="Standalone"
on:click={() => {
testIndex = 0
}}
/>
<Button
label=""
title="Attached"
on:click={() => {
testIndex = 1
}}
/>
<Button
label=""
title="Sensor"
on:click={() => {
testIndex = 2
}}
/>
</Pane>
<div>
<Canvas>
<World>
<Scene {testIndex} />
{#snippet fallback()}
<HTML transform>
<p class="text-xs">
It seems your browser<br />
doesn't support WASM.<br />
I'm sorry.
</p>
</HTML>
{/snippet}
</World>
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
import { T, useTask } from '@threlte/core'
import { AutoColliders, Collider, RigidBody } from '@threlte/rapier'
import { BoxGeometry, Color, MeshStandardMaterial, SphereGeometry } from 'three'
import TestBed from './TestBed.svelte'
const material = new MeshStandardMaterial({ color: new Color(0xff3f00) })
let rigidBody = $state.raw<RapierRigidBody>()
let positionZ = 0
let positionX = 0
const offset = Date.now()
useTask(() => {
positionZ = Math.sin(Date.now() / 2000) * 2.5
positionX = Math.sin((Date.now() + offset) / 1500) * 1.2
rigidBody?.setNextKinematicTranslation({ x: positionX, y: 1, z: positionZ })
})
</script>
<!-- ATTACHED COLLIDER -->
<T.Group position={[0, 2, 0]}>
<RigidBody>
<T.Mesh
castShadow
geometry={new BoxGeometry(2, 2, 2)}
{material}
/>
<Collider
shape="cuboid"
args={[1, 1, 1]}
/>
</RigidBody>
</T.Group>
<!-- TEST SPHERE -->
<T.Group position={[0, 1, 0]}>
<RigidBody
bind:rigidBody
type="kinematicPosition"
lockRotations
>
<AutoColliders shape="ball">
<T.Mesh
castShadow
geometry={new SphereGeometry(1)}
material={new MeshStandardMaterial()}
/>
</AutoColliders>
</RigidBody>
</T.Group>
<TestBed title="Attached Collider">
{#snippet text()}
<div>
<p>
Nesting one or multiple {'<Collider>'} components in a {'<RigidBody>'} component effectively
attaches the colliders to the rigid body and allow the rigid body to be affected by contact forces
and gravity.
</p>
</div>
{/snippet}
</TestBed>
<script lang="ts">
import { useTask } from '@threlte/core'
import type { Euler, Vector3 } from 'three'
import Particle from './Particle.svelte'
const getId = () => {
return Math.random().toString(16).slice(2)
}
const getRandomPosition = (): Parameters<Vector3['set']> => {
return [0.5 - Math.random() * 1, 5 - Math.random() * 1 + 10, 0.5 - Math.random() * 1]
}
const getRandomRotation = (): Parameters<Euler['set']> => {
return [Math.random() * 10, Math.random() * 10, Math.random() * 10]
}
type Body = {
id: string
mounted: number
position: Parameters<Vector3['set']>
rotation: Parameters<Euler['set']>
}
let bodies = $state<Body[]>([])
let lastBodyMounted: number = 0
let bodyEveryMilliseconds = 100
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()
}
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)
})
}
})
</script>
{#each bodies as body (body.id)}
<Particle
position={body.position}
rotation={body.rotation}
/>
{/each}
<script
lang="ts"
module
>
const geometry = new BoxGeometry(0.25, 0.25, 0.25)
const material = new MeshStandardMaterial()
</script>
<script lang="ts">
import { T } from '@threlte/core'
import { Collider, RigidBody } from '@threlte/rapier'
import { BoxGeometry, MeshStandardMaterial, type EulerTuple, type Vector3Tuple } from 'three'
interface Props {
position: Vector3Tuple
rotation: EulerTuple
}
let { position, rotation }: Props = $props()
</script>
<T.Group
{position}
{rotation}
>
<RigidBody type={'dynamic'}>
<Collider
shape={'cuboid'}
args={[0.125, 0.125, 0.125]}
/>
<T.Mesh
castShadow
receiveShadow
{geometry}
{material}
/>
</RigidBody>
</T.Group>
<script lang="ts">
import { T } from '@threlte/core'
import { OrbitControls } from '@threlte/extras'
import { Debug } from '@threlte/rapier'
import AttachedCollider from './AttachedCollider.svelte'
import Sensor from './Sensor.svelte'
import StandaloneCollider from './StandaloneCollider.svelte'
interface Props {
testIndex: number
}
let { testIndex }: Props = $props()
const tests = [StandaloneCollider, AttachedCollider, Sensor]
const SvelteComponent = $derived(tests[testIndex])
</script>
<T.PerspectiveCamera
position.x={12}
position.y={13}
fov={40}
makeDefault
>
<OrbitControls target.x={2.5} />
</T.PerspectiveCamera>
<T.DirectionalLight
castShadow
position={[8, 20, -3]}
/>
<T.GridHelper args={[50]} />
<Debug
depthTest={false}
depthWrite={false}
/>
<SvelteComponent />
<script lang="ts">
import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
import { T, useTask } from '@threlte/core'
import { AutoColliders, Collider, RigidBody } from '@threlte/rapier'
import { Color, MeshStandardMaterial, SphereGeometry } from 'three'
import TestBed from './TestBed.svelte'
const gray = new Color(0x333333)
const brand = new Color(0xff3f00)
const material = new MeshStandardMaterial({ color: gray })
let present = $state(false)
$effect.pre(() => {
material.color = present ? brand : gray
})
let rigidBody = $state<RapierRigidBody>()
let positionZ = 0
let positionX = 0
const offset = Date.now()
useTask(() => {
if (!rigidBody) return
positionZ = Math.sin(Date.now() / 2000) * 2.5
positionX = Math.sin((Date.now() + offset) / 1500) * 1.2
rigidBody.setNextKinematicTranslation({ x: positionX, y: 1, z: positionZ })
})
</script>
<!-- SENSOR -->
<T.Group position={[0, 1, 0]}>
<Collider
onsensorenter={() => (present = true)}
onsensorexit={() => (present = false)}
sensor
shape={'cuboid'}
args={[1, 1, 1]}
/>
</T.Group>
<T.Group position={[0, 1, 0]}>
<RigidBody
bind:rigidBody
type={'kinematicPosition'}
lockRotations
>
<AutoColliders shape={'ball'}>
<T.Mesh
castShadow
geometry={new SphereGeometry(1)}
{material}
/>
</AutoColliders>
</RigidBody>
</T.Group>
<TestBed title={'Sensor Collider'}>
{#snippet text()}
<div>
<p>
This collider is marked as a sensor and as such does<br />
not participate in contacts and collisions and can be<br />
useful to detect presence in areas.
</p>
</div>
{/snippet}
</TestBed>
<script lang="ts">
import { MathUtils } from 'three'
import { T } from '@threlte/core'
import { Collider } from '@threlte/rapier'
import Emitter from './Emitter.svelte'
import TestBed from './TestBed.svelte'
</script>
<!-- STANDALONE COLLIDER -->
<T.Group
rotation={[0, 45 * MathUtils.DEG2RAD, 0]}
position={[0, 1, 0]}
>
<Collider
shape={'cuboid'}
args={[1, 1, 1]}
/>
</T.Group>
<Emitter />
<TestBed title="Standalone Collider">
{#snippet text()}
<div>
<p>
This collider is not a child of a {'<RigidBody>'} component.<br />
It will participate in contacts and collisions but is not affected by gravity or external forces.
This can be useful for the environment.
</p>
</div>
{/snippet}
</TestBed>
<script lang="ts">
import type { Snippet } from 'svelte'
import { T } from '@threlte/core'
import { HTML } from '@threlte/extras'
import { AutoColliders } from '@threlte/rapier'
import { BoxGeometry, MeshStandardMaterial } from 'three'
import { DEG2RAD } from 'three/src/math/MathUtils.js'
interface Props {
title: string
useGround?: boolean
text?: Snippet
children?: Snippet
}
let { title, useGround = true, text, children }: Props = $props()
</script>
{#if useGround}
<T.Group position={[1, -0.5, 0]}>
<AutoColliders shape={'cuboid'}>
<T.Mesh
receiveShadow
geometry={new BoxGeometry(12, 1, 10)}
material={new MeshStandardMaterial()}
/>
</AutoColliders>
</T.Group>
{/if}
<HTML
transform
rotation.z={90 * DEG2RAD}
rotation.x={-90 * DEG2RAD}
position.x={5.8}
pointerEvents={'none'}
scale={0.6}
>
<div class="w-[500px] -translate-y-1/2 transform text-black">
<h2>{title}</h2>
<div class="leading-normal">
{@render text?.()}
</div>
</div>
</HTML>
{@render children?.()}
Component Signature
If a <Collider>
component is not a child of a <RigidBody>
component, the transform properties are reactive.
If you don't provide any of the properties density
, mass
or massProperties
, Rapier will figure that out for you.
You can provide either a property density
, mass
or massProperties
.