Reverse-Engineered Architecture & Asset Explorer

How Townscaper Works Under the Hood

A deep technical breakdown of Oskar Stålberg's procedural town engine: from relaxed irregular quad grids and Dual Marching Cubes to Wave Function Collapse and zero-draw-call dynamic mesh batching.

114
Extracted 3D Models
256
Marching Topologies
128²
Palette Shader Map
60 FPS
WebGL Chunk Batching

Interactive 3D Model Explorer

Explore all 114 real 3D models extracted directly from the game package. These low-poly props are dynamically placed into procedural sockets based on neighbor context.

Live Three.js Viewport
Drag to Orbit • Scroll to Zoom

Asset Inspection & Topology

In Townscaper, walls and roofs are not pre-baked static meshes. Only the decorative attachments (windows, doors, roofs, benches, spires, lanterns) exist as discrete 3D models.

24
Vertices
12
Triangles
0.16 × 0.19
Dimensions
House Palette Color Preview:

Procedural Attachment Rules (`PropPlacer`)

When a wall face is exposed to air, PropPlacer examines surrounding geometry:

  • Ground Level Exterior
    Attaches Door_0 to Door_Store, adds Doorstep, and spawns wall lanterns.
  • Upper Story Facade
    Calculates facade width and selects between Window_1x1, Window_2x1, or Window_2x2.
  • Isolated Tower Top
    Crowns the cell with church belfry spires (Spire_Pointy or Spire_Sphere).

The Irregular Relaxed Quad Grid

Why Townscaper doesn't look like Minecraft: streets curve naturally because the underlying world is a relaxed quadrilateral dual grid rather than a rigid Cartesian grid.

Grid Canvas Simulator (`GridGenerator`)
Hover cells to inspect • Click Relax to smooth

Dual Grid Generation Algorithm

  1. Hexagonal / Triangular Seed
    Starts with a regular triangular lattice and applies random 2D jitter.
  2. Lloyd's Relaxation Smoothing
    Every interior vertex iteratively steps toward the centroid of its neighbors, creating an organic, even spacing.
  3. Quad-Dual Subdivision
    Every triangle is split into 3 quadrilaterals by connecting edge midpoints to the triangle centroid. Corner vertices meet with valencies of 3, 4, 5, or 6!
0
Relaxation Steps
0
Quad Cells
3 to 6
Corner Valency

The "Qube" & Dual Marching Cubes (256 Topologies)

Each vertical cell layer is a "Qube" defined by 8 corner nodes. Toggle the corners below to see how binary states determine walls, roofs, archways, and rooms.

8-Corner Marching Qube Inspector
Click any corner node (c0–c7) to toggle solid/empty

Binary Bitmask Classification

With 8 binary corners (0 = air, 1 = solid), there are exactly 2⁸ = 256 unique corner states. Townscaper collapses rotational and reflectional symmetries into canonical module meshes stored in ModuleLibrary.

Bitmask [c7..c0]:
0
0
1
1
0
0
1
1
0x33
Hex Mask
Straight Wall
Synthesized Geometry

Canonical Topologies

Notice how simple combinations resolve complex architecture:

  • Vertical Face (e.g. c0, c1, c4, c5)
    Generates a flat exterior wall with window sockets.
  • Bottom Ring Only (c0, c1, c2, c3)
    Air above + solid below = terrace roof with railings and chimneys.
  • Top Ring Only (c4, c5, c6, c7)
    Air below + solid above = cobblestone archway or stilt foundation!

Wave Function Collapse (WFC) Step-by-Step Solver

How Townscaper ensures adjacent qubes match seamlessly: each side has a profile hash. Click any cell to place a building or watch the solver auto-propagate constraints!

WFC Constraint Solver (`Placemaker.WaveFunctionCollapse`)
Click any cell to collapse manually • Watch entropy (ψ) reduce

WFC State Metrics

0
Total Steps
0
Propagation Queue
0
Uncollapsed Cells

In Townscaper's code, WaveFunctionCollapse.Iterate0() and Iterate1() manage this constraint queue:

  1. Observation / Collapse
    Picks the cell with minimum Shannon entropy (fewest valid candidate modules remaining) and collapses it to a single state.
  2. Profile Matching (`sideProfiles`)
    Checks the 4 horizontal and 2 vertical interfaces. If a candidate module has an incompatible socket with its neighbor, it is removed from possibilities.
  3. Ripple Propagation
    Whenever a neighbor's possibilities shrink, that neighbor is queued to re-evaluate its own adjacent cells.

Barycentric Trilinear Mesh Deformation

Canonical modules are authored in a normalized cube. Drag the 4 corner handles below to see how windows, doors, and walls dynamically stretch and warp to fit any quad without tearing!

Draggable Quad Deformation (`ModuleMath.MultiplyPoint`)
Drag any corner handle (C0–C3) to warp the quad

The Trilinear Coordinate Formula

Each vertex $(u, v, w) \in [0, 1]^3$ in the canonical mesh is transformed to world position $P$ using the 8 real corner coordinates:

Bilinear / Trilinear Formula (2D Facade)
P(u, v) = (1 - u)(1 - v) · C₀ + 
          u(1 - v)       · C₁ + 
          u · v          · C₂ + 
          (1 - u) · v    · C₃

Why Seams Never Crack: On any edge (e.g. $v = 0$), the formula simplifies to $(1-u)C_0 + uC_1$, which depends strictly on the two shared endpoints. Thus, neighboring cells match with 100% mathematical precision!

The Optimization Techniques That Make It Fast

