DEV Community

hmza
hmza

Posted on

๐Ÿง  #186 โ€” Wave Function Collapse: Overlapping Model Explained with Code & Formulas

๐Ÿง  #186 โ€” Wave Function Collapse: Overlapping Model Explained with Code & Formulas

โ€œA constraint-solving algorithm meets procedural generation โ€” and itโ€™s surprisingly beautiful.โ€

Wave Function Collapse (WFC) is one of the coolest algorithms for procedural generation. Inspired by quantum mechanics, itโ€™s been used to generate textures, maps, dungeons, cities, and even music โ€” all while maintaining global coherence.

In this article, weโ€™ll go deep into the Overlapping Model โ€” the most popular and elegant variant of WFC. Weโ€™ll explain the formulas, the core logic, and include complete Python code to get you started!


๐ŸŒŠ What is Wave Function Collapse?

At its core, WFC is:

  • A constraint-solving algorithm
  • A tile-based procedural generation system
  • That tries to fill a grid with tiles without violating neighbor rules

Inspired by Quantum Mechanics:

Each cell begins in a superposition of all valid tiles (states). As constraints are applied and the system collapses, each cell reduces to a single state.


๐Ÿ” The Overlapping Model

In this variant:

  • You extract nร—n patterns (tiles) from an input image.
  • The output grid is filled such that every tile overlaps correctly with its neighbors.
  • The algorithm uses the frequency of each tile in the input to weight the probabilities.

๐Ÿ“ Core Concepts and Formulas

1. Entropy

Entropy is the measure of uncertainty in a cell:

H = log(\sum w_i) - \frac{\sum w_i \cdot log(w_i)}{\sum w_i}
Enter fullscreen mode Exit fullscreen mode

Where:

  • w_i is the weight (frequency) of the i-th pattern

We always collapse the cell with the lowest non-zero entropy first.


2. Wave Function

Each grid cell maintains a list of possible tiles (pattern indices). This is called the wave:

wave[x][y] = [True, False, True, True, ...]
Enter fullscreen mode Exit fullscreen mode

A True at index i means pattern i is still a possibility for that cell.


3. Propagation

After collapsing one cell to a specific pattern, we propagate constraints:

  • For each direction (N, S, E, W), we eliminate incompatible patterns in neighbors.

This continues until no more eliminations are possible.


๐Ÿงช Python Code: Minimal Working WFC (Overlapping Model)

import numpy as np
from PIL import Image
from collections import defaultdict
from random import choice

N = 3  # pattern size
output_width = 20
output_height = 20

def get_patterns(img, N):
    patterns = defaultdict(int)
    w, h = img.shape
    for y in range(h - N + 1):
        for x in range(w - N + 1):
            patch = tuple(img[y:y+N, x:x+N].flatten())
            patterns[patch] += 1
    return list(patterns.items())

def pattern_match(a, b, direction):
    N = int(len(a)**0.5)
    a = np.array(a).reshape(N, N)
    b = np.array(b).reshape(N, N)
    if direction == 'right':
        return np.array_equal(a[:, 1:], b[:, :-1])
    if direction == 'down':
        return np.array_equal(a[1:, :], b[:-1, :])
    return False

def get_compatibility(patterns):
    compat = {i: {'right': set(), 'down': set()} for i in range(len(patterns))}
    for i, (a, _) in enumerate(patterns):
        for j, (b, _) in enumerate(patterns):
            if pattern_match(a, b, 'right'):
                compat[i]['right'].add(j)
            if pattern_match(a, b, 'down'):
                compat[i]['down'].add(j)
    return compat

def collapse(wave, entropies):
    # Pick the cell with the lowest entropy
    xs, ys = np.where(entropies == np.min(entropies[entropies > 0]))
    x, y = choice(list(zip(xs, ys)))

    options = [i for i, valid in enumerate(wave[x][y]) if valid]
    chosen = choice(options)
    wave[x][y] = [i == chosen for i in range(len(wave[x][y]))]
    return x, y

def propagate(wave, compat, x, y, W, H):
    changed = [(x, y)]
    while changed:
        cx, cy = changed.pop()
        for dx, dy, dir in [(-1, 0, 'right'), (1, 0, 'right'), (0, -1, 'down'), (0, 1, 'down')]:
            nx, ny = cx + dx, cy + dy
            if 0 <= nx < W and 0 <= ny < H:
                for p in range(len(wave[nx][ny])):
                    if wave[nx][ny][p]:
                        valid = False
                        for q in range(len(wave[cx][cy])):
                            if wave[cx][cy][q] and p in compat[q][dir]:
                                valid = True
                                break
                        if not valid:
                            wave[nx][ny][p] = False
                            changed.append((nx, ny))

def run_wfc(patterns, compat, W, H):
    wave = [[[True]*len(patterns) for _ in range(H)] for _ in range(W)]
    entropies = np.full((W, H), len(patterns), dtype=float)

    for _ in range(W * H):
        x, y = collapse(wave, entropies)
        propagate(wave, compat, x, y, W, H)
        # Update entropy
        for i in range(W):
            for j in range(H):
                possibilities = [p for p in wave[i][j] if p]
                if possibilities:
                    entropies[i][j] = len(possibilities)
                else:
                    entropies[i][j] = 0
    return wave

def visualize(wave, patterns, N, W, H):
    tile_size = N
    output = np.zeros((H + N - 1, W + N - 1), dtype=np.uint8)
    for x in range(W):
        for y in range(H):
            idx = wave[x][y].index(True)
            pattern = np.array(patterns[idx][0]).reshape(N, N)
            output[y:y+N, x:x+N] = pattern
    return output

# Example: Use a grayscale image
input_img = Image.open("input.png").convert("L")
img_array = np.array(input_img)

patterns = get_patterns(img_array, N)
compat = get_compatibility(patterns)
wave = run_wfc(patterns, compat, output_width, output_height)
result = visualize(wave, patterns, N, output_width, output_height)

Image.fromarray(result).save("output.png")
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  Real-World Applications

  • Bad North (game) โ€” Island generation
  • Caves of Qud โ€” Puzzle world generation
  • Minecraft mods โ€” City generation
  • AI music โ€” Sequence generation

๐Ÿ”ฎ Why Itโ€™s Still Relevant

Despite being published in 2016, WFC remains one of the most clever procedural generation tools. Its ability to retain local patterns while building large-scale outputs makes it extremely useful in design, games, and AI art tools.


๐Ÿงต Bonus: Pattern Frequency Formula

Let f(p) be the count of pattern p in the input.

The weight of each pattern is:

w_p = \frac{f(p)}{\sum_{q} f(q)}
Enter fullscreen mode Exit fullscreen mode

These weights are used to determine the likelihood of collapsing into that pattern.


๐ŸŽฏ Final Thoughts

Wave Function Collapse is not just a fancy name โ€” itโ€™s a blend of quantum-like logic and classic constraint solving, producing mesmerizing, rule-abiding content.

Want to build infinite Zelda maps, sci-fi textures, or even tile-based emojis?

WFCโ€™s overlapping model is your new best friend.


๐Ÿ“š Resources


Happy collapsing! ๐ŸŒŒ

Top comments (0)