A 3.8 km city generated from a single reference image
· 2 min read · three.js, procedural, game-dev, tooling, webgpu
The drivable city in Threejs-Punk 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 #
- 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.
- 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 #
- Export assets canonically and merge them per asset with a palette index, one material for the city.
- Keep placements as flat arrays and rebuild instance buffers when the camera changes, not every frame.
- Give each expensive pass (shadow, rain collision, AO, reflection) the cheapest geometry that works.
- Fake many lights with a camera-following light map.
The performance side of the same city is in the occlusion culling post. You can drive it at threejspunk.com, and the project is on its project page.
Related projects
- Threejs-Punk — Fully featured Racing Game