Terrain & Heightmap Generation

Procedural terrain generation creates realistic landscapes using mathematical algorithms. Common techniques include Perlin noise, Diamond-Square, and hydraulic erosion.

Heightmap Basics

A heightmap is a 2D array where each value represents elevation:

 1type Heightmap struct {
 2    width, height int
 3    data          [][]float64
 4}
 5
 6func NewHeightmap(width, height int) *Heightmap {
 7    data := make([][]float64, height)
 8    for i := range data {
 9        data[i] = make([]float64, width)
10    }
11    
12    return &Heightmap{
13        width:  width,
14        height: height,
15        data:   data,
16    }
17}
18
19func (hm *Heightmap) Get(x, y int) float64 {
20    if x < 0 || x >= hm.width || y < 0 || y >= hm.height {
21        return 0
22    }
23    return hm.data[y][x]
24}
25
26func (hm *Heightmap) Set(x, y int, value float64) {
27    if x >= 0 && x < hm.width && y >= 0 && y < hm.height {
28        hm.data[y][x] = value
29    }
30}
31
32// Normalize to [0, 1]
33func (hm *Heightmap) Normalize() {
34    min, max := math.MaxFloat64, -math.MaxFloat64
35    
36    for y := 0; y < hm.height; y++ {
37        for x := 0; x < hm.width; x++ {
38            if hm.data[y][x] < min {
39                min = hm.data[y][x]
40            }
41            if hm.data[y][x] > max {
42                max = hm.data[y][x]
43            }
44        }
45    }
46    
47    rangeVal := max - min
48    if rangeVal == 0 {
49        return
50    }
51    
52    for y := 0; y < hm.height; y++ {
53        for x := 0; x < hm.width; x++ {
54            hm.data[y][x] = (hm.data[y][x] - min) / rangeVal
55        }
56    }
57}

1. Perlin Noise

Classic noise function for natural-looking terrain.

  1import "math"
  2
  3type PerlinNoise struct {
  4    permutation []int
  5}
  6
  7func NewPerlinNoise(seed int64) *PerlinNoise {
  8    rand.Seed(seed)
  9    
 10    // Generate permutation table
 11    perm := make([]int, 512)
 12    p := make([]int, 256)
 13    for i := range p {
 14        p[i] = i
 15    }
 16    
 17    // Shuffle
 18    for i := 255; i > 0; i-- {
 19        j := rand.Intn(i + 1)
 20        p[i], p[j] = p[j], p[i]
 21    }
 22    
 23    // Duplicate for overflow
 24    for i := 0; i < 256; i++ {
 25        perm[i] = p[i]
 26        perm[i+256] = p[i]
 27    }
 28    
 29    return &PerlinNoise{permutation: perm}
 30}
 31
 32func (pn *PerlinNoise) Noise2D(x, y float64) float64 {
 33    // Find unit grid cell
 34    X := int(math.Floor(x)) & 255
 35    Y := int(math.Floor(y)) & 255
 36    
 37    // Relative position in cell
 38    x -= math.Floor(x)
 39    y -= math.Floor(y)
 40    
 41    // Fade curves
 42    u := fade(x)
 43    v := fade(y)
 44    
 45    // Hash coordinates of 4 corners
 46    aa := pn.permutation[pn.permutation[X]+Y]
 47    ab := pn.permutation[pn.permutation[X]+Y+1]
 48    ba := pn.permutation[pn.permutation[X+1]+Y]
 49    bb := pn.permutation[pn.permutation[X+1]+Y+1]
 50    
 51    // Blend results from 4 corners
 52    return lerp(v,
 53        lerp(u, grad2D(aa, x, y), grad2D(ba, x-1, y)),
 54        lerp(u, grad2D(ab, x, y-1), grad2D(bb, x-1, y-1)))
 55}
 56
 57func fade(t float64) float64 {
 58    return t * t * t * (t*(t*6-15) + 10)
 59}
 60
 61func lerp(t, a, b float64) float64 {
 62    return a + t*(b-a)
 63}
 64
 65func grad2D(hash int, x, y float64) float64 {
 66    h := hash & 3
 67    u := x
 68    v := y
 69    
 70    if h == 0 {
 71        return u + v
 72    } else if h == 1 {
 73        return -u + v
 74    } else if h == 2 {
 75        return u - v
 76    }
 77    return -u - v
 78}
 79
 80// Generate heightmap using Perlin noise
 81func GeneratePerlinHeightmap(width, height int, seed int64, scale, octaves int, persistence, lacunarity float64) *Heightmap {
 82    hm := NewHeightmap(width, height)
 83    pn := NewPerlinNoise(seed)
 84    
 85    for y := 0; y < height; y++ {
 86        for x := 0; x < width; x++ {
 87            amplitude := 1.0
 88            frequency := 1.0
 89            noiseHeight := 0.0
 90            
 91            // Combine multiple octaves
 92            for i := 0; i < octaves; i++ {
 93                sampleX := float64(x) / float64(scale) * frequency
 94                sampleY := float64(y) / float64(scale) * frequency
 95                
 96                perlinValue := pn.Noise2D(sampleX, sampleY)
 97                noiseHeight += perlinValue * amplitude
 98                
 99                amplitude *= persistence
100                frequency *= lacunarity
101            }
102            
103            hm.Set(x, y, noiseHeight)
104        }
105    }
106    
107    hm.Normalize()
108    return hm
109}

