Lane E R39 (3/n): the notes + the audit — the magpie recommendation with its arithmetic, the arcade's measured kit, and the sizing law promoted to a standing instruction
- LANE_E_NOTES §39: RECOMMENDATION TAKE THE GLB, on three findings in the order they decide it (80.5% of drawn frames are PERCHED and the perch is the pose E's asset already is; E reads pied at 64/48/32 px and under a 25% smear while B reads as a dark aeroplane; and B's white is geometrically buried — the wing bar sits 3 mm ABOVE a horizontal wing plane, so from underneath, which is where the player always is, the black wing occludes its own white flash). - Three asks filed for Lane B, none of them edits: magpie.js's two stale 154-tri comments (:9, :123 against a measured 182); the wing bar's side (y 0.048 -> ~0.042 is +0 tris and fixes ?classic and ?noassets whatever Fable rules); and two GROUND_FALLBACK entries so the arcade is not grey without assets. - The arcade measured, with the awning-post finding: two post rows 1.00 m apart down a 5.00 m lane. - AUDIT §Round 39 §0 makes the sizing law a STANDING INSTRUCTION with two new clauses: draws are counted per CLASS (town-wide InstancedMesh +1 vs per-chunk +1 x 25 live chunks), and for a SKIN the budget is the SLOT, not bytes (36 facade-atlas slots, 22 used, 14 free — past 36 skins.js silently REUSES one). - pipeline/skin_sheet.py: wrap-preview + measured seam error, the check R38's stretched grounds needed before the harvest instead of after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
30b86a9ef7
commit
7fe6a4a40e
@ -1,5 +1,146 @@
|
||||
# LANE E — cross-lane notes
|
||||
|
||||
## Round 39 — the magpie settled, and the arcade measured ⟨v9 39.4⟩
|
||||
|
||||
Full provenance and method: `pipeline/AUDIT.md` § Round 39. Sheets:
|
||||
`docs/shots/laneE/r39_magpie_ab.png` (the ruling picture) · `docs/shots/laneE/r39_arcade_kit.png`.
|
||||
|
||||
### → FABLE: THE MAGPIE — **RECOMMENDATION: TAKE THE GLB.** The picture is `r39_magpie_ab.png`.
|
||||
|
||||
Both birds shot through the **same rig** (`render_views.py --eevee --noemit`), same sky and sun,
|
||||
same lens, each framed to its own extent so neither gets a size advantage, **emission stripped from
|
||||
both** — and B's bird is **B's own code**, not a port: `pipeline/dump_bird.mjs` imports
|
||||
`web/js/world/magpie.js` unmodified (bare `three` resolved to the repo's own vendored build through
|
||||
a node hook) and dumps `birdGeometry()` vertex for vertex.
|
||||
|
||||
**Three findings, in the order they decide it.**
|
||||
|
||||
1. **The player is looking at the PERCH, not the swoop.** Simulated on magpie.js's own clock
|
||||
(`--sim`: DEFEND_R 34 m, drawn out to 74.8 m, COOLDOWN 5.5 s, SWOOP_T 1.25 s, RETURN_T 1.9 s;
|
||||
60 s walk at WALK 4.6 m/s passing 4 m off the perch): the bird is drawn in **54.1 % of frames**,
|
||||
and **of those it is perched 80.5 % · swooping 7.7 % · returning 11.8 %**. The swoop is the
|
||||
memory. The perch is the picture — and **the perch is the pose E's asset already is** (a standing
|
||||
bird with legs), while B's perch is the flight geometry squashed 0.42× in X.
|
||||
2. **At the sizes it is seen, E reads as a magpie and B reads as a dark aeroplane.** 64 / 48 / 32 px,
|
||||
three views, still and under two linear motion-blur levels. E is pied and unmistakable at 64 and
|
||||
48 and still black-and-white at 32, and survives a 25 % smear. B is a grey dash at every size in
|
||||
every view except top-down.
|
||||
3. **And the mechanism is geometric, which is why no re-tint could have saved it.** Three of B's
|
||||
four white marks cannot be seen from where the player stands:
|
||||
- the **wing bars are 3 mm ABOVE the wing planes** (`0.048` vs `0.045`, both horizontal after
|
||||
`rotateX(-π/2)`, both DoubleSide) — so from **underneath**, which is where the player always
|
||||
is, the black wing occludes its own white bar. `magpie.js`'s header says *"that black-and-white
|
||||
flash is the entire read"*; the flash is on the roof of the wing.
|
||||
- the **nape box** (`0.052×0.014×0.055` at y 0.06, z −0.10) sits **inside the head sphere** except
|
||||
for a ~10 mm sliver — at z = −0.10 the head's surface is at y 0.0728 and the box tops out at 0.065.
|
||||
- the **rump box** (y 0.045–0.065 at z 0.035–0.165) is **inside the body ellipsoid** over its
|
||||
front half (body top at z 0.035 is y 0.0744).
|
||||
|
||||
**Cost of the swap, stated plainly:** **+712 triangles at one instance.** Draws are a **wash** —
|
||||
both are one town-wide `InstancedMesh` at count 0 or 1. The GLB adds +1 fetch, +1 material, +1 512²
|
||||
texture, and **it cannot replace the procedural bird**: `?noassets` and `?classic` still need it. So
|
||||
this is `furniture.js`'s **use-if-ready second path** at `birdGeometry()` / `birdMaterial()`, not a
|
||||
substitution. It is also **no longer a blind probe** — R26's ruling was about probing for a file
|
||||
nobody had shipped; `magpie` has been in `manifest.json` and on the public depot since R38 §7a.
|
||||
|
||||
**The one thing that could flip it, said out loud:** B's bird flaps (the `wing` wind weight) and E's
|
||||
GLB is rigid, and a spread-wing silhouette is more bird-shaped in flight than a folded one. That is
|
||||
worth 7.7 % of the frames. If Fable weighs the swoop above the perch, the answer changes — and in
|
||||
that case the honest outcome is **both**, which costs two paths and a second draw, and I do not
|
||||
recommend it.
|
||||
|
||||
### → LANE B: three asks, and one of them costs zero triangles
|
||||
|
||||
**ASK 1 — `magpie.js` has two stale tri comments.** `:9` and `:123` both say **154 triangles**;
|
||||
`:42`'s table says 182 and **182 is what I measured** — by running your own `birdGeometry()` (216
|
||||
verts, index length 546) and again by `glb_stat.py` on the export. The arithmetic agrees: sphere(8,6)
|
||||
80 + sphere(6,5) 48 + cone(5) 10 + 3 boxes 36 + 4 planes 8 = **182**. *Your file, your edit.*
|
||||
|
||||
**ASK 2 — the white bar is on the wrong side of the wing, and moving it costs +0 tris.** See finding
|
||||
3 above. `magpie.js:147-148` puts each bar at `y 0.048` on a wing whose plane is at `y 0.045`. The
|
||||
player is **always below** the bird (perch height 4.32 m, EYE 1.62 m). Moving the bar to **y ≈ 0.042**
|
||||
makes it visible from below and hidden from above — the correct trade at zero cost. A bar on **both**
|
||||
faces is +8 tris total (4 bars × 2). Same class of fix for the nape (raise y 0.06 → ~0.082, or slide
|
||||
it back to z ≈ −0.06 where a nape actually is) and the rump (raise y 0.055 → ~0.075). **This matters
|
||||
whatever Fable rules on the GLB, because `?classic` and `?noassets` will show the procedural bird
|
||||
forever.**
|
||||
|
||||
**ASK 3 — `skins.js`'s `GROUND_FALLBACK` has no entry for either arcade surface.** `arcade-floor`
|
||||
and `arcade-roof` match none of the nine regexes, so both fall to the generic `#6b6b64` grey under
|
||||
`?noassets` or a 404 — and the house law is that the town looks right with zero assets. Suggested,
|
||||
in your vocabulary and prepended like the R38 entries: `[/arcade-floor|terrazzo/, '#c3b6a2']` and
|
||||
`[/arcade-roof|ceiling/, '#ded7c6']`.
|
||||
|
||||
### → LANE B: THE ARCADE, MEASURED — the numbers before the assets
|
||||
|
||||
Reproduce any of this with **`node pipeline/arcade_measure.mjs`** (reads Lane A's own `generatePlan`;
|
||||
nothing below is estimated). Five seeds.
|
||||
|
||||
| | measured |
|
||||
|---|---|
|
||||
| lane | **ONE edge, `kind:'arcade'`, 42.00 m × 5 m**, 2 blocks (one per side) |
|
||||
| shops | **16–17** (17 at seed 20261990 and 20260101, 16 at 7/123456/999) |
|
||||
| frontage | 3.00–4.94 m, **mean 3.87–4.05 m** · depth 5.10–7.99 m · 4–11 of them two-storey |
|
||||
| walkable | lot faces at ±2.50 m ⇒ **5.00 m** between shopfronts |
|
||||
| roof | `AWNING_DEPTH 2.2` from each face ⇒ the two slabs reach ±0.30 m ⇒ **already roofed except a 0.60 m slot** down the centreline, at **y 2.95 m** |
|
||||
| floor | `ground.js`'s arcade branch emits **one** footpath quad, 42.00 × 12.00 m = **504 m²** — the biggest single surface in the district, wearing `footSkin` |
|
||||
|
||||
> **⚠ THE AWNING POSTS ARE IN THE MIDDLE OF THE ARCADE.** `buildings.js:643` plants them at
|
||||
> `d/2 + AWNING_DEPTH − 0.2` = **2.00 m out from each lot face**, i.e. **±0.50 m from the
|
||||
> centreline** — **two rows of posts 1.00 m apart down a 5.00 m lane.** They carry no collider
|
||||
> (`colliders.push(lotCollider(lot))` is the only one), so the charter's 4.99 m walkable stands and
|
||||
> nothing traps the player; it just *reads* as a colonnade planted in its own doorway. `AWNING_DEPTH
|
||||
> 2.2` was sized against `ground.js`'s 3.5 m `FOOT` band, not a 2.5 m half-lane. **A real arcade's
|
||||
> roof spans; it has no posts.** The cheapest fix is a district-keyed depth (or no posts at all for
|
||||
> `district.kind === 'arcade'`), and it is +0 draws either way — two `boxMats` entries fewer per lot.
|
||||
|
||||
**The synthetic-only rule, as a code rule.** `plan_osm.js:305` is one unconditional
|
||||
`addDistrict('mainstreet')`, so **no cache town has an arcade DISTRICT**. Real towns *do* have
|
||||
**1,102 arcade-KIND edges = 39 km of ordinary suburban footpath**. Key every piece of arcade
|
||||
dressing on `district.kind`, never on `edge.kind` (the v9 cut list, restated with the number).
|
||||
|
||||
### → LANE B: the kit, sized by `tris × the instance count YOUR placement rule implies`
|
||||
|
||||
Ranked by **value per draw**, because the margin is eight and this is one district on one town.
|
||||
|
||||
| # | asset | form | instances | cost |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **`facade-shuttered`** | facade skin, 768×595 | 1–2 shops | **+0 draws, +0 tris**, 1 of the **14 free** facade-atlas slots |
|
||||
| 2 | **`ground-arcade-floor`** | ground skin, 512² | 1 class | **+1 draw town-wide**, **+0 tris** (the 504 m² quad *moves* out of `footGeos`) |
|
||||
| 3 | **`ground-arcade-roof`** | ground skin, 512² | 1 quad | **+1 draw in the 1–2 chunks that hold arcade lots**, **+2 tris** |
|
||||
| 4 | **hanging blade signage** | ***no asset — see below*** | 17 | **+0 draws, +0 assets, +4 tris/shop = 68** |
|
||||
| 5 | **`aframe_board`** | GLB | ≤16/town | +1 draw as ONE town-wide `InstancedMesh` |
|
||||
| 6 | **`keycutter_sign`** | GLB | 1 | +1 draw for one object — **lowest value in the kit** |
|
||||
|
||||
**#4 is a refusal with a better answer, and it is the most useful thing in this table.** A blade
|
||||
sign does not need a mesh or a texture. `buildings.js` already draws every shop's sign into a
|
||||
per-chunk 512×168 canvas atlas cell and pushes ONE quad into the merged `signQuads` mesh. **Push a
|
||||
SECOND quad, perpendicular to the facade, with the same `signUV.get(shop.id)` rect** and you get a
|
||||
projecting sign carrying the shop's real name for **+0 draws** (it rides the mesh that already
|
||||
exists) and **+2 tris single-sided / +4 back-to-back** — **68 triangles for all 17 shops.** The
|
||||
bracket is two `boxMats` boxes, which is also +0 draws. A GLB blade sign at 17 instances cannot beat
|
||||
that; it can only look different. Generated anyway (see AUDIT §Round 39) so the option exists, but
|
||||
**wire the quad first**.
|
||||
|
||||
**#2 and #3's `use` values name slots that do not exist yet — deliberately.** R38 recorded `gravel`
|
||||
(`use:'verge'`) and `reddust` (`use:'outback'`) as selected by nothing *by accident*. These two are
|
||||
selected by nothing *on purpose and with the price printed*: the arcade cannot ride `footpath`,
|
||||
because that is one town-wide merged mesh and re-using it would lay terrazzo over every footpath in
|
||||
the town. Both must also be **classic-gated** — `?classic=1` does not move on dressing.
|
||||
|
||||
### → EVERY LATER ASSET ROUND: the sizing law, now standing in AUDIT
|
||||
|
||||
Folded into `pipeline/AUDIT.md` as a **standing instruction**, not a round note: *get the instance
|
||||
count from the consuming lane BEFORE generating anything that tiles, edges, or repeats per-lot.*
|
||||
R39 is the first round that ran it as a script (`pipeline/arcade_measure.mjs`) rather than as a
|
||||
conversation, and it changed the deliverable — item #4 above stopped being an asset.
|
||||
|
||||
**And a second clause the arcade added to it: for a SKIN the budget is not bytes, it is the SLOT.**
|
||||
`skins.js:facadeAtlasUV` has **36 slots (6×6)** and past 36 it `console.warn`s and **reuses one** —
|
||||
so a 37th facade skin does not fail, it silently repaints an existing shop. Measured: the synthetic
|
||||
uses **22 distinct facade skins** (21 at seed 123456), so there are **14 free**. That is the real
|
||||
budget line for a facade skin, and it is the number to ask for before generating one.
|
||||
|
||||
## Round 38 — v8 wave 1: the asset wave ⟨v8 WAVE 1⟩
|
||||
|
||||
Full provenance, measurements and method: `pipeline/AUDIT.md` § Round 38. Contact sheets:
|
||||
|
||||
BIN
docs/shots/laneE/r39_arcade_skins.png
Normal file
BIN
docs/shots/laneE/r39_arcade_skins.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 564 KiB |
@ -830,3 +830,162 @@ and in the manifest — harmless, provenance-recorded, available if a low-count
|
||||
|
||||
**Licence unchanged: 🟢.** The tint is our own numeric edit of our own baked atlas; no asset, no
|
||||
model and no third-party image entered the pipeline for it. **$0 — no cloud key touched.**
|
||||
|
||||
## Round 39 — the magpie A/B, and the arcade's kit (v9 39.4)
|
||||
|
||||
*Every number below was measured on this box today. `$0` — no cloud key touched, no third-party
|
||||
asset or image entered the pipeline, and the 3GOD depot was **not** read as a pool (it stays the
|
||||
read-only archive R38 declared it).*
|
||||
|
||||
### 0. STANDING INSTRUCTION — the sizing law, promoted out of the round notes
|
||||
|
||||
**Before generating anything that tiles, edges, repeats per lot, or is placed by a rule rather than
|
||||
by hand: get the instance count from the CONSUMING LANE. The budget is `tris × that count`, not
|
||||
"≤5 k like a prop".** R38 paid for this with `paling_fence` — a good 1,121-tri panel that became a
|
||||
**rejected asset** at 5 runs × 181 lots = 905 instances = 1.01 M triangles. Two clauses were added
|
||||
in R39 and both are now part of the law:
|
||||
|
||||
1. **Draws are counted per CLASS, and where the class lives decides everything.** A GLB cannot ride
|
||||
`boxMats` (BoxGeometry, one shared untextured shell material), so every distinct GLB geometry is
|
||||
+1 draw. As ONE town-wide `InstancedMesh` (`magpie.js`'s pattern) that is +1 **total**; as a
|
||||
per-chunk furniture class it is +1 × up to **25 live chunks**, which against 8 draws of margin is
|
||||
a breach, not a cost. Ask which one before generating.
|
||||
2. **For a SKIN the budget is not bytes, it is the SLOT.** `skins.js:facadeAtlasUV` has **36 slots
|
||||
(6×6)**; past 36 it `console.warn`s and **reuses one**, i.e. a 37th facade skin silently
|
||||
repaints an existing shop. Measured: the synthetic uses **22 distinct facade skins** (21 at seed
|
||||
123456) ⇒ **14 free**. That is the real budget line for a facade skin.
|
||||
|
||||
The law is now runnable rather than conversational: **`node pipeline/arcade_measure.mjs`** reads
|
||||
Lane A's own `generatePlan` and prints the counts. It changed this round's deliverable — one of the
|
||||
four commissioned assets stopped being an asset (§3, item 4).
|
||||
|
||||
### 1. THE MAGPIE A/B — rendering Lane B's own geometry, not a re-implementation of it
|
||||
|
||||
The R38 debt was *"two magpies exist and nobody has seen them side by side."* The trap in settling
|
||||
it is that a hand-ported strawman proves nothing, so B's bird is not ported:
|
||||
|
||||
- **`pipeline/dump_bird.mjs`** imports `web/js/world/magpie.js` **unmodified** and calls the real
|
||||
`createMagpie()`. The bare `three` / `three/addons/` specifiers its imports use are resolved to
|
||||
the repo's **own vendored build** (`web/vendor/three.module.js`) through a `node:module`
|
||||
`registerHooks` resolver — the importmap from `index.html:22-25`, as a node resolver. The vertices
|
||||
dumped are the vertices the browser gets.
|
||||
**Measured: 182 triangles, 216 verts, bbox 0.680 × 0.166 × 0.670 m.** The arithmetic agrees
|
||||
independently: sphere(8,6) = 8×(2·6−2) = 80 · sphere(6,5) = 48 · cone(5) = 5 side + 5 cap = 10 ·
|
||||
3 boxes = 36 · 4 planes = 8 ⇒ **182**. `magpie.js:9` and `:123` say **154** and are stale.
|
||||
- **`pipeline/bird_to_glb.py`** wraps the dump as a GLB **in E's frame** (glTF +Y up, head at +Z;
|
||||
three's frame is +Y up, nose at −Z, and Blender's `export_yup` writes `glTF = (Bx, Bz, −By)`, so
|
||||
the Blender vertex is `(−xt, zt, yt)` — a 180° yaw, det +1, winding preserved). Without that the
|
||||
two rows of the sheet would be shot from opposite ends of the bird. `--fold` applies the perch
|
||||
pose (`place(..., folded)` scales the instance X by 0.42). `glb_stat.py` re-measures **182**.
|
||||
- **`pipeline/render_views.py --noemit`** strips emission from **both** candidates, and this is the
|
||||
correction that makes the picture usable at all. `normalize.py` gives every baked GLB
|
||||
`emissiveFactor 0.28` **with an `emissiveTexture`** — 0.28 × its own albedo, so blacks stay black
|
||||
and whites brighten. A vertex-coloured mesh **cannot express that in glTF** (COLOR_0 multiplies
|
||||
base colour only), and Blender's exporter silently writes a **flat 0.28**, which lifted B's black
|
||||
bird to grey and produced a first sheet that was worthless in E's favour. Off both sides. The
|
||||
consequence, stated on the sheet: **a baked GLB reads slightly better in game than it does here** —
|
||||
the safe direction to be wrong in.
|
||||
- **`pipeline/view_sheet.py --ab`** lays the distance strip out as a **grid** (one line per
|
||||
candidate, same view and same pixel size stacked) and repeats it under **linear motion blur** —
|
||||
a true N-position average, not a gaussian, with the smear length scaled to the SUBJECT's on-screen
|
||||
width. Two levels, both derived: **25 %** (the approach) and **100 %** (the pass — the swoop
|
||||
covers 34 m in 1.25 s = 27.3 m/s = 0.45 m per 60 fps frame ≈ one body length).
|
||||
|
||||
**The measurement that decided it** (`dump_bird.mjs --sim`, magpie.js's own clock: DEFEND_R 34 m,
|
||||
drawn out to 74.8 m, COOLDOWN 5.5 s, SWOOP_T 1.25 s, RETURN_T 1.9 s; a 60 s walk at WALK 4.6 m/s
|
||||
passing 4 m off the perch):
|
||||
|
||||
> the bird is drawn in **1,949 of 3,600 frames (54.1 %)** — and of those it is
|
||||
> **perched 1,569 (80.5 %) · swooping 150 (7.7 %) · returning 230 (11.8 %)**.
|
||||
|
||||
**And the mechanism, which is why no re-tint could have saved B's bird.** Three of its four white
|
||||
marks cannot be seen from where the player stands:
|
||||
|
||||
| mark | `magpie.js` | why it does not read |
|
||||
|---|---|---|
|
||||
| wing bars ×2 | `:147-148`, plane at **y 0.048** | the wing plane is at **y 0.045** and both are horizontal (`rotateX(-π/2)`) and DoubleSide ⇒ **from underneath the black wing occludes its own white bar**, and the player is always underneath (perch 4.32 m, EYE 1.62 m) |
|
||||
| nape | `:140`, box `0.052×0.014×0.055` at y 0.06, z −0.10 | at z −0.10 the head sphere's surface is **y 0.0728** and the box tops out at **0.065** ⇒ inside it but for a ~10 mm sliver |
|
||||
| rump | `:141`, box y 0.045–0.065, z 0.035–0.165 | at z 0.035 the body ellipsoid's top is **y 0.0744** ⇒ the front half is inside the body |
|
||||
|
||||
**VERDICT — Lane E recommends TAKING THE GLB**, with the cost stated: **+712 tris at one instance**,
|
||||
draws a **wash** (both are one town-wide `InstancedMesh` at count 0 or 1), **+1 fetch / +1 material /
|
||||
+1 512² texture**, and it is a **use-if-ready second path**, not a substitution — `?noassets` and
|
||||
`?classic` still need the procedural bird. No blind probe is implied: `magpie` has been in
|
||||
`manifest.json` and on the public depot since R38 §7a, so this is `furniture.js`'s existing shape.
|
||||
Sheet: **`docs/shots/laneE/r39_magpie_ab.png`**. Fable rules.
|
||||
|
||||
### 2. THE ARCADE, MEASURED (`pipeline/arcade_measure.mjs`, 5 seeds, off Lane A's `generatePlan`)
|
||||
|
||||
| | measured |
|
||||
|---|---|
|
||||
| lane | ONE edge `kind:'arcade'`, **42.00 m × 5 m**, 2 blocks |
|
||||
| shops | **16–17** (17 at 20261990 / 20260101; 16 at 7 / 123456 / 999) |
|
||||
| frontage | 3.00–4.94 m, mean **3.87–4.05 m**; depth 5.10–7.99 m; 4–11 two-storey |
|
||||
| walkable | lot faces at ±2.50 m ⇒ **5.00 m** |
|
||||
| roof | `AWNING_DEPTH 2.2` from each face ⇒ slabs reach ±0.30 m ⇒ **roofed except a 0.60 m centre slot**, ceiling **y 2.95 m** |
|
||||
| floor | `ground.js`'s arcade branch: ONE quad **42.00 × 12.00 m = 504 m²**, wearing `footSkin` |
|
||||
| ⚠ posts | `buildings.js:643` at `d/2 + 2.2 − 0.2` ⇒ **±0.50 m from the centreline: two post rows 1.00 m apart down a 5.00 m lane.** No collider, so it walks — it just reads as a colonnade in its own doorway. → Lane B |
|
||||
|
||||
**Synthetic-only as a code rule:** `plan_osm.js:305` is one unconditional `addDistrict('mainstreet')`
|
||||
⇒ no cache town has an arcade DISTRICT, while the corpus has **1,102 arcade-KIND edges = 39 km of
|
||||
ordinary suburban footpath**. Key on `district.kind`, never `edge.kind`.
|
||||
|
||||
### 3. THE KIT — ranked by value per draw, because the margin is eight
|
||||
|
||||
| # | asset | form | instances | cost | licence |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | `facade-shuttered` | facade skin **768×595** | 1–2 shops | **+0 draws, +0 tris**, 1 of 14 free atlas slots | 🟢 |
|
||||
| 2 | `ground-arcade-floor` | ground skin **512²** | 1 class | **+1 draw town-wide**, **+0 tris** (the 504 m² quad *moves* class) | 🟢 |
|
||||
| 3 | `ground-arcade-roof` | ground skin **512²** | 1 quad | **+1 draw in the 1–2 arcade chunks**, **+2 tris** | 🟢 |
|
||||
| 4 | hanging blade signage | **no asset — a refusal with a better answer** | 17 | **+0 draws, +0 assets, +4 tris/shop = 68** | — |
|
||||
|
||||
**Item 4 is the most useful line in the table.** `buildings.js` already draws each shop's sign into
|
||||
a per-chunk 512×168 canvas atlas cell and pushes ONE quad into the merged `signQuads` mesh. Push a
|
||||
**second, perpendicular quad with the same `signUV.get(shop.id)` rect** and the arcade gets 17
|
||||
projecting signs **carrying each shop's real name**, riding a mesh that already exists: **+0 draws,
|
||||
+2 tris single-sided / +4 back-to-back = 68 triangles for all 17.** The bracket is two `boxMats`
|
||||
boxes, also +0 draws. A GLB blade sign at 17 instances cannot beat that; it can only look different.
|
||||
|
||||
**Aspect is not decoration.** `skins.js` paints every facade into a **square** 341 px atlas slot, and
|
||||
arcade lots are 3.87 × 3.0 m ⇒ **1.29:1**, against the main-street pool's 1.78:1 — a 38 % horizontal
|
||||
stretch if shot to the pool. `facade-shuttered` is therefore shot at 1.29:1 and lands nearly
|
||||
undistorted (1024×768 → square → 1.29:1 quad ≈ 1.03 net). Recorded; not retro-fitted to the other 48.
|
||||
|
||||
**`use` values that name no slot — this time on purpose.** R38 recorded `gravel`/`reddust` as
|
||||
selected by nothing *by accident*. `arcade-floor` and `arcade-ceiling` are selected by nothing *with
|
||||
the price printed*: the arcade cannot ride `footpath` (one town-wide merged mesh — re-using it lays
|
||||
terrazzo over every footpath in town), so each needs its own class and each is classic-gated.
|
||||
|
||||
### 4. `pipeline/seamless_tile.py` — and the guard it needed
|
||||
|
||||
A tiled floor is the one skin class where the repeat seam is a *mistake* rather than a texture:
|
||||
grass hides a joint, terrazzo does not, and the arcade floor is 504 m². The tool finds the pattern's
|
||||
pitch by autocorrelating the mean-absolute column/row gradient (a grout line is a gradient spike) and
|
||||
crops to a whole number of periods — arithmetic instead of feathering, which would smear the grout.
|
||||
|
||||
**It needed a guard, because the naive version made things worse and said nothing.** Measured:
|
||||
|
||||
| skin | pitch found (corr) | seam before → after | kept |
|
||||
|---|---|---|---|
|
||||
| `ground-arcade-roof` | x 212 px (r 0.25) | **x 26.7 → 8.3** | ✅ x cropped to 848 |
|
||||
| `ground-arcade-floor` | x 196 (r 0.26) / y 108 (r 0.30) | **x 27.9 → 31.9**, y 29.6 → 33.4 | ❌ rejected, passed through |
|
||||
|
||||
So the crop is now a **proposal that has to beat the number it is trying to improve, per axis**, and
|
||||
the tool prints `NO IMPROVEMENT AVAILABLE, passed through` when it cannot. **The finding underneath
|
||||
it:** a diffusion-drawn tiled floor is **not periodic enough to cut on** — the model draws tiles with
|
||||
perspective and irregular spacing, so the autocorrelation peaks weakly (r ≈ 0.26) and at the wrong
|
||||
lag. **Fix path for a later round, stated not done:** synthesise the terrazzo procedurally in numpy
|
||||
(the `gen_audio.py` doctrine — original by construction, exactly periodic, 🟢), instead of
|
||||
photographing it with a diffusion model. The shipped floor is the flux one, consistent with the ten
|
||||
photographic grounds already in the pool, seam measured and recorded rather than hidden.
|
||||
|
||||
### 5. LICENCE — every R39 asset lands 🟢
|
||||
|
||||
| class | count | zone | method |
|
||||
|---|---|---|---|
|
||||
| skins | 3 | **🟢 original** | `flux_local` / FLUX.2-klein-4B (Apache-2.0, ungated), on-device |
|
||||
| meshes | see §6 | **🟢 CC0-equivalent** (`AUDIT.md:101` doctrine) | `flux_local` concept → `bg_remove_local` (RMBG-2.0) → `trellis2_mlx` (TRELLIS.2-4B) → `normalize.py` → `bake_lowpoly.py` |
|
||||
| tooling | 4 files | — | `dump_bird.mjs`, `bird_to_glb.py`, `arcade_measure.mjs`, `seamless_tile.py` — all ours |
|
||||
|
||||
**$0 spent.** No cloud key touched. The hardyards set stays REJECTED on provenance (R38), and the
|
||||
3GOD depot stays a read-only archive.
|
||||
|
||||
26
pipeline/batch_bake_r39.json
Normal file
26
pipeline/batch_bake_r39.json
Normal file
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"src": "pipeline/_normalized/procity_street_aframe_board_01.glb",
|
||||
"id": "aframe_board",
|
||||
"category": "street",
|
||||
"voxel": 0.012,
|
||||
"tris": 400,
|
||||
"img": 512
|
||||
},
|
||||
{
|
||||
"src": "pipeline/_normalized/procity_street_blade_sign_01.glb",
|
||||
"id": "blade_sign",
|
||||
"category": "street",
|
||||
"voxel": 0.010,
|
||||
"tris": 300,
|
||||
"img": 512
|
||||
},
|
||||
{
|
||||
"src": "pipeline/_normalized/procity_street_keycutter_sign_01.glb",
|
||||
"id": "keycutter_sign",
|
||||
"category": "street",
|
||||
"voxel": 0.008,
|
||||
"tris": 500,
|
||||
"img": 512
|
||||
}
|
||||
]
|
||||
23
pipeline/batch_r39.json
Normal file
23
pipeline/batch_r39.json
Normal file
@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/aframe-board.glb",
|
||||
"id": "aframe_board",
|
||||
"category": "street",
|
||||
"height_m": 0.9,
|
||||
"tris": 400
|
||||
},
|
||||
{
|
||||
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/blade-sign.glb",
|
||||
"id": "blade_sign",
|
||||
"category": "street",
|
||||
"height_m": 0.55,
|
||||
"tris": 300
|
||||
},
|
||||
{
|
||||
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/keycutter-sign.glb",
|
||||
"id": "keycutter_sign",
|
||||
"category": "street",
|
||||
"height_m": 0.6,
|
||||
"tris": 500
|
||||
}
|
||||
]
|
||||
79
pipeline/skin_sheet.py
Normal file
79
pipeline/skin_sheet.py
Normal file
@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PROCITY Lane E — skin_sheet.py (R39)
|
||||
|
||||
Contact sheet for 2-D skins that answers the two questions a thumbnail cannot: **does it tile**, and
|
||||
**what aspect is it actually displayed at**. R38 shipped four grounds and only afterwards recorded
|
||||
that the eight flux-era ones are 512x279 and stretch under a square UV tile — that is the class of
|
||||
defect this sheet exists to catch before a harvest, not after.
|
||||
|
||||
PY=~/Documents/MODELBEAST/venvs/mflux/bin/python
|
||||
$PY pipeline/skin_sheet.py OUT.png "title" FILE.jpg "caption" [FILE "caption" ...] [--note "line" ...]
|
||||
|
||||
Each row: the skin as shipped, a 3x3 wrap preview (what RepeatWrapping does to it), and the measured
|
||||
wrap-seam error - mean |difference| between the opposite edges, 0-255, printed as a number rather
|
||||
than left to the eye.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
PAD, LBL, TILE = 12, 15, 220
|
||||
|
||||
|
||||
def seam(a):
|
||||
x = float(np.abs(a[:, 0].astype(np.float32) - a[:, -1].astype(np.float32)).mean())
|
||||
y = float(np.abs(a[0].astype(np.float32) - a[-1].astype(np.float32)).mean())
|
||||
return x, y
|
||||
|
||||
|
||||
def main():
|
||||
out_path, title = sys.argv[1], sys.argv[2]
|
||||
args = sys.argv[3:]
|
||||
notes = []
|
||||
if "--note" in args:
|
||||
i = args.index("--note")
|
||||
notes, args = args[i + 1:], args[:i]
|
||||
rows = [(args[i], args[i + 1]) for i in range(0, len(args), 2)]
|
||||
W = PAD + TILE + PAD + TILE + PAD + 700
|
||||
H = 26 + len(rows) * (TILE + LBL + PAD) + PAD + LBL * (sum(len(n) // 110 + 2 for n in notes) + 1)
|
||||
sheet = Image.new("RGB", (W, H), (24, 24, 26))
|
||||
d = ImageDraw.Draw(sheet)
|
||||
d.text((PAD, 6), title, fill=(240, 240, 240))
|
||||
y = 26
|
||||
for path, cap in rows:
|
||||
im = Image.open(path).convert("RGB")
|
||||
a = np.asarray(im)
|
||||
sx, sy = seam(a)
|
||||
# as shipped, letterboxed into a square cell so a non-square skin LOOKS non-square
|
||||
cell = Image.new("RGB", (TILE, TILE), (40, 40, 44))
|
||||
s = im.copy()
|
||||
s.thumbnail((TILE, TILE), Image.LANCZOS)
|
||||
cell.paste(s, ((TILE - s.width) // 2, (TILE - s.height) // 2))
|
||||
sheet.paste(cell, (PAD, y))
|
||||
# 3x3 wrap preview
|
||||
t = Image.new("RGB", (im.width * 3, im.height * 3))
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
t.paste(im, (i * im.width, j * im.height))
|
||||
sheet.paste(t.resize((TILE, TILE), Image.LANCZOS), (PAD + TILE + PAD, y))
|
||||
x0 = PAD + 2 * (TILE + PAD)
|
||||
d.text((x0, y + 2), os.path.basename(path), fill=(250, 210, 120))
|
||||
d.text((x0, y + 2 + LBL), f"{im.width}x{im.height} {im.width / im.height:.2f}:1 "
|
||||
f"{os.path.getsize(path)} B wrap-seam x={sx:.1f} y={sy:.1f} (0-255)",
|
||||
fill=(180, 180, 180))
|
||||
for k, line in enumerate(textwrap.wrap(cap, 96)):
|
||||
d.text((x0, y + 2 + LBL * (2 + k)), line, fill=(215, 215, 215))
|
||||
d.text((PAD + 3, y + TILE + 1), "as shipped", fill=(150, 150, 150))
|
||||
d.text((PAD + TILE + PAD + 3, y + TILE + 1), "3x3 wrap (RepeatWrapping)", fill=(150, 150, 150))
|
||||
y += TILE + LBL + PAD
|
||||
for line in notes:
|
||||
for part in textwrap.wrap(line, 150):
|
||||
d.text((PAD, y), part, fill=(190, 190, 190))
|
||||
y += LBL
|
||||
sheet.crop((0, 0, W, min(sheet.height, y + PAD))).save(out_path)
|
||||
print(f"skin sheet -> {out_path}")
|
||||
|
||||
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user