Skip to content

Building an open world in the browser, part 27: An island from noise, ground that looks like ground

By Oleg Sidorkin, CTO and Co-Founder of Cinevva

New here? Use the series guide. It explains what a spike is and links all parts.

Part 26 put water on the world. This part builds the land under it, in two stages that mirror how a real place comes to be. Spike 54 is the terrain itself: not sculpted by hand but grown from noise, then weathered until it has the drainage and the coastline of somewhere that water has actually run across. Spike 55 is the skin on that terrain: solving the problem that defeats almost every procedural ground, which is that a tiled texture looks tiled. The two together answer one question, can a creator get a plausible island and ground that reads as ground without an artist touching either, and the answer is yes if you copy the right recipes.

A coastline that noise alone won't give you

Open Spike 54 in a new tab ↗ · View source

The heightmap follows the Red Blob Games recipes, which are a stack of small transforms each fixing a specific flaw in raw noise. Fractional Brownian motion sums several octaves of Perlin noise so the terrain has both broad hills and fine detail, but plain FBM looks soft and grid-aligned, so the sample point gets domain-warped first: you offset each coordinate by another noise field, p=p+noise(p)s, which bends the ridges into something organic instead of axis-locked. Then redistribution, raising elevation to a power ek, pushes the midtones down so the world has flat valleys and sharp peaks rather than everything sitting at the same gentle slope. The piece that makes it an island rather than infinite hills is a radial falloff: compute a euclidean distance from center, d=min(1,nx2+ny2), and blend the elevation toward 1d so the terrain drops into ocean at the edges. A second independent noise field becomes a moisture map, which does nothing for the shape but feeds the biome step later.

That gets you a shape, but it's a noise shape. It has no rivers, no valleys cut by water, no sediment fans, because nothing has ever flowed across it. The fix is hydraulic erosion, ported from Sebastian Lague's MIT-licensed implementation. Thousands of water droplets spawn at random and roll downhill, each carrying inertia so it doesn't make hard right-angle turns, picking up sediment when it speeds up and depositing it when it slows or pools, with a precomputed circular brush spreading each erosion event over a small radius so the carve is smooth rather than a one-pixel scratch, and evaporation that retires the droplet over its lifetime. Run enough droplets and the terrain grows the thing noise can't fake: drainage networks that converge, valleys that widen downstream, and flats where sediment settled.

Erosion on the GPU, and rivers that know where they are

The CPU erosion is correct but slow, so the spike also ports it to a WebGPU compute shader, and the interesting part is what GPU atomics force on you. Heights are stored as fixed-point i32 with a scale of one million, because WGSL has no atomic add on floats, and the only way thousands of droplets can deposit and remove sediment from the same cell in parallel without races is atomicAdd and atomicSub on integers. The fixed-point round-trip means a cell can occasionally go slightly negative under heavy concurrent drag, which is harmless and clamped on readback. The CPU version precomputes a per-cell brush table, but at a 10012 grid that table is around 100 MB, so the GPU port recomputes each droplet's brush on the fly instead, trading a little arithmetic for a lot of memory. Droplet start positions still come from the CPU through a Mulberry32 generator so runs are deterministic.

Two more passes turn weathered terrain into a readable map. Drainage uses the Red Blob D8 flow-accumulation method: drop one unit of rain on every cell, sort all cells by elevation descending, and pass each cell's accumulated water to its single lowest neighbor, so water piles up along the natural valleys, and any cell whose accumulation crosses a threshold is marked river. The catch is that an eroded heightmap has pits, local depressions with no downhill exit, and water entering a pit just stops, breaking the accumulation. So before drainage runs, Planchon-Darboux pit filling raises each pit to just above its lowest neighbor, minNeighbour+ϵ, iterating under twenty passes until every cell has somewhere to drain. Finally biomes are assigned the Red Blob way, a two-axis lookup on elevation against moisture so a high-and-dry cell becomes rock while a low-and-wet one becomes marsh, with slope-aware overrides on top: steep faces force to rock regardless of moisture, the waterline band becomes beach, and the highest elevations take snow. The result is an island you can read at a glance, generated end to end from two noise seeds.

The tile that won't stop repeating

Open Spike 55 in a new tab ↗ · View source

