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 massProperties, Rapier will figure that out for you.

You can provide either a property density, mass or massProperties.

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

contactForceEventThreshold
number
no

density
number
no

friction
number
no

frictionCombineRule
CoefficientCombineRule
no

mass
number
no

massProperties
{ mass: number, centerOfMass: Position, principalAngularInertia: Position, angularInertiaLocalFrame: Rotation, }
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.