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 = 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} />

      <HTML
        slot="fallback"
        transform
      >
        <p class="text-xs">
          It seems your browser<br />
          doesn't support WASM.<br />
          I'm sorry.
        </p>
      </HTML>
    </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: 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>

<!-- 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'}>
  <div slot="text">
    <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>
</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: 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()
      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"
  context="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 type { Euler } from 'three'
  import { BoxGeometry, MeshStandardMaterial, Vector3 } from 'three'

  export let position: Parameters<Vector3['set']>
  export let rotation: Parameters<Euler['set']>
</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'

  export let testIndex: number

  const tests = [StandaloneCollider, AttachedCollider, Sensor]
</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}
/>

<svelte:component this={tests[testIndex]} />
<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 = false
  $: material.color = present ? brand : gray

  let rigidBody: 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
    on:sensorenter={() => (present = true)}
    on:sensorexit={() => (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'}>
  <div slot="text">
    <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>
</TestBed>
<script lang="ts">
  import { T } from '@threlte/core'
  import { Collider } from '@threlte/rapier'
  import { DEG2RAD } from 'three/src/math/MathUtils.js'
  import Emitter from './Emitter.svelte'
  import TestBed from './TestBed.svelte'
</script>

<!-- STANDALONE COLLIDER -->
<T.Group
  rotation={[0, 45 * DEG2RAD, 0]}
  position={[0, 1, 0]}
>
  <Collider
    shape={'cuboid'}
    args={[1, 1, 1]}
  />
</T.Group>

<Emitter />

<TestBed title={'Standalone Collider'}>
  <div slot="text">
    <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>
</TestBed>
<script lang="ts">
  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'

  export let title: string
  export let useGround = true
</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">
      <slot name="text" />
    </div>
  </div>
</HTML>

<slot />

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, cleanup: (callback: () => void) => void }

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 manifold: TempContactManifold flipped: boolean maxForceDirection: Vector maxForceMagnitude: number totalForce: Vector totalForceMagnitude: number }

Bindings

name
type

collider
Collider