Procedural Terrain
Generate infinite terrain using multi-octave noise, erosion simulation, and biome blending.
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
Screenshot
How Professional Games Do It
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.
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.
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.
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.
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.
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):
// 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:
[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.
