Files
animation/tools/skin_bone_territory.py
T

82 lines
2.9 KiB
Python
Raw Normal View History

"""Per-finger-bone territory audit: how many verts does each finger bone actually OWN
(dominant weight) and how much total weight mass does it carry?
Why: fin_bones classifies exp05's fist tears as hand<->thumb_02 and hand<->index_02 —
the chain skips the _01 joints. If the _01 bones own no territory, every curl lands as a
hard one-edge step from the palm to phalanx 2, which must stretch. This measures that
directly instead of inferring it. usage: bone_territory.py body.glb
"""
import json
import struct
import sys
from pathlib import Path
DIGITS = ("thumb", "index", "middle", "ring", "pinky")
def read_glb(path):
d = Path(path).read_bytes()
ln = struct.unpack_from("<I", d, 8)[0]
off, g, b = 12, None, None
while off < ln:
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"]]
sz = struct.calcsize(fmt) * nc
stride = bv.get("byteStride") or sz
off = bv.get("byteOffset", 0) + a.get("byteOffset", 0)
out = []
for k in range(a["count"]):
out.append(struct.unpack_from("<" + fmt * nc, b, off + k * stride))
return out
g, b = read_glb(sys.argv[1])
prim = g["meshes"][0]["primitives"][0]
joints = g["skins"][0]["joints"]
names = [g["nodes"][j].get("name", f"node{j}") for j in joints]
J = acc(g, b, prim["attributes"]["JOINTS_0"])
W = acc(g, b, prim["attributes"]["WEIGHTS_0"])
wt = g["accessors"][prim["attributes"]["WEIGHTS_0"]]["componentType"]
sc = 1.0 if wt == 5126 else (1 / 255 if wt == 5121 else 1 / 65535)
own = {n: 0 for n in names} # verts whose LARGEST weight is this bone
mass = {n: 0.0 for n in names} # total weight mass
any_w = {n: 0 for n in names} # verts with any weight >1%
for ji, wi in zip(J, W):
ws = [w * sc for w in wi]
best, bw = None, 0.0
for jj, w in zip(ji, ws):
n = names[jj]
mass[n] += w
if w > 0.01:
any_w[n] += 1
if w > bw:
best, bw = n, w
if best is not None and bw > 0:
own[best] += 1
print(f"{Path(sys.argv[1]).name} {len(J)} verts, {len(joints)} joints")
print(f"{'bone':16s} {'owns':>7s} {'any>1%':>8s} {'mass':>9s}")
for side in ("l", "r"):
print(f"--- hand_{side} chain ---")
for nm in [f"hand_{side}"] + [f"{d}_{p}_{side}" for d in DIGITS
for p in ("01", "02", "03")]:
if nm not in own:
print(f"{nm:16s} (absent from skin)")
continue
flag = " <-- STARVED" if own[nm] == 0 else ""
print(f"{nm:16s} {own[nm]:7d} {any_w[nm]:8d} {mass[nm]:9.1f}{flag}")