Parameters:

  • Scale: Controls zoom level (larger = smoother)
  • Octaves: Number of noise layers (more = more detail)
  • Persistence: Amplitude decrease per octave (0.5 typical)
  • Lacunarity: Frequency increase per octave (2.0 typical)

2. Diamond-Square Algorithm

Classic fractal terrain generation.

 1func GenerateDiamondSquare(size int, roughness float64, seed int64) *Heightmap {
 2    // Size must be 2^n + 1
 3    if !isPowerOfTwoPlusOne(size) {
 4        panic("Size must be 2^n + 1")
 5    }
 6    
 7    rand.Seed(seed)
 8    hm := NewHeightmap(size, size)
 9    
10    // Initialize corners
11    hm.Set(0, 0, rand.Float64())
12    hm.Set(size-1, 0, rand.Float64())
13    hm.Set(0, size-1, rand.Float64())
14    hm.Set(size-1, size-1, rand.Float64())
15    
16    stepSize := size - 1
17    scale := 1.0
18    
19    for stepSize > 1 {
20        halfStep := stepSize / 2
21        
22        // Diamond step
23        for y := halfStep; y < size; y += stepSize {
24            for x := halfStep; x < size; x += stepSize {
25                avg := (hm.Get(x-halfStep, y-halfStep) +
26                       hm.Get(x+halfStep, y-halfStep) +
27                       hm.Get(x-halfStep, y+halfStep) +
28                       hm.Get(x+halfStep, y+halfStep)) / 4.0
29                
30                hm.Set(x, y, avg+randomRange(-scale, scale))
31            }
32        }
33        
34        // Square step
35        for y := 0; y < size; y += halfStep {
36            for x := (y + halfStep) % stepSize; x < size; x += stepSize {
37                sum := 0.0
38                count := 0
39                
40                if y-halfStep >= 0 {
41                    sum += hm.Get(x, y-halfStep)
42                    count++
43                }
44                if y+halfStep < size {
45                    sum += hm.Get(x, y+halfStep)
46                    count++
47                }
48                if x-halfStep >= 0 {
49                    sum += hm.Get(x-halfStep, y)
50                    count++
51                }
52                if x+halfStep < size {
53                    sum += hm.Get(x+halfStep, y)
54                    count++
55                }
56                
57                hm.Set(x, y, sum/float64(count)+randomRange(-scale, scale))
58            }
59        }
60        
61        stepSize /= 2
62        scale *= math.Pow(2, -roughness)
63    }
64    
65    hm.Normalize()
66    return hm
67}
68
69func isPowerOfTwoPlusOne(n int) bool {
70    n--
71    return n > 0 && (n&(n-1)) == 0
72}
73
74func randomRange(min, max float64) float64 {
75    return min + rand.Float64()*(max-min)
76}

Roughness: Controls terrain variation (0.5-1.0 typical)

3. Simplex Noise (Improved Perlin)

