threlte logo
@threlte/rapier

<World>

This component provides the basic physics context and loads rapier.

All components that rely on physics (e.g. <RigidBody> or <Collider>) must be a child of <World>.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import { HTML } from '@threlte/extras'
  import { World } from '@threlte/rapier'
  import { Pane, Text } from 'svelte-tweakpane-ui'
  import Scene from './Scene.svelte'
</script>

<Pane
  title="World"
  position="fixed"
>
  <Text
    value="Use the arrow keys to move around"
    disabled
  />
</Pane>

<div>
  <Canvas>
    <World>
      <Scene />

      <HTML
        slot="fallback"
        transform
      >
        <p>
          It seems your browser<br />
          doesn't support WASM.<br />
          I'm sorry.
        </p>
      </HTML>
    </World>
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
  p {
    font-size: 0.75rem;
    line-height: 1rem;
  }
</style>
<script lang="ts">
  import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
  import { T } from '@threlte/core'
  import { HTML } from '@threlte/extras'
  import { AutoColliders, Collider, CollisionGroups, RigidBody } from '@threlte/rapier'
  import { cubicIn, cubicOut } from 'svelte/easing'
  import { tweened } from 'svelte/motion'
  import { blur } from 'svelte/transition'
  import { BoxGeometry, Euler, type Group, MeshStandardMaterial, Quaternion } from 'three'
  import { DEG2RAD } from 'three/src/math/MathUtils.js'

  let open = false
  let objectsInSensor = 0
  $: open = objectsInSensor > 0

  let group: Group
  let doorRigidBody: RapierRigidBody

  let doorRotationClosed = 0
  let doorRotationOpen = -105 * DEG2RAD
  let doorRotation = tweened(doorRotationClosed)
  $: doorRotation.set(open ? doorRotationOpen : doorRotationClosed, {
    easing: open ? cubicOut : cubicIn
  })

  const q = new Quaternion()
  const e = new Euler()

  const applyDoorRotation = (rotation: number) => {
    if (!group || !doorRigidBody) return
    group.getWorldQuaternion(q)
    e.setFromQuaternion(q)
    e.y += rotation
    q.setFromEuler(e)
    doorRigidBody.setNextKinematicRotation(q)
  }

  $: if (group && doorRigidBody) applyDoorRotation($doorRotation)
</script>

