← Selected work

CELL CITY

A simulation of Shibuya that runs in your browser.

Code availableEnter the whole city ↓
  • JavaScript
  • Three.js
  • Web Workers
How it is built
Three.js / WebGL
Draws the buildings and roads in 3D.
Web Workers
Builds parts of the city off the main thread.
TypedArray / cache
Holds geometry within a chosen memory budget.
On this page

The city is divided before it moves

The city is split into areas, shared by six workers that calculate independently.

01 · One city, six owners.
How it works & what was checked

The geometry starts with a real urban footprint. The supplied processed data contain 7,770 building records divided into 233 spatial chunks; those totals were reconciled against the binary and metadata. Window occupancy is imagined. A lit floor is a procedural value, not an observation of a person or an occupied room.

Ownership is deliberately simple: divide a chunk ID by six and use the remainder as its worker ID. The assignment stays fixed as the camera moves. Five workers own 39 chunks each; the last owns 38. That apparent balance hides a useful difference: chunks contain different numbers and sizes of buildings, so equal chunk counts do not imply equal payloads.

Choose an owner in the figure, then inspect one of its chunks. The grid shows chunk centres rather than individual building outlines. The worker experiment uses bounding boxes derived from the actual vertices for visibility decisions. The table of building positions and dimensions is enough to study ownership and window workloads; loading the complete render geometry is unnecessary for these instruments.

A memory limit needs a breakdown

Add up the data each worker keeps. This budget covers retained arrays, not GPU memory or the whole browser.

02 · Count the retained arrays.
How it works & what was checked

A building table costs 20 payload bytes per building: four Float32 values for position and dimensions, plus four usage and seed bytes. Detailed window state adds one byte for each modelled window, plus eight bytes of window dimensions and four bytes of cached vertical lighting bands per building. The invoice follows the arrays that remain in the worker after a message has been handled.

The worker also owns metadata. Its chunk centres, exact bounds, identifiers and rejection flags total 45 bytes per owned chunk in the current implementation. That cost exists before the first detailed window is admitted. In the figure, metadata belongs to the whole selected worker; the other rows describe the selected chunk. Other cached chunks would add their own arrays.

Expanding every owned chunk gives worker 4 a calculated upper bound of 250,060 bytes, including metadata. This arithmetic was recalculated from the supplied tables. It is not a claim that a camera tour reaches that amount. The data fit within 256 KiB under this model, while the actual retained set depends on viewing history and the chosen budget.

This is a retained typed-array model, not a measurement of total browser memory. JavaScript objects, runtime and code, GPU resources and messages in flight are outside its boundary. The explicit boundary makes the limit testable; it also prevents the small number on the instrument from being mistaken for the cost of the whole city.

Reserve space while the active entry is still present

Keep the data currently in use. Make room by removing older entries; use approximate lighting if detail still will not fit.

03 · Keep the active entry accounted for.
How it works & what was checked

When a nearby chunk asks for detailed windows, its building table is already being used. The worker first asks whether the complete new payload can fit while keeping that active entry present. It then selects older, unpinned entries for eviction until there is enough space. If the active table, metadata and requested detail cannot fit together, it declines the detail and produces analytic lighting.

Window state, window dimensions and the band cache are one admission decision. The accounting does not approve the large array and add the smaller companion arrays afterwards. This matters near a hard limit: each retained byte belongs in the same decision, even when one array looks small beside the others.

The sequence below is an authored fixture, not a sampled city workload: two chunks contain six tall buildings each and share a 64 KiB worker. First both tables arrive, then A obtains detailed windows, then B requests them. The result lists the actual cache entries and eviction count returned by the worker. A read-only inspection also sums the retained entry sizes independently and checks that this sum plus metadata equals the reported total.

Distance has a memory

Move towards a block, away, then back. What stays in memory also depends on where the camera has been.

04 · Returning is different from arriving.
How it works & what was checked

