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
- Game Terrain: Open-world games, flight simulators
- Map Generation: Strategy games, roguelikes
- Visualization: Geographic data, scientific simulations
- Art: Procedural landscapes, wallpapers
When to Use Each Method
| Method | Best For | Pros | Cons |
|---|---|---|---|
| Perlin Noise | Natural terrain | Fast, organic | Can look too smooth |
| Diamond-Square | Rough terrain | Simple, fast | Square artifacts |
| Simplex Noise | High quality | No artifacts, fast | More complex |
| Hydraulic Erosion | Realism | Very realistic | Slow |
| Thermal Erosion | Rocky terrain | Natural slopes | Requires 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 "try") is a tree-like data structure used to store … - Wave Function Collapse
Wave Function Collapse (WFC) is a procedural generation algorithm that creates …