# A 3.8 km city generated from a single reference image

> How Threejs-Punk builds its drivable city offline: a 944x846 map image and a Blender scene become roads, buildings, signs and lamps with no hand placement.

Canonical page: https://andersonmancini.dev/blog/threejs-punk-city-from-an-image

Published: 2026-08-20 · 2 min read · Tags: three.js, procedural, game-dev, tooling, webgpu

The drivable city in [Threejs-Punk](https://andersonmancini.dev/blog/threejs-punk-racing-game.md) is not hand-placed. It comes from two inputs and a pipeline, and when I re-run the pipeline every road, building, lamp and prop is regenerated.

## The two inputs

1. **A reference plan**: a 944x846 px map image with islands, a grid, parks, an airport and a port. One pixel is 4 m, so the playable area is about 3.8 x 3.4 km.
2. **A Blender scene**: the original alley's roughly 20 buildings, 24 skyline towers and street props, exported once as reusable assets.

## From Blender to a building kit

A headless Blender script applies modifiers, fixes winding and puts every asset in a **canonical orientation**: facade facing one axis, base on the ground, centered footprint. That way any asset can be placed with just `(x, z, yaw, scale)`.

A second step merges each asset into one geometry with a per-vertex palette index, so **all buildings, towers and props share one uber material**. LODs come from meshoptimizer's simplification, and the palette carries emissive kinds: interior lights, neon, animated LED screens, curtain walls and signs.

One detail I like: the original alley only modeled the sides its camera saw, so some towers vanished from behind. The kit builder slices each building into 1 m rows, merges slab boxes and adds sealing quads where the view goes too deep into a slab. Those same slabs become the colliders and the rain and shadow proxies.

## From the image to a map

The map extraction classifies the image by color (water, road, park, block, paved), then:

- turns roads into a graph with a distance transform and a skeleton, smooths the polylines and sets widths from the distance field,
- routes highways with Dijkstra over the wide roads,
- finds bridges where a road has water on both sides,
- moves the coastline along a seeded noise field, so the island silhouette is ours and not the reference's,
- places buildings from an occupancy grid: skyline towers, frontage buildings facing each road, and filler masses,
- hangs 2 to 9 signs per facade and scatters street furniture such as lamps, stop signs, dumpsters and wires.

The result is stored as **flat number arrays** (`[asset, x, z, yaw, scale, ...]`) to keep `map.json` small.

## The checklist I would port

1. Export assets canonically and merge them per asset with a palette index, one material for the city.
2. Keep placements as flat arrays and rebuild instance buffers when the camera changes, not every frame.
3. Give each expensive pass (shadow, rain collision, AO, reflection) the cheapest geometry that works.
4. Fake many lights with a camera-following light map.

The performance side of the same city is in [the occlusion culling post](https://andersonmancini.dev/blog/threejs-punk-occlusion-culling.md). You can drive it at [threejspunk.com](https://threejspunk.com/), and the project is on its [project page](https://andersonmancini.dev/projects/threejs-punk.md).

## Related projects

- [Threejs-Punk](https://andersonmancini.dev/projects/threejs-punk.md) — Fully featured Racing Game
