# THEATRON — asset provenance

Everything below was downloaded on 9 Oct 2026 and processed for this concept. Nothing is hotlinked, nothing is
AI-generated, and there are no people in any image. The theatre itself is procedural geometry written for this page
(`js/theatron.js`): every seat block, stair step, slab, curb stone, wall course and chair is modelled to size in code.
It borrows the rhythms of a Greek theatre (orchestra, drainage channel, cavea, radial stairs, walkway, stage
foundations) and reproduces no real building.

## Code

| File | Source | Licence | Notes |
|---|---|---|---|
| `vendor/three-0.185.1/three.core.min.js`, `three.module.min.js`, `LICENSE` | npm `three@0.185.1`, `build/` (copied from the Marbre concept's vendored copy) | MIT | Pinned. Unmodified. |
| `vendor/three-0.185.1/addons/loaders/HDRLoader.js` | same package, `examples/jsm/loaders/` | MIT | Unmodified. |
| `js/theatron.js` | written for this concept | — | Geometry, materials, PCSS soft shadows (after Marbre's `installPCSS`), print grade, camera paths (the descent and, in v3, the climb), the ensemble's shadow map and the close-range weathering. |
| `js/ensembles.js` | written for this concept (v3 fix pass) | — | Standalone: the four seating plans and the chair, stool, stand, score and podium geometry (with shadow proxies). Shared with the Approach (`js/ensembles.READY` marks it final). |

## Textures and light

| File | Source | Author | Licence | What was done |
|---|---|---|---|---|
| `tex/stone-albedo-1k.webp` | ambientCG **Concrete012** (https://ambientcg.com/view?id=Concrete012) and **Rock004** (https://ambientcg.com/view?id=Rock004), 2K-JPG colour maps | ambientCG (Lennart Demes) | CC0 1.0 | Both converted to linear luminance and normalised to their means; mixed as 1 + 0.80·(Concrete012 − 1) + 0.22·(Rock004 − 1) so the pitted, rain-streaked limestone of Concrete012 leads and Rock004 adds grain; clamped, stored with mean 0.5 (the shader doubles it). Greyscale, 1024², Lanczos, WebP q82. |
| `tex/stone-normal-1k.webp` | same two sets, `NormalGL` maps | ambientCG | CC0 1.0 | Whiteout-style blend (Concrete012 at 1.0, Rock004 at 0.45), renormalised, 1024², WebP q88. Applied at 0.34 on vertical faces and 0.6 on treads (0.32 on the orchestra floor). |
| `tex/sky-hdr.js` | Poly Haven **Syferfontein 18d Clear (Pure Sky)**, 1K HDR (https://polyhaven.com/a/syferfontein_18d_clear_puresky) | Greg Zaal (original), Jarod Guest (sky edits) | CC0 1.0 | Converted to luminance (no colour can reach the stone); the sun disc removed (r < 10 px blended into its surrounding ring, highlights capped at 4.0) because the scene's sun is a light; lower hemisphere replaced by a quiet uniform ground (0.32); 1024×512 → 256×128 by area; Radiance RGBE, shipped as base64 in a JS module because the host does not serve `.hdr`. Used only as the sky fill, turned so its bright side sits with the scene's sun. Its measured sun irradiance (13.4 in its own units) sets the scene's sun intensity, so sun and sky stay in proportion. |

Light: one sun at azimuth 60° west of north, elevation 25°, never moving (its shadow map is drawn once); the sky
fill above; and a shadowless bounce from the opposite azimuth at 14°, standing in for the sunlit half of the limestone
bowl lighting the shaded half (without it the shaded rows went to flat black).

Generated at runtime (a few kB of code, no files): per-stone tone and settlement, joint arrises and hairlines
(shader), the sky gradient. (v1's procedural maquis on the hillside is gone: v2 replaces it with the
photographed ground and the pines above.) The print grain and fall-off are
an inline SVG `feTurbulence` and a radial gradient in CSS, laid once over both the still and the live canvas.

## v2 (10 Oct 2026): the ground, the excavation, the pines, the Nocturne

The ground around the theatre is now photographed earth, not procedural noise. All of it is generated or processed
offline by scripts in `tools/` (not published), so it can be re-made exactly.

| File | Source | Author | Licence | What was done |
|---|---|---|---|---|
| `tex/ground-grass-a.webp`, `tex/ground-grass-s.webp` | Poly Haven **Aerial Grass Rock** (https://polyhaven.com/a/aerial_grass_rock), 2K diffuse, displacement, normal, AO; real size 15 m | Rob Tuytel | CC0 1.0 | `tools/bake_ground.py`: albedo = luminance with AO folded in at half strength, normalised to a dry-grass albedo (0.23); **s** = the micro-relief lit by this scene's fixed sun (normal-map N·L over flat, times a soft horizon test through the height map, scaled from the normal map), so tufts and pebbles throw their own small shadows. Albedo 1024² WebP q72, sun term 768² q58. |
| `tex/ground-rocks-a.webp`, `tex/ground-rocks-s.webp` | Poly Haven **Aerial Rocks 02** (https://polyhaven.com/a/aerial_rocks_02); real size 50 m | Rob Tuytel | CC0 1.0 | As above (albedo mean 0.22): the rocky scrub of the hillside and under the pines. 768² WebP. |
| `tex/ground-earth.webp` | Poly Haven **Aerial Ground Rock** (https://polyhaven.com/a/aerial_ground_rock); real size 20 m | Rob Tuytel | CC0 1.0 | Trodden earth with twigs and pebbles (the stage front, the rooms, path verges); its gentle relief folded into the albedo. 1024² WebP q66. |
| `tex/ground-dirt.webp` | Poly Haven **Dirt Aerial 03** (https://polyhaven.com/a/dirt_aerial_03); real size 25 m | Rob Tuytel | CC0 1.0 | Compacted dirt for the paths, relief folded in; mixed in the shader with the trodden earth so the path has pebbles pressed into it. 1024² WebP q70. |
| `tex/site-near.webp`, `tex/site-far.webp` | generated: `tools/site_plan.py` | — | — | The site plan's masks, lossless RGBA: R path, G trodden earth, B pine litter under each crown, A the trees' shadows baked for this sun (used only beyond the live shadow map). Near: 96 × 60 m at 6.25 cm/px; far: 320 m at 63 cm/px. Shapes are designed (paths, the excavation, each crown), never noise. |
| `js/site-plan.js` | generated: `tools/site_plan.py` | — | — | The plan as data: the stage-building walls, stylobate bases, thresholds, fallen pieces, 374 pines, 331 shrubs and 418 low weed clumps hugging the foot of the excavated walls. Trees stand where some frame of the descent sees them, never where their shadow would fall on the seats or the orchestra, and never where they would show above the rim once the sky band opens (p ≥ 0.62). |
| `tex/tuft-atlas.webp` | generated: `tools/make_tufts.py` | — | — | Pine foliage cards drawn needle by needle (~1 mm per pixel; four variants: needle masses and bottle-brush shoots), luminance + alpha, 512² WebP. Seen from 100 m, a crown is built from thousands of these on lobes at the branch ends (`js/flora.js`). (Poly Haven pine_tree_01's twig cards were tried first and rejected: from above they read as oak leaves.) |
| `js/flora.js` | written for this concept | — | — | Pines (two levels of detail) and shrubs, instanced; volumetric normals; alpha-tested so the shadow map sees every gap. |

The stage building's excavation (walls in courses, double-faced with a rubble core where the top course is gone,
stylobate and bases, thresholds, fallen drums and blocks in rows) is procedural geometry in `js/theatron.js`, built from
the plan, with the theatre's own stone textures (more pitted on the foundations).

**References for the realism check** (Wikimedia Commons, kept in `tools/refs/`, never published): *DJI 0203/0206/0215
Güllük Mountain National Park* (Termessos, drone), *Teatri i Butrintit ne Shqiperi* (Butrint, straight down),
*Eretria Theatre3* (aerial), *Theatre of Epidavros - 2 (from the top)*, *Epidaure theatre paysage 2009*, *Panorama
Epidaurus*, *Epidauros BW 2017-10-10 16-09-57*, *Theatre in Heraclea Lyncestis, Bitola*.

## Fonts (concept only)

| File | Family | Licence | Production face to license |
|---|---|---|---|
| `fonts/archivo-latin.woff2`, `fonts/archivo-latin-ext.woff2` | Archivo v25 (Google Fonts; variable: wdth 62–125, wght 100–900), Omnibus-Type | SIL OFL 1.1 (`fonts/OFL-Archivo.txt`) | GT America Extended / Standard (Grilli Type) |
| `fonts/newsreader-italic-latin.woff2`, `fonts/newsreader-italic-latin-ext.woff2` | Newsreader Italic v26 (Google Fonts; variable opsz, wght), Production Type | SIL OFL 1.1 (`fonts/OFL-Newsreader.txt`) | GT Sectra Fine Italic (Grilli Type) |

The latin-ext files load only for characters such as the ē in *orkhēstra* and *mousikē* (unicode-range).

## The stills (first frame = fallback)

All rendered headless from this scene (Playwright Chromium, SwiftShader), 2× supersampled without MSAA, downsampled
with Lanczos 3 and encoded with sharp (AVIF q56, WebP q82, mozjpeg q82):

| Files | What | Used for |
|---|---|---|
| `img/theatron-p0-wide-{2560,1280}.{avif,webp,jpg}`, `img/theatron-p0-tall-{1170,780}.{avif,webp,jpg}` | the descent at p = 0 (high, narrow, near top-down), desktop and phone reference frames | first paint, no-WebGL, reduced motion |
| `img/theatron-p1-wide-{2560,1280}.{avif,jpg}`, `img/theatron-p1-tall-{1170,780}.{avif,jpg}` | the hold at p = 1 (eye height, centre of the orchestra) | reduced motion (second image), no-WebGL dissolve |
| `img/theatron-seat-{trio,quartet,chamber,full}-wide-{2048,1280}.{avif,jpg}`, `-tall-{1170,780}.{avif,jpg}` | v3: the seat view at the top of the stands (p = 2) with each ensemble set out, desktop and phone reference frames, rendered at 1x with 4x MSAA | reduced motion (the picker swaps them), no-WebGL (the climb dissolves to them) |
| `img/rows-{2360,1200}.{avif,jpg}` | a long-lens view across the west rows in raking light | the print in "Where" |

The live camera re-maps its reference projection exactly the way CSS `object-fit: cover` crops each still, so the
still and the first live frame put every stone on the same pixel at any window size.

## Byte budget (measured with `tools/budget.js`, Playwright, 1440×900, files as served without compression)

| | requests | bytes |
|---|---|---|
| v1 first view (page, fonts, first-frame still, three.js, scene module, textures, sky, the first plate still) | 13 | 2.38 MB |
| **v2 first view, desktop** (adds the ground scans, site masks, needle atlas, site plan, flora module) | 24 | **3.76 MB** uncompressed; the ~1 MB of JS and HTML gzips to ~0.28 MB, so about **3.0 MB** on a host that compresses |
| v2 first view, phone (390×844, 2×) | 24 | 3.55 MB |
| v2 whole page, scrolled to the end | 25 | about 3.9 MB |

Budget: first view ≤ 4 MB, everything ≤ 12 MB.

Scene size (triangles, measured): v1 0.50 M; v2 desktop 1.41 M (the theatre and excavation 0.45 M, the ground 0.22 M,
pines 0.47 M, scrub 0.13 M, weeds 0.15 M). Phones (the low tier) skip every other distant pine and the distant scrub,
none of which is in a first frame: 1.12 M.

## v3 (11 Oct 2026): three movements; the Nocturne and the plate removed

- **Removed:** the Nocturne (`?night`, the Day · Night switch, its 28 stills, its sky and grade code) and the separate
  ensembles plate (its second WebGL context and its eight `ens-*` stills). The day scene is unchanged by the removal:
  rendered through the same harness, the first frame (p = 0) and the hold (p = 1) match round five pixel for pixel
  (max difference 0/255, `tools/regress.sh`, `shots/dev6/`). The day p0/p1 stills therefore stay as rendered for v2.
- **Movement iii, the climb** (`js/theatron.js`, `CLIMB`, `climbAt`): from the hold to a seat on the top row, on the axis
  (eye at 21.36 m, 0.8 m above the seat), looking down across the rows at the orchestra. The view turns about 150
  degrees; the camera swings out over the west wing and back so the turn is spent looking across the bowl, and it ends
  in a pull back up the axis stair onto the seat. Angular speed per unit of scroll stays within 1.27x of its mean
  (`tools/climbcheck.mjs`); the page caps it live at 36 degrees and 20 m a second. Seat lens: 18 degrees (desktop), 31
  (phone), so a trio of chairs reads from 57 m.
- **The ensemble at the seat:** the plate's chairs, stands, scores, stools and podium (unchanged models and seating),
  now on the hero's orchestra floor, with its **own shadow map** fitted to the orchestra as the sun sees it (4096 x 2048
  over 21 x 10.5 m: 5 mm texels; 2048 x 1024 on phones), PCSS from the 0.53 degree sun, receiver plane from the
  geometric normal, depth-only clones with `colorWrite: false`. The theatre's own map still never redraws.
- **The site plan** (`tools/site_plan.py`, regenerated): the same seeded candidates, now also kept where the climb or the
  seat sees them but no descent frame does (tested against each still's full reference frame), so the pine wood behind
  the stage clearing and on the west and east hillsides exists: 729 pines (was 374), 589 shrubs (was 331), the 418 weeds
  unchanged; masks regenerated; walls, bases, sills, drums and blocks identical.
- **The far country, seen only from the climb** (`farHills` in `js/theatron.js`): a long low swell (about 30 to 70 m)
  cresting 600 to 700 m out and back down before the terrain ends at 900 m, only at azimuths 70 degrees or more from
  the stage axis, where neither the descent nor the hold looks (the rim hides it from inside the bowl); a designed
  ridge line from a few long waves, never noise. Ground beyond the plan's far mask shades as wooded country. From the
  climb on (p > 1), haze beyond about 250 m lightens toward the horizon sky's own value (`uHazeFar`), so the far
  country meets the sky softly instead of at a hard CG horizon; nearer haze is unchanged, and at p <= 1 so is all of
  it.
- **Weight and load.** First view, desktop: about 3.5 MB as files, about 2.8 MB with the text gzipped (round five
  3.5 / 2.8: the plate's still is gone, the larger plan and masks add about 0.1 MB); phone about 3.7 / 3.0 MB. The seat
  stills load only in reduced motion or without WebGL. The plan flags what only the climb sees (355 pines, 258 shrubs:
  trees' 8th field, shrubs' 9th); it is instanced apart and drawn only from the climb on (and into the once-drawn
  shadow map), so the descent draws what round five drew (1.36 M triangles at the hold, measured with
  `renderer.info`); the seat view draws 2.25 M.
- **Verification** (round six): `tools/regress.sh`, `tools/climbcheck.mjs`, `tools/quick.mjs` (strips through the page's
  `__renderAt`), `tools/verify.js` (desk, phone, w320, diff, reduced, focus), `tools/navfocus.mjs` (keyboard path to the
  seat), `tools/compare6.py` (before/after pairs and real-vs-ours). Round five's shots are kept in `shots/r5/`.

## v3 fix pass (11 Oct 2026, after review)

- **Order, not a heap.** The plans moved to `js/ensembles.js` and were re-spaced for the view from the top row (about
  22 degrees down, where 1.2 m between arcs closed to a sliver and each row's chair backs and desks ran into the next):
  five arcs concentric round the conductor at the floor's centre, 1.65 m apart: three string arcs (4, 6 and 8 desks of
  two), a fourth shared by the horns on the conductor's left, the woodwinds behind the centre and the basses (stools) on
  his right, and a short fifth of woodwinds; 52 players, about 13 m across (65% of the floor). Chamber orchestra: two
  string arcs and a bass, 17. Trio and quartet unchanged in kind (3 and 4 stands). Desks are matte (a satin desk back
  caught the low sun and read as a white card); the parts on them are a step down from white.
- **Continuous thin shadows.** A chair leg's shadow is about a pixel wide from the seat, and point-sampled it came out
  dashed. The ensemble's PCSS now never filters below the pixel's own footprint in the map (`fwidth` of the map
  coordinates), and the shadow map sees each piece through a proxy with its rods 1.6x thicker (`colorWrite: false`).
- **The desktop seat** is a little wider (20 degrees) and lower: the orchestra 35% down, a strip of the top rows and the
  axis stair above the band; the full orchestra still spans about 42% of the width. Phones: 33 degrees, the orchestra
  33% down, the band under 30% of the screen (Inquire inline on the eyebrow's line, the ensembles on two short lines).
- **Stone seen close** (from the climb on, within about 30 m of the lens, so the first frame and the hold are untouched):
  crustose lichen in colonies (spots skewed small, ragged, merging where dense), dusty joints, chipped arrises. The
  pines were not changed.