<T.Group bind:ref={group}>
  <!-- FRAME -->
  <AutoColliders shape={'cuboid'}>
    <!-- SIDE FRAME A -->
    <T.Mesh
      receiveShadow
      castShadow
      position={[0.7, 1.125, 0]}
      geometry={new BoxGeometry(0.3, 2.25, 0.3)}
      material={new MeshStandardMaterial()}
    />

    <!-- SIDE FRAME B -->
    <T.Mesh
      receiveShadow
      castShadow
      position={[-0.7, 1.125, 0]}
      geometry={new BoxGeometry(0.3, 2.25, 0.3)}
      material={new MeshStandardMaterial()}
    />

    <!-- TOP FRAME -->
    <T.Mesh
      receiveShadow
      castShadow
      position.y={2.4}
      geometry={new BoxGeometry(1.4 + 0.3, 0.3, 0.3)}
      material={new MeshStandardMaterial()}
    />
  </AutoColliders>

  <HTML
    transform
    position.y={3}
    pointerEvents={'none'}
  >
    {#key open}
      <small
        in:blur={{
          amount: 15,
          duration: 300
        }}
        out:blur={{
          amount: 15,
          duration: 300
        }}
        class="door"
        class:closed={!open}
        class:open
      >
        {open ? 'UNLOCKED' : 'LOCKED'}
      </small>
    {/key}
  </HTML>

  <!-- DOOR -->
  <T.Group position={[-0.5, 1.125, 0]}>
    <RigidBody
      bind:rigidBody={doorRigidBody}
      type={'kinematicPosition'}
    >
      <AutoColliders shape={'cuboid'}>
        <T.Mesh
          receiveShadow
          castShadow
          position.x={0.5}
          geometry={new BoxGeometry(1, 2.25, 0.1)}
          material={new MeshStandardMaterial()}
        />
      </AutoColliders>
    </RigidBody>
  </T.Group>

  <CollisionGroups groups={[15]}>
    <T.Group position={[0, 1.5, 0]}>
      <Collider
        shape={'cuboid'}
        args={[1, 1.35, 1.5]}
        sensor
        on:sensorenter={() => (objectsInSensor += 1)}
        on:sensorexit={() => (objectsInSensor -= 1)}
      />
    </T.Group>
  </CollisionGroups>
</T.Group>

<style>
  .door {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
    padding-top: 0.25rem;
    padding-bottom: 0.25rem;
    color: rgb(255, 255, 255);
    border-radius: 0.375rem;
    position: absolute;
    transform: translateX(-50%) translateY(-50%);
  }

  .closed {
    background-color: rgb(239, 68, 68);
  }

  .open {
    background-color: rgb(34, 197, 94);
  }
</style>
<script lang="ts">
  import { T } from '@threlte/core'
  import { AutoColliders } from '@threlte/rapier'
</script>

<T.Group position={[0, -0.5, 0]}>
  <AutoColliders shape={'cuboid'}>
    <T.Mesh receiveShadow>
      <T.BoxGeometry args={[100, 1, 100]} />
      <T.MeshStandardMaterial />
    </T.Mesh>
  </AutoColliders>
</T.Group>
<script lang="ts">
  import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat'
  import { T, useTask } from '@threlte/core'
  import { AutoColliders, BasicPlayerController, RigidBody } from '@threlte/rapier'
  import { CapsuleGeometry, Mesh, MeshStandardMaterial, SphereGeometry, Vector3 } from 'three'

  export let position: Parameters<Vector3['set']> | undefined = undefined

  export let playerMesh: Mesh
  let ballMesh: Mesh

  let rigidBody: RapierRigidBody

  const playerPos = new Vector3()
  const ballPos = new Vector3()
  const maxF = 0.05
  const min = new Vector3(-maxF, 0, -maxF)
  const max = new Vector3(maxF, 0, maxF)

  useTask(() => {
    if (!playerMesh || !ballMesh || !rigidBody) return

    playerMesh.getWorldPosition(playerPos)
    ballMesh.getWorldPosition(ballPos)

    const diff = playerPos.sub(ballPos).divideScalar(2000)
    diff.y = 0

    const f = diff.clamp(min, max)

    rigidBody.applyImpulse(f, true)
  })
</script>

<!-- To detect the groundedness of the player, a collider on group 15 is used -->
<BasicPlayerController
  {position}
  speed={3}
  radius={0.3}
  height={1.8}
  jumpStrength={2}
  groundCollisionGroups={[15]}
  playerCollisionGroups={[0]}
>
  <T.Mesh
    bind:ref={playerMesh}
    position.y={0.9}
    receiveShadow
    castShadow
    geometry={new CapsuleGeometry(0.3, 1.8 - 0.3 * 2)}
    material={new MeshStandardMaterial()}
  />
</BasicPlayerController>

<T.Group position={[0, 1, -5]}>
  <RigidBody bind:rigidBody>
    <AutoColliders shape={'ball'}>
      <T.Mesh
        bind:ref={ballMesh}
        receiveShadow
        castShadow
        geometry={new SphereGeometry(0.25)}
        material={new MeshStandardMaterial()}
      >
        <slot />
      </T.Mesh>
    </AutoColliders>
  </RigidBody>
</T.Group>
<script lang="ts">
  import { T, useTask, useThrelte } from '@threlte/core'
  import { Environment } from '@threlte/extras'
  import { AutoColliders, CollisionGroups, Debug } from '@threlte/rapier'
  import { spring } from 'svelte/motion'
  import { BoxGeometry, Mesh, MeshStandardMaterial, Vector3 } from 'three'
  import Door from './Door.svelte'
  import Ground from './Ground.svelte'
  import Player from './Player.svelte'

  let playerMesh: Mesh
  let positionHasBeenSet = false
  const smoothPlayerPosX = spring(0)
  const smoothPlayerPosZ = spring(0)
  const t3 = new Vector3()

  useTask(() => {
    if (!playerMesh) return
    playerMesh.getWorldPosition(t3)
    smoothPlayerPosX.set(t3.x, {
      hard: !positionHasBeenSet
    })
    smoothPlayerPosZ.set(t3.z, {
      hard: !positionHasBeenSet
    })
    if (!positionHasBeenSet) positionHasBeenSet = true
  })

  const { size } = useThrelte()
  $: zoom = $size.width / 8
</script>

<Environment
  path="/hdr/"
  files="shanghai_riverside_1k.hdr"
/>

<T.Group
  position.x={$smoothPlayerPosX}
  position.z={$smoothPlayerPosZ}
>
  <T.Group
    position.y={0.9}
    let:ref={target}
  >
    <T.OrthographicCamera
      makeDefault
      {zoom}
      position={[50, 50, 30]}
      on:create={({ ref }) => {
        ref.lookAt(target.getWorldPosition(new Vector3()))
      }}
    />
  </T.Group>
</T.Group>

<T.DirectionalLight
  castShadow
  position={[8, 20, -3]}
/>

<T.GridHelper
  args={[50]}
  position.y={0.01}
/>

<Debug
  depthTest={false}
  depthWrite={false}
/>

<!--
	The ground needs to be on both group 15 which is the group
	to detect the groundedness of the player as well as on group
	0 which is the group that the player is actually physically
	interacting with.
 -->
<CollisionGroups groups={[0, 15]}>
  <Ground />
</CollisionGroups>

<!--
	All physically interactive stuff should be on group 0
-->
<CollisionGroups groups={[0]}>
  <Player
    bind:playerMesh
    position={[0, 2, -3]}
  />

  <Door />

  <!-- WALLS -->
  <AutoColliders shape={'cuboid'}>
    <T.Mesh
      receiveShadow
      castShadow
      position.x={30 + 0.7 + 0.15}
      position.y={1.275}
      geometry={new BoxGeometry(60, 2.55, 0.15)}
      material={new MeshStandardMaterial({
        transparent: true,
        opacity: 0.5,
        color: 0x333333
      })}
    />
    <T.Mesh
      receiveShadow
      castShadow
      position.x={-30 - 0.7 - 0.15}
      position.y={1.275}
      geometry={new BoxGeometry(60, 2.55, 0.15)}
      material={new MeshStandardMaterial({
        transparent: true,
        opacity: 0.5,
        color: 0x333333
      })}
    />
  </AutoColliders>
</CollisionGroups>

Structure

A typical structure of a physics-enabled wrapper component might look like this:

Wrapper.svelte
<script lang="ts">
  import { Canvas } from '@threlte/core'
  import { World } from '@threlte/rapier'
  import Scene from './Scene.svelte'
</script>

<Canvas>
  <World>
    <Scene />
    <!-- Everything is happening inside this component -->
  </World>
</Canvas>

This structure ensures that all components inside the component <Scene> have access to the physics context.

Fallback

rapier is a Rust-based physics engine and as such bundled and used as a WASM module. If loading of rapier fails for any reason, a slot with the name fallback is mounted to e.g. display a fallback scene without physics.

Wrapper.svelte
<script lang="ts">
  import { Canvas } from '@threlte/core'
  import { World } from '@threlte/rapier'
  import Scene from './Scene.svelte'
  import FallbackScene from './FallbackScene.svelte'
</script>

<Canvas>
  <World>
    <Scene />
    <FallbackScene slot="fallback" />
  </World>
</Canvas>

Component Signature

Props

name
type
required
default

gravity
Position
no
{ y: -9.81 }

rawBodies
RawRigidBodySet
no

rawBroadPhase
RawBroadPhase
no

rawCCDSolver
RawCCDSolver
no

rawColliders
RawColliderSet
no

rawDebugRenderPipeline
RawDebugRenderPipeline
no

rawImpulseJoints
RawImpulseJointSet
no

rawIntegrationParameters
RawIntegrationParameters
no

rawIslands
RawIslandManager
no

rawMultibodyJoints
RawMultibodyJointSet
no

rawNarrowPhase
RawNarrowPhase
no

rawPhysicsPipeline
RawPhysicsPipeline
no

rawQueryPipeline
RawQueryPipeline
no

rawSerializationPipeline
RawSerializationPipeline
no