Creating Procedural Maps with Perlin and Simplex Noise
#procedural-generation
#noise
#maps
#webdev
Overview
Procedural maps are terrain or biome layouts generated algorithmically rather than stored as static images. By stacking noise at different scales and combining multiple noise functions, you can produce natural-looking landscapes, islands, caves, and more. Perlin and Simplex noise are two popular foundations for these maps because they produce smooth, coherent patterns that resemble natural formations.
In this post, we’ll explore how Perlin and Simplex noise differ, when to use each, and how to implement a practical 2D map generator. You’ll learn how to blend octaves, control terrain features, and derive biome classifications from height data.
What is Perlin noise?
Perlin noise is a gradient noise function designed to generate smooth pseudo-random patterns. It works by laying down a grid of gradient vectors and interpolating values inside each grid cell. The result is continuous, non-repetitive noise with good directional coherence, which makes it ideal for terrain heightmaps, cloud textures, and other natural-looking surfaces.
Key points:
- Produces smooth transitions between points.
- Computationally efficient in 2D, with straightforward gradient hashing.
- Often used as the base layer for fractal noise (FBM) by layering octaves.
What is Simplex noise?
Simplex noise is another gradient noise function devised to improve on classic Perlin noise, especially in higher dimensions. It offers:
- Improved computational performance in 3D and higher dimensions.
- Reduced directional artifacts and more isotropic feel.
- Similar concept to Perlin: gradient vectors on a simplex grid, with interpolation.
For 2D maps, Simplex noise can be faster and sometimes produces crisper features at larger scales, depending on parameters and implementation.
Perlin vs Simplex for maps: a quick guide
- 2D terrain: Both work well. If you’re targeting high performance on many tiles, Simplex can be a practical choice.
- Tileable maps: You’ll generally combine techniques (wrap coordinates or use specialized tileable noise patterns). Both Perlin and Simplex can be made tileable with careful input handling.
- Aesthetic: Perlin often yields more “rounded” features at small scales; Simplex can yield slightly crisper features at similar scales. Try both to see what fits your project.
Noise configuration: octaves, persistence, and lacunarity
Most procedural maps use fractal Brownian motion (FBM) to combine multiple octaves of noise:
- Octaves: how many times you layer noise at increasing frequencies.
- Frequency (freq): how quickly patterns change across space.
- Amplitude (amp) and Persistence: how much each octave contributes to the final value.
- Lacunarity: how much the frequency increases per octave.
Common defaults:
- octaves: 4–6
- persistence: 0.4–0.6
- lacunarity: 2.0
Formula (conceptual):
- value = sum over i of noise(noiseInput * freq_i) * amp_i
- freq_i = baseFreq * (lacunarity^i)
- amp_i = (persistence^i)
- Normalize the sum to 0..1
This layering creates large-scale features (mountains, basins) with mid- and small-scale detail (ridges, rocks).
Generating a 2D heightmap (example in JavaScript)
This example uses a noise library (e.g., noisejs) that provides Perlin and Simplex noise. The code demonstrates FBM with a choice of noise type.
// Assume a noise library is loaded (e.g., noisejs) and seeded
noise.seed(42);
function fbm2D(x, y, options) {
const {
octaves = 5,
persistence = 0.5,
lacunarity = 2.0,
type = 'perlin' // 'perlin' or 'simplex'
} = options;
let freq = 1.0;
let amp = 1.0;
let maxAmp = 0.0;
let sum = 0.0;
for (let i = 0; i < octaves; i++) {
const v =
type === 'perlin'
? noise.perlin2(x * freq, y * freq)
: noise.simplex2(x * freq, y * freq);
// Normalize to 0..1 per octave
sum += ((v + 1) / 2) * amp;
maxAmp += amp;
freq *= lacunarity;
amp *= persistence;
}
// Normalize final value to 0..1
return sum / maxAmp;
}
function generateHeightMap(width, height, scale, type = 'perlin') {
const map = new Array(height);
for (let y = 0; y < height; y++) {
map[y] = new Array(width);
for (let x = 0; x < width; x++) {
const nx = x * scale;
const ny = y * scale;
map[y][x] = fbm2D(nx, ny, {
octaves: 5,
persistence: 0.5,
lacunarity: 2.0,
type
});
}
}
return map;
}
This produces a 2D array with values in 0..1. You can render it as grayscale or use the values to drive colors for biomes.
Biome classification from height
Once you have a heightmap, you can map ranges to biomes (e.g., water, beach, plain, forest, mountain). A simple threshold-based approach:
function biomeFromHeight(h) {
if (h < 0.30) return 'water';
if (h < 0.45) return 'beach';
if (h < 0.70) return 'grass';
if (h < 0.85) return 'forest';
return 'stone';
}
You can also layer additional noise to create rivers, cliffs, or fog/shadow effects for more depth.
Tileable maps and edge tiling
Tileable maps are essential for worlds that repeat seamlessly. A straightforward technique is to tile the input coordinates themselves by wrapping space around the edges:
- Use input coordinates that wrap at tile boundaries (e.g., nx = (x / tileW) * scale, ny = (y / tileH) * scale) and wrap x and y within the tile.
- Alternatively, employ a tileable-noise technique that feeds the noise function coordinates computed from sine/cosine components to guarantee continuity at the edges.
Example concept (wrap-based, 2D FBM):
function fbmTileable(x, y, tileW, tileH, scale, options) {
const wrapX = (x % tileW) / tileW;
const wrapY = (y % tileH) / tileH;
// Use wrapped coordinates for each octave
let freq = 1.0, amp = 1.0, sum = 0.0, maxAmp = 0.0;
for (let i = 0; i < options.octaves; i++) {
const v =
options.type === 'perlin'
? noise.perlin2(wrapX * freq, wrapY * freq)
: noise.simplex2(wrapX * freq, wrapY * freq);
sum += ((v + 1) / 2) * amp;
maxAmp += amp;
freq *= options.lacunarity;
amp *= options.persistence;
}
// Normalize
return sum / maxAmp;
}
Note: Tileable noise can get subtle at edges depending on implementation. If you need perfect seamlessness, consider specialized tileable noise formulas or libraries that explicitly support tiling.
Practical map composition: a simple workflow
- Start with a low-frequency FBM to define broad landmasses (large hills and basins).
- Add mid-frequency noise for terrain variety (ridges, plateaus).
- Layer a high-frequency pass for detail (noise at small scales) like rocks and texture.
- Apply a height-to-biome mapping to assign water, beach, grassland, forest, and mountains.
- Optionally add rivers by subtracting low-elevation channels or using a separate noise field to carve paths from terrain.
- If you’re building a tileable map, ensure coordinates wrap or use a tileable-noise approach so edges match when the world repeats.
Performance notes
- In 2D maps, 4–6 octaves are usually plenty. More octaves increase CPU time linearly.
- Simplex noise tends to be faster in higher dimensions; for dense maps with many tiles, consider experimenting with Simplex.
- If rendering in real time (WebGL or canvas), precompute a heightmap once and reuse it, then render slices as needed.
- Cache or memoize repeated noise evaluations if your algorithm samples the same coordinates multiple times.
Putting it together: a small rendering hook
If you’re rendering to a canvas or WebGL texture, you can map the height values to grayscale or color gradients. Here’s a minimal illustration idea (pseudo-code, not a full rendering loop):
- Generate heightmap with fbm2D(width, height, {scale, type})
- Convert height to color using a gradient map (e.g., blue for water, green for plains, brown for mountains)
- Draw the resulting color grid to a canvas
This approach gives you a flexible basis for procedural worlds in games, simulations, or generative art.
Further reading and next steps
- Explore more advanced fractal noise techniques: fractal Brownian motion with lacunarity and multidimensional FBMs.
- Look into gradient hashing and permutation tables for custom noise implementations.
- Experiment with different biome schemes and post-processing passes (erosion, moisture, rivers).
- If you’re targeting 3D terrain, extend these ideas to 3D noise and marching cubes or similar iso-surface techniques.