85 lines
3.0 KiB
Python
85 lines
3.0 KiB
Python
|
|
"""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}")
|