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 { useTweakpane } from '$lib/useTweakpane'

	let testIndex = 0

	const { addButton, action, addInput } = useTweakpane()

	addButton({
		title: 'Standalone Collider',
		onClick: () => (testIndex = 0)
	})
	addButton({
		title: 'Attached Collider',
		onClick: () => (testIndex = 1)
	})
	addButton({
		title: 'Sensor Collider',
		onClick: () => (testIndex = 2)
	})
</script>

<div use:action />

<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>
<script lang="ts">
  import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
  import { T, useFrame } 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()

  useFrame(() => {
    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>

    <p>
      If a collider is attached to a {'<RigidBody>'} its transform properties are applied once on initialization.
    </p>
  </div>
</TestBed>
<script lang="ts">
	import { useFrame } 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

	useFrame(() => {
		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, useFrame } 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()

  useFrame(() => {
    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'
  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>
    <p>Standalone colliders have reactive transform properties.</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'

  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="transform -translate-y-1/2 text-black w-[500px]">
    <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

collisionenter
CustomEvent<{ targetCollider: Collider targetRigidBody: RigidBody | null manifold: TempContactManifold flipped: boolean }>

collisionexit
CustomEvent<{ targetCollider: Collider targetRigidBody: RigidBody | null }>

sensorenter
CustomEvent<{ targetCollider: Collider targetRigidBody: RigidBody | null }>

sensorexit
CustomEvent<{ targetCollider: Collider targetRigidBody: RigidBody | null }>

contact
CustomEvent<{ targetCollider: Collider targetRigidBody: RigidBody | null manifold: TempContactManifold flipped: boolean maxForceDirection: Vector maxForceMagnitude: number totalForce: Vector totalForceMagnitude: number }>

Bindings

name
type

collider
Collider