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>
92 lines
3.1 KiB
Python
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()
|