# Framed closeup renders of a body GLB, for judging geometry and texture separately. # # blender --background --python tools/_render_body_closeup.py -- \ # --glb --out [--yaw 0] [--region torso|hip|full] [--clay] [--wire] # # Why not tools/_glb_render.py: that one frames the whole-scene bounding box, and every body # GLB here carries a stray 1 m-radius Icosphere at the origin, so the figure ends up small in # frame. This one frames an explicit anatomical band on the BODY mesh only. # --clay drops all materials for a neutral grey surface, which is the only honest way to judge # silhouette and shading artifacts — a painted-on bra shadow otherwise reads as a mesh dent. import bpy, sys, math, argparse from mathutils import Vector REGIONS = { # (z_lo, z_hi) in metres on a 1.777 m body "torso": (1.00, 1.52), "hip": (0.78, 1.12), "full": (0.00, 1.80), } def main(): argv = sys.argv[sys.argv.index("--") + 1:] ap = argparse.ArgumentParser() ap.add_argument("--glb", required=True) ap.add_argument("--out", required=True) ap.add_argument("--yaw", type=float, default=0.0) ap.add_argument("--region", default="torso", choices=sorted(REGIONS)) ap.add_argument("--clay", action="store_true") ap.add_argument("--wire", action="store_true") ap.add_argument("--no-shadow", action="store_true", help="disable cast shadows — for measurement renders; in a T-pose the arm " "throws sharp finger-shadow bands across the flank that edge metrics " "misread as surface creases") ap.add_argument("--res", type=int, default=900) a = ap.parse_args(argv) bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.gltf(filepath=a.glb) meshes = [o for o in bpy.data.objects if o.type == 'MESH'] body = max(meshes, key=lambda o: len(o.data.vertices)) for o in meshes: # hide the stray Icosphere and anything else if o is not body: o.hide_render = True if a.clay: body.data.materials.clear() m = bpy.data.materials.new("Clay") m.use_nodes = True bsdf = m.node_tree.nodes["Principled BSDF"] bsdf.inputs["Base Color"].default_value = (0.62, 0.60, 0.58, 1.0) bsdf.inputs["Roughness"].default_value = 0.45 body.data.materials.append(m) if a.wire: body.modifiers.new("Wire", 'WIREFRAME').thickness = 0.0012 z_lo, z_hi = REGIONS[a.region] pts = [body.matrix_world @ v.co for v in body.data.vertices] band = [p for p in pts if z_lo <= p.z <= z_hi] or pts ctr = Vector((0.0, 0.0, (z_lo + z_hi) / 2)) xs = [p.x for p in band] ctr.y = sum(p.y for p in band) / len(band) span = max(max(xs) - min(xs), z_hi - z_lo) w = bpy.data.worlds.new("W") w.color = (0.22, 0.22, 0.24) bpy.context.scene.world = w # three-point-ish lighting: a key sun plus a softer fill, so volume reads without the # single-sun hotspot that flattens a bust into one bright blob key = bpy.data.objects.new("Key", bpy.data.lights.new("Key", 'SUN')) key.data.energy = 3.0 key.data.use_shadow = not a.no_shadow key.rotation_euler = (math.radians(62), 0, math.radians(35 + a.yaw)) bpy.context.collection.objects.link(key) fill = bpy.data.objects.new("Fill", bpy.data.lights.new("Fill", 'SUN')) fill.data.energy = 1.1 fill.rotation_euler = (math.radians(75), 0, math.radians(a.yaw - 110)) bpy.context.collection.objects.link(fill) cam = bpy.data.objects.new("Cam", bpy.data.cameras.new("Cam")) cam.data.lens = 85 bpy.context.collection.objects.link(cam) yaw = math.radians(a.yaw) dist = span * 3.1 cam.location = ctr + Vector((math.sin(yaw) * dist, -math.cos(yaw) * dist, 0.0)) cam.rotation_euler = (ctr - cam.location).to_track_quat('-Z', 'Y').to_euler() bpy.context.scene.camera = cam scn = bpy.context.scene scn.render.engine = 'BLENDER_EEVEE' if bpy.app.version >= (4, 2) else 'BLENDER_EEVEE_NEXT' scn.render.resolution_x = scn.render.resolution_y = a.res scn.render.filepath = a.out bpy.ops.render.render(write_still=True) print("CLOSEUP_OK", a.out) main()