A low-poly 3D island with cliffs, water and scattered conifers

Personal

Island Generator

A procedural island built tile by tile with wave function collapse. No map is authored and no layout is stored: the entire input is a set of hand-modelled pieces and the rules about which may sit next to which.

Open the generator kiara-vong.github.io/animal-crossing · source
RoleEverything
StackReact, Three.js,
WFC solver
Year2024
NoteIf you want to know more, let me know!
Context

Reading about an algorithm is not knowing it

I had read the wave function collapse write-ups and could have described the idea accurately at a whiteboard. Then I tried to implement it and found I understood the idea and none of the engineering, which is a gap that only appears when you build the thing.

The collapse step is easy. Propagation is not: when a cell settles, every neighbour's option set narrows, and theirs after that. Getting that to terminate, stay correct, and run fast enough to watch is the actual problem, and no description had made that clear.

Empty to finished, in one take. Every tile placed is the most constrained cell that was left, which is the heuristic the whole thing rests on.
How it works

Adjacency is the whole ruleset

The solver takes no map. Its entire input is a set of tiles and, for each, what is allowed on each of its edges. Everything the output looks like is emergent from those constraints.

That is what makes it worth building rather than reading about. The design surface is not the picture, it is the ruleset, and the relationship between the two is not obvious in either direction. Tightening one edge rule changes the character of the whole island, and wanting a particular look means reasoning backwards to the constraints that would produce it.

01Lowest entropy firstAlways collapse the most constrained cell. Pick randomly instead and the solver spends its time backtracking out of contradictions it walked into with its eyes open.
02Propagate, then stopA settled cell narrows its neighbours, and theirs. The queue has to drain to a fixed point before the next collapse.
03Backtrack cheaplyWhen a cell runs out of options, rewind to the last one that still had a choice. Most of the work is choosing where to look next.

Three edges, not four

The island is a triangular grid rather than a square one, which is the decision the whole tile set rests on. Three edges instead of four means far fewer unique pieces are needed to cover a surface without a visible seam, and hand-modelling every piece is the expensive part of this kind of project.

The cost is a coordinate system that is genuinely harder to reason about. Each tile has three rotations rather than four, neighbours alternate orientation across a row, and every adjacency rule has to be stated against the right edge of the right rotation. That bookkeeping is where the bugs lived.

Generation is also steerable rather than purely automatic. Clicking any cell forces a specific tile there and re-propagates outward from it, so the solver can be pushed toward a coastline or a cliff and then left to resolve everything the choice implies.

The generator stepped forward by hand, from an empty triangulated board to a finished island

In the app a click does more than advance it: it forces a specific tile into that cell and re-propagates outward from the choice.

Twelve steps out of one run, and yours to walk. The board is the argument on its own: three edges per cell rather than four is why ten pieces cover a surface with no visible seam.

Blender to browser

The pieces are modelled in Blender and exported as glTF, then loaded and instanced through react-three-fiber, which lets the scene graph be declarative JSX instead of imperative Three.js calls. That matters more than it sounds for a generator: the solver produces a list of placements and React renders it, so the algorithm never touches the renderer.

Rock and foliage colour is done in custom GLSL vertex shaders rather than baked into the models, which is what keeps a few hand-modelled pieces from reading as the same object repeated across the island.

The controls panel open on the tile list: Grass, four cliffs, water, two beaches and two triangles
ten pieces

Grass, four cliffs, water, two beaches, two triangles. That is the whole set.

The entire input, on screen and named. Everything above was built out of these and the rules about which may touch which.
The same board before and after one full generation, split by a line the reader can drag
nothing decided every cell settled
Drag the line. Left is the board with nothing decided; right is what the ten pieces and their adjacency rules made of it, in one run, with nothing placed by hand.
What it taught me

Constraint propagation, everywhere after

The thing I did not expect is how often this shape turns up once you have built it once. A design system's token layers are a constraint propagation problem wearing different clothes: change a primitive, and the change has to reach every component that depends on it, exactly once, without cycling.

I recognised that faster at work for having written a solver first.