vidar.games

Wang-tile terrain

2026-11-06 · Discuss on X

Same grass and water grid on both halves, 16 pixel tiles each.
Left: each cell checks 4 neighbours. Right: 2-corner Wang tiles, so every corner gets a curved shore.

The problem

The first autotile most people write looks at a cell's four neighbours (north, east, south, west) and picks a tile with shore on those sides. It never sees the diagonals. Water that touches a grass cell only at a corner gets no shore at all, so lakes come out as hard staircases with bare corners.

How it works

Move the data from cells to corners. Each grid point is grass or water, and a tile sits between four points. Four corners with two states give 2 × 2 × 2 × 2 = 16 tiles, which covers every possible transition.

The tile number is a 4-bit mask: NE counts 1, SE 2, SW 4, NW 8. All grass is 0, all water is 15, a single water corner in the north-east is 1. Two tiles side by side share two corners, so their shared edge always matches. No rule table, no special corner cases.

Painting is cheap too. A brush flips corner values, and only the 4 tiles around each changed corner can change.

There is also a 2-edge version: each side of a tile is on or off, again 16 tiles. That gives paths and rivers instead of areas. tsoding's wang-tiles fills a grid row by row: for each cell, list the 16 tiles whose left and top edges match the neighbours already placed, then pick one at random.

What I hit

Corner tiles sit half a tile off the corner grid. The Wang map draws offset by half a tile, so I fill the outer border with plain grass.

Tiles 5 and 10 (two opposite water corners) are ambiguous. With bilinear blending the tile centre lands exactly halfway, so the per-pixel noise decides whether the two water corners join. A tileset can draw them either way; it just has to pick one.

tsoding's code uses rand(). I swapped in a seeded generator so the video gives the same river map every time.

My build

The corner grid is 34 × 22 points from value noise, giving 693 corner tiles. Each tile is 16 × 16 pixels, drawn in code from the four corner values: bilinear blend plus a little per-pixel noise, water above 0.5, sand above 0.31, grass below. Shown at 4× with nearest filtering on a tilted three.js plane.

The left half uses the same grid points as cells: 16 shore variants for grass plus one water tile.

The 2-edge map at the end is tsoding's grid fill ported to JavaScript: 748 tiles, 0 mismatched edges.

Trade-offs

When to use it

Top-down pixel maps, level editors with a terrain brush, procedural islands and lakes. Use the 2-edge set for roads, pipes and rivers.

New breakdowns go out on X first: @vidar_0208. If this one helped, you can buy me a coffee.

Source and credits

Topic credit: @asynkimo (pixel terrain post comparing a plain autotile with 2-corner and 2-edge Wang tiles).

Wang tile grid: tsoding/wang-tiles by @tsoding, https://github.com/tsoding/wang-tiles, MIT license, branch master at commit 0aebe3bb5d4ded4a163ed597b0cc7790177333e9. Reused: the BLTR edge bitmask and the row-by-row grid fill (rand_tile / generate_grid), ported from C to JavaScript with a seeded random generator in place of rand().

Rendering: three.js by @mrdoob and contributors, https://github.com/mrdoob/three.js, MIT license, branch dev at commit fa01dd6ad164c48554f1bb9456f6500db052ec4f. Used unchanged (WebGLRenderer, CanvasTexture, ShaderMaterial).

My own code: the 2-corner tile set and its corner mask lookup, the 4-neighbour comparison tiles, all pixel tiles (drawn in code), the noise terrain, the brush, camera and post effects. No tiles, art or footage from the original post. No external assets. No IA model.