Article
Game Development · Algorithms · ⏱ ≈ 14 хв читання

Wave Function Collapse — Constraint-Driven Procedural Generation

Wave Function Collapse (WFC) by Maxim Gumin (2016) generates tile-based levels, textures, and 3d voxel worlds that look hand-crafted — by framing procedural generation as a constraint satisfaction problem borrowed from quantum mechanics metaphor. The algorithm "observes" cells one at a time, collapsing their superposition of possible tiles, then propagates the resulting constraints to neighbours via arc consistency. Understanding WFC unlocks a family of AI-driven content generation tools used throughout modern game development.

1. The Superposition Metaphor

Imagine a grid where each cell can be any of T tile types. Initially every cell has all T tiles in its "superposition" (the set of still-possible tiles). The goal is to assign exactly one tile to each cell such that all adjacency constraints are satisfied.

Grid: W × H cells, T tile types State: possibilities[y][x] ⊆ {0 … T−1} Initially: possibilities[y][x] = {0,1,2, … T−1} ∀ (x,y) Goal: |possibilities[y][x]| = 1 ∀ (x,y) and all adjacent tiles satisfy the adjacency rules. Analogy to QM: Uncollapsed cell = particle in superposition "Observe" = collapse to one eigenstate Neighbour propagation = entanglement / decoherence

2. Entropy-Guided Observation

Which cell to collapse next? Always pick the cell with the lowest Shannon entropy (not yet collapsed). Low entropy means it has fewer possibilities — choosing it first minimises contradictions:

Shannon entropy of cell (x,y): H(x,y) = −Σₜ p(t) · log₂ p(t) where p(t) = weight[t] / Σ weight[t'], t ∈ possibilities[y][x] If all weights equal → H = log₂(|possibilities|) Collapsed cell → H = 0 Algorithm step: 1. Find (x,y) = argmin H(x,y) where |possibilities[y][x]| > 1 2. If none found → DONE ✓ 3. Sample a tile t according to weights from possibilities[y][x] 4. Set possibilities[y][x] = {t} 5. Push (x,y) to propagation queue

3. Constraint Propagation (AC-3)

After collapsing a cell, its neighbours may no longer support certain tiles. We propagate by removing tiles from neighbours that have no valid neighbour tile to pair with — similar to the AC-3 arc-consistency algorithm in CSP solving:

For each direction d ∈ {N, E, S, W}: For each tile t ∈ possibilities[ny][nx] (neighbour cell): If NOT ∃ t' ∈ possibilities[cy][cx] : rules[d][t'][t] == true: Remove t from possibilities[ny][nx] If removal happened → push (nx,ny) into propagation queue This continues until queue is empty (fixed-point). rules[d][t_source][t_target] = true means: "tile t_source at current cell is compatible with t_target in direction d" Contradiction: possibilities[y][x] = ∅ → backtrack!

4. Backtracking on Contradiction

When propagation empties a cell's possibilities, the current observation is incompatible with the constraints. The classic WFC approach restarts from scratch; advanced versions implement chronological or dependency-directed backtracking:

