Files
animation/tools/rest_deviation.py
T

57 lines
1.8 KiB
Python
Raw Normal View History

"""Compare finger-bone REST rotations between GLB skeletons.
First GLB is the reference; each other is reported as per-bone angle deviation (degrees).
usage: rest_deviation.py canonical.glb other.glb [more.glb ...]
"""
import json, struct, sys, math
from pathlib import Path
FING = ("thumb", "index", "middle", "ring", "pinky")
def read_gltf(path):
d = Path(path).read_bytes()
jlen = struct.unpack_from("<I", d, 12)[0]
return json.loads(d[20:20 + jlen].decode("utf-8"))
def rests(path):
g = read_gltf(path)
out = {}
for nd in g["nodes"]:
n = nd.get("name", "")
if any(t in n.lower() for t in FING):
out[n] = nd.get("rotation", [0, 0, 0, 1])
return out
def angle_between(a, b):
"""Geodesic angle (deg) between two unit quaternions, sign-insensitive."""
d = abs(sum(x * y for x, y in zip(a, b)))
d = max(-1.0, min(1.0, d))
return math.degrees(2 * math.acos(d))
ref_path = sys.argv[1]
ref = rests(ref_path)
print(f"reference: {Path(ref_path).name} ({len(ref)} finger bones)")
for p in sys.argv[2:]:
other = rests(p)
print(f"\n== {Path(p).name} == {len(other)} finger bones")
missing = sorted(set(ref) - set(other))
extra = sorted(set(other) - set(ref))
if missing:
print(f" MISSING vs ref ({len(missing)}): {', '.join(missing)}")
if extra:
print(f" EXTRA vs ref ({len(extra)}): {', '.join(extra)}")
devs = []
for n in sorted(set(ref) & set(other)):
devs.append((angle_between(ref[n], other[n]), n))
devs.sort(reverse=True)
if not devs:
continue
over = [d for d in devs if d[0] > 1.0]
print(f" shared {len(devs)} | deviating >1deg: {len(over)} | max {devs[0][0]:.1f}deg ({devs[0][1]})")
for d, n in devs[:12]:
print(f" {n:24s} {d:6.1f}deg")