Ollin

TextureSynthesis

the overlapping model of Wave Function Collapse: a small picture written in the source supplies its own square patches, and a much larger texture is built so that every overlap agrees (OverlappingWFC, wfc(from:))

Run
swift run --package-path Examples Example-Patterns-TextureSynthesis
Source
Examples/Patterns/TextureSynthesis/Sketch.swift
import Ollin

/// Wave Function Collapse, the overlapping model: instead of declaring tiles and
/// the edges that may meet, you hand it a small picture and it works the
/// vocabulary out for itself. It cuts the sample into every little square patch
/// the sample contains, notes how often each one turns up and which ones may
/// overlap, then fills a much larger grid so that every overlap agrees.
///
/// What comes out is new, but locally it is made of nothing that wasn't in the
/// sample: the same walls, the same weave, the same flowers, arranged in a way
/// the sample never showed. The samples here are authored right in the source as
/// little character grids, so you can edit one and watch the texture change.
///
/// The `motif` parameter picks a sample, `patternSize` sets how much context a patch
/// carries (2 keeps only the loosest sense of it, 4 reproduces whole motifs), and
/// `symmetry` decides whether turned and mirrored copies are learned too. Flowers
/// know which way is up, so try them at `.none`.
@main
final class TextureSynthesisSketch: Sketch {
    override var canvasSize: CanvasSize { .square(1080) }

    enum Motif: String, CaseIterable, ParamOption {
        case rooms, weave, flowers
    }

    @Param var motif: Motif = .rooms
    @Param(2 ... 4) var patternSize: Int = 3
    @Param var symmetry: WFCSymmetry = .all
    @Param var wrapsSample: Bool = true
    @Param var tileable: Bool = false

    private var texture: Image?
    private var sample: Image?
    private var lastSettings = ""
    private var patternCount = 0

    override func draw() {
        background(Color(hex: 0x0E1116))
        resolveIfNeeded()

        let margin = 44.0 * scale
        let sampleBox = Rectangle(x: margin, y: margin,
                                  width: 240 * scale, height: 240 * scale)
        let outputBox = Rectangle(x: margin, y: sampleBox.y + sampleBox.height + 56 * scale,
                                  width: width - margin * 2,
                                  height: height - (sampleBox.y + sampleBox.height + 56 * scale) - margin)

        if let sample { drawPixels(sample, in: sampleBox) }
        if let texture { drawPixels(texture, in: outputBox) }

        noStroke()
        fill(Color(hex: 0x8A93A3))
        textSize(17 * scale)
        drawText("the sample", at: Vector2(sampleBox.x, sampleBox.y - 14 * scale))
        let learned = "\(patternCount) patterns learned from a \(sample?.width ?? 0)x\(sample?.height ?? 0) sample"
        drawText(learned, at: Vector2(sampleBox.x + sampleBox.width + 28 * scale,
                                      sampleBox.y + 24 * scale))
        fill(Color(hex: 0x5D6675))
        drawText("every square of the picture below is one the sample already contained",
                 at: Vector2(sampleBox.x + sampleBox.width + 28 * scale,
                             sampleBox.y + 52 * scale))
    }

    /// Learn and solve only when something actually changed: both halves are real
    /// work, and neither belongs in a frame loop.
    private func resolveIfNeeded() {
        let settings = "\(motif)-\(patternSize)-\(symmetry)-\(wrapsSample)-\(tileable)"
        guard settings != lastSettings else { return }
        lastSettings = settings

        let source = Self.image(for: motif)
        sample = source
        seed(7)
        guard let model = OverlappingWFC(learningFrom: source, patternSize: patternSize,
                                         symmetry: symmetry, wrapsSample: wrapsSample) else {
            texture = nil
            patternCount = 0
            return
        }
        patternCount = model.patternCount
        texture = wfc(model, width: 72, height: 44, tileable: tileable) ?? texture
    }

    /// Draw an image as one rectangle per pixel, fitted inside `box`.
    ///
    /// `drawImage` would smooth a picture this small into mush when it's blown up
    /// this far; a rectangle per pixel keeps the edges hard, and it's what a
    /// vector export writes out too.
    private func drawPixels(_ image: Image, in box: Rectangle) {
        let cell = min(box.width / Double(image.width), box.height / Double(image.height))
        let originX = box.x + (box.width - cell * Double(image.width)) / 2
        let originY = box.y + (box.height - cell * Double(image.height)) / 2
        noStroke()
        for y in 0 ..< image.height {
            for x in 0 ..< image.width {
                fill(image[x, y])
                drawRect(corner: Vector2(originX + Double(x) * cell, originY + Double(y) * cell),
                         width: cell, height: cell)
            }
        }
    }

    // MARK: - The samples

    /// Three little pictures, authored here as character grids so the whole
    /// input is visible in the source. One color per character.
    private static func image(for motif: Motif) -> Image {
        switch motif {
        case .rooms: return build(rooms, inks: [".": 0x11151C, "#": 0x8FB8DE])
        case .weave: return build(weave, inks: ["." : 0x14100E, "o": 0xE4A03F, "+": 0x8C4A2F])
        case .flowers: return build(flowers, inks: [".": 0x121A26, "|": 0x4E9A57,
                                                    "o": 0xE4572E, "#": 0x6B4A2F])
        }
    }

    private static func build(_ rows: [String], inks: [Character: UInt32]) -> Image {
        let image = Image(width: rows[0].count, height: rows.count)
        for (y, row) in rows.enumerated() {
            for (x, ch) in row.enumerated() {
                image[x, y] = Color(hex: inks[ch] ?? 0xFF00FF)
            }
        }
        return image
    }

    /// Walls that recombine into floorplans the sample never drew.
    private static let rooms = [
        "................",
        "..######..####..",
        "..#....#..#..#..",
        "..#....####..#..",
        "..#..........#..",
        "..#..####....#..",
        "..####..#..###..",
        "........#..#....",
        "..#######..#....",
        "..#.....#..#....",
        "..#.....####....",
        "..#######.......",
        "................",
        "..####..######..",
        "..#..#..#....#..",
        "..#..####....#..",
    ]

    /// Bands crossing over and under, which constrains tightly enough that the
    /// solver has to keep the weave consistent everywhere.
    private static let weave = [
        "..oo....oo......",
        "..oo....oo......",
        "++++++++++++++++",
        "++++++++++++++++",
        "..oo....oo......",
        "..oo....oo......",
        "..oo....oo......",
        "..oo....oo......",
        "++++++++++++++++",
        "++++++++++++++++",
        "..oo....oo......",
        "..oo....oo......",
    ]

    /// Stems standing on ground under sky: a sample that knows which way is up,
    /// so it wants `symmetry: .none` to stay the right way round.
    private static let flowers = [
        "................",
        "................",
        ".....o..........",
        "..o..|.......o..",
        "..|..|.......|..",
        "..|..|..o....|..",
        "..|..|..|....|..",
        "..|..|..|..o.|..",
        "..|..|..|..|.|..",
        "################",
        "################",
    ]
}

↑↓ move ↩ open