Simple restart (Gumin's original): On contradiction → copy initial state, restart Chronological backtracking: Maintain stack of (cell, chosen_tile, state_snapshot) On contradiction → pop, try next tile in possibilities Min-conflict heuristic: When re-observing after backtrack, pick tile that violates fewest remaining constraints (improves success rate on complex rule sets)

5. Overlapping vs Tiled Model

Tiled Model

Explicit set of T distinct tiles with hand-authored or extracted pairwise adjacency rules. Fast, full creative control. Used for most game level generators.

Overlapping Model

Learn N×N patterns from a sample image. Every N×N window in the output must appear in the input. Produces textures that look like the example. Slower but zero manual rule authoring.

3D Extension

6-directional neighbours (±X ±Y ±Z). Same algorithm, larger adjacency tables. Used in voxel worlds (Caves of Qud WFC rooms, shape synthesis research).

Wave Function Collapse Jr.

Simplified version using only tile counts and pair frequencies. Faster but produces less complex structures. Good enough for terrain textures.

6. Extracting Adjacency Rules from Examples

Given a sample image of W_s×H_s pixels, tile size N: Step 1: Extract all N×N sub-images (patterns) → Deduplicate (optionally allow rotations/reflections) → Count frequency(pattern) Step 2: For each pair of patterns (A, B) and direction d: compatible[d][A][B] = true if the N×N overlap between A offset by d equals B's edge For N=3, direction East: A's right 2 columns must match B's left 2 columns (pixel-for-pixel) Weight[pattern] = frequency(pattern) / Σ frequencies → tiles that appear more often are more likely to be chosen

7. JavaScript Implementation

// Minimal WFC tiled model
class WFC {
  constructor(W, H, tiles, rules, weights) {
    // rules[dir][tileA][tileB] = bool; dirs: 0=N 1=E 2=S 3=W
    this.W = W; this.H = H;
    this.T = tiles.length;
    this.rules = rules;
    this.weights = weights ?? new Array(this.T).fill(1);
    this._init();
  }

  _init() {
    // Each cell starts with all tiles possible
    this.grid = Array.from({length: this.H}, () =>
      Array.from({length: this.W}, () =>
        new Set(Array.from({length: this.T}, (_,i) => i))
      )
    );
  }

  entropy(x, y) {
    const poss = this.grid[y][x];
    if (poss.size === 1) return 0;
    const total = [...poss].reduce((s,t) => s + this.weights[t], 0);
    return [...poss].reduce((s,t) => {
      const p = this.weights[t] / total;
      return s - p * Math.log2(p);
    }, 0);
  }

  observe() {
    // Find min-entropy uncollapsed cell
    let minH = Infinity, cx = -1, cy = -1;
    for (let y = 0; y < this.H; y++) {
      for (let x = 0; x < this.W; x++) {
        const sz = this.grid[y][x].size;
        if (sz <= 1) continue;
        const h = this.entropy(x, y);
        if (h < minH) { minH = h; cx = x; cy = y; }
      }
    }
    if (cx === -1) return 'done';

    // Weighted random tile selection
    const poss = [...this.grid[cy][cx]];
    const total = poss.reduce((s,t) => s + this.weights[t], 0);
    let r = Math.random() * total;
    const chosen = poss.find(t => (r -= this.weights[t]) <= 0) ?? poss[poss.length-1];
    this.grid[cy][cx] = new Set([chosen]);
    return this.propagate(cx, cy);
  }

  propagate(startX, startY) {
    const queue = [[startX, startY]];
    const DX = [0,1,0,-1], DY = [-1,0,1,0];
    const OPP = [2,3,0,1);  // opposite direction index
    while (queue.length) {
      const [cx,cy] = queue.shift();
      for (let d = 0; d < 4; d++) {
        const nx = cx+DX[d], ny = cy+DY[d];
        if (nx<0||ny<0||nx>=this.W||ny>=this.H) continue;
        const nbPoss = this.grid[ny][nx];
        const before = nbPoss.size;
        for (const tNb of [...nbPoss]) {
          // Remove tNb if no tile in current cell supports it
          const ok = [...this.grid[cy][cx]].some(tC => this.rules[d][tC][tNb]);
          if (!ok) nbPoss.delete(tNb);
        }
        if (nbPoss.size === 0) return 'contradiction';
        if (nbPoss.size < before) queue.push([nx, ny]);
      }
    }
    return 'ok';
  }

  run(maxRestarts = 50) {
    for (let attempt = 0; attempt < maxRestarts; attempt++) {
      this._init();
      let status;
      do { status = this.observe(); } while (status === 'ok');
      if (status === 'done') return true;
    }
    return false; // failed
  }

  result() {
    return this.grid.map(row => row.map(s => [...s][0]));
  }
}

8. Applications and Extensions

Level Generation

Dungeons, caves, city blocks with consistent styles. Noita uses a procedural tiling system inspired by WFC for its pixel world. Hand-curate a small sample, generate unlimited maps.

Texture Synthesis

Overlapping model on a texture sample produces seamless, infinite textures matching the input statistics. Used for terrain, foliage, brick patterns sans obvious repetition.

3D Shape Synthesis

Academic work (Paul Merrell 2007 predates WFC coinage) generates 3D building facades and urban layouts by learning voxel adjacency from example 3D models.

Music Generation

Apply WFC to musical note sequences: rules encode harmonic compatibility, rhythm patterns. Generates music that follows learned stylistic constraints bar by bar.

Recent extensions include constraint-guided WFC (user paints "must be water" / "must be wall" cells before generation), hierarchical WFC (generate room layout first, then fill each room), and animated WFC (generate tilemaps that also animate correctly by extending rules to include time dimension).

🌀 Open Maze Generator →