PROCITY/pipeline/bird_to_glb.py
m3ultra a569f08b14 Lane E R39 (1/n): THE MAGPIE, SETTLED ON A PICTURE — B's own birdGeometry() dumped vertex-for-vertex (182 tris, not 154), rendered against E's tinted GLB at 64/48/32 px, still and under motion blur
- pipeline/dump_bird.mjs: imports web/js/world/magpie.js UNMODIFIED (bare 'three' resolved to the
  repo's vendored build through a node resolve hook) and dumps birdGeometry() — so the A/B is
  against Lane B's actual bird, not a port. MEASURED 182 tris / 216 verts. --sim walks a player
  past a territory on magpie.js's own clock: drawn 54.1% of frames, and OF THOSE perched 80.5% /
  swooping 7.7% / returning 11.8%.
- pipeline/bird_to_glb.py: wraps the dump as a GLB in E's frame (head +Z, +Y up) so the identical
  render_views.py rig shoots both; --fold applies the perch pose (x x 0.42). glb_stat re-measures 182.
- pipeline/render_views.py --noemit: strips emission from BOTH candidates. normalize.py's
  emissiveFactor 0.28 is albedo-MODULATED; a vertex-coloured mesh cannot express that in glTF and
  Blender writes a flat 0.28 that lifts a black bird to grey. Off both sides or the picture lies.
- pipeline/view_sheet.py --ab: the strip as a GRID (one line per candidate, stacked) plus two
  linear motion-blur blocks at 25% and 100% of the bird's width, and a wrapped notes footer.
- docs/shots/laneE/r39_magpie_ab.png — the sheet Fable rules on.

Recommendation in LANE_E_NOTES; picture first.

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

97 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""PROCITY Lane E — bird_to_glb.py (R39, the magpie A/B)
Turn `pipeline/dump_bird.mjs`'s dump of LANE B'S OWN `birdGeometry()` into a GLB, so the identical
`render_views.py` camera rig that shot E's tinted 894-tri GLB can shoot B's 182-tri procedural bird
from the identical angles under the identical light. Rendering the two through different pipelines
would make the picture arguable; this makes it decisive.
BL=/Applications/Blender.app/Contents/MacOS/Blender
"$BL" --background --python pipeline/bird_to_glb.py -- IN.json OUT.glb [--fold]
`--fold` applies magpie.js's perch pose (`place(..., folded=true)` scales the instance X by 0.42),
because B's bird has TWO silhouettes off one geometry and E's GLB has one.
Vertex colours ride as COLOR_0 and are wired straight into Base Color, matching
`MeshStandardMaterial({ vertexColors: true, roughness: 0.62, metalness: 0, side: DoubleSide })`.
Normals are the ones three.js computed, imported as custom split normals — a Blender-recomputed
normal would shade the sphere differently from the browser.
"""
import bpy, sys, json
ARGV = sys.argv[sys.argv.index("--") + 1:]
SRC, OUT = ARGV[0], ARGV[1]
FOLD = "--fold" in ARGV
FOLD_X = 0.42 # magpie.js: the perched instance is squashed in X, wings in
d = json.load(open(SRC))
pos, nrm, col, idx = d["position"], d["normal"], d["color"], d["index"]
nv = len(pos) // 3
# ── FRAME. three: +Y up, nose along Z. E's published magpie GLB: +Y up, HEAD AT +Z (that is what
# render_views.py's rig assumes — "glTF +Z head -> Blender -Y"). Blender's glTF exporter with
# export_yup writes glTF = (Bx, Bz, By). So to land B's bird in E's frame we want
# glTF = (xt, yt, zt) (a 180° yaw, det = +1 ⇒ winding preserved)
# which means the Blender vertex must be (xt, zt, yt). Without this the two rows of the sheet
# would be shot from opposite ends of the bird and the comparison would be worthless.
def to_blender(x, y, z):
return (-x, z, y)
fx = FOLD_X if FOLD else 1.0
verts = [to_blender(pos[i * 3] * fx, pos[i * 3 + 1], pos[i * 3 + 2]) for i in range(nv)]
faces = [tuple(idx[i:i + 3]) for i in range(0, len(idx), 3)] if idx else \
[(i, i + 1, i + 2) for i in range(0, nv, 3)]
for o in list(bpy.data.objects):
bpy.data.objects.remove(o, do_unlink=True)
me = bpy.data.meshes.new("bird")
me.from_pydata(verts, [], faces)
me.update()
ob = bpy.data.objects.new("bird", me)
bpy.context.collection.objects.link(ob)
# three's own normals, per corner (folding in X inverts nothing: 0.42 > 0, so only a rescale)
if nrm:
loops = []
for p in me.polygons:
for vi in p.vertices:
loops.append(to_blender(nrm[vi * 3], nrm[vi * 3 + 1], nrm[vi * 3 + 2]))
try:
me.normals_split_custom_set(loops)
except Exception as e: # never let shading cosmetics kill the export
print("custom normals skipped:", e)
# COLOR_0 — the whole argument for B's bird is that the white is IN the vertices
if col:
ca = me.color_attributes.new(name="Col", type='FLOAT_COLOR', domain='POINT')
for i in range(nv):
ca.data[i].color = (col[i * 3], col[i * 3 + 1], col[i * 3 + 2], 1.0)
me.color_attributes.active_color = ca
me.attributes.active_color = ca
mat = bpy.data.materials.new("birdMat")
mat.use_nodes = True
mat.use_backface_culling = False # three: side = DoubleSide
nt = mat.node_tree
bsdf = nt.nodes["Principled BSDF"]
bsdf.inputs["Roughness"].default_value = 0.62
bsdf.inputs["Metallic"].default_value = 0.0
if col:
cattr = nt.nodes.new("ShaderNodeVertexColor")
cattr.layer_name = "Col"
nt.links.new(cattr.outputs["Color"], bsdf.inputs["Base Color"])
# FAIRNESS. E's published magpie carries normalize.py's emissiveFactor 0.28 WITH an
# emissiveTexture, i.e. 0.28 × its own albedo — a self-lit copy of its own markings. Handing B's
# bird the identical treatment (0.28 × its vertex colour) is the only way the two rows of the
# sheet are lit the same; without it the render flatters E by construction and the ruling is
# worthless. `--noemit` drops it, for the ?noassets-honest version of B's material.
if "--noemit" not in ARGV:
nt.links.new(cattr.outputs["Color"], bsdf.inputs["Emission Color"])
bsdf.inputs["Emission Strength"].default_value = 0.28
me.materials.append(mat)
bpy.ops.export_scene.gltf(filepath=OUT, export_format='GLB',
export_apply=True, export_yup=True,
export_normals=True, export_vertex_color='ACTIVE')
print(f"WROTE {OUT} tris={len(faces)} verts={nv} folded={FOLD}")