82 lines
2.9 KiB
Python
82 lines
2.9 KiB
Python
|
|
"""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}")
|