100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
|
|
"""Extract a hand pose (40 finger-bone quaternions) from a GLB clip at a chosen frame,
|
||
|
|
report per-bone curl (deviation from skeleton rest), optionally dump JSON.
|
||
|
|
|
||
|
|
usage: python extract_pose.py <glb> <animName> [--frame N | --max-curl] [--dump out.json]
|
||
|
|
python extract_pose.py <glb> --rest --dump out.json (rest pose itself)
|
||
|
|
"""
|
||
|
|
import json, struct, sys, math
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
FINGER_TOKENS = ("thumb", "index", "middle", "ring", "pinky")
|
||
|
|
|
||
|
|
def read_glb(path):
|
||
|
|
data = Path(path).read_bytes()
|
||
|
|
magic, ver, length = struct.unpack_from("<III", data, 0)
|
||
|
|
off = 12
|
||
|
|
gltf = None; binc = 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: binc = chunk
|
||
|
|
off += clen
|
||
|
|
return gltf, binc
|
||
|
|
|
||
|
|
def acc_data(gltf, binc, idx):
|
||
|
|
acc = gltf["accessors"][idx]
|
||
|
|
bv = gltf["bufferViews"][acc["bufferView"]]
|
||
|
|
n = {"SCALAR":1, "VEC3":3, "VEC4":4}[acc["type"]]
|
||
|
|
off = bv.get("byteOffset", 0) + acc.get("byteOffset", 0)
|
||
|
|
vals = struct.unpack_from("<%d%s" % (acc["count"]*n, "f"), binc, off)
|
||
|
|
return [vals[i*n:(i+1)*n] for i in range(acc["count"])]
|
||
|
|
|
||
|
|
def qangle(a, b):
|
||
|
|
d = min(1.0, abs(sum(x*y for x, y in zip(a, b))))
|
||
|
|
return 2*math.degrees(math.acos(d))
|
||
|
|
|
||
|
|
def main():
|
||
|
|
glb = sys.argv[1]
|
||
|
|
gltf, binc = read_glb(glb)
|
||
|
|
nodes = gltf["nodes"]
|
||
|
|
names = [nd.get("name", f"n{i}") for i, nd in enumerate(nodes)]
|
||
|
|
finger_idx = {i: names[i] for i, nd in enumerate(nodes)
|
||
|
|
if any(t in names[i].lower() for t in FINGER_TOKENS)}
|
||
|
|
rest = {i: tuple(nodes[i].get("rotation", [0, 0, 0, 1])) for i in finger_idx}
|
||
|
|
|
||
|
|
dump = None
|
||
|
|
if "--dump" in sys.argv:
|
||
|
|
dump = sys.argv[sys.argv.index("--dump")+1]
|
||
|
|
|
||
|
|
if "--rest" in sys.argv:
|
||
|
|
pose = {names[i]: list(rest[i]) for i in finger_idx}
|
||
|
|
label = "REST"
|
||
|
|
else:
|
||
|
|
aname = sys.argv[2]
|
||
|
|
anim = next(a for a in gltf["animations"] if a.get("name") == aname)
|
||
|
|
# collect finger rotation samplers
|
||
|
|
tracks = {}
|
||
|
|
times_ref = None
|
||
|
|
for ch in anim["channels"]:
|
||
|
|
t = ch["target"]
|
||
|
|
if t.get("path") != "rotation" or t["node"] not in finger_idx: continue
|
||
|
|
samp = anim["samplers"][ch["sampler"]]
|
||
|
|
quats = acc_data(gltf, binc, samp["output"])
|
||
|
|
tracks[t["node"]] = quats
|
||
|
|
times_ref = acc_data(gltf, binc, samp["input"])
|
||
|
|
nframes = min(len(q) for q in tracks.values())
|
||
|
|
if "--max-curl" in sys.argv:
|
||
|
|
best, bestf = -1, 0
|
||
|
|
for f in range(nframes):
|
||
|
|
curl = sum(qangle(tracks[i][f], rest[i]) for i in tracks)
|
||
|
|
if curl > best: best, bestf = curl, f
|
||
|
|
frame = bestf
|
||
|
|
elif "--frame" in sys.argv:
|
||
|
|
frame = int(sys.argv[sys.argv.index("--frame")+1])
|
||
|
|
else:
|
||
|
|
frame = 0
|
||
|
|
t = times_ref[min(frame, len(times_ref)-1)][0] if times_ref else 0
|
||
|
|
pose = {names[i]: list(tracks[i][frame]) for i in tracks}
|
||
|
|
# fill missing finger bones from rest
|
||
|
|
for i in finger_idx:
|
||
|
|
pose.setdefault(names[i], list(rest[i]))
|
||
|
|
label = f"{aname} frame {frame} (t={t:.2f}s)"
|
||
|
|
|
||
|
|
# curl report per finger chain (sum of deviations from rest), L hand only for brevity
|
||
|
|
print(f"pose: {label} ({len(pose)} bones)")
|
||
|
|
for hand in ("_l", "_r"):
|
||
|
|
parts = []
|
||
|
|
for fing in ("thumb", "index", "middle", "ring", "pinky"):
|
||
|
|
tot = sum(qangle(pose[n], rest[i]) for i, n in finger_idx.items()
|
||
|
|
if n.startswith(fing) and n.endswith(hand))
|
||
|
|
parts.append(f"{fing} {tot:.0f}")
|
||
|
|
print(f" {hand}: curl-vs-rest deg " + " ".join(parts))
|
||
|
|
if dump:
|
||
|
|
Path(dump).write_text(json.dumps({"source": f"{Path(glb).name}:{label}",
|
||
|
|
"bones": pose}, indent=1))
|
||
|
|
print("dumped ->", dump)
|
||
|
|
|
||
|
|
main()
|