PROCITY/pipeline/view_sheet.py
m3ultra 74d6257940 Lane E R38 s2: the three GLBs PUBLISHED + sha1-verified off the public depot; the magpie is a magpie
A. DEPOT — closed, under John's R22 standing Lane-E authorization (the authority that
   shipped sit.glb in R28 and the nine R36 sweep assets). Pushed via the passwordless
   tailnet ingress: _published.json 53 -> 56, drift check clean. Then verified the way
   this lane verifies — FETCHED BACK OFF THE PUBLIC DEPOT and hashed, not "the upload
   said 200": magpie/paling_fence/water_tank all HTTP 200, byte counts exact, sha1
   identical to local, 3/3. _r38_results.PENDING_PUBLISH.json renamed, build_manifest
   re-run (GLBs 33 -> 36), validate_manifest 0 errors / 0 warnings.

B. THE MAGPIE TINT — done, not offered. The bird shipped as an all-black crow (R38 4d
   traced the loss to trellis2_mlx's PBR bake). "Hand-tint the atlas" is not a paint job:
   the bake is a smart-UV unwrap of ~100 islands and none of them announces itself as a
   nape. So pipeline/tint_atlas.py defines the tint in the MESH's frame and projects it
   through the UVs — rasterize every UV triangle to texel->(position,normal), evaluate
   soft anatomical windows on the positions (white nape behind a black crown, shoulder
   bar on the flanks only so the dorsal midline stays black, rump + tail base stopping
   short of the black terminal band), paint keeping the bake's own luminance as shading,
   then dilate 5 texels into the UV padding so no black fringe survives bilinear/mip.
   3-D masks give one consistent soft edge across every island at once; a 2-D blur could
   not (it would bleed island-to-island through the black background).
   GEOMETRY UNTOUCHED byte for byte: still 894 tris / 1 mtl / 1 img / 512^2 WebP / no
   Draco / 0.155x0.358x0.404 m. Painted luma median 0.84 (p90 0.95) vs the bake's 0.13.
   VERIFIED IN THE GAME'S OWN RENDERER: the PUBLISHED file loaded off digalot.fyi through
   the vendored GLTFLoader + three.js — 1 mesh, 894 tris, 512x512 texture decoded (which
   also proves the PIL-written WebP decodes in a browser).
   VERDICT: it reads. Pied and unmistakable at 64 and 48 px, still black-and-white at 32.
   Honest caveat: from directly below-and-in-front it stays dark — correct, a real
   magpie's bib and belly ARE black. Render: docs/shots/laneE/r38_magpie_tint.png.
   Standing after B's 4514cb9: an UPGRADE PATH, not a dependency — B's 182-tri procedural
   bird is 4.9x lighter and the right call. Nothing probes for the GLB and nothing should.

C. CORRECTIONS FOLDED INTO THE RECORD so the next asset round does not re-learn them:
   skins are 9.7 s not 5.7 s (1024^2, 1.78x the pixels, per-pixel identical) - the reuse
   list's "one line each" holds for 5 of 9 (extractGLB keeps only the FIRST material) -
   normalize.py's collapse decimator floors at the shell count on TRELLIS output while
   bake_lowpoly.py clears it every time - the 489 s outlier at 3.8x median means
   throughput plans on a tail, not a mean - furniture.js:30's stale comment is fixed by B.
   NEW, and the one worth keeping: an asset's tri budget is tris x THE INSTANCE COUNT THE
   PLACEMENT RULE IMPLIES. paling_fence is a good 1,121-tri panel and a rejected asset —
   905 instances = 1.01M tris, ~47x over. Ask the consuming lane for the instance count
   BEFORE generating anything that tiles, edges or repeats per-lot.

New tools, all on-device and $0: tint_atlas.py, render_views.py (view sheet + --eevee +
roster-consistent --thumb), view_sheet.py (the distance strip no 256px thumbnail can make).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:02:06 +10:00

92 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Compose render_views.py output into one labelled contact sheet + a distance strip.
PY=~/Documents/MODELBEAST/venvs/mflux/bin/python
$PY pipeline/view_sheet.py OUT.png "title" "row label" a.png.views.txt ["row 2" b...txt] ...
One row of views per input (each composited over sky — bird-against-sky is the contrast that
matters), then a strip that re-samples the swoop / flank / rear views to 64 / 48 / 32 px and
magnifies them NEAREST: a 0.36 m bird at 3-10 m on a 1080-high screen is 50-100 px, so 64 px is
the honest test of whether a marking reads and 32 px is the pessimistic one.
"""
import sys
from PIL import Image, ImageDraw
SKY = (158, 184, 219)
PAD, LBL = 10, 16
STRIP = ("swoop", "flank", "rear34")
SIZES = (64, 48, 32)
CELL = 112
def over_sky(p, bg=SKY):
im = Image.open(p).convert("RGBA")
out = Image.new("RGB", im.size, bg)
out.paste(im, (0, 0), im)
return out
def load(path):
views = {}
for line in open(path):
k, v = line.rstrip("\n").split("\t")
if k != "LABEL":
views[k] = over_sky(v)
return views
def main():
out_path, title = sys.argv[1], sys.argv[2]
rows = []
args = sys.argv[3:]
extra = None
if "--extra" in args: # --extra IMG.png "caption": pasted under the sheet
i = args.index("--extra")
extra = (args[i + 1], args[i + 2])
args = args[:i]
for i in range(0, len(args), 2):
rows.append((args[i], load(args[i + 1])))
w, h = next(iter(rows[0][1].values())).size
ncol = max(len(v) for _, v in rows)
n_strip = sum(len([n for n in STRIP if n in v]) * len(SIZES) for _, v in rows)
ex = Image.open(extra[0]).convert("RGB") if extra else None
W = max(PAD + ncol * (w + PAD), PAD + n_strip * (CELL + PAD), (ex.width + 2 * PAD) if ex else 0)
H = 20 + len(rows) * (h + LBL + PAD) + LBL + CELL + 18 + PAD
if ex:
H += LBL + ex.height + PAD
sheet = Image.new("RGB", (W, H), (24, 24, 26))
d = ImageDraw.Draw(sheet)
d.text((PAD, 4), title, fill=(240, 240, 240))
y = 22
for label, views in rows:
d.text((PAD, y), label, fill=(250, 210, 120))
y += LBL
for i, (n, im) in enumerate(views.items()):
x = PAD + i * (w + PAD)
sheet.paste(im, (x, y))
d.text((x + 4, y + 2), n, fill=(40, 40, 40))
y += h + PAD
d.text((PAD, y), "AT THE SIZE IT IS ACTUALLY SEEN (rendered px, magnified NEAREST):",
fill=(250, 210, 120))
y += LBL
i = 0
for label, views in rows:
for n in STRIP:
if n not in views:
continue
for s in SIZES:
x = PAD + i * (CELL + PAD)
sheet.paste(views[n].resize((s, s), Image.LANCZOS).resize((CELL, CELL), Image.NEAREST),
(x, y))
d.text((x + 3, y + CELL + 2), f"{label[:9]} {n} {s}px", fill=(175, 175, 175))
i += 1
if ex:
y += CELL + 18
d.text((PAD, y), extra[1], fill=(250, 210, 120))
sheet.paste(ex, (PAD, y + LBL))
sheet.save(out_path)
print(f"sheet -> {out_path} {sheet.size}")
main()