83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
|
|
"""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)
|