3D timelapse · 100% local

The history of a city, growing on the map

From the first settlement to today: the city is born and grows in 3D, with the year, population, the flag of each era and cards with sources. No video AI and no render cost.

Banner: History in 3D
In short

historia-timelapse is a set of scripts that turns the history of a city into a 3D video, as a timelapse. It uses the real terrain relief and the OpenStreetMap buildings: each building "is born" in the year its neighborhood was settled, and the city grows on screen along with the IBGE population counter. Events appear in cards with a number, an explanation and a source. To use it, you need a computer with a graphics card, Python and Node, and historical research on the city.

Videos

The videos

Made with this engine, on the Aventuras INEMA channel (in Portuguese). Each city has a version without narration and a narrated one.

Florianópolis — from Desterro to capital

6 min: shell mounds, Dias Velho, fortresses, Azoreans, the 1777 Spanish invasion, bridges and landfills. Narrated: here. Open on YouTube

Rio de Janeiro — from 1500 to the metropolis

7 min: Tupinambá, France Antarctique, the Court in 1808, Empire and Republic. Narrated: here. Open on YouTube

Itaguaí — from village to port

6 min: from the Jesuit village to the Santa Cruz estate and the port. Narrated: here. Open on YouTube

Downtown Itaguaí — the streets, up close

5 min, with the camera in the streets and historic buildings. Narrated: here. Open on YouTube

Porto Alegre with realistic lighting (Mitsuba)

1min35, narrated: the same Porto Alegre with terrain and buildings lit by GPU path tracing — real shadows and buildings rising in their neighborhood's year. Open on YouTube

Porto Alegre — 1772 to 2024

1min35, narrated (the pilot): founding, Ragamuffin sieges, the 1941 flood, the Mauá Wall and the 2024 flood. Open on YouTube

What it is

A history engine on a 3D map

Real data instead of made-up images.

Banner with the features of historia-timelapse

🏙️ The city is born year by year

