3ba86b2ea8
Bulk import of the working lanes that were living untracked on the PC. Content: - characters/ Lena/male body lanes, bakes, texture work, run logs - clothing/ garment pipeline, configs, gates, contract docs - garments/ MD-authored garment sources (.zprj/.zpac) - UAL-Lib/ Universal Animation Library 2 source (.blend/.fbx/.glb) - tools/ blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent - docs/, plans/, dev/, .agents/plans/ Repo hygiene: - .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out on purpose — ~250 are already tracked raw and converting them would rewrite every one without shrinking history. - .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies dropped by a `>/dev/null` redirect on Windows). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
99 lines
4.2 KiB
Python
99 lines
4.2 KiB
Python
# Framed closeup renders of a body GLB, for judging geometry and texture separately.
|
|
#
|
|
# blender --background --python tools/_render_body_closeup.py -- \
|
|
# --glb <a.glb> --out <a.png> [--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()
|