@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 />
{#snippet fallback()}
<HTML transform>
<p>
It seems your browser<br />
doesn't support WASM.<br />
I'm sorry.
</p>
</HTML>
{/snippet}
</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,
MathUtils
} from 'three'
let open = false
let objectsInSensor = 0
$: open = objectsInSensor > 0
let group: Group
let doorRigidBody: RapierRigidBody
let doorRotationClosed = 0
let doorRotationOpen = -105 * MathUtils.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
onsensorenter={() => (objectsInSensor += 1)}
onsensorexit={() => (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(() => {
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, Group, 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
let target: Group
</script>
<Environment
path="/hdr/"
files="shanghai_riverside_1k.hdr"
/>
<T.Group
position.x={$smoothPlayerPosX}
position.z={$smoothPlayerPosZ}
>
<T.Group
position.y={0.9}
bind:ref={target}
>
<T.OrthographicCamera
makeDefault
{zoom}
position={[50, 50, 30]}
oncreate={({ 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 />
{#snippet fallback()}
<FallbackScene />
{/snippet}
</World>
</Canvas>