Terrain with rapier physics
This example shows how to include user-generated random terrain as a fixed <RigidBody>, within a Rapier world.
This is an adaption of Rapier’s demo (select “Demo: triangle mesh”).
<script lang="ts"> import Scene from './Scene.svelte' import { Button, Pane } from 'svelte-tweakpane-ui' import { Canvas } from '@threlte/core' import { World } from '@threlte/rapier'
let resetCounter = $state(0) let showDebug = $state(false)</script>
<Pane title="" position="fixed"> <Button title="Reset" on:click={() => { resetCounter += 1 }} /> <Button title="Toggle Debug" on:click={() => { showDebug = !showDebug }} /></Pane>
<div> <Canvas> <World> <Scene {resetCounter} {showDebug} /> </World> </Canvas></div>
<style> div { height: 100%; }</style><script lang="ts" module> const radius = 0.25 const cuboid: Shape = { autoCollider: 'cuboid', color: 'hotpink', geometry: new BoxGeometry(radius, radius, radius) }
const shapes: Shape[] = [ cuboid, { autoCollider: 'ball', color: 'cyan', geometry: new SphereGeometry(radius) }, { autoCollider: 'convexHull', color: 'green', geometry: new CylinderGeometry(radius, radius, radius * 2) }, { autoCollider: 'convexHull', color: 'orange', geometry: new ConeGeometry(radius, radius * 3, 10) } ]
const getRandomShape = (defaultShape = cuboid): Shape => { return shapes[Math.floor(Math.random() * shapes.length)] ?? defaultShape }</script>
<script lang="ts"> import type { AutoCollidersShapes } from '@threlte/rapier' import { Quaternion, type BufferGeometry, type ColorRepresentation } from 'three' import type { Snippet } from 'svelte' import { AutoColliders, RigidBody } from '@threlte/rapier' import { BoxGeometry, ConeGeometry, CylinderGeometry, SphereGeometry, Vector3 } from 'three' import { T } from '@threlte/core'
type Shape = { autoCollider: AutoCollidersShapes color: ColorRepresentation geometry: BufferGeometry }
let { children }: { children?: Snippet<[{ shape: Shape }]> } = $props()
const offset = new Vector3(-2.5, 2.5, -2.5)
type Body = { position: Vector3 quaternion: Quaternion }
const bodies: Body[] = [] const count = 50 for (let i = 0; i < count; i += 1) { bodies.push({ position: new Vector3().random().multiplyScalar(5).add(offset), quaternion: new Quaternion().random() }) }</script>
{#each bodies as body} {@const shape = getRandomShape()} <T.Group position={body.position.toArray()} quaternion={body.quaternion.toArray()} > <RigidBody type="dynamic"> <AutoColliders shape={shape.autoCollider}> {@render children?.({ shape })} </AutoColliders> </RigidBody> </T.Group>{/each}<script lang="ts"> import FallingShapes from './FallingShapes.svelte' import RAPIER from '@dimforge/rapier3d-compat' import { Collider, Debug, RigidBody } from '@threlte/rapier' import { DoubleSide, PlaneGeometry, MathUtils } from 'three' import { Environment, OrbitControls, Suspense } from '@threlte/extras' import { SimplexNoise } from 'three/examples/jsm/math/SimplexNoise.js' import { T } from '@threlte/core'
let { resetCounter = 0, showDebug = false }: { resetCounter?: number; showDebug?: boolean } = $props()
const nsubdivs = 10 const size = 10
const heights: number[] = []
const geometry = new PlaneGeometry(size, size, nsubdivs, nsubdivs)
const noise = new SimplexNoise() const positions = geometry.getAttribute('position').array
for (let x = 0; x <= nsubdivs; x++) { for (let y = 0; y <= nsubdivs; y++) { const height = noise.noise(x / 4, y / 4) const vertIndex = (x + (nsubdivs + 1) * y) * 3 positions[vertIndex + 2] = height const heightIndex = y + (nsubdivs + 1) * x heights[heightIndex] = height } }
// needed for lighting geometry.computeVertexNormals()
const scale = new RAPIER.Vector3(size, 1, size)</script>
<T.PerspectiveCamera makeDefault position.y={10} position.z={10}> <OrbitControls /></T.PerspectiveCamera>
<Suspense> {#key resetCounter} <FallingShapes> {#snippet children({ shape })} <T.Mesh castShadow receiveShadow geometry={shape.geometry} > <T.MeshStandardMaterial color={shape.color} /> </T.Mesh> {/snippet} </FallingShapes> {/key}
<Environment url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr" />
<T.DirectionalLight castShadow position={[5, 5, 5]} />
<T.Mesh receiveShadow {geometry} rotation.x={MathUtils.DEG2RAD * -90} > <T.MeshStandardMaterial color="teal" opacity={0.8} transparent side={DoubleSide} /> </T.Mesh> <RigidBody type="fixed"> <Collider shape="heightfield" args={[nsubdivs, nsubdivs, new Float32Array(heights), scale]} /> </RigidBody>
{#if showDebug} <Debug /> {/if}</Suspense>Heightfield colliders
Section titled “Heightfield colliders”A heightmap is generated by looping over the vertices of a plane geometry. The height information is passed to a <Collider> and wrapped in a <RigidBody>.
<RigidBody type="fixed"> <Collider shape="heightfield" args={[nsubdivs, nsubdivs, heights, scale]} /></RigidBody>Giving the <RigidBody> a "fixed" type prevents it from being affected by gravity.
In the <FallingShapes> component, a bunch of random geometries are positioned above the plane and dropped. Each geometry is wrapped in a <RigidBody> with a type of "dynamic". This will cause it to be affected by gravity.