Procedural generation creates game content algorithmically rather than by hand. For terrain, this means generating landscapes, elevation maps, rivers, forests, and biomes from mathematical functions and random seeds. The result is a world that feels organic and explorable, unique to each player or playthrough, without requiring an artist to paint every tile. For browser games where download size matters and replayability drives engagement, procedural terrain is one of the highest-impact techniques available.
This guide walks through the complete pipeline for generating terrain in JavaScript: from noise functions to heightmaps, biome assignment, feature placement, and the chunk-loading architecture that makes infinite worlds possible in a browser tab.
All procedural terrain starts with noise. A noise function takes a coordinate (x, y) and returns a smooth, pseudo-random value, typically between -1 and 1. The "smooth" part is critical: unlike raw random numbers, noise values change gradually across space, creating the continuous gradients that look like natural terrain.
Perlin noise, invented by Ken Perlin in 1983, is the classic choice. It generates smooth gradients by interpolating between random gradient vectors at grid points. However, it has known directional artifacts (visible grid alignment at certain scales) and its computational cost scales with dimension.
Simplex noise, also by Perlin, addresses both issues. It uses a simplex grid (triangles in 2D, tetrahedra in 3D) instead of a square grid, eliminating directional artifacts and reducing computation. For browser games where every millisecond counts, simplex noise is the better choice. The OpenSimplex variant avoids the original simplex noise patent (now expired) and is freely available in JavaScript implementations.
| Property | Perlin Noise | Simplex Noise | Value Noise |
|---|---|---|---|
| Visual quality | Good (slight grid artifacts) | Excellent | Fair (blocky) |
| Speed (2D) | Fast | Faster | Fastest |
| Speed (3D+) | Slow (scales with dimension) | Fast (linear scaling) | Fast |
| Directional artifacts | Moderate | None | Strong |
| JS libraries | noisejs, simplex-noise | simplex-noise, open-simplex | Custom (trivial) |
A single layer of noise produces smooth, rolling terrain that looks unrealistic because natural landscapes have detail at every scale: mountain ranges, hills, rocks, and pebbles. Fractal Brownian Motion (fBm) layers multiple octaves of noise at different frequencies and amplitudes to create this multi-scale detail.
Each octave doubles the frequency (smaller features) and halves the amplitude (less extreme values). The standard approach uses 4-8 octaves. The ratio between successive frequencies is called lacunarity (typically 2.0) and the ratio between successive amplitudes is called persistence (typically 0.5). Adjusting these parameters dramatically changes the terrain character: higher persistence creates rougher, more mountainous terrain, while lower persistence creates gentle, rolling hills.
The raw noise output needs to be transformed into a usable heightmap. The basic pipeline is: generate noise values across your world grid, normalize them to a 0-1 range, apply a redistribution curve to control the elevation profile, and assign terrain types based on height thresholds.
Raw noise produces a roughly Gaussian distribution of heights, which means most of the world is at mid-elevation with few extreme highs or lows. This looks flat and boring. A power curve (raising the noise value to a power greater than 1) pushes more area toward low elevations with dramatic peaks, creating a more realistic distribution where flat lowlands dominate and mountains are rare. For a farming game, a power of 1.5-2.0 creates plenty of flat farmland with occasional hills.
With normalized, redistributed heights, assign terrain types by threshold:
A single heightmap produces monotonous terrain because every area at the same elevation looks identical. Real landscapes vary in temperature, moisture, soil type, and vegetation. Biome generation uses additional noise layers to create this variety.
The classic approach uses two independent noise layers: one for temperature and one for moisture. Combined with elevation, these three values map to a biome lookup table. High temperature plus high moisture equals tropical forest. High temperature plus low moisture equals desert. Low temperature plus high moisture equals tundra. This creates visually diverse landscapes from simple inputs.
For a farming game, biomes directly influence gameplay: different biomes support different crops, have different soil fertility, and present different challenges. A temperate grassland is ideal for wheat and corn. A riverside floodplain is perfect for rice. A hillside orchard zone grows fruit trees. These biome-specific mechanics give procedural terrain gameplay significance rather than being purely cosmetic.
Raw terrain needs features to feel inhabited: trees, rocks, bushes, flowers, ruins, and resource deposits. Procedural placement uses noise-based density maps combined with rules that ensure features make ecological sense.
Random placement creates clusters and gaps that look unnatural. Poisson disk sampling distributes points with a minimum guaranteed distance between each, producing the kind of even-but-not-grid distribution found in natural forests and meadows. The Bridson algorithm generates Poisson disk samples efficiently in O(n) time and adapts well to variable density (more trees in forests, fewer in clearings).
After generating candidate positions, filter them against terrain rules: trees do not grow in water or on steep slopes, rocks appear more frequently at higher elevations, flowers prefer meadow biomes. Each rule is a simple function that accepts or rejects a position based on the heightmap and biome data at that point. Layering multiple rules creates realistic, varied landscapes without any manual placement.
An infinite procedural world cannot fit in memory all at once. Chunk-based loading divides the world into fixed-size regions (typically 16x16 or 32x32 tiles) that are generated, loaded, and unloaded as the player moves. Only chunks within the player's view distance plus a buffer zone are kept in memory.
The hardest part of chunk-based generation is making chunk borders invisible. Because noise functions are continuous, the heightmap values will match perfectly across chunk boundaries. But features placed by Poisson disk sampling or rule-based systems can create visible seams if they only consider data within their own chunk. The solution is to generate features using an expanded region that overlaps neighboring chunks by a few tiles, then clip to the chunk boundary. This ensures trees near the edge consider neighbors on both sides.
Rivers and roads are linear features that do not emerge naturally from heightmap-based generation. They require dedicated algorithms. For rivers, a common approach is: pick high-elevation source points, simulate water flow downhill following the gradient of the heightmap, merge streams when they meet, and widen the channel as more tributaries join. The result is a dendritic (tree-like) river network that looks natural because it follows the terrain's actual geometry.
Roads connect points of interest (towns, farms, resources) using A* pathfinding on the heightmap, with cost functions that prefer flat terrain and penalize steep slopes. This produces roads that follow valleys and wind around hills, just as real roads do.
Procedural generation in JavaScript can be surprisingly fast. A 64x64 chunk with 6 octaves of simplex noise, biome assignment, and feature placement typically generates in 2-5ms on modern hardware. Using Web Workers ensures this computation never blocks the main thread. For chunks that need more complex generation (erosion simulation, structure placement), time-slicing across multiple frames prevents any single generation from exceeding 16ms.
Memory is the more pressing constraint in browsers. Each chunk stores heightmap data, biome data, feature lists, and tile metadata. A Float32Array heightmap for a 64x64 chunk uses 16KB. With 100 chunks loaded, that is 1.6MB just for heights. Keep data structures tight, use typed arrays throughout, and set aggressive cache eviction to stay well under the browser's memory limits.
Procedural terrain generation transforms a browser game from a finite, hand-crafted experience into an infinite world that rewards exploration. Combined with the farming mechanics discussed in our farming simulation design guide, procedurally generated biomes give every player a unique starting landscape to cultivate, ensuring no two farms are ever the same.