A terrain needs a material that covers kilometers from a texture that's a couple of meters across, and the moment you scale a tile up to cover ground, the eye locks onto the repetition. Spike 55 is an A/B bench of four sampling modes on the same geometry and lighting, so the only variable is how the texture is read. The baseline is plain: one sample at the scaled UV, which tiles obviously every few meters and exists only as the thing to beat. The free PBR layers come from Poly Haven over their CDN, diffuse plus the packed ARM map (ambient occlusion, roughness, metalness) plus a GL normal, all CC0, loaded with anisotropy 8 and mipmaps.

The interesting three are different attacks on the repeat. The hex-linear mode is the Heitz-Neyret triangle grid: lay a skewed triangular lattice over the surface, and each fragment sits inside one triangle whose three vertices each take a randomly offset tap, blended by barycentric weights so neighboring regions sample different parts of the texture and the large-scale tiling dissolves. But a plain linear blend of three taps averages their colors at the triangle edges, which leaves a visible triangular pattern, the exact artifact spike 48 ran into. The hex-vp mode is the published fix from the same 2018 paper: after the barycentric sum, subtract the texture's mean color, rescale by the inverse square root of the sum of squared weights to hold variance constant, then add the mean back. That keeps contrast steady across the blend so the triangles vanish, which is why the material does a 1-by-1 readback of each loaded texture to estimate its mean color up front. The fourth mode, iq-untiled, is Inigo Quilez's Texture Repetition technique 3: two offset taps blended by a low-frequency variation pattern, only two samples instead of three and no triangle structure to begin with, the cheap option that holds up.

On top of any mode sits optional triplanar projection, which is what lets the same material wrap a cliff without stretching. Instead of one UV, the shader samples three world-axis projections, the X-facing surfaces read the YZ plane, Y reads ZX, Z reads XY, and blends them by abs(normalWorld) raised to a sharpening power between four and eight so a 45-degree slope doesn't ghost all three projections into mush. The bench makes the tradeoff legible: variance-preserving hex blending is the quality winner and Quilez's two-tap is the performance-conscious one, and both beat plain by a wide enough margin that no creator should ever ship the plain tile.

Technology referenced in this chapter

Recipe-stacked heightmap generation. Red Blob Games recipes compose raw noise into terrain: domain-warped FBM (p=p+noise(p)s) for organic ridges, ek redistribution for flat valleys and sharp peaks, and a euclidean radial falloff blended toward 1d to drop the edges into ocean and make an island. A second noise field is the moisture map for biome assignment. See landscape generation.

Hydraulic erosion, CPU and GPU. Sebastian Lague's droplet model (inertia, carrying capacity, a precomputed circular deposit brush, evaporation) carves real drainage that noise can't fake. The WebGPU compute port stores heights as fixed-point i32 at a scale of one million so droplets can atomicAdd/atomicSub the same cell in parallel, recomputes each droplet's brush on the fly to avoid a ~100 MB table at 10012, and seeds start positions from a deterministic Mulberry32 generator.

D8 drainage with pit filling. Red Blob D8 flow accumulation drops one unit of rain per cell, sorts cells by descending elevation, and passes water to each cell's lowest neighbor, marking rivers above a threshold. Planchon-Darboux pit filling first raises every local depression to minNeighbour+ϵ (iterating under twenty passes) so water never gets trapped and accumulation stays connected. A two-axis elevation-by-moisture lookup assigns biomes, with slope overrides forcing rock on cliffs, beach at the waterline, and snow on peaks.

Breaking texture tiling four ways. On identical geometry and lighting: plain (one tap, tiles obviously), hex-linear (Heitz-Neyret triangle grid, linear blend leaves a triangle pattern), hex-vp (the EGSR 2018 §3.3 variance-preserving fix that subtracts the mean, rescales by inverse-sqrt of squared weights, and re-adds the mean, needing a 1×1 mean readback per texture), and iq-untiled (Inigo Quilez two-tap technique 3). Triplanar projection samples three world-axis planes blended by abs(normalWorld) to a power of 4-8 so cliffs don't stretch. Variance-preserving wins on quality, two-tap on cost. See terrain materials.


Part 27 of 29. Previous: Part 26 - Three ways to make water Next: Part 28 - Grass to the horizon, and ground that hides itself Series guide: /blog/2026-02-25-open-world-browser-series-guide