AVAILABLE FOR FULL-TIME ROLES OPEN TO FREELANCE CONTRACTS AI FULLSTACK ENGINEER · AGENTIC AI AGENT-READY · HUMANS WELCOME AI AGENTS · LLM TOOLING · EVALS NEXT.JS · TYPESCRIPT · REACT BASED IN LISBON · REMOTE WORLDWIDE
← All writing

Mēxihco Tenochtitlan: flying over 1507 in the browser

A 12-minute looping day over Tenochtitlan, from dawn to the New Fire of 1507, flown live in three.js: the dossiers, the Nahuatl captions and the inside-out serpent.

Cover image for Mēxihco Tenochtitlan: flying over 1507 in the browser

Mēxihco Tenochtitlan is a 12-minute day that runs live in your browser. You ride the back of a feathered serpent over the Aztec capital in 1507, from sunrise behind the Iztaccíhuatl to the night of the New Fire, when every fire in the valley was put out and relit from one hilltop. Take the controls and you can fly to the volcanoes. Let go for four seconds and the serpent finds its way back to the route.

It’s the second demo in the series, after Tupi. Same setup: one long conversation with Claude Opus 5.5 in Claude Code, which wrote the research, the three.js code and the Blender scripts, and ran the review loop. Integration started on 30 September 2026. It went live on 4 October.

The 1524 Nuremberg map of Tenochtitlan next to a frame of the island from the demo

The prompt

The brief was two messages, translated here from Portuguese:

Another cool demo would be an Aztec warrior flying over the Aztec capital riding Quetzalcoatl.

Let’s make a demo just of Tenochtitlan, but name it in the Nahuatl language and put all the captions in Nahuatl too, okay? Do historical research, and you can use the same setup with Blender MCP + mint.gg. It has to look realistic and fabulous. The hard part is that it has to loop, and I can’t picture how we’ll do that loop.

Then two additions: the user should be able to steer the serpent and fly close to the city, the forest and the volcanoes, and time should pass, “from the full moon to the morning sun and the dusk”. Every request after that is on the demos page, behind the ✦ prompt button on the Tenochtitlan card.

Five dossiers before any geometry

The research came in five dossiers: the city, the characters, the Nahuatl language, the iconography and the written eyewitness accounts. Every claim carries a confidence level (high, medium, low or contested), and quotes were checked against the original text. Anything that couldn’t be checked is marked unverified. Some of what came out of them:

  • The year. Sahagún says it outright: “La última solemnidad que hizieron de este fuego nuevo fue el año de mil y quinientos y siete.” The New Fire marked the end of a 52-year cycle. 1507 was the last one celebrated in public, under Motecuhzoma II.
  • The hill. The fire was drilled on the Huixachtécatl, today’s Cerro de la Estrella: 11.1 km from the Templo Mayor at an azimuth of 157°, 224 m above the valley. Sahagún’s “dos leguas de México” matches.
  • The sky. The priests waited for the Pleiades to cross the meridian at midnight. Precessed to 1507, they culminate 3° from the zenith at that latitude. The sky module precesses its star catalogue to 1507, so that’s where they are.
  • The temple. In 1507 the Templo Mayor was in its sixth stage, or the seventh under construction. The final stage stood on a platform of 84 × 77.2 m and reached about 45 m with its two shrines (López Luján 2001). Bernal Díaz’s famous 114 steps belong to the temple of Tlatelolco, not this one.
  • What was buried. The Coyolxauhqui disc everyone knows was already under later construction by 1507. What sat at the foot of the stairs was the Tlaltecuhtli monolith, 4.17 × 3.62 m. The brief for the build: Tlaltecuhtli in view, no Coyolxauhqui.
  • Where the serpent lands. The temple of Ehécatl-Quetzalcóatl was round, 18 m across, facing east, in front of the Templo Mayor. It’s the god of the 52-year count, so it’s where the flight takes off and lands.
  • Scale. Seen from the city, Popocatépetl and Iztaccíhuatl rise only 2.3° to 2.7° above the horizon. The dossier says not to enlarge them: the feeling of “huge volcano” comes from altitude, and the flight gets there.
  • The grid. The streets and the temple’s axis are rotated about 7.5° clockwise from north (Šprajc 2000). The city generator uses that rotation.

The most useful finding was a grammar point. Bernal, from the top of the Tlatelolco temple, wrote that they saw “cues é adoratorios á manera de torres é fortalezas, y todas blanqueando… y las casas de azuteas”. “Todas blanqueando” agrees with the temples, not the houses. The houses are only “de azuteas”, flat-roofed. The first build had painted the entire island white because of that one line. In house and canal frames, 33% to 45% of the pixels were above 200 luminance. In Thomas Kole’s reconstruction, it’s under 1%. The rebuild keeps fresh lime for temples and palaces and mixes adobe and worn plaster for everyone else.

