threlte logo
@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)
  let version = $state(0)
</script>

<Pane
  title="Colliders"
  position="fixed"
>
  <Button
    label="type"
    title="Standalone"
    on:click={() => {
      testIndex = 0
      version += 1
    }}
  />
  <Button
    label=""
    title="Attached"
    on:click={() => {
      testIndex = 1
      version += 1
    }}
  />
  <Button
    label=""
    title="Sensor"
    on:click={() => {
      testIndex = 2
      version += 1
    }}
  />
</Pane>

<div>
  <Canvas>
    <World>
      {#key version}
        <Scene {testIndex} />
      {/key}

      {#snippet fallback()}
        <HTML transform>
          <p class="text-xs">
            It seems your browser 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 { Vector3 } from 'three'
  import TestBed from './TestBed.svelte'

  let rigidBody = $state.raw<RapierRigidBody>()

  const position = new Vector3(0, 1, 0)
  let elapsed = 0

  useTask((dt) => {
    elapsed += dt
    position.x = Math.sin(elapsed)
    position.z = Math.cos(elapsed)
    rigidBody?.setNextKinematicTranslation(position)
  })
</script>

<!-- ATTACHED COLLIDER -->
<T.Group position={[0, 2, 0]}>
  <RigidBody>
    <T.Mesh castShadow>
      <T.MeshStandardMaterial color={0xff3f00} />
      <T.BoxGeometry args={[2, 2, 2]} />
    </T.Mesh>
    <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>
        <T.SphereGeometry args={[1]} />
        <T.MeshStandardMaterial />
      </T.Mesh>
    </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 { Quaternion, type QuaternionTuple, type Vector3Tuple } from 'three'
  import Particle from './Particle.svelte'
  import { SvelteSet } from 'svelte/reactivity'

  interface Body {
    mounted: number
    position: Vector3Tuple
    quaternion: QuaternionTuple
  }

  let bodies = new SvelteSet<Body>()

  let lastBodyMounted = 0
  let bodyEveryMilliseconds = 100
  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"
  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 QuaternionTuple, type Vector3Tuple } from 'three'

  interface Props {
    position: Vector3Tuple
    quaternion: QuaternionTuple
  }

  let { position, quaternion }: Props = $props()
</script>

<T.Group
  {position}
  {quaternion}
>
  <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
  oncreate={(ref) => ref.lookAt(2.5, 0, 0)}
>
  <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 } from 'three'
  import TestBed from './TestBed.svelte'

  const gray = new Color(0x333333)
  const brand = new Color(0xff3f00)

  let present = $state(false)

  let rigidBody = $state.raw<RapierRigidBody>()
  const offset = Date.now()

  useTask(() => {
    const positionZ = Math.sin(Date.now() / 2000) * 2.5
    const 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>
        <T.SphereGeometry />
        <T.MeshStandardMaterial color={present ? brand : gray} />
      </T.Mesh>
    </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 { MathUtils } from 'three'

  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>
        <T.BoxGeometry args={[12, 1, 10]} />
        <T.MeshStandardMaterial />
      </T.Mesh>
    </AutoColliders>
  </T.Group>
{/if}

<HTML
  transform
  rotation.z={90 * MathUtils.DEG2RAD}
  rotation.x={-90 * MathUtils.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 the full mass properties, Rapier will figure that out for you.

Provide density on its own, mass on its own, or mass together with centerOfMass, principalAngularInertia and angularInertiaLocalFrame.

Props

name
type
required

args
Parameters<typeof ColliderDesc[Shape]>
yes

shape
'ball' | 'capsule' | 'segment' | 'triangle' | 'roundTriangle' | 'polyline' | 'trimesh' | 'cuboid' | 'roundCuboid' | 'heightfield' | 'cylinder' | 'roundCylinder' | 'cone' | 'roundCone' | 'convexHull' | 'convexMesh' | 'roundConvexHull' | 'roundConvexMesh'
yes

angularInertiaLocalFrame
[number, number, number]
no

centerOfMass
[number, number, number]
no

contactForceEventThreshold
number
no

density
number
no

friction
number
no

frictionCombineRule
CoefficientCombineRule
no

mass
number
no

principalAngularInertia
[number, number, number]
no

restitution
number
no

restitutionCombineRule
CoefficientCombineRule
no

sensor
boolean
no

Events

name
payload

create
ref: Collider

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, maxForceDirection: Vector, maxForceMagnitude: number, totalForce: Vector, totalForceMagnitude: number }

Bindings

name
type

collider
Collider

Exports

name
type
description

refresh
() => void
Invoke this function to update the colliders transforms.