More efficient and fewer artifacts than Perlin.

 1type SimplexNoise struct {
 2    perm []int
 3}
 4
 5func NewSimplexNoise(seed int64) *SimplexNoise {
 6    rand.Seed(seed)
 7    
 8    perm := make([]int, 512)
 9    p := make([]int, 256)
10    for i := range p {
11        p[i] = i
12    }
13    
14    for i := 255; i > 0; i-- {
15        j := rand.Intn(i + 1)
16        p[i], p[j] = p[j], p[i]
17    }
18    
19    for i := 0; i < 256; i++ {
20        perm[i] = p[i]
21        perm[i+256] = p[i]
22    }
23    
24    return &SimplexNoise{perm: perm}
25}
26
27func (sn *SimplexNoise) Noise2D(x, y float64) float64 {
28    const F2 = 0.5 * (math.Sqrt(3.0) - 1.0)
29    const G2 = (3.0 - math.Sqrt(3.0)) / 6.0
30    
31    // Skew input space
32    s := (x + y) * F2
33    i := int(math.Floor(x + s))
34    j := int(math.Floor(y + s))
35    
36    t := float64(i+j) * G2
37    X0 := float64(i) - t
38    Y0 := float64(j) - t
39    x0 := x - X0
40    y0 := y - Y0
41    
42    // Determine simplex
43    var i1, j1 int
44    if x0 > y0 {
45        i1, j1 = 1, 0
46    } else {
47        i1, j1 = 0, 1
48    }
49    
50    x1 := x0 - float64(i1) + G2
51    y1 := y0 - float64(j1) + G2
52    x2 := x0 - 1.0 + 2.0*G2
53    y2 := y0 - 1.0 + 2.0*G2
54    
55    // Calculate contributions
56    n0, n1, n2 := 0.0, 0.0, 0.0
57    
58    t0 := 0.5 - x0*x0 - y0*y0
59    if t0 > 0 {
60        t0 *= t0
61        gi := sn.perm[(i+sn.perm[j&255])&255] % 12
62        n0 = t0 * t0 * dot2D(grad3[gi], x0, y0)
63    }
64    
65    t1 := 0.5 - x1*x1 - y1*y1
66    if t1 > 0 {
67        t1 *= t1
68        gi := sn.perm[(i+i1+sn.perm[(j+j1)&255])&255] % 12
69        n1 = t1 * t1 * dot2D(grad3[gi], x1, y1)
70    }
71    
72    t2 := 0.5 - x2*x2 - y2*y2
73    if t2 > 0 {
74        t2 *= t2
75        gi := sn.perm[(i+1+sn.perm[(j+1)&255])&255] % 12
76        n2 = t2 * t2 * dot2D(grad3[gi], x2, y2)
77    }
78    
79    return 70.0 * (n0 + n1 + n2)
80}
81
82var grad3 = [][3]float64{
83    {1, 1, 0}, {-1, 1, 0}, {1, -1, 0}, {-1, -1, 0},
84    {1, 0, 1}, {-1, 0, 1}, {1, 0, -1}, {-1, 0, -1},
85    {0, 1, 1}, {0, -1, 1}, {0, 1, -1}, {0, -1, -1},
86}
87
88func dot2D(g [3]float64, x, y float64) float64 {
89    return g[0]*x + g[1]*y
90}

4. Hydraulic Erosion

Simulate water erosion for realistic terrain.

 1func ApplyHydraulicErosion(hm *Heightmap, iterations int, rainAmount, evaporation, erosionRate, depositionRate float64) {
 2    water := NewHeightmap(hm.width, hm.height)
 3    sediment := NewHeightmap(hm.width, hm.height)
 4    
 5    for iter := 0; iter < iterations; iter++ {
 6        // Add rain
 7        for y := 0; y < hm.height; y++ {
 8            for x := 0; x < hm.width; x++ {
 9                water.data[y][x] += rainAmount
10            }
11        }
12        
13        // Flow water
14        for y := 0; y < hm.height; y++ {
15            for x := 0; x < hm.width; x++ {
16                if water.Get(x, y) <= 0 {
17                    continue
18                }
19                
20                // Find lowest neighbor
21                lowestHeight := hm.Get(x, y)
22                lowestX, lowestY := x, y
23                
24                neighbors := [][2]int{{0, -1}, {1, 0}, {0, 1}, {-1, 0}}
25                for _, n := range neighbors {
26                    nx, ny := x+n[0], y+n[1]
27                    if nx >= 0 && nx < hm.width && ny >= 0 && ny < hm.height {
28                        neighborHeight := hm.Get(nx, ny)
29                        if neighborHeight < lowestHeight {
30                            lowestHeight = neighborHeight
31                            lowestX, lowestY = nx, ny
32                        }
33                    }
34                }
35                
36                // Calculate height difference
37                heightDiff := hm.Get(x, y) - lowestHeight
38                
39                if heightDiff > 0 {
40                    // Erode
41                    erosionAmount := math.Min(heightDiff, water.Get(x, y)*erosionRate)
42                    hm.data[y][x] -= erosionAmount
43                    sediment.data[y][x] += erosionAmount
44                    
45                    // Move water and sediment
46                    if lowestX != x || lowestY != y {
47                        waterToMove := water.Get(x, y) * 0.5
48                        water.data[y][x] -= waterToMove
49                        water.data[lowestY][lowestX] += waterToMove
50                        
51                        sedimentToMove := sediment.Get(x, y) * 0.5
52                        sediment.data[y][x] -= sedimentToMove
53                        sediment.data[lowestY][lowestX] += sedimentToMove
54                    }
55                } else {
56                    // Deposit sediment
57                    depositAmount := sediment.Get(x, y) * depositionRate
58                    hm.data[y][x] += depositAmount
59                    sediment.data[y][x] -= depositAmount
60                }
61            }
62        }
63        
64        // Evaporate water
65        for y := 0; y < hm.height; y++ {
66            for x := 0; x < hm.width; x++ {
67                water.data[y][x] *= (1.0 - evaporation)
68            }
69        }
70    }
71}

