Procedural World Generation
Generate infinite terrain, city layouts, vegetation, weather systems, and chunk-based scrolling worlds.
The core principle: noise-sampled fields
Most procedural world generation layers multiple noise() calls to create different scales of variation:
function heightAt(x, y) {
let continent = noise(x * 0.001, y * 0.001) * 1.0; // large-scale landmass
let region = noise(x * 0.005, y * 0.005) * 0.5; // medium hills
let detail = noise(x * 0.02, y * 0.02) * 0.2; // small rocky detail
let fine = noise(x * 0.08, y * 0.08) * 0.05; // tiny surface texture
return continent + region + detail + fine; // 0 to ~1.75
}
Heightmap terrain
let SEED = 1337;
let W = 200, H = 150;
let heightmap;
function setup() {
createCanvas(800, 600);
noiseSeed(SEED);
heightmap = new Float32Array(W * H);
for (let x = 0; x < W; x++) {
for (let y = 0; y < H; y++) {
heightmap[x + y * W] = heightAt(x, y);
}
}
}
const SEA_LEVEL = 0.45;
const SAND = 0.50;
const GRASS = 0.65;
const ROCK = 0.80;
function draw() {
loadPixels();
let cw = width / W, ch = height / H;
for (let x = 0; x < W; x++) {
for (let y = 0; y < H; y++) {
let h = heightmap[x + y * W];
let col = biomeColor(h);
let px = x * cw, py = y * ch;
for (let dx = 0; dx < cw; dx++) {
for (let dy = 0; dy < ch; dy++) {
let i = ((px + dx) + (py + dy) * width) * 4;
pixels[i] = red(col);
pixels[i+1] = green(col);
pixels[i+2] = blue(col);
pixels[i+3] = 255;
}
}
}
}
updatePixels();
}
function biomeColor(h) {
if (h < SEA_LEVEL) return color(20, 60, 140); // deep water
if (h < SAND) return color(200, 190, 140); // beach
if (h < GRASS) return color(60, 130, 50); // grass
if (h < ROCK) return color(100, 90, 80); // rock
return color(240, 240, 255); // snow
}
Chunk-based infinite world
Divide the world into fixed-size chunks. Only generate and render chunks near the camera:
const CHUNK_SIZE = 64;
let chunks = new Map();
let camX = 0, camY = 0;
function chunkKey(cx, cy) {
return `${cx},${cy}`;
}
function getOrCreateChunk(cx, cy) {
let key = chunkKey(cx, cy);
if (!chunks.has(key)) {
chunks.set(key, generateChunk(cx, cy));
}
return chunks.get(key);
}
function generateChunk(cx, cy) {
let tiles = [];
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
for (let ly = 0; ly < CHUNK_SIZE; ly++) {
let wx = cx * CHUNK_SIZE + lx;
let wy = cy * CHUNK_SIZE + ly;
tiles.push({
type: tileType(heightAt(wx * 0.05, wy * 0.05)),
variant: floor(noise(wx * 0.3, wy * 0.3) * 4)
});
}
}
return tiles;
}
function draw() {
background(10);
// Camera movement
if (keyIsDown(LEFT_ARROW)) camX -= 2;
if (keyIsDown(RIGHT_ARROW)) camX += 2;
if (keyIsDown(UP_ARROW)) camY -= 2;
if (keyIsDown(DOWN_ARROW)) camY += 2;
let viewChunkX = floor(camX / CHUNK_SIZE);
let viewChunkY = floor(camY / CHUNK_SIZE);
let viewRadius = 3;
for (let cx = viewChunkX - viewRadius; cx <= viewChunkX + viewRadius; cx++) {
for (let cy = viewChunkY - viewRadius; cy <= viewChunkY + viewRadius; cy++) {
let chunk = getOrCreateChunk(cx, cy);
renderChunk(chunk, cx, cy);
}
}
// Unload distant chunks
if (chunks.size > 200) {
let toDelete = [...chunks.keys()].slice(0, 50);
toDelete.forEach(k => chunks.delete(k));
}
}
City layout generation
Voronoi-based district partitioning:
function generateCity(numDistricts) {
// Random district seeds
let seeds = Array.from({ length: numDistricts }, () => ({
x: random(width),
y: random(height),
type: random(['residential', 'commercial', 'industrial', 'park'])
}));
// For each cell in the grid, find the nearest seed (Voronoi)
return function(x, y) {
let nearest = seeds.reduce((best, s) => {
let d = dist(x, y, s.x, s.y);
return d < best.d ? { d, type: s.type } : best;
}, { d: Infinity, type: null });
return nearest.type;
};
}
// Usage
let cityFn = generateCity(20);
function drawCity() {
let blockSize = 20;
for (let x = 0; x < width; x += blockSize) {
for (let y = 0; y < height; y += blockSize) {
let district = cityFn(x, y);
fill(districtColor(district));
noStroke();
rect(x + 1, y + 1, blockSize - 2, blockSize - 2);
}
}
}
Vegetation placement
Use density maps + rejection sampling:
function placeVegetation(heightmap, densityMap, numAttempts) {
let plants = [];
for (let i = 0; i < numAttempts; i++) {
let x = random(width);
let y = random(height);
let h = sampleHeightmap(heightmap, x, y);
let d = sampleHeightmap(densityMap, x, y);
// Only place in valid height range and pass density test
if (h > GRASS && h < ROCK && random() < d) {
plants.push({
x, y,
type: h < 0.70 ? 'tree' : 'shrub',
scale: 0.8 + random(0.4)
});
}
}
return plants;
}
Weather systems
Layer weather on top of terrain:
function weatherAt(wx, wy, time) {
let cloudDensity = noise(wx * 0.003 + time * 0.0002, wy * 0.003);
let precipitation = cloudDensity > 0.6 ? (cloudDensity - 0.6) * 2.5 : 0;
let wind = {
x: map(noise(wx * 0.002, wy * 0.002, time * 0.001), 0, 1, -1, 1),
y: map(noise(wx * 0.002 + 100, wy * 0.002, time * 0.001), 0, 1, -0.5, 0.5)
};
return { cloudDensity, precipitation, wind };
}
Key takeaways
- Layer multiple noise octaves (continent + region + detail + fine) for realistic height variation
- Biome classification is just a series of height thresholds applied to the heightmap
- Chunk-based worlds generate and cache tiles on demand; unload distant chunks to manage memory
- Voronoi partitioning from random seeds creates natural-looking district and region layouts
- Vegetation placement uses height constraints + density maps + rejection sampling
- Weather systems are simply additional noise layers sampled over world coordinates + time