"""Music-timed, 50 Hz Electric Slide choreography for a Microduck flock. The motion is deliberately kinematic for the first visual prototype: root poses and servo poses are sampled directly in MuJoCo. That makes the 25-duck show repeatable while keeping a clean seam for replacing the sampler with an ONNX policy later. """ from __future__ import annotations from dataclasses import dataclass import math BPM = 111.0 BEAT_SECONDS = 60.0 / BPM COUNTS_PER_WALL = 18 WALLS = 4 SHOW_BEATS = COUNTS_PER_WALL * WALLS SHOW_SECONDS = SHOW_BEATS * BEAT_SECONDS CONTROL_HZ = 50 # Official STAND keyframe, in official actuator/joint order. STAND_POSE = ( 0.0, -0.08726646259971647, -0.457924, -0.004940, 0.452984, 0.3490658503988659, 0.3490658503988659, 0.0, 0.0, 0.0, 0.08726646259971647, 0.457924, 0.004940, -0.452984, ) # Per-count root translation in the duck's local frame (forward, left) and yaw. # Right is negative local y. Four 18-count walls trace a closed square. ROOT_STEPS = ( (0.00, -0.10, 0.0), # grapevine right: side (0.00, -0.04, 0.0), # cross behind (0.00, -0.10, 0.0), # side (0.00, 0.00, 0.0), # touch (0.00, 0.10, 0.0), # grapevine left: side (0.00, 0.04, 0.0), # cross behind (0.00, 0.10, 0.0), # side (0.00, 0.00, 0.0), # touch (-0.08, 0.00, 0.0), # walk back (-0.08, 0.00, 0.0), (-0.08, 0.00, 0.0), (0.00, 0.00, 0.0), # touch (0.08, 0.00, 0.0), # rock forward (0.00, 0.00, 0.0), # toe touch + scoop (-0.08, 0.00, 0.0), # rock back (0.00, 0.00, 0.0), # toe touch + lean (0.05, 0.00, 0.0), # step into turn (0.00, 0.00, math.pi / 2.0), # scuff + quarter turn left ) @dataclass(frozen=True) class DuckPose: position: tuple[float, float, float] quaternion: tuple[float, float, float, float] joints: tuple[float, ...] def formation_positions(grid: int = 5, spacing: float = 0.52) -> list[tuple[float, float]]: """Return a centered, row-major square formation.""" center = (grid - 1) / 2.0 return [ ((col - center) * spacing, (center - row) * spacing) for row in range(grid) for col in range(grid) ] def _smoothstep(value: float) -> float: value = max(0.0, min(1.0, value)) return value * value * (3.0 - 2.0 * value) def _rotate(x: float, y: float, yaw: float) -> tuple[float, float]: cosine, sine = math.cos(yaw), math.sin(yaw) return cosine * x - sine * y, sine * x + cosine * y def root_motion(beat: float) -> tuple[float, float, float]: """Integrate the 18-count pattern through the supplied beat (0..72).""" beat = max(0.0, min(float(SHOW_BEATS), beat)) whole = int(math.floor(beat)) fraction = _smoothstep(beat - whole) x = y = yaw = 0.0 for index in range(whole): dx, dy, dyaw = ROOT_STEPS[index % COUNTS_PER_WALL] world_dx, world_dy = _rotate(dx, dy, yaw) x += world_dx y += world_dy yaw += dyaw if whole < SHOW_BEATS: dx, dy, dyaw = ROOT_STEPS[whole % COUNTS_PER_WALL] world_dx, world_dy = _rotate(dx, dy, yaw) x += world_dx * fraction y += world_dy * fraction yaw += dyaw * fraction return x, y, yaw def _quaternion(yaw: float, pitch: float, roll: float) -> tuple[float, float, float, float]: """MuJoCo wxyz quaternion from ZYX yaw, pitch, and roll.""" cy, sy = math.cos(yaw / 2), math.sin(yaw / 2) cp, sp = math.cos(pitch / 2), math.sin(pitch / 2) cr, sr = math.cos(roll / 2), math.sin(roll / 2) return ( cr * cp * cy + sr * sp * sy, sr * cp * cy - cr * sp * sy, cr * sp * cy + sr * cp * sy, cr * cp * sy - sr * sp * cy, ) def sample_pose(time_seconds: float, duck_index: int, base_xy: tuple[float, float]) -> DuckPose: """Sample one duck at show time, including subtle flock-specific attitude.""" beat = (time_seconds / BEAT_SECONDS) % SHOW_BEATS count = int(beat) % COUNTS_PER_WALL phase = beat - math.floor(beat) step_pulse = math.sin(math.pi * phase) groove = math.sin(2.0 * math.pi * (beat / 2.0 + (duck_index % 5) * 0.035)) row = duck_index // 5 col = duck_index % 5 accent = (row - 2) * 0.018 + (col - 2) * 0.012 travel_x, travel_y, yaw = root_motion(beat) z = 0.12 + 0.006 * abs(groove) pitch = 0.025 * groove roll = -0.035 * groove + accent pose = list(STAND_POSE) # Continuous knee compression gives the loose, grounded look in the clip. compression = 0.035 + 0.055 * abs(groove) pose[3] += compression pose[13] -= compression pose[2] += compression * 0.45 pose[12] -= compression * 0.45 # Alternating weight shift and foot pickup for traveling counts. if count in (0, 2, 4, 6, 8, 10): lead_left = count in (4, 6, 10) lift = 0.16 * step_pulse if lead_left: pose[2] += lift pose[3] += lift * 0.70 pose[4] -= lift * 0.45 roll += 0.055 * step_pulse else: pose[11] -= lift pose[12] -= lift * 0.70 pose[13] += lift * 0.45 roll -= 0.055 * step_pulse # Touches are visible: a toe point, a beak nod, and a tiny hold. if count in (3, 7, 11): pose[6] -= 0.13 * step_pulse pose[7] += 0.14 * groove z -= 0.008 * step_pulse # The reference clip's character section: low scoop forward, lean back, # then let the head lead the new wall. if count in (12, 13): scoop = step_pulse if count == 13 else _smoothstep(phase) z -= 0.022 * scoop pitch += 0.11 * scoop pose[5] += 0.24 * scoop pose[6] -= 0.22 * scoop pose[7] += (0.18 + accent) * scoop pose[8] -= 0.14 * groove elif count in (14, 15): lean = step_pulse if count == 15 else _smoothstep(phase) pitch -= 0.09 * lean pose[5] -= 0.18 * lean pose[6] += 0.15 * lean pose[8] += 0.18 * groove elif count in (16, 17): turn_lead = step_pulse if count == 16 else math.sin(math.pi * phase) pose[7] += 0.38 * turn_lead pose[8] += 0.13 * groove z += 0.012 * step_pulse # Keep the flock coordinated while the head-roll accent ripples by row. pose[8] += 0.075 * math.sin(2 * math.pi * (beat / 4.0 + row * 0.08)) return DuckPose( position=(base_xy[0] + travel_x, base_xy[1] + travel_y, z), quaternion=_quaternion(yaw, pitch, roll), joints=tuple(pose), ) def sample_flock(time_seconds: float, grid: int = 5, spacing: float = 0.52) -> list[DuckPose]: positions = formation_positions(grid, spacing) return [sample_pose(time_seconds, index, xy) for index, xy in enumerate(positions)]