Wave function collapse

웨이브 함수 붕괴

An algorithm that fills a large area with locally-plausible patterns from just a small tile set and "what can sit next to what" rules. Released by Maxim Gumin in 2016.

Also known as: WFCConstraint-based tiling
···
js
const c = document.createElement('canvas');
document.body.appendChild(c);
c.style.width = '100%'; c.style.height = '100%';
const ctx = c.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const ACCENT = cs.getPropertyValue('--accent').trim() || '#5b5bf7';
// 타일 소켓: [N,E,S,W] 1=연결됨 0=끊김
const TILES = [[0, 0, 0, 0], [0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 1, 0], [0, 0, 1, 1], [1, 0, 0, 1], [1, 1, 0, 0], [1, 1, 1, 1]];
let w, h, cols, rows, SIZE = 42, cellsGrid = [], order = [], revealed = 0, genStart = 0;
function collapse() {
  const n = cols * rows;
  cellsGrid = new Array(n).fill(-1);
  order = [...Array(n).keys()];
  for (let i = order.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [order[i], order[j]] = [order[j], order[i]]; }
  for (const idx of order) {
    const x = idx % cols, y = (idx / cols) | 0;
    let candidates = TILES.map((_, ti) => ti);
    const nb = [[x, y - 1, 2, 0], [x + 1, y, 3, 1], [x, y + 1, 0, 2], [x - 1, y, 1, 3]];
    for (const [nx, ny, mySide, theirSide] of nb) {
      if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
      const other = cellsGrid[ny * cols + nx];
      if (other < 0) continue;
      candidates = candidates.filter((ti) => TILES[ti][mySide] === TILES[other][theirSide]);
    }
    cellsGrid[idx] = candidates.length ? candidates[Math.floor(Math.random() * candidates.length)] : 0;
  }
  revealed = 0;
  genStart = 0;
}
function resize() {
  const dpr = Math.min(devicePixelRatio || 1, 2);
  w = innerWidth; h = innerHeight;
  c.width = w * dpr; c.height = h * dpr;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  cols = Math.max(4, Math.floor(w / SIZE)); rows = Math.max(3, Math.floor(h / SIZE));
  collapse();
}
addEventListener('resize', resize);
resize();

function drawTile(cx, cy, sockets) {
  ctx.strokeStyle = ACCENT; ctx.lineWidth = 5; ctx.lineCap = 'round';
  const half = SIZE / 2;
  const dirs = [[0, -half], [half, 0], [0, half], [-half, 0]];
  for (let i = 0; i < 4; i++) if (sockets[i]) { ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + dirs[i][0], cy + dirs[i][1]); ctx.stroke(); }
  ctx.fillStyle = ACCENT; ctx.beginPath(); ctx.arc(cx, cy, 3, 0, 7); ctx.fill();
}
function draw() {
  ctx.fillStyle = '#0d0d12'; ctx.fillRect(0, 0, w, h);
  for (let i = 0; i < revealed; i++) {
    const idx = order[i], x = idx % cols, y = (idx / cols) | 0;
    drawTile(x * SIZE + SIZE / 2, y * SIZE + SIZE / 2, TILES[cellsGrid[idx]]);
  }
}
// 칸 수와 무관하게 항상 같은 시간 안에 다 드러나도록 경과 시간 비율로 reveal 한다
const REVEAL_MS = 900, HOLD_MS = 2200;
draw();
(function loop(t) {
  if (!genStart) genStart = t;
  const elapsed = t - genStart;
  revealed = Math.min(order.length, Math.floor(order.length * Math.min(1, elapsed / REVEAL_MS)));
  if (elapsed > REVEAL_MS + HOLD_MS) collapse();
  draw();
  requestAnimationFrame(loop);
})(0);

The name borrows quantum mechanics' metaphor: before observation, many states are superposed at once, and observing forces a collapse into one. Every cell starts holding "every possible tile" simultaneously. Pick a cell and collapse it to one tile, and any tile that can't match up with that tile's edge gets removed from the neighbouring cell's list of possibilities — a constraint that keeps propagating outward. This demo shows the process with just 8 tiles that connect or don't, like pipe segments (blank, straight, corner and crossing socket combinations).

The key point: each tile only defines a trivial local rule ("this edge connects / doesn't connect"), yet the whole grid ends up reading as a large, locally-coherent maze- or circuit-like pattern. Gumin's original implements the full algorithm (minimum-entropy cell selection plus backtracking); this demo uses a simplified version that fills cells in random order, purely to show the core idea.

It's widely used for tile-based game map generation, textures whose patterns connect naturally, and procedural level design in general.

When to use

Use it to procedurally generate tile-based game maps and levels, or pipe/circuit-like connected patterns. This is a simplified version — use the original algorithm (with backtracking) for real projects.