PROCITY/pipeline/thumbs.py
m3ultra 8ad446a48a Lane E R41 (§41.1+§41.2): 46 clips and 110 fittings — and every handover number was the wrong unit
MOTION (§41.1). The clip library goes 8 -> 46: ten idles, eight browse, eight sit/lean,
eight social, six locomotion, six venue, in SIX grouped GLBs (one fetch each), 3.35 MB —
LESS than the 4.29 MB the old eight cost, via lossless dedup + int16 rotations (worst
error 0.0034 deg). All six verified skeleton-only (tris 0, meshes 0, nodes 66): ZERO DRAW,
which is what makes this round affordable. No retarget was run and none was wanted — the
bank and the peds are the same mixamorig skeleton, so retargeting would add error AND bake
the ped mesh into the clip, ending zero-draw. MIRPAMO 'make smoke' green to prove the tool,
then deliberately unused. Three seeds in the brief were duds and got substituted: the whole
*_Degree_Turn set is RIFLE-AIMING, and two 'examine' clips are 262 KB static poses, not
motion. R16 flat-body trap re-checked on all 46 (spine tilt 24 samples/clip, 0 frames >75
deg); turn_in_place auto-demoted from loopable on a 36.7 deg seam.

RULING 1 — the Bandai hazard, closed non-destructively and better than specified. 3,077 CC
BY-NC clips sat unzoned in a neutrally-named path. Renamed PER FILE (3,077/3,077) not just
the directory, because mirpamo names output <rig>@<clip>.glb — the _NC-research marker now
PROPAGATES into any retargeted GLB automatically. Nothing deleted; ultra's red/bandai
re-verified as canonical. Ledger corrected: the manifest claimed CC-BY-NC-ND, the bundled
licence text says CC BY-NC, no ND.

PROPS (§41.2). 110 assets from three libraries, published, sha1-verified, 0 validator errors
both modes. THE FINDING: every handover number was a TRIANGLE count, and triangles were
never the binding constraint — DRAW CALLS were. As handed over this cargo cost 14,140 draws
against a 162-draw margin; it ships at 117. '3dstore passes as-is' was true on tris/metres/
Draco/textures and FALSE on draws — 30 of 46 files carried up to 16 materials on one mesh
(one per record sleeve), so a 1,020-tri tub cost 16 draws; baked to COLOR_0, 288 -> 40, zero
tri drift, A/B identical. dj-gear's own manifest claimed median 6,288 tris / 18 under budget;
measured 51,528 and 12, with draws to 2,145. Pub props were NOT metre-correct (every source
unit-normalised to max dim 1.00 m) and 40x decimation was impossible as specified (loungeChair
is 94% non-manifold).

Ruling 5 vs the draw budget was a real conflict (one mixer = 4.6x a whole room); resolved by
joining SCENERY selectively while every control node keeps its object/name/pivot — verified
by parsing the SHIPPED GLB, not the tool that wrote it: PASS 4 / STATIC 10 / N/A 17 / FAIL 0.
deck_1200_rigged = 5 draws with Platter_SPIN, Arm_YAW, Fader_PITCH, Btn_STARTSTOP intact.

The R40 four-surface transmission gate FIRED ON LIVE CARGO: 5 genuinely transmissive
materials (cartridge dust-covers, a mixer meter window) that would each have doubled every
opaque draw in the room. 9 assets rejected on eyeball after decimation rather than shipping
mush. Two existing-tooling bugs fixed: normalize.py's yaw/up were silent no-ops (the jukebox
exported 0.40 m instead of 0.97 m), and footprints now measure the shipped GLB.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 18:10:12 +10:00