How Townscaper renders thousands of buildings and tens of thousands of details at 60 FPS in WebGL.

1. `BigMeshMaster` Dynamic Batching

Instead of instantiating individual GameObjects for every wall, roof, and archway, BigMeshMaster dynamically merges hundreds of procedural mesh fragments into single unified vertex/index buffers. This turns 5,000 potential draw calls into fewer than 10.

Single Draw-Call Buffer

2. 128×128 Palette Shader (`House.png`)

Townscaper never swaps textures or materials. The entire game uses a tiny 128×128 palette lookup texture. Mesh UV.y coordinates select the building color (from the 15 UI swatches), while UV.x selects the surface type (plaster, roof tile, foundation, window trim).

Zero Material Swaps

3. Vertex-Color Ambient Occlusion (AO)

Traditional shadow maps and Screen-Space Ambient Occlusion (SSAO) are heavy on WebGL and mobile. Townscaper pre-computes cavity and ground occlusion at corner nodes and bakes the darkness directly into vertex color channels ($RGBA$).

Zero Shadow Passes

4. Bitwise State Flags & Zero-GC

Every cell state, corner presence, and adjacency profile is stored as primitive 8-bit or 16-bit bitmasks. Checking if two modules connect is a bitwise AND operation, preventing garbage collection (GC) allocation spikes during real-time placement.

Zero-GC Allocation

5. Spatial Pooling (`PropPlacer`)

Windows, seaside ladders, cafe chairs, and bird models are pooled inside propPools. When a building section is demolished or reconfigured, props are returned to pools rather than instantiated or destroyed.

Object Pooling

6. Lazy Patch Motivation (`GridGenerator`)

The infinite ocean grid is partitioned into patches and clusters. Methods like MotivatePatch() and UnmotivatePatch() dynamically generate and relax only the grid quads within the camera frustum and near the player's cursor.

Frustum / Spatial Culling

Production Code Blueprints for Your Game

Ready-to-use algorithms in GLSL, Three.js/JavaScript, and Unity C# to replicate Townscaper's procedural engine in your own project.

TownscaperPaletteShader.frag
// GLSL Palette Lookup Shader (Matches Townscaper's House.png workflow)
precision mediump float;

uniform sampler2D u_PaletteTex; // 128x128 House.png
uniform float     u_ColorRow;   // Selected color row [0.0 - 1.0]

varying vec2      v_Uv;         // UV coordinate (x = surface type)
varying vec4      v_Color;      // Vertex Color (contains baked AO in .a)
varying vec3      v_Normal;

void main() {
    // Sample palette: X = material type (wall, roof, trim), Y = building color swatch
    vec2 paletteCoord = vec2(v_Uv.x, u_ColorRow);
    vec4 albedo = texture2D(u_PaletteTex, paletteCoord);
    
    // Multiply by vertex color AO
    float ao = v_Color.a;
    vec3 finalColor = albedo.rgb * ao;
    
    gl_FragColor = vec4(finalColor, 1.0);
}
BarycentricTrilinearDeform.js
// Transforms canonical unit-cube vertex (u, v, w) in [0, 1]³ to deformed quad voxel world space
export function deformVertex(u, v, w, corners) {
  // corners: Array of 8 Vector3 positions [0..3 bottom CCW, 4..7 top CCW]
  const w0 = (1 - u) * (1 - v) * (1 - w);
  const w1 = u * (1 - v) * (1 - w);
  const w2 = u * v * (1 - w);
  const w3 = (1 - u) * v * (1 - w);
  
  const w4 = (1 - u) * (1 - v) * w;
  const w5 = u * (1 - v) * w;
  const w6 = u * v * w;
  const w7 = (1 - u) * v * w;

  return {
    x: w0*corners[0].x + w1*corners[1].x + w2*corners[2].x + w3*corners[3].x +
       w4*corners[4].x + w5*corners[5].x + w6*corners[6].x + w7*corners[7].x,
    y: w0*corners[0].y + w1*corners[1].y + w2*corners[2].y + w3*corners[3].y +
       w4*corners[4].y + w5*corners[5].y + w6*corners[6].y + w7*corners[7].y,
    z: w0*corners[0].z + w1*corners[1].z + w2*corners[2].z + w3*corners[3].z +
       w4*corners[4].z + w5*corners[5].z + w6*corners[6].z + w7*corners[7].z
  };
}
QubeModuleBuilder.cs
using UnityEngine;

public class QubeModuleBuilder : MonoBehaviour {
    // Evaluates 8 corner presence into an 8-bit index (0..255)
    public int GetModuleIndex(bool[] corners) {
        int mask = 0;
        for (int i = 0; i < 8; i++) {
            if (corners[i]) mask |= (1 << i);
        }
        return mask;
    }

    // Apply canonical module mesh to distorted quad cell
    public void DeformMesh(Mesh srcMesh, Vector3[] cellCorners, Mesh targetMesh) {
        Vector3[] verts = srcMesh.vertices;
        Vector3[] deformed = new Vector3[verts.Length];

        for (int i = 0; i < verts.Length; i++) {
            Vector3 p = verts[i]; // normalized [-1, 1]
            float u = (p.x + 1f) * 0.5f;
            float v = (p.z + 1f) * 0.5f;
            float w = (p.y + 1f) * 0.5f;

            deformed[i] = TrilinearInterpolate(u, v, w, cellCorners);
        }

        targetMesh.vertices = deformed;
        targetMesh.triangles = srcMesh.triangles;
        targetMesh.uv = srcMesh.uv;
        targetMesh.RecalculateNormals();
    }
}