DramaticShapeVoxelMod archival mirror · read-only
39e855da34e26ce55d45a4537d82e8d9082a90c1

update readme, mod card

DramaticShape · 2026-07-26 · parent 7046ead648

update readme, mod card

Changed files

.modkitignore |  30 +++
 CHANGELOG.md  | 830 ++++++++++++++++++++++++++++++++++++++----------------------------------------
 README.md     | 155 ++-------------
 mod.card      |   8 +-
 4 files changed, 447 insertions(+), 576 deletions(-)

Diff

diff --git a/.modkitignore b/.modkitignore
new file mode 100644
index 0000000..c40bb83
--- /dev/null
+++ b/.modkitignore
@@ -0,0 +1,30 @@
+# Files that stay in the repo and out of the distributed package.
+# Exact relative paths, one per line -- modkit matches literally, no globs.
+
+# The SDK suite and the probes it grew out of. A shipped test that requires
+# an engine module reads as a private require against the archive
+# (CONTRIBUTING-mods.md "What the PR must contain", 2).
+tests/dramatic_shape_test.lua
+tests/voxel_anim_probe.lua
+tests/voxel_door_probe.lua
+tests/voxel_mem_probe.lua
+tests/voxel_perf_probe.lua
+tests/voxel_pipeline_probe.lua
+tests/voxel_seam_shots.lua
+tests/voxel_shadow_probe.lua
+tests/voxel_survey.lua
+tests/voxel_void_probe.lua
+
+# Authoring tools. These run by hand on a machine that has imported its own
+# ROM, read the local cache, and emit assets/voxels/*.lua; the carved models
+# are what a player installs, the carving is not.
+tools/build_voxels.py
+tools/building_images.py
+tools/building_voxels.py
+tools/voxel-survey.md
+assets/docs/buidling_to_voxel/sprite_extract.py
+assets/docs/buidling_to_voxel/voxel_build_verify.py
+
+# assets/docs/**.md stays in: data/voxel_heights.lua points a modder at the
+# building catalogue, and the methodology is where the geometry's provenance
+# is written down.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2b074e..186eede 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,22 @@
 # Changelog
 
-## Unreleased
+## 1.0.0
+
+First release, ported from the engine-internal voxel branch onto the
+`render_pipelines` mod API.
+
+Interior furniture gets the shapes it depicts
+(mods/DRAMATIC_SHAPE/tools/voxel-survey.md is the procedure that found and
+verified these).
+
+Buildings stop being boxes wearing their own elevation. A profiled
+building is voxelized from its own sprite, band by band -- the pipeline
+written up in `assets/docs/buidling_to_voxel/`.
+
+Terrain meshing goes asynchronous, instanced and bounded. The first voxel
+frame used to build every neighbourhood map synchronously (a ~2.4s
+freeze), retain every map's analysis forever (gigabytes over a
+cross-region trek), and string stray pixels along map seams.
 
 Real shadows. The sun moves to the southeast and drops to 45 degrees, so
 shadows fall northwest -- up and to the left on screen -- and run about as
@@ -8,6 +24,136 @@ long as the thing throwing them is tall.
 
 ### Added
 
