| """Keyframe Animation Controller for G1 robot.""" |
| import numpy as np |
| from .base import BaseController |
|
|
|
|
| class KeyframeController(BaseController): |
| """ |
| Controller that plays back keyframe animations with PD tracking. |
| |
| Supports multiple animations that can be selected at runtime. |
| """ |
|
|
| |
| LEFT_ARM = slice(15, 22) |
| RIGHT_ARM = slice(22, 29) |
|
|
| def __init__(self, num_joints: int = 29): |
| super().__init__(num_joints) |
|
|
| |
| self.base_pose = np.zeros(num_joints) |
| |
| leg_pose = [-0.1, 0.0, 0.0, 0.3, -0.2, 0.0] |
| self.base_pose[0:6] = leg_pose |
| self.base_pose[6:12] = leg_pose |
| |
| self.base_pose[16] = 0.3 |
| self.base_pose[18] = 0.3 |
| self.base_pose[23] = -0.3 |
| self.base_pose[25] = 0.3 |
|
|
| |
| self.kp = np.zeros(num_joints) |
| self.kd = np.zeros(num_joints) |
|
|
| |
| |
| leg_kp = [100, 100, 100, 150, 40, 40] |
| leg_kd = [2, 2, 2, 4, 2, 2] |
| self.kp[0:6] = leg_kp |
| self.kd[0:6] = leg_kd |
| self.kp[6:12] = leg_kp |
| self.kd[6:12] = leg_kd |
| |
| self.kp[12:15] = 100.0 |
| self.kd[12:15] = 5.0 |
| |
| self.kp[15:29] = 50.0 |
| self.kd[15:29] = 3.0 |
|
|
| |
| self.current_animation = "wave" |
| self.animation_speed = 1.0 |
|
|
| |
| |
| self.animations = { |
| "wave": self._create_wave_animation(), |
| "squat": self._create_squat_animation(), |
| "arms_up": self._create_arms_up_animation(), |
| "idle": self._create_idle_animation(), |
| } |
|
|
| def _create_wave_animation(self): |
| """Wave right hand animation - subtle to maintain balance.""" |
| |
| |
| keyframes = [] |
| duration = 4.0 |
|
|
| |
| |
| wave_poses = [ |
| (0.0, {22: 0.0, 23: -0.3, 25: 0.3}), |
| (0.8, {22: -0.8, 23: -0.6, 24: 0.0, 25: 0.8}), |
| (1.2, {22: -0.8, 23: -0.6, 24: 0.2, 25: 0.8}), |
| (1.6, {22: -0.8, 23: -0.6, 24: -0.2, 25: 0.8}), |
| (2.0, {22: -0.8, 23: -0.6, 24: 0.2, 25: 0.8}), |
| (2.4, {22: -0.8, 23: -0.6, 24: -0.2, 25: 0.8}), |
| (3.2, {22: 0.0, 23: -0.3, 25: 0.3}), |
| (4.0, {22: 0.0, 23: -0.3, 25: 0.3}), |
| ] |
|
|
| for t, offsets in wave_poses: |
| pose = np.zeros(self.num_joints) |
| for idx, val in offsets.items(): |
| pose[idx] = val |
| keyframes.append((t, pose)) |
|
|
| return {"keyframes": keyframes, "duration": duration, "loop": True} |
|
|
| def _create_squat_animation(self): |
| """Simple squat animation - subtle for stability.""" |
| keyframes = [] |
| duration = 6.0 |
|
|
| |
| |
| |
|
|
| squat_poses = [ |
| (0.0, {}), |
| (2.0, {0: -0.3, 3: 0.6, 4: -0.3, |
| 6: -0.3, 9: 0.6, 10: -0.3}), |
| (4.0, {0: -0.3, 3: 0.6, 4: -0.3, |
| 6: -0.3, 9: 0.6, 10: -0.3}), |
| (6.0, {}), |
| ] |
|
|
| for t, offsets in squat_poses: |
| pose = np.zeros(self.num_joints) |
| for idx, val in offsets.items(): |
| pose[idx] = val |
| keyframes.append((t, pose)) |
|
|
| return {"keyframes": keyframes, "duration": duration, "loop": True} |
|
|
| def _create_arms_up_animation(self): |
| """Raise both arms animation - symmetric for balance.""" |
| keyframes = [] |
| duration = 4.0 |
|
|
| arms_poses = [ |
| (0.0, {}), |
| (1.5, {15: -1.2, 16: 0.3, 18: 0.0, |
| 22: -1.2, 23: -0.3, 25: 0.0}), |
| (2.5, {15: -1.2, 16: 0.3, 18: 0.0, |
| 22: -1.2, 23: -0.3, 25: 0.0}), |
| (4.0, {}), |
| ] |
|
|
| for t, offsets in arms_poses: |
| pose = np.zeros(self.num_joints) |
| for idx, val in offsets.items(): |
| pose[idx] = val |
| keyframes.append((t, pose)) |
|
|
| return {"keyframes": keyframes, "duration": duration, "loop": True} |
|
|
| def _create_idle_animation(self): |
| """Subtle idle breathing motion.""" |
| keyframes = [] |
| duration = 4.0 |
|
|
| idle_poses = [ |
| (0.0, {}), |
| (2.0, {14: 0.05}), |
| (4.0, {}), |
| ] |
|
|
| for t, offsets in idle_poses: |
| pose = np.zeros(self.num_joints) |
| for idx, val in offsets.items(): |
| pose[idx] = val |
| keyframes.append((t, pose)) |
|
|
| return {"keyframes": keyframes, "duration": duration, "loop": True} |
|
|
| def set_animation(self, name: str): |
| """Set current animation by name.""" |
| if name in self.animations: |
| self.current_animation = name |
| self.time = 0.0 |
|
|
| def get_animation_names(self): |
| """Get list of available animation names.""" |
| return list(self.animations.keys()) |
|
|
| def _interpolate_keyframes(self, keyframes, t): |
| """Interpolate between keyframes at time t.""" |
| |
| prev_kf = keyframes[0] |
| next_kf = keyframes[-1] |
|
|
| for i, (kf_time, kf_pose) in enumerate(keyframes): |
| if kf_time <= t: |
| prev_kf = (kf_time, kf_pose) |
| if i + 1 < len(keyframes): |
| next_kf = keyframes[i + 1] |
| else: |
| next_kf = (kf_time, kf_pose) |
| break |
|
|
| prev_t, prev_pose = prev_kf |
| next_t, next_pose = next_kf |
|
|
| |
| if next_t - prev_t > 0: |
| alpha = (t - prev_t) / (next_t - prev_t) |
| |
| alpha = alpha * alpha * (3 - 2 * alpha) |
| else: |
| alpha = 1.0 |
|
|
| return prev_pose + alpha * (next_pose - prev_pose) |
|
|
| def compute_action(self, obs: np.ndarray, data) -> np.ndarray: |
| """Compute control action from keyframe animation.""" |
| joint_pos = obs[13:13 + self.num_joints] |
| joint_vel = obs[13 + self.num_joints:13 + 2 * self.num_joints] |
|
|
| |
| anim = self.animations[self.current_animation] |
| keyframes = anim["keyframes"] |
| duration = anim["duration"] |
|
|
| |
| anim_time = (self.time * self.animation_speed) % duration |
|
|
| |
| pose_offset = self._interpolate_keyframes(keyframes, anim_time) |
| target_pose = self.base_pose + pose_offset |
|
|
| |
| pos_error = target_pose - joint_pos |
| vel_error = -joint_vel |
|
|
| |
| gain_scale = min(1.0, self.time / 1.0) |
|
|
| torques = gain_scale * (self.kp * pos_error + self.kd * vel_error) |
|
|
| return torques |
|
|
| @property |
| def name(self) -> str: |
| return f"Animation: {self.current_animation}" |
|
|