# Stage 47: pose QC on the grafted body. Deformation is what exposes bad weights — a T-pose hides # every one of them. # # blender --background --python 47_pose_qc.py -- # # Two independent checks per pose, because a render alone can hide a defect that faces away and a # number alone cannot tell you whether it looks wrong: # EDGE STRETCH — every edge's deformed length over its rest length. Skin stretches smoothly; # a mis-weighted vertex tears one edge far beyond its neighbours, so the p99.99 # and the max are the tell, not the mean. # RENDER — the same poses, so anything the numbers miss is still visible. # Fingers get their own pose: close-packed digits are where this lane historically mangled weights. import bpy, sys, os, math, time import numpy as np from mathutils import Vector, Euler argv = sys.argv[sys.argv.index("--") + 1:] BLEND, OUT = argv[0], os.path.abspath(argv[1]) os.makedirs(OUT, exist_ok=True) t0 = time.time() bpy.ops.wm.open_mainfile(filepath=BLEND) arm = next(o for o in bpy.data.objects if o.type == 'ARMATURE') body = max([o for o in bpy.data.objects if o.type == 'MESH'], key=lambda o: len(o.data.vertices)) me = body.data names = [b.name for b in arm.data.bones] print(f"BONES {len(names)}") for key in ("Upperarm", "Forearm", "Hand", "Thigh", "Calf", "Foot", "Index", "Spine", "Head"): hit = [n for n in names if key in n and n.startswith(("CC_Base_L", "CC_Base_"))][:4] print(f" {key:10} -> {hit}") n = len(me.vertices) ev = np.empty(len(me.edges) * 2, dtype=np.int32); me.edges.foreach_get("vertices", ev) ev = ev.reshape(-1, 2) rest = np.empty(n * 3); me.vertices.foreach_get("co", rest); rest = rest.reshape(-1, 3) rest_len = np.linalg.norm(rest[ev[:, 0]] - rest[ev[:, 1]], axis=1) ok = rest_len > 1e-9 UNITM = 1.777 / (rest[:, 2].max() - rest[:, 2].min()) def deformed(): dg = bpy.context.evaluated_depsgraph_get() ob = body.evaluated_get(dg) m = ob.to_mesh() a = np.empty(len(m.vertices) * 3) m.vertices.foreach_get("co", a) ob.to_mesh_clear() return a.reshape(-1, 3) def pose(spec): bpy.ops.object.select_all(action='DESELECT') arm.select_set(True) bpy.context.view_layer.objects.active = arm bpy.ops.object.mode_set(mode='POSE') for pb in arm.pose.bones: pb.rotation_mode = 'XYZ' pb.rotation_euler = (0, 0, 0) applied = 0 for frag, rot in spec: for pb in arm.pose.bones: # Twist bones are driven helpers that sit BETWEEN a joint and its child. Matching them # by substring and rotating them by the parent's angle compounds the rotation and # produces 14x edge stretch that looks like broken weights but is entirely self- # inflicted — exclude them and pose only the primary joints. if "Twist" in pb.name: continue if frag in pb.name: pb.rotation_euler = Euler([math.radians(v) for v in rot]) applied += 1 bpy.ops.object.mode_set(mode='OBJECT') bpy.context.view_layer.update() return applied POSES = [ ("rest", []), ("arms_down", [("Upperarm", (0, 0, -55))]), ("elbows", [("Upperarm", (0, 0, -40)), ("Forearm", (0, 55, 0))]), ("fingers", [("Index2", (0, 0, 62)), ("Index3", (0, 0, 55)), ("Mid2", (0, 0, 62)), ("Mid3", (0, 0, 55)), ("Ring2", (0, 0, 62)), ("Ring3", (0, 0, 55)), ("Pinky2", (0, 0, 62)), ("Pinky3", (0, 0, 55))]), ("knees", [("Thigh", (35, 0, 0)), ("Calf", (-70, 0, 0))]), ("hips_twist", [("Spine", (0, 0, 25)), ("Thigh", (20, 0, 0))]), ] scn = bpy.context.scene scn.render.engine = 'BLENDER_EEVEE' scn.render.resolution_x = scn.render.resolution_y = 1000 wd = bpy.data.worlds.new("w"); wd.use_nodes = True wd.node_tree.nodes["Background"].inputs[0].default_value = (0.20, 0.20, 0.22, 1) scn.world = wd for r in (Euler((math.radians(58), 0, math.radians(-35))), Euler((math.radians(115), 0, math.radians(150)))): ld = bpy.data.lights.new("s", 'SUN'); ld.energy = 2.4; ld.use_shadow = False lo = bpy.data.objects.new("s", ld); lo.rotation_euler = r bpy.context.collection.objects.link(lo) cam = bpy.data.cameras.new("c"); cam.lens = 70 cob = bpy.data.objects.new("c", cam); bpy.context.collection.objects.link(cob); scn.camera = cob print("\n=== EDGE STRETCH (deformed length / rest length) ===") print(f"{'pose':<12} {'bones':>6} {'p50':>7} {'p99':>7} {'p99.99':>8} {'max':>8} " f"{'>2x':>6} {'>4x':>6}") worst = {} for name, spec in POSES: nb = pose(spec) D = deformed() dl = np.linalg.norm(D[ev[:, 0]] - D[ev[:, 1]], axis=1) r = dl[ok] / rest_len[ok] worst[name] = float(r.max()) print(f"{name:<12} {nb:6d} {np.percentile(r,50):7.3f} {np.percentile(r,99):7.3f} " f"{np.percentile(r,99.99):8.3f} {r.max():8.3f} {int((r>2).sum()):6d} " f"{int((r>4).sum()):6d}") # Explicit camera placement, copied from 09_beauty.py which frames this exact body correctly. # A to_track_quat aim was tried here and put her off-frame entirely. for view, loc, rot in (("", Vector((0.0, -2.05, 0.55)), Euler((math.radians(90), 0, 0))), ("_34", Vector((-1.35, -1.55, 0.55)), Euler((math.radians(90), 0, math.radians(-41))))): cob.location = loc cob.rotation_euler = rot scn.render.filepath = os.path.join(OUT, f"{name}{view}.png") bpy.ops.render.render(write_still=True) pose([]) print("\nverdict: skin stretches smoothly, so a healthy pose keeps max near p99.99.") print("a lone edge far above the rest is a mis-weighted vertex, not a pose.") print("POSE_QC_DONE")