The night took a decision instead of a fact. The dossier’s astronomy puts the Pleiades at the meridian at midnight on 5 or 6 November 1507 (Julian), close to a new moon. The Nahuatl text places the ceremony in the month of Panquetzaliztli, mid-to-late November, when the moon would be full. The brief asked for a full moon. The demo uses the Panquetzaliztli date, and the “about” note says the exact night is debated.

The sources, if you want to check them:

Captions only from the sources

The brief asked for the title and every caption in Nahuatl. That’s the request I was most worried about. A model will happily write fluent-looking Nahuatl, and I can’t read it.

So the language dossier set one rule: no caption is invented. There are 19 captions in the main script. Each is quoted from a source with normalised spelling or cut from an attested sentence without changing a word. The only adaptations are two optional variants, and each has a pure excerpt as the alternative. The sources are the Florentine Codex, checked against the Anderson and Dibble transcription in the Getty’s Digital Florentine Codex, the Crónica Mexicáyotl and Chimalpahin. The New Fire captions all come from book 7 of the Florentine Codex, the Nahua account of the ceremony itself. On screen a caption carries its translation and its source, for example In izqui ohtli ihuan in izqui acalohtli, “all the roads and all the canals”, Florentine Codex book 12, chapter 21.

The dossier also threw some things out:

  • “Tenōchtitlan”. The popular spelling with a long ō is wrong. Carochi marks the first three syllables short. The title is “Mēxihco Tenochtitlan”: a long ē and a glottal stop in Mēxihco, no macrons in Tenochtitlan.
  • “Ayac quimati in Tenochtitlan”. It looked like a good epigraph. The dossier found no attestation in the sources it checked and suspects a mix-up with a different line of the Cantares mexicanos, so it was dropped.
  • The interface. There’s no attested Nahuatl for “loading” or “back to route”, so the UI stays in Portuguese and English.

The stack

  • Engine. The Tupi engine: three.js r186 on WebGPU with TSL materials, a WebGL2 fallback, Vite, and modules that each export create(ctx) returning { update(t, dt) }. Round 0 integrated 16 of them with zero errors: environment, terrain, lake, city, precinct, vegetation, people, fire, flight, serpent, rider, camera, birds, audio, overlay and post. The lakeshore towns came later as a 17th.
  • Terrain. Copernicus GLO-30 elevation data for the 122.88 km square around the city and GLO-90 for the horizon ring. The modern city is removed with a 210 m morphological opening, and the 1507 lakes are rebuilt with a threshold over the drained lakebed plus hand-drawn polygons for the dike, the island and the isthmuses.
  • Sky. Sun, moon and star precession from Meeus’s Astronomical Algorithms, bright stars from the Yale Bright Star Catalogue, and atmosphere coefficients from Bruneton and Neyret (2008) and Hillaire (2020).
  • Rider. Mint, then Rodin Gen-2, then Blender: 180k triangles up close and 45k at distance, 4K textures and a six-bone rig.
  • Serpent. A procedural body that follows the head’s trail. The head is a TRELLIS.2 generation, at about US$ 0.74 a run.
  • Surfaces. Poly Haven CC0 scans for plaster, adobe, basalt, thatch and bark.

Making a day loop

The loop was the part I couldn’t picture, and it ended up being the simplest rule in the codebase: everything is a pure function of t. Sky, sun, moon, stars, house fires and the New Fire timeline read only the clock, never the camera’s position. Autopilot follows a closed spline whose position is also a function of t. When you fly free, the serpent keeps its own position and heading but the clock keeps running. Four seconds after you let go, a pursuit steers it back to wherever the autopilot would be right now. Hold Shift and it goes at 560 m/s, which is how you get to a volcano 70 km away.

Four frames of the loop: sunrise over the lake, the sacred precinct in the morning, the city at dusk as fires go out, and the New Fire on the hill at night

The first version looped in 360 seconds. My review was “time passes way too fast”. Doubling the loop sounds like one constant, but it touches every system that reads the clock, and some of them shouldn’t slow down. The fix split time in two:

  • A day clock at half speed (τ = t/2), which drives the sky, the fire timeline, the house hearths and people’s schedules.
  • A motion period that stays at 360 seconds, so walking, paddling, water, wind and feathers keep their speed.

It was measured, not assumed. 400 pedestrians averaged 1.1533 m/s before and after. 2,915 canoes averaged 1.2932 m/s before and after. The route doubled from 76.5 km to 152 km so the serpent flies at the same speed over new places: Iztapalapa, Coyoacán, Xochimilco, the Chalco lake toward the volcanoes. The seam where 719.9 s wraps to 0 changes the image by 7.34 on the capture metric, against an average of 8.6 for an ordinary 0.1 s step. You can’t see the loop.

