Files

128 lines
5.7 KiB
Python
Raw Permalink Normal View History

# 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 -- <grafted.blend> <out_dir>
#
# 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")