5. Thermal Erosion

Simulate rock/soil sliding down slopes.

 1func ApplyThermalErosion(hm *Heightmap, iterations int, talusAngle float64) {
 2    for iter := 0; iter < iterations; iter++ {
 3        for y := 0; y < hm.height; y++ {
 4            for x := 0; x < hm.width; x++ {
 5                currentHeight := hm.Get(x, y)
 6                maxDiff := 0.0
 7                targetX, targetY := x, y
 8                
 9                // Find steepest neighbor
10                neighbors := [][2]int{{0, -1}, {1, 0}, {0, 1}, {-1, 0}}
11                for _, n := range neighbors {
12                    nx, ny := x+n[0], y+n[1]
13                    if nx >= 0 && nx < hm.width && ny >= 0 && ny < hm.height {
14                        diff := currentHeight - hm.Get(nx, ny)
15                        if diff > maxDiff {
16                            maxDiff = diff
17                            targetX, targetY = nx, ny
18                        }
19                    }
20                }
21                
22                // If slope exceeds talus angle, move material
23                if maxDiff > talusAngle {
24                    amount := 0.5 * (maxDiff - talusAngle)
25                    hm.data[y][x] -= amount
26                    hm.data[targetY][targetX] += amount
27                }
28            }
29        }
30    }
31}

Complete Terrain Generation Pipeline

 1func GenerateRealisticTerrain(width, height int, seed int64) *Heightmap {
 2    // 1. Base terrain with Perlin noise
 3    hm := GeneratePerlinHeightmap(width, height, seed, 100, 6, 0.5, 2.0)
 4    
 5    // 2. Add mountain ranges with higher frequency
 6    mountains := GeneratePerlinHeightmap(width, height, seed+1, 50, 4, 0.6, 2.5)
 7    for y := 0; y < height; y++ {
 8        for x := 0; x < width; x++ {
 9            hm.data[y][x] = hm.data[y][x]*0.7 + mountains.data[y][x]*0.3
10        }
11    }
12    
13    // 3. Apply hydraulic erosion
14    ApplyHydraulicErosion(hm, 50, 0.01, 0.5, 0.3, 0.1)
15    
16    // 4. Apply thermal erosion
17    ApplyThermalErosion(hm, 10, 0.1)
18    
19    // 5. Normalize
20    hm.Normalize()
21    
22    return hm
23}

Biome Assignment

 1type Biome int
 2
 3const (
 4    Ocean Biome = iota
 5    Beach
 6    Plains
 7    Forest
 8    Hills
 9    Mountains
10    Snow
11)
12
13func AssignBiomes(hm *Heightmap, moisture *Heightmap) [][]Biome {
14    biomes := make([][]Biome, hm.height)
15    for i := range biomes {
16        biomes[i] = make([]Biome, hm.width)
17    }
18    
19    for y := 0; y < hm.height; y++ {
20        for x := 0; x < hm.width; x++ {
21            height := hm.Get(x, y)
22            m := moisture.Get(x, y)
23            
24            if height < 0.3 {
25                biomes[y][x] = Ocean
26            } else if height < 0.35 {
27                biomes[y][x] = Beach
28            } else if height < 0.5 {
29                if m < 0.3 {
30                    biomes[y][x] = Plains
31                } else {
32                    biomes[y][x] = Forest
33                }
34            } else if height < 0.7 {
35                biomes[y][x] = Hills
36            } else if height < 0.85 {
37                biomes[y][x] = Mountains
38            } else {
39                biomes[y][x] = Snow
40            }
41        }
42    }
43    
44    return biomes
45}

