karate-wiener / comic_shots.py
polats's picture
Deploy Karate Wiener (kimodo kata maker)
c9dd954 verified
Raw
History Blame
3.36 kB
"""Shot grammar for the 4-koma stance comics: named shots -> deterministic camera,
computed purely from the skeleton frame (bbox + hip heading). The LLM never sees any
of this; it only ever picks captions. Cameras are plain pinhole (eye/target/fov/roll)
so the same parameters port 1:1 to the three.js viewer later."""
import numpy as np
PARENTS = [-1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 12, 13, 14, 16, 17, 18, 19]
PEL, LHIP, RHIP, HEAD = 0, 1, 2, 15
LEFTJ = {1, 4, 7, 10, 13, 16, 18, 20}
RIGHTJ = {2, 5, 8, 11, 14, 17, 19, 21}
# az = horizontal angle of the camera relative to the character's facing direction
# (0 = head-on). el = elevation in degrees (negative = camera low, looking up).
# margin = how much air around the joint bbox. roll = dutch tilt.
SHOTS = {
"establishing": dict(az=38.0, el=10.0, margin=1.55, roll=0.0, fov=40.0),
"action": dict(az=55.0, el=4.0, margin=1.22, roll=-4.0, fov=46.0),
"gag": dict(az=22.0, el=16.0, margin=1.12, roll=13.0, fov=50.0),
"hero": dict(az=32.0, el=-16.0, margin=1.42, roll=0.0, fov=38.0),
}
def heading(Pf):
"""Facing angle about Y from the hip line (0 = facing +Z), as in _build_stances."""
d = Pf[RHIP] - Pf[LHIP]
return float(np.arctan2(d[2], -d[0]))
def make_camera(shot_name, Pf):
"""Camera dict for one skeleton frame [22,3]. Frames the joint bbox with the
shot's margin, oriented relative to the character's heading."""
s = SHOTS[shot_name]
lo, hi = Pf.min(axis=0), Pf.max(axis=0)
target = (lo + hi) / 2.0
radius = float(np.linalg.norm(Pf - target, axis=1).max())
fov = np.radians(s["fov"])
dist = s["margin"] * radius / np.tan(fov / 2.0)
theta = heading(Pf) + np.radians(s["az"])
el = np.radians(s["el"])
direction = np.array([np.sin(theta) * np.cos(el), np.sin(el), np.cos(theta) * np.cos(el)])
eye = target + direction * dist
# Don't let the camera dip below the floor (hero shots on grounded poses).
eye[1] = max(eye[1], 0.12)
return dict(eye=eye, target=target, fov=fov, roll=np.radians(s["roll"]))
def project(points, cam, W, H):
"""Pinhole-project [N,3] world points -> ([N,2] pixel coords, [N] view depth)."""
pts = np.asarray(points, np.float64).reshape(-1, 3)
eye, target = cam["eye"], cam["target"]
fwd = target - eye
fwd = fwd / np.linalg.norm(fwd)
right = np.cross(fwd, np.array([0.0, 1.0, 0.0]))
right = right / (np.linalg.norm(right) + 1e-9)
up = np.cross(right, fwd)
r = cam["roll"]
if abs(r) > 1e-6: # dutch tilt: rotate the basis about the view axis
c, s = np.cos(r), np.sin(r)
right, up = c * right + s * up, -s * right + c * up
rel = pts - eye
x = rel @ right
y = rel @ up
z = rel @ fwd
z = np.maximum(z, 1e-4)
f = (H / 2.0) / np.tan(cam["fov"] / 2.0)
px = W / 2.0 + f * x / z
py = H / 2.0 - f * y / z
return np.stack([px, py], axis=1), z
def ground_grid(center, span=2.6, step=0.65):
"""3D segments of a floor grid (y=0) around the character, for scene depth."""
segs = []
cx, cz = float(center[0]), float(center[2])
ticks = np.arange(-span, span + 1e-6, step)
for t in ticks:
segs.append(((cx - span, 0.0, cz + t), (cx + span, 0.0, cz + t)))
segs.append(((cx + t, 0.0, cz - span), (cx + t, 0.0, cz + span)))
return segs