The current worker asks for detailed windows when a chunk is visible and its centre is less than 560 metres away. It releases that window payload after the camera moves beyond 1,150 metres. Between these thresholds, a previously allocated window array can remain in memory even though the current output uses a cheaper analytic band or sends no lighting update.

A snapshot of distance alone therefore cannot explain the retained set. Arriving at 600 metres with an empty cache differs from returning there after a close visit. Building tables have another lifetime: distance does not remove them from the cache; pressure does. The invoice and the current lighting mode answer different questions.

This instrument keeps one real chunk's payload, fixes simulation time, and supplies all-visible test planes. It then follows an authored out-and-back distance sequence. The deliberate simplification isolates admission and release from camera-frustum changes; it is not a replay of the full rendered tour. Compare the two visits to 600 metres and watch which arrays remain.

Same workload, different budget

Change the memory budget and repeat the same work. Compare the data removed from memory; this does not measure drawing speed.

05 · Change one budget, keep the workload.
How it works & what was checked

The recorded curve uses the same sparse workload at every budget: 54 exact camera poses, two worker batches per pose, fixed times and real vertex-derived bounds. Each run starts with fresh workers at the target budget and eagerly sends their building tables. The record used Node 22.11.0. It is a source replay with a stated input sequence, not a continuous browser tour or a drawing-speed benchmark.

The article and full demo use the same worker source. The article exposes additional budget presets. Select one worker and run a budget locally, or sweep the 13 presets from 8 to 192 KiB. The 256 KiB option is a separate reference. Circles show the recorded Node result; squares show counters actually returned in this browser. Results that differ remain visible. The time spent computing or waiting is not hidden inside an invented frame-rate number.

Less memory does not guarantee a smooth, monotone response. A very small budget can decline an expensive window state and use an analytic band. A slightly larger budget can admit it, then evict tables needed later in the sequence. Read the eviction, fallback and lighting-update counters together. A missed lighting update is not a missing building: geometry remains resident in the GPU in the full demo, and an unchanged band can remain on it.

For the recorded sequence, 128 KiB and above produced no evictions. That observation belongs to this workload; changing the path, warm-up or message sequence changes the experiment. Optional sound is off by default. When enabled, a short low-level synthesized cue is eligible only after an actual worker batch reports an increased eviction count, with a bounded rate. It is an instrument signal, not a city soundtrack.

Some requests do not fit in memory

A taller building asks for more window data. Try a height and see whether that request fits the worker’s budget.

06 · A detailed request can be too large.
How it works & what was checked

The window model derives floors and columns from building dimensions, then clamps them. In the inspected source, a building can request at most 80 modelled floors and 72 columns. Those limits bound the procedural workload. They do not change the source building geometry, and they do not establish that a height attribute was measured in the field.

The instrument builds an explicit synthetic request: 46 buildings with the selected height and a radius of 33 metres. The displayed invoice includes their tables, detailed states, dimensions, band cache and owned metadata. Increase the height to change the request, then run the worker against a 256 KiB budget. The input is authored for the experiment; these dimensions are not attributed to a real Shibuya building.

The useful result is the admission decision. If the complete detail can fit, the worker retains it. If it cannot, the detailed arrays are not allocated and analytic lighting is returned. The measured retained bytes can be far below the calculated request because a declined request is not retained memory. This is why the figure keeps requested and retained quantities separate.

Read the worker contract ↗

Evidence & method

Verification notes

The supplied data contains 7,770 building records in 233 chunks. The byte calculation covers worker metadata and retained arrays; GPU geometry and JavaScript runtime overhead are outside its scope.

The recorded run uses 54 camera positions, six workers and 14 memory budgets in Node 22.11.0, with fresh initialization for each trace. Browser measurements are shown separately in the controls above.

Download the data and source identifiers ↓

Explore the whole city

Opens here. Your browser runs the world.

The film · 4:30

Buildings: PLATEAU / MLIT · Roads: © OpenStreetMap contributors