Export to Image

 1import "image"
 2import "image/color"
 3import "image/png"
 4
 5func (hm *Heightmap) ToGrayscale() *image.Gray {
 6    img := image.NewGray(image.Rect(0, 0, hm.width, hm.height))
 7    
 8    for y := 0; y < hm.height; y++ {
 9        for x := 0; x < hm.width; x++ {
10            value := uint8(hm.Get(x, y) * 255)
11            img.SetGray(x, y, color.Gray{Y: value})
12        }
13    }
14    
15    return img
16}
17
18func (hm *Heightmap) ToColoredTerrain() *image.RGBA {
19    img := image.NewRGBA(image.Rect(0, 0, hm.width, hm.height))
20    
21    for y := 0; y < hm.height; y++ {
22        for x := 0; x < hm.width; x++ {
23            h := hm.Get(x, y)
24            var c color.RGBA
25            
26            if h < 0.3 {
27                c = color.RGBA{30, 144, 255, 255} // Deep water
28            } else if h < 0.35 {
29                c = color.RGBA{238, 214, 175, 255} // Beach
30            } else if h < 0.5 {
31                c = color.RGBA{34, 139, 34, 255} // Grass
32            } else if h < 0.7 {
33                c = color.RGBA{107, 142, 35, 255} // Hills
34            } else if h < 0.85 {
35                c = color.RGBA{139, 137, 137, 255} // Mountains
36            } else {
37                c = color.RGBA{255, 250, 250, 255} // Snow
38            }
39            
40            img.Set(x, y, c)
41        }
42    }
43    
44    return img
45}
46
47func SaveHeightmap(hm *Heightmap, filename string) error {
48    img := hm.ToColoredTerrain()
49    
50    f, err := os.Create(filename)
51    if err != nil {
52        return err
53    }
54    defer f.Close()
55    
56    return png.Encode(f, img)
57}

Applications

  1. Game Terrain: Open-world games, flight simulators
  2. Map Generation: Strategy games, roguelikes
  3. Visualization: Geographic data, scientific simulations
  4. Art: Procedural landscapes, wallpapers

When to Use Each Method

MethodBest ForProsCons
Perlin NoiseNatural terrainFast, organicCan look too smooth
Diamond-SquareRough terrainSimple, fastSquare artifacts
Simplex NoiseHigh qualityNo artifacts, fastMore complex
Hydraulic ErosionRealismVery realisticSlow
Thermal ErosionRocky terrainNatural slopesRequires base terrain

Combine multiple methods for best results!

Related Snippets

  • Binary Search
    Binary search is an efficient algorithm for finding a target value in a sorted …
  • Binary Tree
    A binary tree is a tree data structure where each node has at most two children, …
  • General Tree
    A tree is a hierarchical data structure consisting of nodes connected by edges, …
  • Heap Data Structure
    A heap is a specialized tree-based data structure that satisfies the heap …
  • Heap Sort
    Heap sort is a comparison-based sorting algorithm that uses a binary heap data …
  • Linked List Operations
    Optimal techniques for common linked list operations using pointer manipulation, …
  • LRU and LFU Cache
    Cache replacement policies determine which items to evict when the cache is …
  • Merge Sort
    Merge sort is a stable, comparison-based sorting algorithm that uses the …
  • Quadtree
    A Quadtree is a tree data structure where each internal node has exactly four …
  • Quick Sort
    Quick sort is an efficient, in-place, comparison-based sorting algorithm that …
  • String Similarity Algorithms
    Algorithms for measuring similarity between strings, used in spell checking, DNA …
  • Trie (Prefix Tree)
    A Trie (pronounced &quot;try&quot;) is a tree-like data structure used to store …
  • Wave Function Collapse
    Wave Function Collapse (WFC) is a procedural generation algorithm that creates …