"""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}")