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 own demo (select “Demo: triangle mesh”).
<script lang="ts">
import { Canvas } from '@threlte/core'
import { World } from '@threlte/rapier'
import Scene from './Scene.svelte'
import { Pane, Button } from 'svelte-tweakpane-ui'
let reset: () => any | undefined
let toggleDebug: () => any | undefined
</script>
<Pane
title=""
position="fixed"
>
<Button
title="Reset"
on:click={reset}
/>
<Button
title="Toggle Debug"
on:click={toggleDebug}
/>
</Pane>
<div>
<Canvas>
<World>
<Scene
bind:reset
bind:toggleDebug
/>
</World>
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import { T } from '@threlte/core'
import { Collider, RigidBody, AutoColliders } from '@threlte/rapier'
import { BoxGeometry, SphereGeometry, CylinderGeometry, ConeGeometry } from 'three'
const radius = 0.25
const shapes = [
{
geometry: new BoxGeometry(radius, radius, radius),
autoCollider: 'cuboid',
color: 'hotpink'
},
{
geometry: new SphereGeometry(radius),
autoCollider: 'ball',
color: 'cyan'
},
{
geometry: new CylinderGeometry(radius, radius, radius * 2),
autoCollider: 'convexHull',
color: 'green'
},
{
geometry: new ConeGeometry(radius, radius * 3, 10),
autoCollider: 'convexHull',
color: 'orange'
}
]
const bodies = new Array(50).fill(0).map((_, index) => {
const position: Parameters<Vector3['set']> = [
Math.random() * 5 - 2.5,
Math.random() * 5,
Math.random() * 5 - 2.5
]
const rotation: Parameters<Euler['set']> = [
Math.random() * 10,
Math.random() * 10,
Math.random() * 10
]
const shape = shapes[Math.floor(Math.random() * shapes.length)]
return {
id: index,
position,
rotation,
...shape
}
})
</script>
{#each bodies as body (body.id)}
<T.Group
position={body.position}
rotation={body.rotation}
>
<RigidBody type={'dynamic'}>
<AutoColliders shape={body.autoCollider}>
<T.Mesh
castShadow
receiveShadow
geometry={body.geometry}
>
<T.MeshStandardMaterial color={body.color} />
</T.Mesh>
</AutoColliders>
</RigidBody>
</T.Group>
{/each}
<script lang="ts">
import { PlaneGeometry, MeshStandardMaterial } from 'three'
import { DEG2RAD } from 'three/src/math/MathUtils.js'
import { createNoise2D } from 'simplex-noise'
import { T } from '@threlte/core'
import { OrbitControls } from '@threlte/extras'
import RAPIER from '@dimforge/rapier3d-compat'
import { Collider, Debug, RigidBody } from '@threlte/rapier'
import FallingShapes from './FallingShapes.svelte'
let nsubdivs = 10
let heights = []
const geometry = new PlaneGeometry(10, 10, nsubdivs, nsubdivs)
const noise = createNoise2D()
const vertices = geometry.getAttribute('position').array
for (let x = 0; x <= nsubdivs; x++) {
for (let y = 0; y <= nsubdivs; y++) {
let height = noise(x, y)
const vertIndex = (x + (nsubdivs + 1) * y) * 3
//@ts-ignore
vertices[vertIndex + 2] = height
const heightIndex = y + (nsubdivs + 1) * x
heights[heightIndex] = height
}
}
// needed for lighting
geometry.computeVertexNormals()
const scale = new RAPIER.Vector3(10.0, 1, 10)
let resetCounter = 0
export const reset = () => {
resetCounter += 1
}
let debugEnabled = false
export const toggleDebug = () => {
debugEnabled = !debugEnabled
}
</script>
<T.PerspectiveCamera
makeDefault
position.y={10}
position.z={10}
lookAt.y={0}
>
<OrbitControls enableZoom={true} />
</T.PerspectiveCamera>
<T.DirectionalLight position={[3, 10, 10]} />
<T.HemisphereLight intensity={0.2} />
{#key resetCounter}
<FallingShapes />
{/key}
<T.Mesh
receiveShadow
{geometry}
rotation.x={DEG2RAD * -90}
rotation.z={DEG2RAD * 0}
>
<T.MeshStandardMaterial
color="teal"
opacity="0.8"
transparent
/>
</T.Mesh>
<RigidBody type={'fixed'}>
<Collider
shape={'heightfield'}
args={[nsubdivs, nsubdivs, heights, scale]}
/>
</RigidBody>
{#if debugEnabled === true}
<Debug />
{/if}
How does it work
- Similar to the 3D noise example, loop over the vertices of a PlaneGeometry, and use a noise map to create a heightfield array
heights
. - Attach the heightfield to a rapier
<Collider>
<Collider
shape={'heightfield'}
args={[nsubdivs, nsubdivs, heights, scale]}
/>
- Wrap in a fixed
<RigidBody>
. We don’t want this terrain to respond to gravity and fall downwards when rapier physics simulation begins.
<RigidBody type={'fixed'}>
<Collider ... />
</RigidBody>
- Add some falling random shapes to the scene to prove that the terrain is functioning properly (see “FallingShapes.svelte”)
-
define some shapes in an array
- geometry
<AutoCollider>
type- color
-
fill an array of 50 items with a random shape, with a random position and rotation, and loop over it in markup
-
add a
<T.Mesh>
to the scene, provide it with the chosen geometry, and a material with the chosen color -
wrap that in an
<AutoColliders>
with the chosen AutoCollider -
wrap in a
'dynamic'
<RigidBody>
. Dynamic because we want these shapes to fall in the scene according to gravity. -
finally, wrap in a
<T.Group>
to set our random position and rotation