A cinematic floating miniature diorama of Middle-earth rendered with three.js WebGPU + TSL: terrain from community GIS data, atmosphere, vegetation, effects and 24 landmarks, captured deterministically for the film Journey Through Middle-Earth. Non-commercial fan project.
TypeScript
4
341 commits
updated Oct 6, 2026
A cinematic floating miniature of Middle-earth, rendered with three.js WebGPU,
and the world of the short film Journey Through Middle-Earth.
▶ Watch Journey Through Middle-Earth on YouTube
The map at golden hour: 1,600 × 960 km of Middle-earth on a floating slab, rendered in the browser (the same view as the day / night pair below).
A miniature Middle-earth that you can fly over, from the Grey Havens to Mount Doom. The terrain is built from community GIS data, 24 landmarks are modelled in code, and the sky, water, smoke and light are all rendered live: nothing is pre-rendered. It runs in the browser with three.js WebGPU, and the film was captured from it frame by frame. The look aims at the bigatures of Peter Jackson's films: a physical model under real light, never a toy.
Status: V1 is complete (v1.0.0): the locked static world and the finished film, 227.75 s at 1080p24.
The film is a west → east grand tour via Rohan, loosely on Frodo's road from the Shire to Mount Doom, with a
luminous gold route line, floating labels, title cards and an original orchestral score.
Watch it on YouTube.
The Doors of Durin · Lothlórien · The Argonath
Minas Morgul · The Black Gate · Mount Doom
The map: 11:00 and 23:00, settlement lights and Mordor's fires under the moon.
Minas Tirith: early morning, and the beacon lit on Mindolluin.
Rivendell: an October afternoon, and a full moon over the gorge.
Third-party data is baked once into a deterministic world. Landmarks only declare what they need, and a
small set of shared systems realizes it. A timeline turns time into a complete SceneState, and the
renderer turns that state into pixels: in the interactive explorer, or accumulated offline for stills and
film frames.
flowchart TB
S["ME-DEM · ME-GIS · Arda<br/>(fetched, never committed)"] -->|"pnpm bake (Python)"| B["baked world<br/>heights · water · landcover"]
A["data/world<br/>places · regions · looks"] --> W
B --> W["World + HeightField"]
T["Timeline.evaluate(t)<br/>→ SceneState"] --> Y
L["24 landmark declarations<br/>stamps · kit · lights · emitters"] --> W
W --> Y["systems<br/>terrain · water · vegetation · environment<br/>landmarks · emission · effects"]
Y --> R["WebGPU / TSL render × spp<br/>DOF · grade · grain"]
R --> E["explorer"]
R --> C["readback → stills · QA · film frames"]
The contracts are in docs/ARCHITECTURE.md. The roadmap and the current state are
in docs/PROJECT_STATE.md.
| Path | What lives there |
|---|---|
src/ | the renderer: core / timeline, world services, terrain, water, vegetation, environment, landmarks, emission, effects, materials, diorama slab, camera, render (post, capture) |
tools/ | the Python bake, capture / QA / review, the film renderer (pnpm film), the score (tools/music, Python), checks and the camera probe, Blender model scripts |
data/world, data/tour, data/qa | authored world data, the film's route, shot list and picture-locked timeline, QA shots / sets / locks |
docs/ | architecture, project state, early research briefs, README images |
You need Google Chrome with hardware WebGPU (the capture tools drive the installed Chrome; other WebGPU
browsers are untested for the explorer), Node 24 (≥ 22.12), pnpm 11 (corepack enable picks the
pinned version) and uv for the Python bake and the score. Allow about 2.5 GB
of disk for the data, the bake and its Python environment. The film also needs an
FFmpeg 8 full build (see The film). Blender 4.5 is only needed to rebuild
the Argonath model, which is committed (pnpm models; set MOME_BLENDER to its binary off Windows).
pnpm install
pnpm data:fetch # ≈ 1.2 GB of source data and CC0 textures (their own terms)
pnpm bake # builds data/baked (Python 3.12 via uv)
pnpm dev # explorer at localhost:5173 (?quality=review|final)
pnpm shots --shot minas-tirith-close --quality final --spp 12 # a still
The explorer and the captures need the fetched data and the bake; there is no prebuilt world. The explorer
has orbit controls and a panel for the QA shots and landmark bookmarks, time of day, day of the year (sun
and moon), field of view and exposure. Stills are written to renders/shots/latest/ (1920×1080 by
default). pnpm typecheck and pnpm check run without any third-party data. Without a bake, pnpm check
runs the place, landmark-definition, asset-credit, light-gate, static effects (textures, presets, declarations)
and film-structure checks, and skips the baked-world gates (rivers, stamps, hydro), the landmark build and
bookmark framing gates, the effects' world placement, particle and budget checks and the compiled-film gates.
The project was developed on a Windows 11 laptop with 8 GB of RAM and an Intel UHD iGPU, and the real-time
preview tier is tuned for that machine. The capture, bake and QA tooling (GPU job lock, memory guard) is
written for and tested on Windows. The bake and captures first wait for free RAM (2.5 / 1.8 GB); if they
wait in vain (e.g. on macOS, where Node under-reports free memory), set MOME_MEM_GUARD=0.
Journey Through Middle-Earth runs 227.75 s (5,466 frames at 24 fps, 1920×1080). It is rendered from the
picture-locked timeline (data/tour/timeline.json, film hash 1c3edf71) at the final tier: 12 samples per
pixel, up to 48 on fast moves for the motion blur of a 180° shutter, subtle depth of field on the four close
hero holds (Hobbiton, Rivendell, the Argonath, Mount Doom) and film grain. The finished film is
on YouTube. Its files live in the gitignored final/ folder:
journey-through-middle-earth-1080p.mp4 (H.264, AAC 320k), the master
journey-through-middle-earth-1080p-master.mov (ProRes 422 HQ 10-bit, 24-bit PCM) and ABOUT.txt
(provenance); neither they nor the score are distributed with this repository.
To reproduce them (with the data fetched and baked: the cue export compiles the film on the baked world), render
the score first (uv; the fetch puts the CC0 samples and the sfizz renderer, ≈ 1.5 GB, in data/music-src/):
uv run --project tools/music python tools/music/fetch.py
node --import tsx tools/check/film.ts --cues renders/music/cues-v2.json # the cue sheet of the locked picture
pnpm music --cues renders/music/cues-v2.json --label v4-lock # → renders/music/<stamp>-v4-lock/master.wav
Then render the picture in its own PowerShell window. tools/capture/film-overnight.ps1 runs
pnpm film --render with no time budget, resumes after a failure and holds off idle sleep; -Chrome sets
MOME_CHROME to a private copy of Chrome 154.0.8037.95, the build the world was locked on, so an auto-update
cannot change pixels mid-render. A run refuses another Chrome, another film hash, a dirty tree or a commit
that changes the render inputs.
Start-Process pwsh -ArgumentList '-NoExit','-File','tools\capture\film-overnight.ps1','-Run','renders\film\<dir>','-Chrome',"$env:LOCALAPPDATA\map-of-middle-earth\chrome-154.0.8037.95\chrome.exe",'-FilmArgs:"--tier final --spp 12 --spp-ladder 36:16,48:24,72:32,96:48 --shutter 0.5 --label final-1080p"'
$env:MOME_CHROME = "$env:LOCALAPPDATA\map-of-middle-earth\chrome-154.0.8037.95\chrome.exe"
pnpm film --verify-resume --run renders\film\<dir> # re-renders sample frames: same sha256?
pnpm film --assemble --run renders\film\<dir> --audio renders\music\<stamp>-v4-lock\master.wav --mp4 <abs>.mp4 --master <abs>.mov
This needs FFmpeg 8.x (full build) on PATH, or MOME_FFMPEG / MOME_FFPROBE, and about 11 GB of free disk
for what stays (≈ 5 GB of ProRes chunks, ≈ 5 GB for the master, ≈ 0.5 GB for the mp4), plus a temporary concat
copy of the chunks while assembling (--assemble first checks for 2.2 × the chunks' size + 1 GB free). The
final render took 11.1 h on the dev laptop (one unattended overnight run, 2026-10-05 → 06).
| Session | Status | |
|---|---|---|
| 1 | Foundation and geography: world data, systems, capture and QA | ✅ |
| 2 | World look: bake v2, atmosphere, regional grades, terrain and vegetation | ✅ |
| 3 | All 24 landmarks: kit, stamps, emission, Blender pipeline | ✅ |
| 4 | Look and still effects: sky, night, water, effects, lens and grade, hero polish | ✅ |
| 4.5 | Static world locked (v0.5.0), README, repository polish | ✅ |
| 5 | The journey: camera rig and timeline, gold route line, titles, picture lock, original score, 720p review cut | ✅ |
| 6 | Final render (1080p24, final tier), V1 release (v1.0.0) | ✅ |
Deferred work and polish candidates are listed under "V2 candidates" in docs/PROJECT_STATE.md.
The geography comes from the ME-DEM project of the Outerra Worlds Forum (elevation model by monks and
Redrobes, place names by monks, SeerBlue and Redrobes, later maintained by jvangeld), the ME-GIS vector
layers created for ME-DEM by monks and SeerBlue and improved by jvangeld
(andrewheiss/ME-GIS), and the Arda packaging by bburns, with
the GeoPackage conversion by tetrakai1 (bburns/Arda). The data authors
are being asked for their blessing for this use. Terrain detail textures are from
Poly Haven (CC0). The fonts are Cinzel, Cinzel Decorative, Cormorant Garamond,
and EB Garamond (SIL OFL 1.1). The score is played by the
VSCO 2 Community Edition samples of Versilian Studios (CC0) through
sfizz (BSD-2-Clause), both used at render time only and never
redistributed. The project is built with three.js, Vite,
TypeScript, Playwright, sharp and
Blender, and the film is encoded with FFmpeg (LGPL-2.1+ / GPL
depending on the build; called as an external program, not shipped). Every asset and source is listed in
CREDITS.md.
public/models): MIT.OFL.txt sits in public/fonts/).pnpm data:fetch downloads it from the
authors' repositories, and their terms apply: ME-GIS asks users to request permission before use, and the
licence of the Arda DEM and vectors is uncertain.docs/images/) show terrain derived from that data. They are not covered by the MIT licence
and no reuse licence is granted; they are here to illustrate the project.A non-commercial fan project. It is not affiliated with or endorsed by the Tolkien Estate, Middle-earth
Enterprises, HarperCollins, New Line Cinema, Warner Bros., Wētā Workshop, or any data author, font designer
or artist credited in CREDITS.md. The Lord of the Rings, The Hobbit and Middle-earth are
trademarks of their respective owners. The project contains no film footage, artwork, logos or typography
from the films, no Tengwar or Cirth, and its music is original.
A cinematic floating miniature diorama of Middle-earth rendered with three.js WebGPU + TSL: terrain from community GIS data, atmosphere, vegetation, effects and 24 landmarks, captured deterministically for the film Journey Through Middle-Earth. Non-commercial fan project.
TypeScript
4
341 commits
updated Oct 6, 2026
A cinematic floating miniature of Middle-earth, rendered with three.js WebGPU,
and the world of the short film Journey Through Middle-Earth.
▶ Watch Journey Through Middle-Earth on YouTube
The map at golden hour: 1,600 × 960 km of Middle-earth on a floating slab, rendered in the browser (the same view as the day / night pair below).
A miniature Middle-earth that you can fly over, from the Grey Havens to Mount Doom. The terrain is built from community GIS data, 24 landmarks are modelled in code, and the sky, water, smoke and light are all rendered live: nothing is pre-rendered. It runs in the browser with three.js WebGPU, and the film was captured from it frame by frame. The look aims at the bigatures of Peter Jackson's films: a physical model under real light, never a toy.
Status: V1 is complete (v1.0.0): the locked static world and the finished film, 227.75 s at 1080p24.
The film is a west → east grand tour via Rohan, loosely on Frodo's road from the Shire to Mount Doom, with a
luminous gold route line, floating labels, title cards and an original orchestral score.
Watch it on YouTube.
The Doors of Durin · Lothlórien · The Argonath
Minas Morgul · The Black Gate · Mount Doom
The map: 11:00 and 23:00, settlement lights and Mordor's fires under the moon.
Minas Tirith: early morning, and the beacon lit on Mindolluin.
Rivendell: an October afternoon, and a full moon over the gorge.
Third-party data is baked once into a deterministic world. Landmarks only declare what they need, and a
small set of shared systems realizes it. A timeline turns time into a complete SceneState, and the
renderer turns that state into pixels: in the interactive explorer, or accumulated offline for stills and
film frames.
flowchart TB
S["ME-DEM · ME-GIS · Arda<br/>(fetched, never committed)"] -->|"pnpm bake (Python)"| B["baked world<br/>heights · water · landcover"]
A["data/world<br/>places · regions · looks"] --> W
B --> W["World + HeightField"]
T["Timeline.evaluate(t)<br/>→ SceneState"] --> Y
L["24 landmark declarations<br/>stamps · kit · lights · emitters"] --> W
W --> Y["systems<br/>terrain · water · vegetation · environment<br/>landmarks · emission · effects"]
Y --> R["WebGPU / TSL render × spp<br/>DOF · grade · grain"]
R --> E["explorer"]
R --> C["readback → stills · QA · film frames"]
The contracts are in docs/ARCHITECTURE.md. The roadmap and the current state are
in docs/PROJECT_STATE.md.
| Path | What lives there |
|---|---|
src/ | the renderer: core / timeline, world services, terrain, water, vegetation, environment, landmarks, emission, effects, materials, diorama slab, camera, render (post, capture) |
tools/ | the Python bake, capture / QA / review, the film renderer (pnpm film), the score (tools/music, Python), checks and the camera probe, Blender model scripts |
data/world, data/tour, data/qa | authored world data, the film's route, shot list and picture-locked timeline, QA shots / sets / locks |
docs/ | architecture, project state, early research briefs, README images |
You need Google Chrome with hardware WebGPU (the capture tools drive the installed Chrome; other WebGPU
browsers are untested for the explorer), Node 24 (≥ 22.12), pnpm 11 (corepack enable picks the
pinned version) and uv for the Python bake and the score. Allow about 2.5 GB
of disk for the data, the bake and its Python environment. The film also needs an
FFmpeg 8 full build (see The film). Blender 4.5 is only needed to rebuild
the Argonath model, which is committed (pnpm models; set MOME_BLENDER to its binary off Windows).
pnpm install
pnpm data:fetch # ≈ 1.2 GB of source data and CC0 textures (their own terms)
pnpm bake # builds data/baked (Python 3.12 via uv)
pnpm dev # explorer at localhost:5173 (?quality=review|final)
pnpm shots --shot minas-tirith-close --quality final --spp 12 # a still
The explorer and the captures need the fetched data and the bake; there is no prebuilt world. The explorer
has orbit controls and a panel for the QA shots and landmark bookmarks, time of day, day of the year (sun
and moon), field of view and exposure. Stills are written to renders/shots/latest/ (1920×1080 by
default). pnpm typecheck and pnpm check run without any third-party data. Without a bake, pnpm check
runs the place, landmark-definition, asset-credit, light-gate, static effects (textures, presets, declarations)
and film-structure checks, and skips the baked-world gates (rivers, stamps, hydro), the landmark build and
bookmark framing gates, the effects' world placement, particle and budget checks and the compiled-film gates.
The project was developed on a Windows 11 laptop with 8 GB of RAM and an Intel UHD iGPU, and the real-time
preview tier is tuned for that machine. The capture, bake and QA tooling (GPU job lock, memory guard) is
written for and tested on Windows. The bake and captures first wait for free RAM (2.5 / 1.8 GB); if they
wait in vain (e.g. on macOS, where Node under-reports free memory), set MOME_MEM_GUARD=0.
Journey Through Middle-Earth runs 227.75 s (5,466 frames at 24 fps, 1920×1080). It is rendered from the
picture-locked timeline (data/tour/timeline.json, film hash 1c3edf71) at the final tier: 12 samples per
pixel, up to 48 on fast moves for the motion blur of a 180° shutter, subtle depth of field on the four close
hero holds (Hobbiton, Rivendell, the Argonath, Mount Doom) and film grain. The finished film is
on YouTube. Its files live in the gitignored final/ folder:
journey-through-middle-earth-1080p.mp4 (H.264, AAC 320k), the master
journey-through-middle-earth-1080p-master.mov (ProRes 422 HQ 10-bit, 24-bit PCM) and ABOUT.txt
(provenance); neither they nor the score are distributed with this repository.
To reproduce them (with the data fetched and baked: the cue export compiles the film on the baked world), render
the score first (uv; the fetch puts the CC0 samples and the sfizz renderer, ≈ 1.5 GB, in data/music-src/):
uv run --project tools/music python tools/music/fetch.py
node --import tsx tools/check/film.ts --cues renders/music/cues-v2.json # the cue sheet of the locked picture
pnpm music --cues renders/music/cues-v2.json --label v4-lock # → renders/music/<stamp>-v4-lock/master.wav
Then render the picture in its own PowerShell window. tools/capture/film-overnight.ps1 runs
pnpm film --render with no time budget, resumes after a failure and holds off idle sleep; -Chrome sets
MOME_CHROME to a private copy of Chrome 154.0.8037.95, the build the world was locked on, so an auto-update
cannot change pixels mid-render. A run refuses another Chrome, another film hash, a dirty tree or a commit
that changes the render inputs.
Start-Process pwsh -ArgumentList '-NoExit','-File','tools\capture\film-overnight.ps1','-Run','renders\film\<dir>','-Chrome',"$env:LOCALAPPDATA\map-of-middle-earth\chrome-154.0.8037.95\chrome.exe",'-FilmArgs:"--tier final --spp 12 --spp-ladder 36:16,48:24,72:32,96:48 --shutter 0.5 --label final-1080p"'
$env:MOME_CHROME = "$env:LOCALAPPDATA\map-of-middle-earth\chrome-154.0.8037.95\chrome.exe"
pnpm film --verify-resume --run renders\film\<dir> # re-renders sample frames: same sha256?
pnpm film --assemble --run renders\film\<dir> --audio renders\music\<stamp>-v4-lock\master.wav --mp4 <abs>.mp4 --master <abs>.mov
This needs FFmpeg 8.x (full build) on PATH, or MOME_FFMPEG / MOME_FFPROBE, and about 11 GB of free disk
for what stays (≈ 5 GB of ProRes chunks, ≈ 5 GB for the master, ≈ 0.5 GB for the mp4), plus a temporary concat
copy of the chunks while assembling (--assemble first checks for 2.2 × the chunks' size + 1 GB free). The
final render took 11.1 h on the dev laptop (one unattended overnight run, 2026-10-05 → 06).
| Session | Status | |
|---|---|---|
| 1 | Foundation and geography: world data, systems, capture and QA | ✅ |
| 2 | World look: bake v2, atmosphere, regional grades, terrain and vegetation | ✅ |
| 3 | All 24 landmarks: kit, stamps, emission, Blender pipeline | ✅ |
| 4 | Look and still effects: sky, night, water, effects, lens and grade, hero polish | ✅ |
| 4.5 | Static world locked (v0.5.0), README, repository polish | ✅ |
| 5 | The journey: camera rig and timeline, gold route line, titles, picture lock, original score, 720p review cut | ✅ |
| 6 | Final render (1080p24, final tier), V1 release (v1.0.0) | ✅ |
Deferred work and polish candidates are listed under "V2 candidates" in docs/PROJECT_STATE.md.
The geography comes from the ME-DEM project of the Outerra Worlds Forum (elevation model by monks and
Redrobes, place names by monks, SeerBlue and Redrobes, later maintained by jvangeld), the ME-GIS vector
layers created for ME-DEM by monks and SeerBlue and improved by jvangeld
(andrewheiss/ME-GIS), and the Arda packaging by bburns, with
the GeoPackage conversion by tetrakai1 (bburns/Arda). The data authors
are being asked for their blessing for this use. Terrain detail textures are from
Poly Haven (CC0). The fonts are Cinzel, Cinzel Decorative, Cormorant Garamond,
and EB Garamond (SIL OFL 1.1). The score is played by the
VSCO 2 Community Edition samples of Versilian Studios (CC0) through
sfizz (BSD-2-Clause), both used at render time only and never
redistributed. The project is built with three.js, Vite,
TypeScript, Playwright, sharp and
Blender, and the film is encoded with FFmpeg (LGPL-2.1+ / GPL
depending on the build; called as an external program, not shipped). Every asset and source is listed in
CREDITS.md.
public/models): MIT.OFL.txt sits in public/fonts/).pnpm data:fetch downloads it from the
authors' repositories, and their terms apply: ME-GIS asks users to request permission before use, and the
licence of the Arda DEM and vectors is uncertain.docs/images/) show terrain derived from that data. They are not covered by the MIT licence
and no reuse licence is granted; they are here to illustrate the project.A non-commercial fan project. It is not affiliated with or endorsed by the Tolkien Estate, Middle-earth
Enterprises, HarperCollins, New Line Cinema, Warner Bros., Wētā Workshop, or any data author, font designer
or artist credited in CREDITS.md. The Lord of the Rings, The Hobbit and Middle-earth are
trademarks of their respective owners. The project contains no film footage, artwork, logos or typography
from the films, no Tengwar or Cirth, and its music is original.