Files
animation/tools/hand_bone_ownership.py
T

85 lines
3.0 KiB
Python
Raw Normal View History

"""For each finger bone, report how much geometry it actually OWNS (verts where it is the
dominant influence) and where that geometry sits. On a two-finger hand rig the five-finger
bone set is present but several chains own nothing, or several chains share one fused mass.
usage: hand_bone_ownership.py body.glb [side l|r]
"""
import json, struct, sys, math
from pathlib import Path
from collections import defaultdict
FING = ("thumb", "index", "middle", "ring", "pinky")
side = sys.argv[2] if len(sys.argv) > 2 else None
def read_glb(p):
d = Path(p).read_bytes()
length = struct.unpack_from("<I", d, 8)[0]
off = 12
g = b_ = None
while off < length:
clen, ct = struct.unpack_from("<II", d, off)
off += 8
if ct == 0x4E4F534A:
g = json.loads(d[off:off + clen])
else:
b_ = d[off:off + clen]
off += clen
return g, b_
def acc(g, b, i):
a = g["accessors"][i]
bv = g["bufferViews"][a["bufferView"]]
nc = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16}[a["type"]]
fmt = {5121: "B", 5123: "H", 5125: "I", 5126: "f"}[a["componentType"]]
size = struct.calcsize(fmt) * nc
stride = bv.get("byteStride") or size
off = bv.get("byteOffset", 0) + a.get("byteOffset", 0)
return [struct.unpack_from("<%d%s" % (nc, fmt), b, off + k * stride)
for k in range(a["count"])], a["componentType"]
g, b = read_glb(sys.argv[1])
names = [nd.get("name", "") for nd in g["nodes"]]
prim = g["meshes"][0]["primitives"][0]
att = prim["attributes"]
skin = next(nd["skin"] for nd in g["nodes"] if nd.get("mesh") == 0 and "skin" in nd)
joints = g["skins"][skin]["joints"]
jname = [names[j] for j in joints]
P, _ = acc(g, b, att["POSITION"])
J, _ = acc(g, b, att["JOINTS_0"])
W, wt = acc(g, b, att["WEIGHTS_0"])
wsc = 1.0 if wt == 5126 else (1 / 255 if wt == 5121 else 1 / 65535)
own = defaultdict(list)
for vi, (p, jrow, wrow) in enumerate(zip(P, J, W)):
best = (0.0, None)
for j, w in zip(jrow, wrow):
w *= wsc
if w > best[0]:
best = (w, jname[j])
if best[1] and any(t in best[1].lower() for t in FING):
if side and not best[1].lower().endswith("_" + side):
continue
own[best[1]].append(p)
print(f"{Path(sys.argv[1]).name} dominant-owner geometry per finger bone"
f"{' (side ' + side + ')' if side else ''}\n")
print(f" {'bone':20s} {'verts':>7s} {'z-centre':>9s} {'z-span':>8s} {'x-centre':>9s}")
for fam in FING:
rows = [(n, v) for n, v in own.items() if fam in n.lower()]
if not rows:
print(f" {fam:20s} {'0':>7s} -- owns no geometry --")
continue
for n in sorted(r[0] for r in rows):
ps = own[n]
zc = sum(p[2] for p in ps) / len(ps) * 100
zs = (max(p[2] for p in ps) - min(p[2] for p in ps)) * 100
xc = sum(p[0] for p in ps) / len(ps) * 100
print(f" {n:20s} {len(ps):7d} {zc:8.1f}cm {zs:7.1f}cm {xc:8.1f}cm")
print()
tot = sum(len(v) for v in own.values())
print(f" total finger-owned verts: {tot}")