Add a tool that measures emotes from a store's 3D preview

tools/capture drives a store page's skin viewer in headless Firefox, stepping its clock one tick at
a time and taking every tick from ten fixed cameras. It then fits our own rig to the frames by
rendering the model (a Python port of LimbBend and PoseApplier) and matching outlines and colours,
and writes the result as Emotecraft JSON. It measures pixels only and never reads the page's
animation data. The README covers the steps, the checks and the limits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-29 16:55:08 +02:00
co-authored by claude
parent 7f8de39253
commit df9a2587e6
19 changed files with 1785 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
"""The back-to-front twin of a rig, which covers the same space with the textures turned round.
The head and torso boxes are centred on their pivots, so spinning either half a turn around its own
vertical axis leaves it covering the same space; only which face shows changes. The arms hang from
the torso's sides, so spinning the torso carries each arm to the other side. That twin (torso and
head facing backwards, arms swapped) is a separate local minimum the search can't walk out of, so
measure.py jumps to it directly, refines it, and keeps whichever scores lower. Only the colours tell
them apart.
In rig terms: the torso and head get R Ry(pi); each arm takes the other's rotation times Ry(pi) (a
half turn around its own length, which moves its off-centre box to the other side of its pivot); and
bends keep their shape with their direction turned half round, since the frame they're measured in
turned. The legs hang from the hips and don't change.
"""
import numpy as np
from model import INDEX, rotation_zyx
SPIN = np.diag([-1.0, 1.0, -1.0]) # Ry(pi)
ARMS = {"right_arm": "left_arm", "left_arm": "right_arm"}
def euler_zyx(r):
"""(pitch, yaw, roll) with rotation_zyx(pitch, yaw, roll) == r."""
yaw = np.arcsin(np.clip(-r[2, 0], -1, 1))
return np.arctan2(r[2, 1], r[2, 2]), yaw, np.arctan2(r[1, 0], r[0, 0])
def _spun(rig, source):
g = lambda c: rig[INDEX[f"{source}_{c}"]]
return euler_zyx(rotation_zyx(g("pitch"), g("yaw"), g("roll")) @ SPIN)
def flip_head(rig):
"""Just the head spun round: a cube on its pivot, so the same space facing the other way."""
out = rig.copy()
for c, v in zip(("pitch", "yaw", "roll"), _spun(rig, "head")):
out[INDEX[f"head_{c}"]] = v
return out
def twins(rig):
"""Every back-to-front twin worth trying: the torso with its arms, the head, and both."""
return [flip(rig), flip_head(rig), flip_head(flip(rig))]
def flip(rig):
out = rig.copy()
for part, source in [("body", "body"), ("head", "head"), *ARMS.items()]:
for c, v in zip(("pitch", "yaw", "roll"), _spun(rig, source)):
out[INDEX[f"{part}_{c}"]] = v
for part, source in [("body", "body"), *ARMS.items()]:
out[INDEX[f"{part}_bend"]] = rig[INDEX[f"{source}_bend"]]
out[INDEX[f"{part}_axis"]] = rig[INDEX[f"{source}_axis"]] + np.pi
return out