+- `voxel` render pipeline: 3D diorama overworld with extruded terrain,
+  depth-buffered occlusion, leaning sprite billboards, drop shadows and
+  contact AO. VOXEL options row and hotkey `3` (OFF / 15 / 35 / 50 / 75).
+- `tiltshift` render pipeline: a `worldPresent` post-process giving the
+  miniature-photo look. T-SHIFT options row and hotkey `6` (OFF / 1 / 2 / 3).
+- Carved voxel models for 67 overworld sprites, plus the
+  `tools/build_voxels.py` that produced them.
+- `data/voxel_heights.lua`, the hand-authored tile shape profile.
+
+- Furniture shape classes in the profile: `bed` (a low slab wearing its
+  top-down art), `table` and `desk` (boxes at their drawn height whose
+  faces fold the artwork up), `relief` (a prop drawn from above -- a
+  game console -- lying flat and extruding a few voxels inside its
+  outline), the standee pools `billboard` (10px) / `prop` (5px) /
+  `stool` (5px, and characters standing on its walkable cell sit at
+  seat height) / `cutout` (one voxel: pure profile, for the vase on the
+  table), and the stair archetypes `stair_e`/`stair_w` (a rising flight
+  of real steps) and `stair_down_e`/`stair_down_w` (a sunken stairwell
+  descending below the floor -- stairs that lead down).  Separate pools
+  cluster separately, so touching drawings never merge into one cutout.
+- Standee clusters split into per-pixel connected components, each
+  standing on its own feet in the depth band of the row it is drawn in:
+  two stools stacked in adjacent cells become two stools, and no
+  fragment of a drawing ever floats at its bounding-box height.  A
+  `cutout` keeps only its largest component -- a cast shadow's drawn
+  edge is background, not a floating scrap.
+- `bookcase` class: free-standing shelf drawings collapse in ranks onto
+  one-cell-deep boxes at their full drawn height, back rows becoming
+  hidden floor; the trim row above a rank -- undetected structure or a
+  row pinned `table`, since the same trim tiles cap other furniture --
+  is adopted as its cap.  Pinned for Oak's Lab (`DOJO`).
+- Oak's Lab tables pinned `table`: the starter-ball display and the
+  north tables stand at real table height, the display frame's black
+  corner brackets no longer auto-extract into standing prisms, and the
+  Poke Ball / Pokedex sprites ride the authored height onto the
+  tabletops.
+- Pinned props drawn directly above a pinned box stand ON it: the PC
+  monitor on its desk, the flower pot on the dining table.
+- Profile pins for `REDS_HOUSE_1` / `REDS_HOUSE_2` (Red's house and the
+  Copycat's, both floors): bed, stools, tables, PC desk with its
+  standing monitor, bookcases, TV standing on the floor behind the game
+  console's relief, potted plant, flower pot, both staircases, and the
+  wall/window band.
+- `mods/DRAMATIC_SHAPE/tests/voxel_survey.lua`: screenshot-survey driver
+  behind the repeatable inspection procedure (SURVEY_MAP / SURVEY_SPOTS /
+  SURVEY_LEVELS / SHOT_DIR), documented in
+  mods/DRAMATIC_SHAPE/tools/voxel-survey.md.
+
+- `lib/Buildings.lua`: a building archetype. Where the volume path folds a
+  whole drawing upright (roof, facade and sloped ends alike) into one box,
+  this classifies each BAND of the drawing by the 3D surface it depicts and
+  applies the matching operation: top-facing rows lay flat over the
+  footprint, the facade extrudes straight back, an awning band juts past
+  the walls, and the drawn taper at the ends becomes a stepped slope in
+  elevation. Every visible voxel carries a real texel of the drawing, so
+  the model recolours with the atlas.
+- A `buildings` section in `data/voxel_heights.lua`. A building is matched
+  by its exact tile grid (the drawings are catalogued in `assets/docs/buildings/`),
+  so one entry covers every map that places the same art -- Red's house,
+  Blue's house, Bill's, the Copycat's and the two Fuchsia houses are one
+  seven-placement entry, and Oak's lab is a second. Only the band table is
+  authored; the silhouette, the taper rate, the eave height and every
+  window and doorway are measured off the pixels.
+- The flat-roofed civic block and its sixteen relatives -- every Pokemon
+  Center and every Poke Mart, Fuchsia Gym, the museum, the Game Corner,
+  Celadon Mansion and its department store, the Power Plant, the Route 5
+  and Route 22 gates, Silph Co and five anonymous scenery blocks. They are
+  one architecture drawn at eleven different footprints, from 4x4 cells up
+  to Silph Co's 8x12, and they share one band table: their lattice is drawn
+  from straight above, so the measured taper comes out flat and the whole
+  band is depth under one level roof. No new roof mode was needed -- the
+  drawn profile was always the shape, and a drawing with no taper simply
+  yields a level one.
+- Pewter's museum hall (`assets/docs/buildings/B24`). It is the one
+  sloped-roof building in this pass, and the only one so far whose roof
+  texture is not a plain repeat: the drawing states its own period by
+  repeating the whole lattice-and-course motif, rows 8..31 again at 32..55,
+  so the cycle is 24 rows and the roof carries its drawn courses across the
+  depth instead of a bare lattice. The band below them is the roof's
+  fascia, wider than the wall it covers, so it belongs to the roof and
+  lands on the south rim. Note the museum is TWO drawings: this hall and
+  the east entrance beside it, which is B18 and shares its drawing with the
+  Route 2 gate.
+- The rest of the 2:1 sloped-roof buildings: both gym drawings (the
+  standard one at Cinnabar, Pewter, Vermilion and Viridian plus the
+  Fighting Dojo, and the wider Celadon / Cerulean / Saffron one), the 4x2
+  cottage that houses Mr Fuji, the Cubone house, Bill's grandpa, the Name
+  Rater and the Viridian school, Cerulean's three wide houses, the day
+  care, and three scenery blocks -- among them B01, which at 19 placements
+  is the commonest drawing in the game. Each one's roof band is pixel for
+  pixel one of the three already authored, so they take that sibling's band
+  table unchanged: 16 rows for Red's house's, 32 for Oak's lab's, 64 for
+  the museum's.
+- The Safari Zone rest houses and the Victory Road entrance, the first
+  buildings outside the `OVERWORLD` tileset -- `buildings` is keyed by
+  tileset and until now only had the one section. The rest house's
+  corrugated roof repeats every 5 rows rather than the overworld lattice's
+  8, which is the point of `roofCycle` being authored per building.
+- Route 10's scenery block, via a new `seal` field. Its drawing has no
+  black base course -- it ends on a row of light brick -- so the silhouette
+  flood climbed in from the south border through the mortar and hollowed
+  the wall out, leaving 72% of the sprite in 65 pieces. `seal` names the
+  sides a drawing runs off rather than closing, and the flood does not seed
+  there: sealed, it is 95% in one piece, and the model is its twin the
+  museum's. No other building sets it, and none changes by a voxel.
+- Together these take the mod to 31 of the catalogue's 34 drawings and 144
+  of its 147 placements. The last three, and why each resists, are written
+  up in `assets/docs/buildings/REMAINING.md`.
+- A roof no longer runs past the drawing's own silhouette. These sprites
+  are inset from their boxes, and the columns outside the inset carry no
+  roof: they now get none, and the fascia belongs to the outermost columns
+  the drawing actually paints. Before, they raised a four-voxel slab of
+  black kerb the full depth of the building, flanking its walls at ground
+  level. Nothing shipped hit this -- Red's house and Oak's lab are drawn
+  edge to edge -- so no existing model changes by a single voxel.
+- Windows and doorways sink a voxel behind their frames, found rather than
+  listed: a pane is a non-black region the drawing seals off behind its own
+  black outline. A nested frame (the door's own little window) layers for
+  free.
+- `tools/building_voxels.py`: the reference implementation of the same
+  algorithm, with the geometric asserts and isometric previews Stage 5 of
+  the methodology calls for. It and the runtime agree exactly on voxel and
+  shell counts for all 19 templates -- 96,617 / 12,866 for Red's house up
+  to 3,715,963 / 146,762 for Silph Co, which ship as 3,410 and 13,994
+  quads. Its slope asserts read the drawn columns only and stand down for a
+  roof with no taper, where the check is that the roof is level instead;
+  its previews fit the projection to the model rather than to a fixed
+  camera, so a building taller or deeper than the first two still lands on
+  the canvas.
+
 - A sky at the 75-degree rung. Pitched that far over, the horizon comes
   into frame and a good part of the picture is void, so the void gets
   filled instead of reading as the black plate it does at every rung below.
@@ -30,12 +176,12 @@ long as the thing throwing them is tall.
 - The mod's four controls sit on adjacent keys, and the two that never had
   one now have a hotkey at all:
 
-  | key | control | was |
-  | --- | --- | --- |
-  | 3 | VOXEL, the camera ladder | 6 |
-  | 5 | V-GRID, the wireframe | menu only |
-  | 6 | T-SHIFT, the blur ladder | 9 |
-  | 7 | V-CURVE, the horizon bend | menu only |
+  | key | control |
+  | --- | --- |
+  | 3 | VOXEL, the camera ladder |
+  | 5 | V-GRID, the wireframe |
+  | 6 | T-SHIFT, the blur ladder |
+  | 7 | V-CURVE, the horizon bend |
 
   Only 6 arrives by the documented route. `Game:keypressed` answers the
   engine's own display keys first and returns -- 2 COLORS, 3 TILT, 4 ZOOM,
@@ -160,63 +306,258 @@ long as the thing throwing them is tall.
   whatever the player left in `options.lua`, and the world view size (which
   the light frustum is fitted to) swings 3x across that range.
 
-### Fixed
-
-- Cycling palette modes with voxel mode on eventually killed the pipeline
-  outright: `attempt to call field 'atlasImageData' (a nil value) --
-  disabled for this session`. Nothing brought it back short of a restart.
-
-  `TerrainAtlas` reads three engine seams to animate water and flowers in
-  the terrain texture, and this build ships only one of them
-  (`defaultAnimatedTiles`). The tile clock, `animFrame`, was already read
-  guarded and simply degrades. `atlasImageData` was called straight -- but
-  only down the branch where the mod had NOT baked the atlas itself, which
-  is why it looked stable until a palette changed. Every mode with no world
-  palette for the map (`PaletteFX.pal` answering nil), plus RED++ and any
-  trueColor tileset, takes that branch, so the first map with animated
-  tiles entered under one of them threw out of `drawWorld` and the engine
-  disabled the pass for the session, exactly as it should.
-
-  The seam is now read guarded like its sibling, and when it is absent the
-  pixels are recovered rather than given up on. An atlas neither we nor
-  RED++ replaced is the tileset art itself, so animation carries on from
-  the art on disk. RED++'s per-map bake exists only as a texture --
-  `getGbcAtlas` throws its `ImageData` away -- so that one comes back off
-  the GPU: the atlas is drawn 1:1 into a canvas and read back, once per map,
-  with the pass's own render target captured and restored around it (the
-  usual `setCanvas()` would drop the rest of the frame). A driver that
-  refuses the readback declines to animate and keeps the static atlas.
-  Worst case now costs one animation, never the pipeline.
-
-- Water and flowers did not animate in voxel mode at all, and had not since
-  the mode shipped -- a silent one, since the terrain was otherwise correct.
+- A `counter` class (8px, upright): half-cell furniture. One 8px band,
+  so exactly the drawing's bottom row stands up as the front and every
+  row above it rides the top face in drawn order. `table`'s 12px could
+  not be retuned for it; the houses share that class.
 
-  The tile clock is the third seam, and this build does not export it
-  either. Being read guarded, it answered 0 forever instead of throwing,
-  which pinned every animated tile at step 0. `animFrame` is a plain local
-  in `TileRenderer`, but an upvalue of the exported `tick()`, so the mod now
-  reads the real counter through it. That it is the ENGINE's counter is the
-  point: the flat tile layer draws from the same number, so toggling voxel
-  mode mid-cycle continues the animation rather than restarting it. A build
-  that exports `animFrame()` outright is preferred; a build that hides the
-  local falls back to wall time in 60Hz steps, which free-runs against the
-  2D path but still moves the water.
+- `tilesets.POKECENTER` in `data/voxel_heights.lua`. Before it, the
+  detector merged the wall-touching counters and healing machines into
+  the wall band and towered them 3-6 blocks, flattened the machines'
+  near-black screens to void, read the pillar bases, plant pots and
+  machine bodies as ponds (the $14/$32/$48 stale-cache water fallback),
+  boxed each plant pair into one hedge cube, and extruded the lounge
+  seat -- a PERSON is drawn into its tile art -- into a monolith wearing
+  his face.
+- The pins, by shape: the wall band, windows, poster, pillars and the
+  16px machine bodies are `wall`; the counters (with the nurse's tray)
+  are `counter` and the PC's desk is `table`; the machine screens and
+  the PC are `billboard`, standing on the pinned boxes below them; the
+  potted plants are `prop` standees like every other interior plant.
+- The lounge couch with the man sitting on it is a `counter` box: its
+  bottom row stands up as the couch's front and the cushion and the man
+  ride the top face, each drawn exactly once.
+  He cannot be stood upright, and the reason is structural rather than a
+  tuning question. His skin pixels span two tile rows and stop dead at
+  the row 9/10 seam; folding two rows upright requires both to share a
+  class, which makes the box two tiles deep, and a fully folded box
+  repeats its north row across its whole top face. So every upright
+  arrangement puts his head on screen two or three times -- as a 16px
+  seat-back, on the front and twice more on the top; as a 32px bookcase,
+  a cabinet taller than the room's own walls. Dropping the seat in front
+  of him to floor level only changes which copy you see. Nor can he be a
+  standee: the drawing has no floor margin, so all three non-black
+  shades touch the cluster rim and the mask drains 307 of its 420
+  interior pixels -- 46% of him even segmented alone, because his skin
+  is the same light shade as the couch behind him.
+- Survey evidence: full before/after passes of VIRIDIAN_POKECENTER at
+  15/35/50 degrees, plus spot-checks of CELADON_POKECENTER and
+  CELADON_HOTEL (shared tileset, both inherit correctly) and of
+  REDS_HOUSE_1F, OAKS_LAB and VIRIDIAN_CITY (unchanged -- the new class
+  is additive and no other tileset lists it).
 
-- Toggling palettes in voxel mode flashed the flat 2D world for a moment on
-  every switch.
+### Changed
 
-  `PaletteFX.setMode` reloads the live map to rebuild its atlas, and this
-  mod dropped that map's terrain mesh on any `map.reloaded` at all. Mesh
-  builds are asynchronous, so the frames between the drop and the first
-  rebuilt mesh had no terrain to draw -- and a voxel `drawWorld` with no
-  terrain returns nil, which is exactly how the pipeline asks for the 2D
-  fallback. The flash was the mod correctly reporting that it had nothing
-  to show.
+- Pinned props are segmented the way the art is authored: objects wear a
+  black outline, so background is the shades touching the cluster's edge
+  (white floor around a TV, grey tabletop around a vase) flooded in from
+  the aprons; the outline, its interior, paint whites and anything they
+  enclose survive -- pixel-perfect cutouts on any surface.
+- Indoor structure analysis floods background from all four aprons and
+  accepts ground contact on any side (outdoors keeps the south-only rule
+  that protects roofs), so face-on furniture drawings voxelize per pixel
+  instead of rising as wall-height volumes.
+- Profile-pinned standees are 10px deep (detected props stay 6px), so a
+  deliberate object like a TV keeps a body at shallow camera angles.
+- Authored upright boxes fold their artwork up every face (flanks and
+  back wear the front stack darkened) and top faces keep the drawn
+  tabletop: face-on rows wear the row above the fold instead of
+  repeating their front art lying flat, and a run that folded entirely
+  tops with the furniture row drawn above it (a bookcase's shelf trim).
+- Characters no longer ride a pinned stair tile's class height: stairs
+  are walked through at floor level, fixing the step-up onto thin air in
+  front of stairwells.
 
-  The geometry was never stale: the mesher reads block layout and tile ids
-  and never reads colour, and the palette lives entirely in the texture
-  `TerrainAtlas` hands back per frame, keyed by palette and so already
-  rebuilt by the next frame. A reload whose reason is `colors` now keeps
+- A voxelized building is as tall as its facade plus its roof slab rather
+  than as tall as its drawing: the roof rows are DEPTH now, not height, so
+  Red's house is 36px over a 4x3-cell plot instead of a 48px cube.
+- Round trees (the `cylinder` pin: lone canopies and the border tree
+  wall) stop being lathes -- the sprite wrapped around a 12-segment
+  column read as exactly that, art smeared on a barrel. Each cell is now
+  a real voxel hull: the canopy is segmented out of its cell as the
+  darkest-pixel outline plus everything it encloses (which also drops
+  the background grass that used to inflate every row to full width, and
+  the cast shadow under the ball), and each mask row runs its own span's
+  circular chord in depth -- the front view is the sprite pixel for
+  pixel, the plan view is the sprite's width profile turned in depth.
+  Dithered art with no closed outline (the tree wall) falls back to
+  light-shades-only flooding, per the methodology doc's boundary rule.
+  Sides de-outline like building extrusions so flanks read as canopy
+  rather than solid black, and dome caps keep their outline on the rim
+  while the interior samples the canopy a couple of rows deeper.
+- A `post` standee pool, pinned for the overworld's vertical fence-post
+  cell (tiles 14/85 -- across every map the pair appears only as this
+  cell). The detector already turns HORIZONTAL fence runs (tile 57) into
+  per-post standees, but a vertical run of repeated cells trips its
+  scenery-repetition guard and fell to the volume path as a
+  fence-textured tower (Viridian's west line, Route 25).
+  `post` extracts every CELL as its own cluster -- pooled clustering
+  would stand the whole line up as one drawing-tall slab at one depth --
+  and classifies pixels the way the detector does (non-white is body)
+  rather than by the pinned-prop outline rule, which would strip the
+  posts to black skeletons; at the detector's own 6px depth, pinned and
+  detected fences look alike.
+- Town signs move from the `billboard` pool to a new `signpost` pool: the
+  same per-pixel standing slab, but 2 voxels thin instead of 10. A sign
+  is a plate on a stick, and the standee body that keeps a TV from
+  vanishing at shallow angles read as a solid block of furniture here.
+- The ground under a round tree matches the tree's own drawn background
+  instead of the map's commonest ground tile. The hull's segmentation
+  already knows which pixels are NOT the tree; those pixels are scored
+  against every flat ground tile the map places and the closest art
+  wins, per template -- so border trees drawn over checker grass stand
+  on checker grass even on a map that is mostly pale path (the old
+  fallback painted path under every mid-forest tree, which has no flat
+  neighbour to vote with). The drawn cast shadow stays out of the score:
+  no ground tile carries a shadow, and its darks would drag every match.
+
+- Mesh builds stream in the background. `ChunkMesher` queues per-map
+  build jobs and `pump()` -- driven from the pipeline's update -- runs
+  them inside a few-millisecond frame budget (`lib/BuildBudget.lua`
+  suspends the build coroutine mid-loop when the slice is spent). The
+  camera tween holds at flat until the current map's terrain exists, so
+  toggling voxel mode shows a handful of flat frames instead of a frozen
+  one; neighbours pop in as they finish. Warp fades prefetch the
+  destination (the pipeline update ticks while the Transition covers the
+  screen, with a wider pump slice), so a door exit lands on terrain that
+  is already built.
+- Vertex packing goes through FFI into one native buffer
+  (`Mesh:setVertices(ByteData)`) instead of a Lua table per vertex --
+  the headless table path remains for the pure `geometry()` API and its
+  suite.
+- Round-tree hulls are carved once per (tileset, art, ground set) and
+  kept as stamps -- template plus cell offset, expanded during vertex
+  packing -- instead of materialized per-cell quad tables. A route's
+  border forest was ~500 quads x hundreds of cells of retained heap.
+- Mesh and analysis caches evict down to the live neighbourhood (current
+  map + rendered neighbours, plus one set of history so a house
+  round-trip keeps the town warm). Evicted meshes are released
+  explicitly. Memory over Pallet -> Mt Moon: was ~2.9GB and monotonic,
+  now oscillates between ~90 and 200MB.
+
+- `Voxel3D.SHADOW_KX/KZ` are -0.85 / -0.55, from +0.30 / +0.45: the sun
+  crosses to the southeast and drops from 62 degrees to 45. The bearing
+  leans WEST of northwest on purpose -- a character is drawn as a slab
+  leaning away from the camera, which covers the ground due north of its
+  feet, so a shadow thrown straight up-screen lands entirely underneath
+  the figure casting it and is never seen.
+- `Voxel3D.SHADOW_ALPHA` 0.32 -> 0.40, a quarter darker.
+- `FACE_SHADE` east 0.78 -> 0.84 and west 0.78 -> 0.72. The two were equal
+  because the old sun sat due northwest and they were symmetric about it;
+  under a southeastern sun east is a lit flank and west a shaded one.
+- A character's shadow lookup runs off the UPRIGHT card the sun saw, not
+  the leaning slab the camera sees (`Voxel3D.draw`'s `sunModel`). Casting
+  the leaning slab instead would shrink every shadow to nothing as the
+  camera flattened toward top-down; looking up with the leaned position
+  put each sprite's own card across its front.
+- The contact-shadow term in `ChunkMesher` was a one-directional stripe
+  keyed to a northwestern sun -- two neighbours, one corner, top faces
+  only. It is now the ambient occlusion above.
+- The light frustum is fitted to the ground the CAMERA CAN SEE rather than
+  to a view-sized box around the focus, and both of its margins are now
+  asymmetric -- for opposite reasons. The camera sits south of its focus
+  and looks north, so the ground it sees runs far north and barely south;
+  the sun sits southeast, so the casters for that ground stand south and
+  east of it. Paying for a view-sized box plus caster margin on all four
+  sides covered about a third of what was on screen at 75 degrees, and
+  overpaid at 15.
+- Shadows ease off at the frustum's rim instead of ending on it. Past the
+  low rungs the horizon is further out than any box worth paying for, and
+  a covered region that simply stops draws a hard line across the middle
+  distance where every shadow ends at once.
+
+The Pokemon Center interiors. One `POKECENTER` group in
+`data/voxel_heights.lua` plus one new class, and because
+VIRIDIAN_POKECENTER places every tile the tileset's other maps use, the
+one pin set covers all eleven Centers and the Celadon Hotel.
+
+### Fixed
+
+- The generic town-house tileset (`HOUSE` -- Blue's house, Daisy at her
+  table, and eighteen more homes, the schoolhouse and the trashed house
+  among them) is now pinned in `data/voxel_heights.lua` the way Red's
+  rooms already were: the dining table stops towering as a wall-height
+  volume and sits at table height with its front folded upright, stools
+  become seat-high boxes that characters sit on, the corner potted
+  plants become per-pixel standees instead of texture-smeared box
+  stacks, the bookcases get clean capped tops, and the wall band (with
+  its window, picture and the schoolhouse blackboard) stays one 16px
+  face.  The schoolhouse's open book stands on the pinned tabletop as a
+  cutout, and the trashed house's ransacked table corner keeps table
+  height.
+- Interior door mats lie flat again in Red's and the generic houses.
+  Their collision tile is $14, which the engine's stale-cache fallback
+  counts as water in every tileset, so the rug recessed into a pond lip;
+  a `ground` pin now overrides the water read.
+
+- Stray pixels along map seams: ring props (border-tree hulls) whose
+  quad CENTER sat exactly on a neighbour body's edge line escaped the
+  strict point-in-rect mask and survived as fragments of otherwise
+  dropped trees. Object quads now keep/drop by their full extent,
+  boundary inclusive; props straddling the body edge also stay whole
+  instead of shedding their outer half.
+- The one-step "ledge hop" when crossing a connection into a tree-ringed
+  map: the seam step stands the player one cell off the new map, where
+  `Map:cellTile` border-extends into the borderBlock -- a raised tile on
+  maps ringed with trees. Off-map ground now reads as height 0 (the
+  departed neighbour's flat walkway, which is what is actually rendered
+  there).
+
+- Cycling palette modes with voxel mode on eventually killed the pipeline
+  outright: `attempt to call field 'atlasImageData' (a nil value) --
+  disabled for this session`. Nothing brought it back short of a restart.
+
+  `TerrainAtlas` reads three engine seams to animate water and flowers in
+  the terrain texture, and this build ships only one of them
+  (`defaultAnimatedTiles`). The tile clock, `animFrame`, was already read
+  guarded and simply degrades. `atlasImageData` was called straight -- but
+  only down the branch where the mod had NOT baked the atlas itself, which
+  is why it looked stable until a palette changed. Every mode with no world
+  palette for the map (`PaletteFX.pal` answering nil), plus RED++ and any
+  trueColor tileset, takes that branch, so the first map with animated
+  tiles entered under one of them threw out of `drawWorld` and the engine
+  disabled the pass for the session, exactly as it should.
+
+  The seam is now read guarded like its sibling, and when it is absent the
+  pixels are recovered rather than given up on. An atlas neither we nor
+  RED++ replaced is the tileset art itself, so animation carries on from
+  the art on disk. RED++'s per-map bake exists only as a texture --
+  `getGbcAtlas` throws its `ImageData` away -- so that one comes back off
+  the GPU: the atlas is drawn 1:1 into a canvas and read back, once per map,
+  with the pass's own render target captured and restored around it (the
+  usual `setCanvas()` would drop the rest of the frame). A driver that
+  refuses the readback declines to animate and keeps the static atlas.
+  Worst case now costs one animation, never the pipeline.
+
+- Water and flowers did not animate in voxel mode at all, and had not since
+  the mode shipped -- a silent one, since the terrain was otherwise correct.
+
+  The tile clock is the third seam, and this build does not export it
+  either. Being read guarded, it answered 0 forever instead of throwing,
+  which pinned every animated tile at step 0. `animFrame` is a plain local
+  in `TileRenderer`, but an upvalue of the exported `tick()`, so the mod now
+  reads the real counter through it. That it is the ENGINE's counter is the
+  point: the flat tile layer draws from the same number, so toggling voxel
+  mode mid-cycle continues the animation rather than restarting it. A build
+  that exports `animFrame()` outright is preferred; a build that hides the
+  local falls back to wall time in 60Hz steps, which free-runs against the
+  2D path but still moves the water.
+
+- Toggling palettes in voxel mode flashed the flat 2D world for a moment on
+  every switch.
+
+  `PaletteFX.setMode` reloads the live map to rebuild its atlas, and this
+  mod dropped that map's terrain mesh on any `map.reloaded` at all. Mesh
+  builds are asynchronous, so the frames between the drop and the first
+  rebuilt mesh had no terrain to draw -- and a voxel `drawWorld` with no
+  terrain returns nil, which is exactly how the pipeline asks for the 2D
+  fallback. The flash was the mod correctly reporting that it had nothing
+  to show.
+
+  The geometry was never stale: the mesher reads block layout and tile ids
+  and never reads colour, and the palette lives entirely in the texture
+  `TerrainAtlas` hands back per frame, keyed by palette and so already
+  rebuilt by the next frame. A reload whose reason is `colors` now keeps
   the mesh, and the new palette lands on the diorama already on screen in
   one frame. Every other reload -- warps re-entering a map, hot reload, a
   replaced block -- still drops it.
@@ -297,373 +638,6 @@ long as the thing throwing them is tall.
   Nothing showed the fault until the voxel wireframe drew the grid those
   pixels were supposed to be sitting on.
 
-### Changed
-
-- `Voxel3D.SHADOW_KX/KZ` are -0.85 / -0.55, from +0.30 / +0.45: the sun
-  crosses to the southeast and drops from 62 degrees to 45. The bearing
-  leans WEST of northwest on purpose -- a character is drawn as a slab
-  leaning away from the camera, which covers the ground due north of its
-  feet, so a shadow thrown straight up-screen lands entirely underneath
-  the figure casting it and is never seen.
-- `Voxel3D.SHADOW_ALPHA` 0.32 -> 0.40, a quarter darker.
-- `FACE_SHADE` east 0.78 -> 0.84 and west 0.78 -> 0.72. The two were equal
-  because the old sun sat due northwest and they were symmetric about it;
-  under a southeastern sun east is a lit flank and west a shaded one.
-- A character's shadow lookup runs off the UPRIGHT card the sun saw, not
-  the leaning slab the camera sees (`Voxel3D.draw`'s `sunModel`). Casting
-  the leaning slab instead would shrink every shadow to nothing as the
-  camera flattened toward top-down; looking up with the leaned position
-  put each sprite's own card across its front.
-- The contact-shadow term in `ChunkMesher` was a one-directional stripe
-  keyed to a northwestern sun -- two neighbours, one corner, top faces
-  only. It is now the ambient occlusion above.
-- The light frustum is fitted to the ground the CAMERA CAN SEE rather than
-  to a view-sized box around the focus, and both of its margins are now
-  asymmetric -- for opposite reasons. The camera sits south of its focus
-  and looks north, so the ground it sees runs far north and barely south;
-  the sun sits southeast, so the casters for that ground stand south and
-  east of it. Paying for a view-sized box plus caster margin on all four
-  sides covered about a third of what was on screen at 75 degrees, and
-  overpaid at 15.
-- Shadows ease off at the frustum's rim instead of ending on it. Past the
-  low rungs the horizon is further out than any box worth paying for, and
-  a covered region that simply stops draws a hard line across the middle
-  distance where every shadow ends at once.
-
-The Pokemon Center interiors. One `POKECENTER` group in
-`data/voxel_heights.lua` plus one new class, and because
-VIRIDIAN_POKECENTER places every tile the tileset's other maps use, the
-one pin set covers all eleven Centers and the Celadon Hotel.
-
-### Added
-
-- A `counter` class (8px, upright): half-cell furniture. One 8px band,
-  so exactly the drawing's bottom row stands up as the front and every
-  row above it rides the top face in drawn order. `table`'s 12px could
-  not be retuned for it; the houses share that class.
-
-- `tilesets.POKECENTER` in `data/voxel_heights.lua`. Before it, the
-  detector merged the wall-touching counters and healing machines into
-  the wall band and towered them 3-6 blocks, flattened the machines'
-  near-black screens to void, read the pillar bases, plant pots and
-  machine bodies as ponds (the $14/$32/$48 stale-cache water fallback),
-  boxed each plant pair into one hedge cube, and extruded the lounge
-  seat -- a PERSON is drawn into its tile art -- into a monolith wearing
-  his face.
-- The pins, by shape: the wall band, windows, poster, pillars and the
-  16px machine bodies are `wall`; the counters (with the nurse's tray)
-  are `counter` and the PC's desk is `table`; the machine screens and
-  the PC are `billboard`, standing on the pinned boxes below them; the
-  potted plants are `prop` standees like every other interior plant.
-- The lounge couch with the man sitting on it is a `counter` box: its
-  bottom row stands up as the couch's front and the cushion and the man
-  ride the top face, each drawn exactly once.
-  He cannot be stood upright, and the reason is structural rather than a
-  tuning question. His skin pixels span two tile rows and stop dead at
-  the row 9/10 seam; folding two rows upright requires both to share a
-  class, which makes the box two tiles deep, and a fully folded box
-  repeats its north row across its whole top face. So every upright
-  arrangement puts his head on screen two or three times -- as a 16px
-  seat-back, on the front and twice more on the top; as a 32px bookcase,
-  a cabinet taller than the room's own walls. Dropping the seat in front
-  of him to floor level only changes which copy you see. Nor can he be a
-  standee: the drawing has no floor margin, so all three non-black
-  shades touch the cluster rim and the mask drains 307 of its 420
-  interior pixels -- 46% of him even segmented alone, because his skin
-  is the same light shade as the couch behind him.
-- Survey evidence: full before/after passes of VIRIDIAN_POKECENTER at
-  15/35/50 degrees, plus spot-checks of CELADON_POKECENTER and
-  CELADON_HOTEL (shared tileset, both inherit correctly) and of
-  REDS_HOUSE_1F, OAKS_LAB and VIRIDIAN_CITY (unchanged -- the new class
-  is additive and no other tileset lists it).
-
-## 1.3.0
-
-Terrain meshing goes asynchronous, instanced and bounded. The first voxel
-frame used to build every neighbourhood map synchronously (a ~2.4s
-freeze), retain every map's analysis forever (gigabytes over a
-cross-region trek), and string stray pixels along map seams.
-
-### Changed
-
-- Mesh builds stream in the background. `ChunkMesher` queues per-map
-  build jobs and `pump()` -- driven from the pipeline's update -- runs
-  them inside a few-millisecond frame budget (`lib/BuildBudget.lua`
-  suspends the build coroutine mid-loop when the slice is spent). The
-  camera tween holds at flat until the current map's terrain exists, so
-  toggling voxel mode shows a handful of flat frames instead of a frozen
-  one; neighbours pop in as they finish. Warp fades prefetch the
-  destination (the pipeline update ticks while the Transition covers the
-  screen, with a wider pump slice), so a door exit lands on terrain that
-  is already built.
-- Vertex packing goes through FFI into one native buffer
-  (`Mesh:setVertices(ByteData)`) instead of a Lua table per vertex --
-  the headless table path remains for the pure `geometry()` API and its
-  suite.
-- Round-tree hulls are carved once per (tileset, art, ground set) and
-  kept as stamps -- template plus cell offset, expanded during vertex
-  packing -- instead of materialized per-cell quad tables. A route's
-  border forest was ~500 quads x hundreds of cells of retained heap.
-- Mesh and analysis caches evict down to the live neighbourhood (current
-  map + rendered neighbours, plus one set of history so a house
-  round-trip keeps the town warm). Evicted meshes are released
-  explicitly. Memory over Pallet -> Mt Moon: was ~2.9GB and monotonic,
-  now oscillates between ~90 and 200MB.
-
-### Fixed
-
-- Stray pixels along map seams: ring props (border-tree hulls) whose
-  quad CENTER sat exactly on a neighbour body's edge line escaped the
-  strict point-in-rect mask and survived as fragments of otherwise
-  dropped trees. Object quads now keep/drop by their full extent,
-  boundary inclusive; props straddling the body edge also stay whole
-  instead of shedding their outer half.
-- The one-step "ledge hop" when crossing a connection into a tree-ringed
-  map: the seam step stands the player one cell off the new map, where
-  `Map:cellTile` border-extends into the borderBlock -- a raised tile on
-  maps ringed with trees. Off-map ground now reads as height 0 (the
-  departed neighbour's flat walkway, which is what is actually rendered
-  there).
-
-## 1.2.0
-
-Buildings stop being boxes wearing their own elevation. A profiled
-building is voxelized from its own sprite, band by band -- the pipeline
-written up in `assets/docs/buidling_to_voxel/`.
-
-### Added
-
-- `lib/Buildings.lua`: a building archetype. Where the volume path folds a
-  whole drawing upright (roof, facade and sloped ends alike) into one box,
-  this classifies each BAND of the drawing by the 3D surface it depicts and
-  applies the matching operation: top-facing rows lay flat over the
-  footprint, the facade extrudes straight back, an awning band juts past
-  the walls, and the drawn taper at the ends becomes a stepped slope in
-  elevation. Every visible voxel carries a real texel of the drawing, so
-  the model recolours with the atlas.
-- A `buildings` section in `data/voxel_heights.lua`. A building is matched
-  by its exact tile grid (the drawings are catalogued in `assets/docs/buildings/`),
-  so one entry covers every map that places the same art -- Red's house,
-  Blue's house, Bill's, the Copycat's and the two Fuchsia houses are one
-  seven-placement entry, and Oak's lab is a second. Only the band table is
-  authored; the silhouette, the taper rate, the eave height and every
-  window and doorway are measured off the pixels.
-- The flat-roofed civic block and its sixteen relatives -- every Pokemon
-  Center and every Poke Mart, Fuchsia Gym, the museum, the Game Corner,
-  Celadon Mansion and its department store, the Power Plant, the Route 5
-  and Route 22 gates, Silph Co and five anonymous scenery blocks. They are
-  one architecture drawn at eleven different footprints, from 4x4 cells up
-  to Silph Co's 8x12, and they share one band table: their lattice is drawn
-  from straight above, so the measured taper comes out flat and the whole
-  band is depth under one level roof. No new roof mode was needed -- the
-  drawn profile was always the shape, and a drawing with no taper simply
-  yields a level one.
-- Pewter's museum hall (`assets/docs/buildings/B24`). It is the one
-  sloped-roof building in this pass, and the only one so far whose roof
-  texture is not a plain repeat: the drawing states its own period by
-  repeating the whole lattice-and-course motif, rows 8..31 again at 32..55,
-  so the cycle is 24 rows and the roof carries its drawn courses across the
-  depth instead of a bare lattice. The band below them is the roof's
-  fascia, wider than the wall it covers, so it belongs to the roof and
-  lands on the south rim. Note the museum is TWO drawings: this hall and
-  the east entrance beside it, which is B18 and shares its drawing with the
-  Route 2 gate.
-- The rest of the 2:1 sloped-roof buildings: both gym drawings (the
-  standard one at Cinnabar, Pewter, Vermilion and Viridian plus the
-  Fighting Dojo, and the wider Celadon / Cerulean / Saffron one), the 4x2
-  cottage that houses Mr Fuji, the Cubone house, Bill's grandpa, the Name
-  Rater and the Viridian school, Cerulean's three wide houses, the day
-  care, and three scenery blocks -- among them B01, which at 19 placements
-  is the commonest drawing in the game. Each one's roof band is pixel for
-  pixel one of the three already authored, so they take that sibling's band
-  table unchanged: 16 rows for Red's house's, 32 for Oak's lab's, 64 for
-  the museum's.
-- The Safari Zone rest houses and the Victory Road entrance, the first
-  buildings outside the `OVERWORLD` tileset -- `buildings` is keyed by
-  tileset and until now only had the one section. The rest house's
-  corrugated roof repeats every 5 rows rather than the overworld lattice's
-  8, which is the point of `roofCycle` being authored per building.
-- Route 10's scenery block, via a new `seal` field. Its drawing has no
-  black base course -- it ends on a row of light brick -- so the silhouette
-  flood climbed in from the south border through the mortar and hollowed
-  the wall out, leaving 72% of the sprite in 65 pieces. `seal` names the
-  sides a drawing runs off rather than closing, and the flood does not seed
-  there: sealed, it is 95% in one piece, and the model is its twin the
-  museum's. No other building sets it, and none changes by a voxel.
-- Together these take the mod to 31 of the catalogue's 34 drawings and 144
-  of its 147 placements. The last three, and why each resists, are written
-  up in `assets/docs/buildings/REMAINING.md`.
-- A roof no longer runs past the drawing's own silhouette. These sprites
-  are inset from their boxes, and the columns outside the inset carry no
-  roof: they now get none, and the fascia belongs to the outermost columns
-  the drawing actually paints. Before, they raised a four-voxel slab of
-  black kerb the full depth of the building, flanking its walls at ground
-  level. Nothing shipped hit this -- Red's house and Oak's lab are drawn
-  edge to edge -- so no existing model changes by a single voxel.
-- Windows and doorways sink a voxel behind their frames, found rather than
-  listed: a pane is a non-black region the drawing seals off behind its own
-  black outline. A nested frame (the door's own little window) layers for
-  free.
-- `tools/building_voxels.py`: the reference implementation of the same
-  algorithm, with the geometric asserts and isometric previews Stage 5 of
-  the methodology calls for. It and the runtime agree exactly on voxel and
-  shell counts for all 19 templates -- 96,617 / 12,866 for Red's house up
-  to 3,715,963 / 146,762 for Silph Co, which ship as 3,410 and 13,994
-  quads. Its slope asserts read the drawn columns only and stand down for a
-  roof with no taper, where the check is that the roof is level instead;
-  its previews fit the projection to the model rather than to a fixed
-  camera, so a building taller or deeper than the first two still lands on
-  the canvas.
-
-### Changed
-
-- A voxelized building is as tall as its facade plus its roof slab rather
-  than as tall as its drawing: the roof rows are DEPTH now, not height, so
-  Red's house is 36px over a 4x3-cell plot instead of a 48px cube.
-- Round trees (the `cylinder` pin: lone canopies and the border tree
-  wall) stop being lathes -- the sprite wrapped around a 12-segment
-  column read as exactly that, art smeared on a barrel. Each cell is now
-  a real voxel hull: the canopy is segmented out of its cell as the
-  darkest-pixel outline plus everything it encloses (which also drops
-  the background grass that used to inflate every row to full width, and
-  the cast shadow under the ball), and each mask row runs its own span's
-  circular chord in depth -- the front view is the sprite pixel for
-  pixel, the plan view is the sprite's width profile turned in depth.
-  Dithered art with no closed outline (the tree wall) falls back to
-  light-shades-only flooding, per the methodology doc's boundary rule.
-  Sides de-outline like building extrusions so flanks read as canopy
-  rather than solid black, and dome caps keep their outline on the rim
-  while the interior samples the canopy a couple of rows deeper.
-- A `post` standee pool, pinned for the overworld's vertical fence-post
-  cell (tiles 14/85 -- across every map the pair appears only as this
-  cell). The detector already turns HORIZONTAL fence runs (tile 57) into
-  per-post standees, but a vertical run of repeated cells trips its
-  scenery-repetition guard and fell to the volume path as a
-  fence-textured tower (Viridian's west line, Route 25).
-  `post` extracts every CELL as its own cluster -- pooled clustering
-  would stand the whole line up as one drawing-tall slab at one depth --
-  and classifies pixels the way the detector does (non-white is body)
-  rather than by the pinned-prop outline rule, which would strip the
-  posts to black skeletons; at the detector's own 6px depth, pinned and
-  detected fences look alike.
-- Town signs move from the `billboard` pool to a new `signpost` pool: the
-  same per-pixel standing slab, but 2 voxels thin instead of 10. A sign
-  is a plate on a stick, and the standee body that keeps a TV from
-  vanishing at shallow angles read as a solid block of furniture here.
-- The ground under a round tree matches the tree's own drawn background
-  instead of the map's commonest ground tile. The hull's segmentation
-  already knows which pixels are NOT the tree; those pixels are scored
-  against every flat ground tile the map places and the closest art
-  wins, per template -- so border trees drawn over checker grass stand
-  on checker grass even on a map that is mostly pale path (the old
-  fallback painted path under every mid-forest tree, which has no flat
-  neighbour to vote with). The drawn cast shadow stays out of the score:
-  no ground tile carries a shadow, and its darks would drag every match.
-
-### Fixed
-
-- The generic town-house tileset (`HOUSE` -- Blue's house, Daisy at her
-  table, and eighteen more homes, the schoolhouse and the trashed house
-  among them) is now pinned in `data/voxel_heights.lua` the way Red's
-  rooms already were: the dining table stops towering as a wall-height
-  volume and sits at table height with its front folded upright, stools
-  become seat-high boxes that characters sit on, the corner potted
-  plants become per-pixel standees instead of texture-smeared box
-  stacks, the bookcases get clean capped tops, and the wall band (with
-  its window, picture and the schoolhouse blackboard) stays one 16px
-  face.  The schoolhouse's open book stands on the pinned tabletop as a
-  cutout, and the trashed house's ransacked table corner keeps table
-  height.
-- Interior door mats lie flat again in Red's and the generic houses.
-  Their collision tile is $14, which the engine's stale-cache fallback
-  counts as water in every tileset, so the rug recessed into a pond lip;
-  a `ground` pin now overrides the water read.
-
-## 1.1.0
-
-Interior furniture gets the shapes it depicts
-(mods/DRAMATIC_SHAPE/tools/voxel-survey.md is the procedure that found and
-verified these).
-
-### Added
-
-- Furniture shape classes in the profile: `bed` (a low slab wearing its
-  top-down art), `table` and `desk` (boxes at their drawn height whose
-  faces fold the artwork up), `relief` (a prop drawn from above -- a
-  game console -- lying flat and extruding a few voxels inside its
-  outline), the standee pools `billboard` (10px) / `prop` (5px) /
-  `stool` (5px, and characters standing on its walkable cell sit at
-  seat height) / `cutout` (one voxel: pure profile, for the vase on the
-  table), and the stair archetypes `stair_e`/`stair_w` (a rising flight
-  of real steps) and `stair_down_e`/`stair_down_w` (a sunken stairwell
-  descending below the floor -- stairs that lead down).  Separate pools
-  cluster separately, so touching drawings never merge into one cutout.
-- Standee clusters split into per-pixel connected components, each
-  standing on its own feet in the depth band of the row it is drawn in:
-  two stools stacked in adjacent cells become two stools, and no
-  fragment of a drawing ever floats at its bounding-box height.  A
-  `cutout` keeps only its largest component -- a cast shadow's drawn
-  edge is background, not a floating scrap.
-- `bookcase` class: free-standing shelf drawings collapse in ranks onto
-  one-cell-deep boxes at their full drawn height, back rows becoming
-  hidden floor; the trim row above a rank -- undetected structure or a
-  row pinned `table`, since the same trim tiles cap other furniture --
-  is adopted as its cap.  Pinned for Oak's Lab (`DOJO`).
-- Oak's Lab tables pinned `table`: the starter-ball display and the
-  north tables stand at real table height, the display frame's black
-  corner brackets no longer auto-extract into standing prisms, and the
-  Poke Ball / Pokedex sprites ride the authored height onto the
-  tabletops.
-- Pinned props drawn directly above a pinned box stand ON it: the PC
-  monitor on its desk, the flower pot on the dining table.
-- Profile pins for `REDS_HOUSE_1` / `REDS_HOUSE_2` (Red's house and the
-  Copycat's, both floors): bed, stools, tables, PC desk with its
-  standing monitor, bookcases, TV standing on the floor behind the game
-  console's relief, potted plant, flower pot, both staircases, and the
-  wall/window band.
-- `mods/DRAMATIC_SHAPE/tests/voxel_survey.lua`: screenshot-survey driver
-  behind the repeatable inspection procedure (SURVEY_MAP / SURVEY_SPOTS /
-  SURVEY_LEVELS / SHOT_DIR), documented in
-  mods/DRAMATIC_SHAPE/tools/voxel-survey.md.
-
-### Changed
-
-- Pinned props are segmented the way the art is authored: objects wear a
-  black outline, so background is the shades touching the cluster's edge
-  (white floor around a TV, grey tabletop around a vase) flooded in from
-  the aprons; the outline, its interior, paint whites and anything they
-  enclose survive -- pixel-perfect cutouts on any surface.
-- Indoor structure analysis floods background from all four aprons and
-  accepts ground contact on any side (outdoors keeps the south-only rule
-  that protects roofs), so face-on furniture drawings voxelize per pixel
-  instead of rising as wall-height volumes.
-- Profile-pinned standees are 10px deep (detected props stay 6px), so a
-  deliberate object like a TV keeps a body at shallow camera angles.
-- Authored upright boxes fold their artwork up every face (flanks and
-  back wear the front stack darkened) and top faces keep the drawn
-  tabletop: face-on rows wear the row above the fold instead of
-  repeating their front art lying flat, and a run that folded entirely
-  tops with the furniture row drawn above it (a bookcase's shelf trim).
-- Characters no longer ride a pinned stair tile's class height: stairs
-  are walked through at floor level, fixing the step-up onto thin air in
-  front of stairwells.
-
-## 1.0.0
-
-First release, ported from the engine-internal voxel branch onto the
-`render_pipelines` mod API.
-
-### Added
-
-- `voxel` render pipeline: 3D diorama overworld with extruded terrain,
-  depth-buffered occlusion, leaning sprite billboards, drop shadows and
-  contact AO. VOXEL options row and hotkey `6` (OFF / 15 / 35 / 50).
-- `tiltshift` render pipeline: a `worldPresent` post-process giving the
-  miniature-photo look. T-SHIFT options row and hotkey `9` (OFF / 1 / 2 / 3).
-- Carved voxel models for 67 overworld sprites, plus the
-  `tools/build_voxels.py` that produced them.
-- `data/voxel_heights.lua`, the hand-authored tile shape profile.
-
 ### Changed from the pre-mod version
 
 - The level is no longer the mod's to keep. The engine owns the ladder, the
@@ -671,7 +645,7 @@ First release, ported from the engine-internal voxel branch onto the
   keeps only the camera-angle tween.
 - Persistence moved from `save.options.voxel` / `save.options.tiltshift` to
   `save.options.pipelines.voxel` / `.tiltshift`.
-- Hotkeys moved from `4`/`9` to `6`/`9`: the fork already uses `4` for
+- Hotkeys moved from `4`/`9` to `3`/`6`: the fork already uses `4` for
   survey zoom.
 - The tilt-shift pass is a declared `worldPresent` stage rather than a call
   spliced into the world draw, so it composes with any world pipeline
diff --git a/README.md b/README.md
index 596adea..de48a41 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,8 @@
 # Dramatic Shape Voxel Mod
 
+A mod for the [Pokémon Gen 1 Recompilation
+Project](https://github.com/bryanthaboi/pokemon-gen1-recomp-project).
+
 The overworld as a 3D diorama. Terrain is extruded into real geometry,
 occlusion comes from a depth buffer rather than a y-sort, characters stand
 as leaning sprite slabs, a shadow map throws real cast shadows across
@@ -11,7 +14,10 @@ or scripts — it changes what the world *looks* like and nothing about what
 it *is*. Battles, menus and cutscenes are untouched; only the free-roam
 overworld draws differently.
 
-## Using it
+## Controls
+
+Every key is free-roam only, and each one is also a row on the OPTIONS
+menu.
 
 | control | does |
 | --- | --- |
@@ -20,146 +26,7 @@ overworld draws differently.
 | `6`, or the **T-SHIFT** options row | OFF → 1 → 2 → 3 → OFF (miniature blur) |
 | `7`, or the **V-CURVE** options row | OFF → 1 → 2 → 3 — bend the world over the horizon |
 
-The four keys sit together, and two of them are taken off the engine: `3`
-was **TILT** and `5` was **GBC FX**. Neither is reachable by key while this
-mod is enabled, and both are still on the OPTIONS menu. Every key is
-free-roam only, and a screen that handles its own keys keeps them, so
-nothing here fires while a menu or a text box has focus.
-
-**`3` also turns TILT and GBC FX off**, on every press — including the one
-that cycles the ladder back round to OFF. Both fight the diorama: TILT is
-the flat fake of what this mode does for real, and GBC FX lays a
-full-screen present pass over the top of it. Since `3` is now the only key
-that reaches either, it is also the way back from having left one on.
-
-75 degrees is a rung the engine's flat **TILT** could not have: it projects
-the world canvas as one rigid plane, which degenerates into a horizon line
-down there, where real geometry only gets more of itself to show.
-
-That rung is also the one that gets a **sky**. Pitched that far over the
-horizon comes into frame, so the void behind the diorama is filled rather
-than left as the black plate it reads as at every rung below — enough to
-stand the world under something without a skybox to draw. Outdoors only: a
-house, a cave or a gym is a room with a ceiling, and the void past its
-walls is the outside of a box, not open air. The colour follows the display
-mode like everything else here — blue in the colour modes, grey under
-GRAY, green under CLASSIC — and fades in with the camera tween rather than
-snapping on at the keypress.
-
-V-CURVE is the *curved world* — every vertex pushed down by the square of
-its horizontal distance from the camera's focus, so the ground under you
-stays flat and the map bends away over a near horizon. It is not a fisheye:
-a fisheye is a lens, a screen-space warp that bends straight lines
-everywhere and resamples every pixel to do it. This bends the geometry, so
-lines near the camera stay straight and the art stays crisp.
-
-Both pipeline levels persist in `save.options.pipelines`, and V-GRID and
-V-CURVE in `save.options.modOptions.DRAMATIC_SHAPE` — the same place the mod
-manager's own settings page for this mod reads, so each row is one setting
-in two places. VOXEL and the engine's **TILT** are two answers to the same
-question, so switching one on switches the other off.
-
-Without shader or depth-canvas support the rows still cycle but the world
-stays 2D — the mode reports itself unavailable and the engine keeps the
-vanilla path.
-
-## How it is built
-
-This is a *rendering pipeline* mod (see `docs/modding.md`). `main.lua`
-registers two records in the engine's `render_pipelines` registry and
-writes almost nothing else — the ladder, the options rows, persistence and
-the free-roam gate are all engine plumbing driven from those records:
-
-- **`voxel`** — a `drawWorld` pipeline. Replaces the world pass with a
-  rendered 3D scene and returns it as one window-resolution canvas.
-- **`tiltshift`** — a `worldPresent` pipeline. Post-processes the finished
-  world *before* the UI composites, so the diorama blurs and the text boxes
-  in front of it stay crisp.
-
-Everything else lives in `lib/`, loaded through the mod's own namespace
-(`V.require`) rather than `package.path`, which a mod directory is not on:
-
-| module | role |
-| --- | --- |
-| `Voxel3D` | the 3D pass: shader, depth buffer, camera, projection |
-| `ShadowMap` | the sun's own pass: what it cannot see is in shadow |
-| `WorldCurve` | the curved world: the Animal Crossing horizon |
-| `VoxelGrid` | the wireframe toggle |
-| `ModSetting` | a setting of this mod's own: ladder, store, options row |
-| `Mat4` | the 4x4 matrix math the camera and model transforms need |
-| `VoxelState` | the camera angle and its ease; the engine owns the level |
-| `VoxelScene` | assembles one frame: shadows, terrain, characters, grass |
-| `ChunkMesher` | a map's block layer → a terrain mesh, cached per map |
-| `Structures` | flood-fills drawn things and decides how tall each is |
-| `Buildings` | a profiled building sprite → a voxel model, band by band |
-| `TileShape` | resolves every tile to an extrusion class and art mode |
-| `TerrainAtlas` | the texture geometry samples: SGB palette bakes, and the animated water/flower tiles |
-| `SpriteBillboards` | a sprite frame → a leaning slab with side relief |
-| `VoxelModels` | the carved character models in `assets/voxels/` |
-| `TiltShift` | the two-pass separable blur and saturation lift |
-
-`data/voxel_heights.lua` is the hand-authored shape profile: listing a tile
-there pins its height and art mode over the automatic detector, and its
-`buildings` section names whole building sprites to voxelize band by band
-(`assets/docs/buidling_to_voxel/` is the pipeline; `assets/docs/buildings/` catalogues the
-drawings). Another mod can pin its own tiles by shipping the same file, and
-can replace a character's model with `overrides/voxels/<name>.lua` — models
-resolve through the engine's asset search path before falling back to ours.
-
-`tools/build_voxels.py` is what carved `assets/voxels/` out of the
-overworld sprite sheets. `tools/building_voxels.py` is the reference
-implementation of the building voxelizer — it builds the same models
-offline, asserts their geometry and renders isometric previews, and its
-voxel and shell counts are what `lib/Buildings.lua` is checked against.
-
-## Engine seams it relies on
-
-The mod reaches into a few engine internals (hence `"permissions":
-["engine_internals"]`). All but the last are read-only:
-
-- `Game:keypressed` — **wrapped, not read.** The only thing here that
-  changes engine behaviour, and the one seam worth arguing about. The
-  engine answers its own display keys and returns before a mod's pipeline
-  is offered the key, expressly so a pipeline cannot shadow one — so `3`
-  (TILT) and `5` (GBC FX) cannot be claimed the documented way, and V-GRID
-  and V-CURVE own no render pass and have no registry to claim from at all.
-  Only `6` arrives by the supported route. Polling the keyboard instead
-  would fire *alongside* the engine's handler rather than instead of it, so
-  `3` would cycle both this mode and TILT on one press. The wrapper
-  delegates everything it can: `Pipelines.hotkey` still applies its own
-  gate and ladder, the settings borrow the voxel pipeline's free-roam gate,
-  unclaimed keys pass straight through, and a screen with its own handler
-  keeps the keyboard.
-- `Player:pose()` / `NPC:pose()` — the sheet, position, facing and step
-  phase a frame renders to, so 2D and 3D can never disagree about a pose.
-- `SpriteRenderer:resolveImage()` and `SpriteRenderer.STAND/WALK` — the
-  live sprite image and frame tables, so palette modes and sprite-replacing
-  mods apply to the geometry with no second copy of the logic.
-- `TileRenderer.recolorSample` — the exact shade cutoffs the 2D palette
-  shader uses, so baked terrain lands on the same colours as flat tiles.
-- `TileRenderer.animFrame()` / `.atlasImageData()` / `.defaultAnimatedTiles()`
-  — the tile-animation clock, the pixels behind the atlas texture, and the
-  vanilla water/flower spec. A static mesh cannot overdraw animated cells
-  the way the tile layer does, so it animates the texture instead, and
-  these are what let it do that on the engine's own clock and colours.
-
-  Only `defaultAnimatedTiles` is required. The other two are OPTIONAL and
-  read guarded, because an engine build is free not to carry them — this
-  one does not carry either. Missing `animFrame` freezes the tile clock at
-  step 0 (terrain stops animating in voxel mode; everything else is
-  unaffected). Missing `atlasImageData` costs nothing where the mod baked
-  the atlas itself, falls back to the tileset art where the engine is
-  drawing that art unmodified, and skips animating a RED++ per-map bake,
-  whose pixels are unreachable without the seam. None of the three may ever
-  be called unguarded: a throw inside `drawWorld` costs the player the whole
-  pipeline for the session, not one frame of moving water.
-
-## Tests
-
-```sh
-luajit mods/DRAMATIC_SHAPE/tests/dramatic_shape_test.lua
-```
-
-Runs headless: the mod loads, both pipelines land in the registry with the
-shape the engine dispatches on, the ladders and persistence round-trip, and
-the whole thing stays inert on a machine with no 3D support.
+Two of those keys are taken off the engine: `3` was **TILT** and `5` was
+**GBC FX**. Neither is reachable by key while this mod is enabled, and both
+are still on the OPTIONS menu. Pressing `3` also switches both off — they
+fight the diorama, and it is the way back from having left one on.
\ No newline at end of file
diff --git a/mod.card b/mod.card
index 9e5f37c..e53f6e2 100644
--- a/mod.card
+++ b/mod.card
@@ -2,8 +2,8 @@
 -- and the manager detail pane; never by the loader's merge.
 return {
   summary = "The overworld as a 3D diorama: extruded terrain, real occlusion, voxel characters, tilt-shift miniature blur.",
-  author = "Pokemon Gen 1 Recompilation Project",
-  contact = "https://github.com/bryanthaboi/pokemon-gen1-recomp-project",
+  author = "DramaticShape",
+  contact = "https://github.com/DramaticShape/DRAMATIC_SHAPE",
   tags = { "graphics", "3d", "voxel", "render-pipeline", "presentation" },
   differences = {
     changed = {
@@ -14,7 +14,7 @@ return {
       "the VOXEL key (3) turns TILT and GBC FX off on every press -- both fight the diorama, and 3 is now the only key that reaches either",
     },
     added = {
-      "VOXEL options row and hotkey 3 (OFF / 15 / 35 / 50 degrees)",
+      "VOXEL options row and hotkey 3 (OFF / 15 / 35 / 50 / 75 degrees)",
       "T-SHIFT options row and hotkey 6 (OFF / 1 / 2 / 3), the miniature blur",
       "V-GRID on hotkey 5 and V-CURVE on hotkey 7",
       "a sky behind the diorama at the 75-degree rung, outdoor maps only, coloured by the active palette mode",
@@ -30,5 +30,5 @@ return {
   credits = {
     { who = "pret/pokered", for_ = "the tile and sprite data the geometry is derived from" },
   },
-  compat = { engine = ">=0.1.0 <2.0.0", modApi = 2 },
+  compat = { engine = ">=0.1.28 <2.0.0", modApi = 2 },
 }