30 m Copernicus terrain and OpenStreetMap buildings (with Microsoft's open dataset where OSM has little). Each building gets a year from its neighborhood and the population curve.

📜 Cards with sources

Each event has a title, a big number, an explanation and a source. The year, population (IBGE censuses) and the flag of the regime change in the corner of the screen.

🎬 History effects

A globe zooming in to the city, a timeline, fleets sailing, sieges, fortresses, bridges being drawn, landfills growing in color and floods rising.

How it works

From data to video

Everything runs on your machine: deck.gl in Chrome with the GPU, about 0.1 s per frame at 1080p.

Historical research→ OSM + terrain→ Year of each building→ Script (blocks)→ Scene→ Render + soundtrack
Opening with the globe
Opening: globe zooming in to the city
Anhatomirim Fortress highlighted
Anhatomirim Fortress (1739) highlighted
Florianópolis in 1961
Florianópolis in 1961: UFSC is founded
Porto Alegre during the 2024 flood
Porto Alegre, May 2024: the water returns to the city center
Prerequisites

What you need

Linux with a graphics card, Python 3, Node 20+ and ffmpeg. The data are public files downloaded by a script; there is no API and no cost.

Software

Python, Node, ffmpeg and git.

sudo apt install python3-venv ffmpeg git
# Node 20+: via nvm (nvm install 20) or NodeSource — apt's nodejs is too old

Graphics card

The render uses Chrome with the GPU (Vulkan). Without Vulkan, run with GL="--use-angle=gl" before the command (slower).

# ~0.1 s per frame at 1080p on an NVIDIA GPU

Disk space

The regional OSM extract is hundreds of MB (southern Brazil, ~430 MB). Each terrain tile is 20–50 MB.

# everything stays in the dl/ folder (outside git)
User guide · step by step

Make the video of your city

Each city lives in cidades/<id>/. The scripts read the city from the CIDADE variable. The examples use Florianópolis (floripa): replace it with your city id in every command and keep export CIDADE=<id> in the terminal (without it the scripts use poa).

0

Download the project and install

Creates the Python environment (venv = a folder with the libraries for this project only) and installs Playwright, which opens Chrome to record the frames.

git clone https://github.com/inematds/historia-timelapse && cd historia-timelapse
python3 -m venv .venv
.venv/bin/pip install osmium tifffile imagecodecs \
    shapely numpy pyproj scipy pillow
cd motor && npm i && npx playwright install --with-deps chromium && cd ..
1

Create the city configuration

bbox is the map rectangle in degrees: west, south, east, north (you can draw it at bboxfinder.com). municipio_osm is the exact name of the municipality in OpenStreetMap. centro_antigo is the point where the city began.

# cidades/floripa/config.json
{ "nome": "Florianópolis", "bbox": [-48.80, -27.92, -48.33, -27.33],
  "pbf": "dl/sul.osm.pbf",
  "pbf_url": "https://download.geofabrik.de/south-america/brazil/sul-latest.osm.pbf",
  "municipio_osm": "Florianópolis", "centro_antigo": [-48.5490, -27.5965],
  "pais_microsoft": "Brazil" }
# optional: "efeitos" (bridges, forts, landfills), "tex" (texture size),
# "agua_baixa_m", "pop_modelo_extra" — see cidades/floripa/config.json
2

Download the public data

The script downloads the OSM extract (.pbf format), the terrain tiles that cover your bbox and, with --microsoft, the Microsoft buildings for places where OSM has few buildings.

export CIDADE=floripa
.venv/bin/python -I scripts/baixar_dados.py --microsoft
3

Do the historical research

Three files go in cidades/<id>/dados/pesquisa/. Copy the Florianópolis ones as a template and replace the content.

# populacao.json — censuses and estimates
{ "serie": [ { "ano": 1872, "pop": 25709, "fonte_url": "https://..." } ] }
# eventos.json — one event per item, with coordinates and source
{ "eventos": [ { "id": "ponte-hercilio", "ano": 1926, "titulo_curto_PT": "A PONTE HERCÍLIO LUZ",
    "legenda": "...", "detalhe": "...", "numero_impacto": "821 METROS",
    "local": { "lat": -27.5935, "lon": -48.5649 }, "fontes": ["https://..."] } ] }
# bairros_epocas.json — year each neighborhood began to be settled
{ "bairros": [ { "bairro": "Trindade", "ano_aprox_ocupacao": 1960 } ] }
4

Prepare the map

Cuts out buildings, water and neighborhoods, builds the terrain with the sea, gives each building a birth year and prepares bridges, forts and landfills.

.venv/bin/python -I scripts/extrai_osm.py   # uses "pbf" from the config
.venv/bin/python -I scripts/extrai_msbuild.py   # only if you downloaded the Microsoft buildings
.venv/bin/python -I scripts/prep_terreno.py
.venv/bin/python -I scripts/prep_predios.py
.venv/bin/python -I scripts/modelo_anos.py
.venv/bin/python -I scripts/extrai_feicoes.py
.venv/bin/python -I scripts/prep_efeitos.py
5

Write the script and generate the scene

roteiro.json is an object with a "blocos" list (plus "linhas", "fortes", "areas" and "regimes" if you use them). For a new city you write this file by hand, or copy and adapt cidades/floripa/roteiro_floripa.py, which builds it from eventos.json. Each block has the years (start, event year, end), the starting and arriving camera, the duration and the card. In Florianópolis it is assembled by roteiro_floripa.py from the research; use it as a template.

# one block of roteiro.json
{ "id": "ponte-hercilio", "anos": [1895, 1926, 1927], "pulo": 2.0, "dur": 9.5,
  "cam": [ {"lon": -48.552, "lat": -27.600, "zoom": 14, "pitch": 58, "bearing": -40},
           {"lon": -48.570, "lat": -27.602, "zoom": 13.7, "pitch": 60, "bearing": -70} ],
  "cartao": { "numero": "821 METROS", "titulo": "A ponte Hercílio Luz",
              "legenda": "...", "detalhe": "...", "fonte": "IBGE" },
  "destaca": ["linha:hercilio"] }
# anos: [start, event year, end]; "pulo": seconds in which time runs up to the event year
# cam: center lon/lat, zoom (12 = neighborhood, 14 = block), pitch (tilt 0–60), bearing (heading in degrees)
# "destaca": "linha:<id>" | "forte:<id>" | "area:<id>" — ids defined in "efeitos" in the config and listed at the top of the script
# adjust the camera by looking at the frames from step 6
python3 cidades/floripa/roteiro_floripa.py
.venv/bin/python -I scripts/gerar_cena.py   # generates cidades/floripa/cena/cena.js
6

Check a few frames

Records only the requested seconds, so you can look at framing, cards and effects before the whole video.

cd motor && node render.mjs ../cidades/floripa/cena --quadros 20,60,120 && cd ..
# the q_*.jpg images stay in cidades/floripa/cena/
7

Render, add the music and join

The render is done in 60 s chunks (the browser restarts between them, to avoid running out of memory). Put your own instrumental tracks (.mp3) in cidades/floripa/musica/; they play in alphabetical order, looping.

bash motor/render_trechos.sh cidades/floripa/cena cidades/floripa/final/video.mp4
.venv/bin/python -I scripts/montar_musica.py   # cidades/floripa/final/audio.wav
ffmpeg -i cidades/floripa/final/video.mp4 -i cidades/floripa/final/audio.wav \
  -map 0:v -map 1:a -c:v copy -c:a aac -shortest cidades/floripa/final/floripa.mp4
8

Narrated version (optional)

The narration is an audio layer over the same video: one text per block in cidades/<id>/narracao.json, fitted to the start of each event. The recording uses the author's local voice engine (cardshorts, outside this repository); with another voice, record one file per block and fill in narracao/tempos.json.

# narracao/tempos.json, if you record with another voice: {"<block>": {"dur": 6.2, "wav": "path.wav", "texto": "...", "ouvido": "..."}}
python3 scripts/narrar.py          # only with cardshorts; with another voice, skip it and fill tempos.json
python3 scripts/montar_narrada.py   # final/audio-narrado.wav
ffmpeg -i cidades/floripa/final/video.mp4 -i cidades/floripa/final/audio-narrado.wav -map 0:v -map 1:a -c:v copy -c:a aac -shortest cidades/floripa/final/floripa-narrado.mp4
9

Realistic lighting render (optional, NVIDIA GPU)

The same video with terrain and buildings lit by path tracing in Mitsuba 3: shadows between buildings, a scale-model look and buildings rising in their own year. The engine still draws the HUD, cards and effects on top. About 45 min for 95 s of video (the normal render takes ~5 min). Details (in Portuguese): docs/RENDER-LUZ-REAL.md.

The two formats and Blender (same script, same cards and effects; only terrain and buildings change):

  • Standard (deck.gl in Chrome): 0.07–0.11 s per frame. Porto Alegre (95 s) in ~5 min, Florianópolis (6 min) in ~17 min. Used for every video and every review.
  • Realistic light (Mitsuba + engine on top): ~0.75–0.95 s per frame. Porto Alegre in ~45 min, Florianópolis in ~3 h (estimated). Showcase version, after the standard one is approved.
  • Blender (not used): runs on CPU only here, ~40 s per frame. Porto Alegre would take ~32 h and Florianópolis ~5 days. There is no official GPU Blender for Linux ARM64.
.venv/bin/pip install mitsuba==3.9.1 drjit==1.5.0 mapbox-earcut
bash motor/render_luz.sh cidades/floripa/cena cidades/floripa/final/floripa-mitsuba-video.mp4 64
Roadmap

Next steps

What already exists and what is coming.

Done
Porto Alegre and FlorianópolisEngine with globe, timeline, cards with sources, floods, sieges, fortresses, bridges and landfills.
New
Realistic light with Mitsuba (GPU)Model-like shadows and light through path tracing. Measured comparison: deck.gl × Blender × Mitsuba (in Portuguese).
Next
Version with historical imagesPublic-domain engravings and historical photos appearing alongside the events.
Later
More cities and a skillTurn the workflow into a skill to make a new city from the research alone.