Draw the ground as an exercise mat that rotates with the figure
The floor rectangle was screen-locked, which broke the illusion the moment the camera orbited. It is now a world-space quad on the ground plane, sized to each motion's projected footprint across its key frames and rotated through the same camera as the figure - a long rectangle in profile, a parallelogram mid-orbit, end-on when face-on. Both renderers in lockstep; fixtures unaffected (the mat is a pure addition). Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
+48
-18
@@ -102,7 +102,7 @@ def _bucket(depth):
|
||||
return round(depth / DEPTH_BUCKET)
|
||||
|
||||
|
||||
def frame_geometry(nf, prof, cam, flipped=False, pitch=CAMERA_PITCH):
|
||||
def frame_geometry(nf, prof, cam, flipped=False, pitch=CAMERA_PITCH, mat=None):
|
||||
"""Resolve one normalized frame into drawable 2D geometry.
|
||||
|
||||
Returns (nf with IK-resolved angles and original pins, geo, order, shade):
|
||||
@@ -167,6 +167,18 @@ def frame_geometry(nf, prof, cam, flipped=False, pitch=CAMERA_PITCH):
|
||||
geo["girdle"] = [attach("shoulder_l", "arm_l"), neck_b, attach("shoulder_r", "arm_r")]
|
||||
geo["pelvisBar"] = [attach("hip_l", "leg_l"), pelvis, attach("hip_r", "leg_r")]
|
||||
|
||||
# The exercise mat: a world-space quad on the ground plane, rotating with
|
||||
# the camera about the figure's vertical axis (`mat` = screen-x bounds of
|
||||
# the motion's footprint in the authored view).
|
||||
if mat is not None:
|
||||
yg = -(GROUND_Y + 4 - anchor[1])
|
||||
rc = K.mmul(K.rot_x(pitch), K.rot_y(-cam))
|
||||
geo["floor"] = [scr(K.mvec(rc, (dx, yg, dz))[:2] + (0,))
|
||||
for dx, dz in ((mat[0] - anchor[0], FLOOR_HALF_DEPTH),
|
||||
(mat[1] - anchor[0], FLOOR_HALF_DEPTH),
|
||||
(mat[1] - anchor[0], -FLOOR_HALF_DEPTH),
|
||||
(mat[0] - anchor[0], -FLOOR_HALF_DEPTH))]
|
||||
|
||||
# The nose tick rides the head's anterior axis; it foreshortens naturally
|
||||
# and disappears when the face points at (or away from) the camera.
|
||||
nd = p["nose_dir"]
|
||||
@@ -183,6 +195,19 @@ def frame_geometry(nf, prof, cam, flipped=False, pitch=CAMERA_PITCH):
|
||||
return work, geo, order, shade
|
||||
|
||||
|
||||
def mat_bounds(norms, prof, cam, pitch=CAMERA_PITCH):
|
||||
"""The motion's footprint in the authored view: min/max screen x of every
|
||||
figure point across the key frames, padded - the exercise mat spans it."""
|
||||
lo, hi = float("inf"), float("-inf")
|
||||
for nf in norms:
|
||||
_, geo, _, _ = frame_geometry(nf, prof, cam, pitch=pitch)
|
||||
xs = [geo["head"][0] - geo["headR"], geo["head"][0] + geo["headR"]]
|
||||
for part in ("arm_r", "arm_l", "leg_r", "leg_l", "spine", "girdle", "pelvisBar"):
|
||||
xs += [pt[0] for pt in geo.get(part, [])]
|
||||
lo, hi = min(lo, min(xs)), max(hi, max(xs))
|
||||
return lo - 12, hi + 12
|
||||
|
||||
|
||||
def ease(t):
|
||||
return 3 * t * t - 2 * t * t * t
|
||||
|
||||
@@ -345,13 +370,15 @@ def draw_prims(d, prims, colors, scale):
|
||||
|
||||
# ------------------------------------------------------------------- drawing
|
||||
|
||||
def floor_rect_svg(colors):
|
||||
"""The floor plane under the elevated camera: a rectangle straddling the
|
||||
ground line (the old horizontal line was the pitch-zero degenerate case)."""
|
||||
fh = FLOOR_HALF_DEPTH * math.sin(math.radians(CAMERA_PITCH))
|
||||
return (f' <rect x="16" y="{GROUND_Y + 4 - fh:.1f}" width="{CANVAS[0] - 32}"'
|
||||
f' height="{2 * fh:.1f}" rx="3" fill="none"'
|
||||
f' stroke="{colors["ground"]}" stroke-width="3"/>')
|
||||
def floor_svg(geo, colors):
|
||||
"""The exercise mat: a world-space quad on the ground plane, sized to the
|
||||
motion's footprint and rotating with the camera."""
|
||||
quad = geo.get("floor")
|
||||
if not quad:
|
||||
return ""
|
||||
d = "M " + " L ".join(f"{x:.1f} {y:.1f}" for x, y in quad) + " Z"
|
||||
return (f' <path d="{d}" fill="none" stroke="{colors["ground"]}"'
|
||||
f' stroke-width="3" stroke-linejoin="round"/>')
|
||||
|
||||
|
||||
def part_style(part, working, colors, shade):
|
||||
@@ -378,7 +405,7 @@ def svg_for_frame(name, geo, order, shade, working, colors, props=None):
|
||||
w, h = CANVAS
|
||||
parts = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" fill="none">',
|
||||
f' <title>{name}</title>',
|
||||
floor_rect_svg(colors)]
|
||||
floor_svg(geo, colors)]
|
||||
parts += svg_prims(bg, colors)
|
||||
for part in order:
|
||||
if part == "head":
|
||||
@@ -431,10 +458,8 @@ def draw_geo(geo, order, shade, working, colors, scale=2, font=None, props=None)
|
||||
for x, y in (pts[0], pts[-1]):
|
||||
d.ellipse([x - r, y - r, x + r, y + r], fill=color)
|
||||
|
||||
fh = FLOOR_HALF_DEPTH * math.sin(math.radians(CAMERA_PITCH))
|
||||
d.rounded_rectangle([16 * scale, (GROUND_Y + 4 - fh) * scale,
|
||||
(CANVAS[0] - 16) * scale, (GROUND_Y + 4 + fh) * scale],
|
||||
radius=3 * scale, outline=colors["ground"], width=3 * scale)
|
||||
if geo.get("floor"):
|
||||
line(geo["floor"] + geo["floor"][:1], colors["ground"], 3)
|
||||
draw_prims(d, bg, colors, scale)
|
||||
for part in order:
|
||||
if part == "head":
|
||||
@@ -511,8 +536,10 @@ def render_exercise(folder, figure="neutral", flip=False, strict=False):
|
||||
hide = set(motion.get("hide", []))
|
||||
norms, prof, cam, pitch, props = prepare(motion, figure, flip, strict)
|
||||
|
||||
mat = mat_bounds(norms, prof, cam, pitch)
|
||||
|
||||
def geometry(nf):
|
||||
_, geo, order, shade = frame_geometry(nf, prof, cam, flip, pitch)
|
||||
_, geo, order, shade = frame_geometry(nf, prof, cam, flip, pitch, mat)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
return geo, order, shade
|
||||
@@ -520,7 +547,7 @@ def render_exercise(folder, figure="neutral", flip=False, strict=False):
|
||||
resolved = []
|
||||
key_geos = []
|
||||
for nf in norms:
|
||||
out, geo, order, shade = frame_geometry(nf, prof, cam, flip, pitch)
|
||||
out, geo, order, shade = frame_geometry(nf, prof, cam, flip, pitch, mat)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
resolved.append(out)
|
||||
@@ -559,6 +586,7 @@ def render_orbit(folder, figure="neutral"):
|
||||
hide = set(motion.get("hide", []))
|
||||
norms, prof, cam, pitch, props = prepare(motion, figure)
|
||||
resolved = [frame_geometry(nf, prof, cam, pitch=pitch)[0] for nf in norms]
|
||||
mat = mat_bounds(norms, prof, cam, pitch)
|
||||
font = legend_font()
|
||||
colors = PALETTES["default"]
|
||||
ticks = timeline(resolved)
|
||||
@@ -569,7 +597,7 @@ def render_orbit(folder, figure="neutral"):
|
||||
# then rotate the posed body - never re-pin in the rotated view.
|
||||
posed, _, _, _ = frame_geometry(nf, prof, cam, pitch=pitch)
|
||||
posed["pins"] = {}
|
||||
_, geo, order, shade = frame_geometry(posed, prof, yaw, pitch=pitch)
|
||||
_, geo, order, shade = frame_geometry(posed, prof, yaw, pitch=pitch, mat=mat)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
imgs.append(draw_geo(geo, order, shade, working, colors, font=font, props=props))
|
||||
@@ -585,8 +613,9 @@ def contact_sheet(folders, figure="neutral", out=None):
|
||||
motion = load_motion(folder)
|
||||
working, hide = set(motion.get("working", [])), set(motion.get("hide", []))
|
||||
norms, prof, cam, pitch, props = prepare(motion, figure)
|
||||
mat = mat_bounds(norms, prof, cam, pitch)
|
||||
for i, nf in enumerate(norms, start=1):
|
||||
_, geo, order, shade = frame_geometry(nf, prof, cam, pitch=pitch)
|
||||
_, geo, order, shade = frame_geometry(nf, prof, cam, pitch=pitch, mat=mat)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
cells.append((f"{motion['name']} {i}/{len(norms)}",
|
||||
@@ -611,7 +640,8 @@ def demo_sheet(folder):
|
||||
cells = []
|
||||
for label, figure, flip, palette in variants:
|
||||
norms, prof, cam, pitch, props = prepare(motion, figure, flip)
|
||||
_, geo, order, shade = frame_geometry(norms[idx], prof, cam, flip, pitch)
|
||||
mat = mat_bounds(norms, prof, cam, pitch)
|
||||
_, geo, order, shade = frame_geometry(norms[idx], prof, cam, flip, pitch, mat)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
cells.append((f"{motion['name']} — {label}",
|
||||
|
||||
Reference in New Issue
Block a user