Files
animation/tools/handpose_scan_clips.py
T

83 lines
3.2 KiB
Python
Raw Normal View History

"""Scan GLBs: list finger joints and which animations have live (non-frozen) finger rotation tracks."""
import json, struct, sys, math
from pathlib import Path
FINGER_TOKENS = ("thumb", "index", "middle", "ring", "pinky", "finger")
def read_glb(path):
data = Path(path).read_bytes()
magic, ver, length = struct.unpack_from("<III", data, 0)
assert magic == 0x46546C67, "not glb"
off = 12
gltf = None
bin_chunk = None
while off < length:
clen, ctype = struct.unpack_from("<II", data, off)
off += 8
chunk = data[off:off+clen]
if ctype == 0x4E4F534A:
gltf = json.loads(chunk.decode("utf-8"))
elif ctype == 0x004E4942:
bin_chunk = chunk
off += clen
return gltf, bin_chunk
def accessor_data(gltf, binc, idx):
acc = gltf["accessors"][idx]
bv = gltf["bufferViews"][acc["bufferView"]]
comp = {5126: ("f", 4)}[acc["componentType"]]
n = {"SCALAR":1, "VEC3":3, "VEC4":4}[acc["type"]]
off = bv.get("byteOffset", 0) + acc.get("byteOffset", 0)
count = acc["count"]
vals = struct.unpack_from("<%d%s" % (count*n, comp[0]), binc, off)
return [vals[i*n:(i+1)*n] for i in range(count)]
def scan(path, verbose_joints=False):
gltf, binc = read_glb(path)
nodes = gltf.get("nodes", [])
names = [nd.get("name", f"node{i}") for i, nd in enumerate(nodes)]
# joints from skins
joint_set = set()
for skin in gltf.get("skins", []):
joint_set.update(skin.get("joints", []))
fingers = sorted(n for i in joint_set for n in [names[i]] if any(t in n.lower() for t in FINGER_TOKENS))
print(f"\n== {Path(path).name} ==")
print(f"joints: {len(joint_set)}, finger joints: {len(fingers)}")
if verbose_joints:
for n in sorted(names[i] for i in joint_set):
print(" ", n)
elif fingers:
print(" finger joints:", ", ".join(fingers))
for anim in gltf.get("animations", []):
aname = anim.get("name", "?")
live, frozen = [], []
for ch in anim.get("channels", []):
tgt = ch["target"]
if tgt.get("path") != "rotation":
continue
nname = names[tgt["node"]]
if not any(t in nname.lower() for t in FINGER_TOKENS):
continue
samp = anim["samplers"][ch["sampler"]]
quats = accessor_data(gltf, binc, samp["output"])
# measure max angular deviation from first frame
q0 = quats[0]
maxdot = 1.0
for q in quats[1:]:
d = abs(sum(a*b for a, b in zip(q0, q)))
maxdot = min(maxdot, min(d, 1.0))
ang = 2*math.degrees(math.acos(maxdot))
(live if ang > 2.0 else frozen).append((nname, ang))
total = len(live) + len(frozen)
if total:
print(f" anim '{aname}': {total} finger rot tracks, {len(live)} live (>2deg), {len(frozen)} frozen")
if live:
top = sorted(live, key=lambda x: -x[1])[:4]
print(" top movers:", ", ".join(f"{n} {a:.0f}deg" for n, a in top))
else:
print(f" anim '{aname}': NO finger tracks")
if __name__ == "__main__":
for p in sys.argv[1:]:
scan(p, verbose_joints="--joints" in sys.argv)