threlte logo

Terrain with 3D noise

Noise is often used in graphics and game development to create “smooth randomness”.

Three.js has a SimplexNoise addon that can be used for this purpose. In the example below, it is used to generate a smooth random surface.

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

  let autoRotate = $state(true)
  let flatness = $state(4)

  let scene = $state()
</script>

<Pane
  title="3D noise terrain"
  position="fixed"
>
  <Checkbox
    label="Auto-rotate Camera"
    bind:value={autoRotate}
  />
  <Slider
    bind:value={flatness}
    label="flatness"
    min={1}
    max={10}
    step={1}
  />
</Pane>

<div>
  <Canvas>
    <Scene
      {autoRotate}
      {flatness}
    />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { Environment, OrbitControls } from '@threlte/extras'
  import { DoubleSide, PlaneGeometry } from 'three'
  import { SimplexNoise } from 'three/examples/jsm/Addons.js'
  import { T } from '@threlte/core'

  let { autoRotate = false, flatness = 4 }: { autoRotate?: boolean; flatness?: number } = $props()

  const geometry = new PlaneGeometry(10, 10, 100, 100)
  const positions = geometry.getAttribute('position')

  const noise = new SimplexNoise()

  $effect(() => {
    for (let i = 0; i < positions.count; i += 1) {
      const x = positions.getX(i) / flatness
      const y = positions.getY(i) / flatness
      positions.setZ(i, noise.noise(x, y))
    }

    positions.needsUpdate = true

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

<T.PerspectiveCamera
  makeDefault
  position={10}
>
  <OrbitControls
    {autoRotate}
    autoRotateSpeed={0.5}
  />
</T.PerspectiveCamera>

<Environment url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr" />

<T.Mesh
  {geometry}
  rotation.x={-1 * 0.5 * Math.PI}
>
  <T.MeshStandardMaterial side={DoubleSide} />
</T.Mesh>

Setting the Height of Each Vertex

After the geometry is created, the z-value of each vertex’s position is set with a value generated from the noise function.

const noise = new SimplexNoise()

for (let i = 0; i < positions.count; i += 1) {
  const x = positions.getX(i) / flatness
  const y = positions.getY(i) / flatness
  positions.setZ(i, noise.noise(x, y))
}

The flatness variable scales down the x and y values that are passed to the noise function. A higher flatness value corresponds to smaller changes between noise values thus a flatter surface.

When updating attributes of a geometry after the first render, you may have to set attribute.needsUpdate to true. It may also be necessary to recalculate the geometry’s vertex normals using geometry.computeVertexNormals().

Rotating the Geometry

One important thing to note is that the plane geometry is created in the xy-plane. This is why the z-value is treated as the height of the vertex and the geometry is rotated 90 degrees. The Z-up coordinate system is very common to see especially in such areas as structural design and 3D-printing.

Deterministic Noise Values

SimplexNoise.noise is deterministic. In other words, when given the same x and y, the output is always the same. If you want to produce different results, you can offset the x and y inputs by some amount.

const noise = new SimplexNoise()

$effect(() => {
  const randomOffset = Math.random()
  for (let i = 0; i < positions.count; i += 1) {
    const x = positions.getX(i) / flatness + randomOffset
    const y = positions.getY(i) / flatness + randomOffset
    positions.setZ(i, noise.noise(x, y))
  }

  // ...
})