threlte logo

Terrain with 3D noise

The key to creating “random” yet smooth terrain is using “noise”.

To create a terrain map, we want to be able to input the x and y coordinate , and return a height value. Therefore we want to utilise 2D noise, and this example uses the function createNoise2D from the package simplex-noise.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import { World } from '@threlte/rapier'
  import Scene from './Scene.svelte'
  import { Pane, Checkbox } from 'svelte-tweakpane-ui'
  import { showCollider, autoRotate } from './state'
</script>

<Pane
  title=""
  position="fixed"
>
  <Checkbox
    label="Show Collider"
    bind:value={$showCollider}
  />
  <Checkbox
    label="AutoRotate"
    bind:value={$autoRotate}
  />
</Pane>

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

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { PlaneGeometry } 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 { AutoColliders, Debug } from '@threlte/rapier'
  import { showCollider, autoRotate } from './state'

  const geometry = new PlaneGeometry(10, 10, 100, 100)

  const noise = createNoise2D()
  const vertices = geometry.getAttribute('position').array

  for (let i = 0; i < vertices.length; i += 3) {
    const x = vertices[i]
    const y = vertices[i + 1]
    // @ts-ignore
    vertices[i + 2] = noise(x / 4, y / 4)
  }

  // needed for lighting
  geometry.computeVertexNormals()
</script>

<Debug visible={$showCollider} />

<T.PerspectiveCamera
  makeDefault
  position.y={5}
  position.z={10}
  lookAt.y={2}
>
  <OrbitControls
    autoRotate={$autoRotate}
    enableZoom={false}
    maxPolarAngle={DEG2RAD * 80}
  />
</T.PerspectiveCamera>

<T.DirectionalLight position={[3, 10, 10]} />
<T.HemisphereLight intensity={0.2} />

<AutoColliders shape="trimesh">
  <T.Mesh
    {geometry}
    rotation.x={DEG2RAD * -90}
  >
    <T.MeshStandardMaterial />
  </T.Mesh>
</AutoColliders>
import { writable } from 'svelte/store'

export let showCollider = writable(false)
export let autoRotate = writable(true)

How is this done?

In Scene.svelte:

  1. Create noise map

const noise = createNoise2D()

  1. Create plane geometry

const geometry = new PlaneGeometry(10, 10, 100, 100)

Reminder that planes are on their side by default!

i.e. they extend in the x and y directions, and the z value of each vertex is 0

  1. Extract the “position” (vertices) array from the PlaneGeometry

    const geometry = geometry.getAttribute('position').array

  2. Loop over the vertices, setting each z value, using our noise map

for (let i = 0; i < vertices.length; i += 3) {
  const x = vertices[i]
  const y = vertices[i + 1]
  // @ts-ignore
  vertices[i + 2] = noise(x / 4, y / 4)
}

Why i+=3?

The position array is a flat array of vertices, in the recurring format “x y z x y z…” So if we want to set each vertex’s z value from it’s x and y, we need to loop in triplets.

  1. Attach the plane geometry to a mesh, and rotate
<T.Mesh
	{geometry}
	rotation.x={DEG2RAD * -90}
>

This example intentionally used only part of the noise map (the middle 25%), in order to generate gentler terrain.

To “see” the noise map in full, change noise(x/4, y/4) to noise(x, y).

You’ll see that the terrain is much more hilly!