113 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Render a 256px manifest thumbnail for each GLB — many per Blender session.
Separate from `normalize.py`'s `render_thumb` on purpose: that one is part of the normalize pass
and shades with BLENDER_WORKBENCH `color_type='TEXTURE'`, which renders a **vertex-coloured,
material-less** mesh flat grey. Two of R41's three libraries are exactly that (the 3dstore
fittings after `merge_prims.py`, and the `art_incoming` props, which carry COLOR_0 and no material
block at all), so their thumbnails would have shipped colourless. This renders in EEVEE off the
real material graph, so what Lane C sees in the thumb is what the game draws.
BL=/Applications/Blender.app/Contents/MacOS/Blender
"$BL" --background --python pipeline/thumbs.py -- OUT_DIR GLB [GLB ...]
"""
import bpy, sys, os, math
from mathutils import Vector
ARGV = sys.argv[sys.argv.index("--") + 1:]
OUT_DIR = ARGV[0]
FILES = [f for f in ARGV[1:] if f.lower().endswith(".glb")]
RES = 256
os.makedirs(OUT_DIR, exist_ok=True)
def wipe():
for o in list(bpy.data.objects):
bpy.data.objects.remove(o, do_unlink=True)
for blk in (bpy.data.meshes, bpy.data.materials, bpy.data.images):
for d in list(blk):
if d.users == 0:
blk.remove(d)
def bounds(objs):
lo = Vector((1e9,) * 3); hi = Vector((-1e9,) * 3)
for o in objs:
if o.type != 'MESH':
continue
for c in o.bound_box:
w = o.matrix_world @ Vector(c)
for k in range(3):
lo[k] = min(lo[k], w[k]); hi[k] = max(hi[k], w[k])
return lo, hi
def setup_world():
w = bpy.data.worlds.get("tw") or bpy.data.worlds.new("tw")
bpy.context.scene.world = w
w.use_nodes = True
w.node_tree.nodes["Background"].inputs[0].default_value = (0.85, 0.87, 0.92, 1)
w.node_tree.nodes["Background"].inputs[1].default_value = 1.6
def main():
scn = bpy.context.scene
for e in ('BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'):
try:
scn.render.engine = e
break
except TypeError:
continue
scn.render.film_transparent = True
scn.render.resolution_x = scn.render.resolution_y = RES
scn.render.image_settings.file_format = 'PNG'
scn.render.image_settings.color_mode = 'RGBA'
scn.view_settings.view_transform = 'Standard'
ok = 0
for path in FILES:
wipe()
setup_world()
try:
bpy.ops.import_scene.gltf(filepath=path)
except Exception as ex:
print(f"IMPORT FAIL {path}: {ex}")
continue
meshes = [o for o in bpy.data.objects if o.type == 'MESH']
if not meshes:
print(f"NO MESH {path}")
continue
lo, hi = bounds(meshes)
ctr = (lo + hi) / 2
size = max((hi - lo).x, (hi - lo).y, (hi - lo).z) or 1.0
bpy.ops.object.light_add(type='AREA',
location=(ctr.x + size, ctr.y - size, hi.z + size * 1.2))
L = bpy.context.object
L.data.energy = 260 * size * size + 40
L.data.size = size * 2.5
L.rotation_euler = (math.radians(40), 0, math.radians(35))
bpy.ops.object.light_add(type='AREA',
location=(ctr.x - size, ctr.y - size * 1.4, ctr.z))
L2 = bpy.context.object
L2.data.energy = 90 * size * size + 15
L2.data.size = size * 3
bpy.ops.object.camera_add()
cam = bpy.context.object
scn.camera = cam
cam.data.type = 'ORTHO'
cam.data.ortho_scale = size * 1.5
d = Vector((0.82, -1.0, 0.62)).normalized()
cam.location = ctr + d * (size * 4 + 2)
cam.rotation_euler = (-d).to_track_quat('-Z', 'Y').to_euler()
cam.data.clip_end = size * 20 + 50
scn.render.filepath = os.path.join(OUT_DIR, os.path.basename(path)[:-4] + ".png")
bpy.ops.render.render(write_still=True)
ok += 1
print(f"THUMBS_DONE {ok}/{len(FILES)} -> {OUT_DIR}")
main()