"""PROCITY view sheet — render a normalized GLB from the angles it is actually SEEN from, plus a distance strip at the pixel sizes it actually occupies. Written for R38's magpie tint verdict: "does the marking read at the distance and speed the bird is seen at" is a question no 256 px thumbnail can answer. BL=/Applications/Blender.app/Contents/MacOS/Blender "$BL" --background --python pipeline/render_views.py -- IN.glb OUT.png [LABEL] Views (Blender frame after glTF import: glTF +Z head -> Blender -Y): flank · rear-quarter high (the nape read) · under-front (the swoop, bird above the player) · top-down · straight rear. Each is composited over sky, not over transparency, because the contrast that matters is bird-against-sky. The bottom strip re-samples the flank + swoop view to 96 / 64 / 48 / 32 px — 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 and 32 px is the pessimistic one. """ import bpy, sys, os, math from mathutils import Vector ARGV = sys.argv[sys.argv.index("--") + 1:] SRC, OUT = ARGV[0], ARGV[1] LABEL = ARGV[2] if len(ARGV) > 2 else "" RES = 384 SKY = (0.62, 0.72, 0.86) def wipe(): for o in list(bpy.data.objects): bpy.data.objects.remove(o, do_unlink=True) def bounds(o): cs = [o.matrix_world @ Vector(c) for c in o.bound_box] return (Vector((min(c.x for c in cs), min(c.y for c in cs), min(c.z for c in cs))), Vector((max(c.x for c in cs), max(c.y for c in cs), max(c.z for c in cs)))) def main(): wipe() bpy.ops.import_scene.gltf(filepath=SRC) objs = [o for o in bpy.data.objects if o.type == 'MESH'] ob = objs[0] mn, mx = bounds(ob) ctr = (mn + mx) / 2 size = max((mx - mn).x, (mx - mn).y, (mx - mn).z) or 1.0 scn = bpy.context.scene if "--eevee" in ARGV: # closest cheap proxy for the in-game look: the GLB's OWN material (base colour + the # 0.28 emissive normalize.py adds) under a bright sky-coloured world + a sun. eng = [e.identifier for e in type(scn).bl_rna.properties['render'].fixed_type.properties['engine'].enum_items] scn.render.engine = 'BLENDER_EEVEE_NEXT' if 'BLENDER_EEVEE_NEXT' in eng else 'BLENDER_EEVEE' w = bpy.data.worlds.get("sky") or bpy.data.worlds.new("sky") scn.world = w w.use_nodes = True bg = w.node_tree.nodes["Background"] bg.inputs[0].default_value = (SKY[0], SKY[1], SKY[2], 1.0) bg.inputs[1].default_value = 1.6 bpy.ops.object.light_add(type='SUN', location=(size * 3, -size * 3, size * 4)) bpy.context.object.data.energy = 3.0 bpy.context.object.rotation_euler = (math.radians(50), 0, math.radians(35)) else: scn.render.engine = 'BLENDER_WORKBENCH' scn.display.shading.light = 'STUDIO' scn.display.shading.color_type = 'TEXTURE' scn.display.shading.show_cavity = False scn.view_settings.view_transform = 'Standard' scn.render.film_transparent = True scn.render.resolution_x = scn.render.resolution_y = RES bpy.ops.object.camera_add() cam = bpy.context.object scn.camera = cam cam.data.lens = 50 if "--thumb" in ARGV: # byte-for-byte the same camera/lighting as bake_lowpoly.render_thumb, so a re-tinted asset # gets a thumbnail that matches the rest of the roster instead of a new angle. scn.render.engine = 'BLENDER_WORKBENCH' scn.display.shading.light = 'STUDIO' scn.display.shading.color_type = 'TEXTURE' scn.display.shading.show_cavity = True w = bpy.data.worlds.get("tw") or bpy.data.worlds.new("tw") scn.world = w w.use_nodes = True w.node_tree.nodes["Background"].inputs[1].default_value = 1.05 bpy.ops.object.light_add(type='AREA', location=(ctr.x + size, ctr.y - size, mx.z + size)) bpy.context.object.data.energy = 800 * size bpy.context.object.data.size = 6 r = size * 1.9 # --rear swings the same rig to the other quarter. Needed for the magpie: the roster's # standard front-quarter looks straight at a magpie's black face and bib, so it records # none of the plumage — the one thing this asset's thumb has to show. cam.location = ctr + Vector((r * 0.8, r if "--rear" in ARGV else -r, size * 0.6)) cam.rotation_euler = (ctr - cam.location).to_track_quat('-Z', 'Y').to_euler() scn.render.resolution_x = scn.render.resolution_y = 256 scn.render.filepath = OUT bpy.ops.render.render(write_still=True) print("RENDERED thumb -> " + OUT) return r = size * 2.2 # head is -Y after import; +Y is behind the bird views = { "flank": Vector((r, -r * 0.15, size * 0.15)), "rear34": Vector((r * 0.55, r * 0.85, size * 0.85)), # over the shoulder: the nape "swoop": Vector((r * 0.35, -r * 0.80, -size * 0.75)), # bird overhead, coming at you "top": Vector((0.001, 0.001, r * 1.15)), "rear": Vector((0.0, r * 1.1, size * 0.2)), } tmp = os.path.join(os.path.dirname(OUT), "_v_") paths = [] for name, loc in views.items(): cam.location = ctr + loc cam.rotation_euler = (ctr - cam.location).to_track_quat('-Z', 'Y').to_euler() p = tmp + name + ".png" scn.render.filepath = p bpy.ops.render.render(write_still=True) paths.append((name, p)) print("RENDERED " + " ".join(n for n, _ in paths)) with open(OUT + ".views.txt", "w") as f: f.write("\n".join(f"{n}\t{p}" for n, p in paths) + f"\nLABEL\t{LABEL}\n") main()