The Gauntlet, again

I reused the loop from the browser FPS and from Tupi: parallel builders, critics with a clean context that only see pixels, a “does this read as real?” classifier, A/B captures, and a regression hunter. I wrote it up in Inside the Gauntlet loop.

The classifier gave the first round 3.2 out of 10. Tupi had started at 4.96 with the same kind of classifier. Round 1 then improved almost everything it measured: brighter whitewash on the temples, a real blue hour, crowds lifted from L* 15 to 43, canoes going from 2,700 to 9,000. The score came back at 3.15. A tie.

The reason was in the critics’ notes. The serpent was in every frame, and it was the number one defect in every frame. As long as the hero looked like a toy, no amount of sky or water work moved the average. That stayed true for a long time.

The serpent was inside out

From round 8 on, the serpent stayed the number one defect, and the symptoms kept coming back: a “black stripe” along the body, a “black tube”, “the belly seen from above”. Rounds 9 and 10 attacked it with materials: scales baked in Cycles, parallax, feather atlases, a sky rim light, and a fix for a three.js r186 lighting bug that ignored specularIntensity. Each one helped a little.

Round 12 found the actual cause. The index order in body.js built the triangles with their normals pointing inward. With front-face culling, the renderer threw away the outer wall of the body and the camera was looking at the inside of the far wall. One winding-order change fixed the number one defect since round 8. It also meant the material calibrations from rounds 9 and 10 had been tuned against an inside-out mesh, so they were redone. The shadowed flank went from L* 2 to 5 up to 13 to 15.

The same frame before and after the fix: on the left the inside-out body reads as a flat black tube, on the right the scaled flank is lit

The lesson I took: when every critic names the same defect round after round and parameter work barely moves it, stop tuning and look for something structurally wrong.

A black screen from a dependency cache

Round 10 rewrote temporal antialiasing after finding it wasn’t accumulating at all. Forty static frames with and without it differed by 0.76. Part of the cause: the previous-position node ignored custom vertex positions, so 5.6% of the screen had no valid history.

The new TRAA then produced, now and then, a capture that was black from start to finish. The cause had nothing to do with rendering. The forked TRAA imported a helper from three/addons that nothing else used. The dev server discovered it at runtime, re-optimised its dependencies and changed its hash. The capture page ignored the reload, so two copies of three/webgpu ended up on the same page. A TSL If read a null stack from the wrong copy, the material failed to resolve, and three.js replaced it with an empty one. It reproduced 2 out of 2 times when editing the file with the server running. The fix inlined the helpers, added a pass-through fallback if the TRAA fails, and made the capture script retry any session with errors or black frames.

Measure the instrument

Round 8 also found bugs in the measuring:

  • The frame timer was quantised. frameMs followed requestAnimationFrame at 120 Hz, so it only ever read 8.3, 16.7 or 25 ms. Real cost needed a separate throughput probe.
  • The GPU was shared. A Flurry screensaver and another project’s Playwright Chromium were competing with the captures.
  • Captures weren’t deterministic. The same code gave a difference of 9 to 13 between loads. The cause was the warm-up counters shifting the TRAA’s sub-pixel jitter. After the fix, two loads differ by 0.01 to 0.03.

Before that fix, any before/after difference smaller than about 10 was inside the noise.

The budget

On 1 October I set a ceiling of US$ 20 for paid generations, and every run went into a ledger: model, prediction id, cost. The rule was to stop and ask before going over. Three Seedance clips went in as a visual reference for light, haze and weathering, not as footage. They were wrong in useful ways: Mount Fuji, palm trees, Egyptian pyramids, gabled roofs, a single staircase. On 2 October I lifted the ceiling: “Don’t worry about cost, always about quality.”

I don’t have a clean total for this demo the way I did for Tupi. TRELLIS runs are about US$ 0.74 each. Seedance and Rodin prices weren’t exposed by the API, and the ledger says “to be confirmed on the dashboard”. I’d rather leave the number out than guess it.

What’s still weak

  • The serpent. Even after the winding fix it was still the classifier’s top item in round 12: “a striped garland” from a distance, “a pile of feathers with no body” up close.
  • The night. Moonlit roofs still read as a cloudy day to the critics.
  • Houses up close. From a distance the architecture holds. Close to the rooftops it still looks like a scale model.

When a source is ambiguous, put the ambiguity in the work: the exact night of the New Fire and the red of Huitzilopochtli’s shrine are flagged as contested in the demo’s about note, and the serpent is labelled as myth.

Open Mēxihco Tenochtitlan on a desktop with WebGPU, wait for dusk, and take the controls.