AppleyAppley Docs
Docs/Terrain

Procedural Terrain

Generate infinite terrain using multi-octave noise, erosion simulation, and biome blending.

15 min read Advanced Updated 2026-07-01 v2.0 ~50 min to complete

Introduction

Professional games like No Mans Sky and Minecraft generate terrain procedurally so players never run out of world. This guide explains how AAA studios layer noise functions, simulate hydraulic erosion, and blend biomes — then shows how to build the same systems in Appley using FluxScript and Node Scripting.

Advanced Guide 1 of 25

This is part of the Advanced Documentation collection. Category: Terrain. Estimated completion: 50 minutes. Difficulty: Advanced/Professional.

Screenshot

Procedural Terrain
Procedural Terrain

How Professional Games Do It

1

Understand noise layering

Professional terrain uses 4-8 octaves of Perlin or Simplex noise at different frequencies. Low frequencies create continents, high frequencies add detail. Each octave has half the amplitude of the previous.

2

Implement biome blending

Use a separate noise layer as a temperature/moisture map. Blend between biome presets (desert, forest, tundra) based on these values. This is how Minecraft and Valheim create varied landscapes.

3

Simulate hydraulic erosion

Drop virtual water particles on the heightmap. Each particle carries sediment downhill and deposits it on flat areas. This creates realistic river valleys and ridges that pure noise cannot produce.

4

Use domain warping

Warp the noise input coordinates with a second noise function. This creates swirling, organic terrain features instead of predictable grid patterns. Used heavily in Astroneer and No Mans Sky.

5

Generate terrain at runtime

For infinite worlds, generate terrain chunks on demand as the player moves. Cache generated chunks and unload distant ones. This is the core of Minecraft chunk streaming.

6

Apply thermal erosion

After hydraulic erosion, simulate material sliding down steep slopes. This creates talus slopes and smooths cliffs, making terrain look weathered rather than synthetic.

FluxScript Implementation

Here is how to build this system in Appley using FluxScript (Appley Scripting):

Procedural Terrain — FluxScript
// Professional procedural terrain with domain warping + erosion
on start {
  // Multi-octave noise with domain warping
  for x = 0 to chunkSize {
    for z = 0 to chunkSize {
      // Domain warp: distort input coords with separate noise
      wx = x + noise.simplex(x * 0.01, z * 0.01) * 50
      wz = z + noise.simplex(x * 0.01 + 100, z * 0.01) * 50

      // 6 octaves of fractal Brownian motion
      h = 0
      amp = 1
      freq = 0.005
      maxAmp = 0
      for octave = 0 to 6 {
        h += noise.perlin(wx * freq, wz * freq) * amp
        maxAmp += amp
        amp *= 0.5
        freq *= 2.0
      }
      h = (h / maxAmp) * mountainHeight

      // Biome blending via temperature/moisture maps
      temp = noise.perlin(wx * 0.002, wz * 0.002)
      moisture = noise.perlin(wx * 0.003 + 500, wz * 0.003)
      biome = getBiome(h, temp, moisture)

      terrain.setHeight(x, z, h)
      terrain.setBiome(x, z, biome)
    }
  }

  // Hydraulic erosion simulation
  for drop = 0 to 50000 {
    simulateErosionDrop()
  }

  // Thermal erosion — smooth steep slopes
  thermalErode(35, 0.01)

  terrain.rebuild()
}

func simulateErosionDrop() {
  // Start at random position
  px = math.random(chunkSize)
  pz = math.random(chunkSize)
  sediment = 0
  water = 1.0
  velocity = vec3(0, 0, 0)

  for step = 0 to 64 {
    // Get surface gradient (downhill direction)
    h = terrain.getHeight(px, pz)
    hx = terrain.getHeight(px + 1, pz)
    hz = terrain.getHeight(px, pz + 1)
    gradient = vec3(hx - h, 0, hz - h)

    // Move downhill
    velocity = velocity * 0.9 - gradient * 0.05
    px += velocity.x
    pz += velocity.z

    // Erode or deposit sediment
    newH = terrain.getHeight(px, pz)
    deltaSediment = h - newH
    if deltaSediment > 0 {
      // Moving downhill — erode
      carry = math.min(deltaSediment * 0.3, water * 0.1)
      terrain.setHeight(px, pz, newH - carry)
      sediment += carry
    } else {
      // Moving uphill or flat — deposit
      deposit = sediment * 0.1
      terrain.setHeight(px, pz, newH + deposit)
      sediment -= deposit
    }

    water *= 0.98  // Evaporation
    if water < 0.01 break
  }
}

func getBiome(height, temp, moisture) {
  if height < 2 return "beach"
  if height > 40 return "mountain"
  if temp < -0.3 return "tundra"
  if temp > 0.3 && moisture < 0 return "desert"
  if moisture > 0.3 return "jungle"
  return "forest"
}

Node Graph Equivalent

The same system implemented as a Node Graph in Appley Node Scripting:

Procedural Terrain — Node Graph
[On Start] --> [For Each Pixel in Chunk]
   --> [Domain Warp: distort coords with simplex noise]
   --> [6 Octaves fBm: perlin at increasing frequency]
   --> [Normalize: h / maxAmp * mountainHeight]
   --> [Get Biome: temperature + moisture maps]
   --> [Set Height + Biome]

   --> [Hydraulic Erosion: 50,000 drops]
        --> [For Each Drop]
             --> [Start random pos, water=1, sediment=0]
             --> [Calculate: surface gradient]
             --> [Move: downhill with velocity damping]
             --> [Branch: going downhill?]
                  +-- Yes --> [Erode: carry sediment]
                  +-- No --> [Deposit: leave sediment]
             --> [Evaporate: water *= 0.98]

   --> [Thermal Erosion: smooth steep slopes]
   --> [Rebuild]

Professional Optimization Tips

How AAA studios optimize these systems — applicable to your Appley project:

  • Use domain warping before noise sampling. Warping input coordinates with a second noise function breaks up the grid-like regularity of Perlin noise, producing organic terrain features. This is the single biggest quality improvement for procedural terrain.
  • Run erosion on the CPU in a background thread or web worker. 50,000 erosion drops on a 256x256 heightmap takes 200-500ms. Do not run it on the main thread during gameplay — generate chunks ahead of the player.
  • Cache generated chunks and store them. For infinite worlds, persist generated chunks to cloud storage or IndexedDB. Without caching, players see terrain pop-in when backtracking.
  • Use biome blending with smooth transitions. Hard biome borders look artificial. Blend biome textures over 5-10 meters using a smoothstep function on the temperature/moisture boundaries.

Common Mistakes

Single octave noise looks artificial

Layer 4-8 octaves. Single-octave Perlin produces rolling hills with no detail. Multi-octave fBm creates mountains, valleys, and fine detail simultaneously.

Erosion on the main thread

Erosion simulation is expensive. Run it in a web worker or pre-generate chunks. Main-thread erosion causes multi-second freezes when chunks load.