diff --git a/src/humanoid_3d_animation/humanoid.xml b/src/humanoid_3d_animation/humanoid.xml index a263bdbcf5..881178f198 100644 --- a/src/humanoid_3d_animation/humanoid.xml +++ b/src/humanoid_3d_animation/humanoid.xml @@ -1,121 +1,99 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/humanoid_3d_animation/humanoid_simulation.py b/src/humanoid_3d_animation/humanoid_simulation.py index 4eb868e559..350a6e0290 100644 --- a/src/humanoid_3d_animation/humanoid_simulation.py +++ b/src/humanoid_3d_animation/humanoid_simulation.py @@ -6,7 +6,7 @@ import threading import signal import sys -import select +import random from dataclasses import dataclass import numpy as np import matplotlib.pyplot as plt @@ -17,8 +17,6 @@ @dataclass class SimConfig: """仿真配置类:集中管理所有可配置参数""" - # 文件路径配置 - xml_filename: str = "humanoid.xml" # 仿真参数 timestep: float = 0.005 sim_frequency: float = 2.0 @@ -27,18 +25,21 @@ class SimConfig: cam_distance: float = 2.0 cam_azimuth: float = 45.0 cam_elevation: float = -20.0 - # 关节运动幅度配置 + # 关节运动幅度配置(针对不同动作优化) joint_amplitudes = { - "left_shoulder": 1.0, "right_shoulder": 1.0, - "left_elbow": 0.5, "right_elbow": 0.5, - "left_hip": 0.8, "right_hip": 0.8, - "left_knee": 0.6, "right_knee": 0.6 + "left_shoulder": 1.2, "right_shoulder": 1.2, + "left_elbow": 1.0, "right_elbow": 1.0, + "left_hip": 1.0, "right_hip": 1.0, + "left_knee": 1.2, "right_knee": 1.2 } - # 控制模式 - default_mode: str = "sin" + # 控制模式(新增行走和挥手动作) + default_mode: str = "walk" # 可视化配置 plot_update_interval: int = 50 # 绘图更新间隔(帧数) max_plot_points: int = 200 # 图表最大显示数据点 + # 动作参数 + walk_stride: float = 0.8 # 行走步幅 + wave_frequency: float = 1.5 # 挥手频率 # 全局变量 @@ -68,9 +69,12 @@ def __init__(self, config: SimConfig): self.joint_qpos_indices = {} self.current_mode = config.default_mode self.last_ctrl_signals = {} - self.input_thread_running = False - # 新增:可视化相关变量 + # 新增:动作状态变量 + self.walk_phase = 0.0 # 行走相位 + self.wave_arm = "right" # 当前挥动手臂 + + # 可视化相关变量 self.plot_data = {name: [] for name in self.joint_names} self.time_data = [] self.frame_counter = 0 @@ -80,150 +84,234 @@ def __init__(self, config: SimConfig): self.lines = {} self.ani = None - def create_xml_file(self, file_path): - """创建人形机器人XML文件""" - xml_content = f""" + def load_model(self): + """加载MuJoCo模型(完全修复XML格式)""" + # 完全兼容所有MuJoCo版本的XML + xml_content = """ - """ - with open(file_path, "w", encoding="utf-8") as f: - f.write(xml_content) - print(f"✅ 已在 {file_path} 创建XML文件!") - - def load_model(self): - """加载MuJoCo模型""" - current_dir = os.path.dirname(os.path.abspath(__file__)) - self.model_path = os.path.join(current_dir, self.config.xml_filename) - if not os.path.exists(self.model_path): - self.create_xml_file(self.model_path) - else: - print(f"ℹ️ XML文件已存在(路径:{self.model_path}),无需重新创建!") + + +""" try: - with open(self.model_path, "r", encoding="utf-8") as f: - xml_content = f.read() + # 直接从XML字符串加载模型 self.model = mujoco.MjModel.from_xml_string(xml_content) self.data = mujoco.MjData(self.model) print("✅ 模型加载成功!") except Exception as e: print(f"❌ 模型加载失败:{e}") + import traceback + traceback.print_exc() sys.exit(1) - # 预存关节ID + # 映射关节ID + print("\n🔍 关节ID映射结果:") for name in self.joint_names: - ctrl_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_ACTUATOR, f"{name}_motor") - if ctrl_id == -1: - ctrl_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_ACTUATOR, name) + # 直接使用关节名作为电机名 + ctrl_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_ACTUATOR, name) self.joint_ctrl_ids[name] = ctrl_id + # 获取关节ID和位置索引 joint_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, name) if joint_id != -1: - self.joint_qpos_indices[name] = 7 + joint_id + self.joint_qpos_indices[name] = self.model.jnt_qposadr[joint_id] else: self.joint_qpos_indices[name] = -1 self.last_ctrl_signals[name] = 0.0 - def get_joint_ctrl_signal(self, name, t): - """生成关节控制信号""" + # 打印详细信息 + print(f" {name}: ctrl_id={ctrl_id}, qpos_idx={self.joint_qpos_indices[name]}") + + # 验证控制信号数组 + print(f"\n📊 控制信号数组长度:{len(self.data.ctrl)}") + print(f"📊 关节位置数组长度:{len(self.data.qpos)}") + + def get_walk_action(self, name, t): + """生成行走动作控制信号""" amplitude = self.config.joint_amplitudes[name] - freq = self.config.sim_frequency + stride = self.config.walk_stride - if self.current_mode == "sin": - if "left" in name or "hip" in name or "knee" in name: - if "shoulder" in name or "elbow" in name: - signal = math.sin(t * freq) * amplitude - else: - signal = math.cos(t * freq) * amplitude + # 更新行走相位 + self.walk_phase = (self.walk_phase + 0.01) % (2 * math.pi) + + if "hip" in name: + # 髋关节交替摆动 + if "left" in name: + signal = math.sin(self.walk_phase) * amplitude * stride else: - if "shoulder" in name or "elbow" in name: - signal = -math.sin(t * freq) * amplitude - else: - signal = -math.cos(t * freq) * amplitude + signal = math.sin(self.walk_phase + math.pi) * amplitude * stride + elif "knee" in name: + # 膝关节配合髋关节运动 + if "left" in name: + signal = math.cos(self.walk_phase) * amplitude * stride * 1.2 + else: + signal = math.cos(self.walk_phase + math.pi) * amplitude * stride * 1.2 + elif "shoulder" in name: + # 手臂自然摆动(与对侧腿相反) + if "left" in name: + signal = math.sin(self.walk_phase + math.pi) * amplitude * 0.5 + else: + signal = math.sin(self.walk_phase) * amplitude * 0.5 + elif "elbow" in name: + # 肘部轻微弯曲 + if "left" in name: + signal = -math.fabs(math.sin(self.walk_phase + math.pi)) * amplitude * 0.6 + else: + signal = -math.fabs(math.sin(self.walk_phase)) * amplitude * 0.6 + else: + signal = 0.0 + + return signal + + def get_wave_action(self, name, t): + """生成挥手动作控制信号""" + amplitude = self.config.joint_amplitudes[name] + freq = self.config.wave_frequency + + # 每2秒切换一次挥动手臂 + if int(t) % 2 == 0: + self.wave_arm = "right" + else: + self.wave_arm = "left" + + signal = 0.0 + + # 挥动手臂的肩部和肘部运动 + if f"{self.wave_arm}_shoulder" == name: + # 肩部上下摆动 + signal = math.sin(t * freq) * amplitude * 1.2 + elif f"{self.wave_arm}_elbow" == name: + # 肘部配合弯曲 + signal = -math.fabs(math.sin(t * freq)) * amplitude * 1.0 + # 另一只手臂保持自然下垂 + elif ("shoulder" in name and self.wave_arm not in name): + signal = -0.2 + elif ("elbow" in name and self.wave_arm not in name): + signal = -0.8 + # 腿部保持稳定 + elif "hip" in name or "knee" in name: + signal = 0.0 + + return signal + + def get_joint_ctrl_signal(self, name, t): + """生成关节控制信号(支持多种动作模式)""" + # 根据当前模式选择动作 + if self.current_mode == "walk": + signal = self.get_walk_action(name, t) + elif self.current_mode == "wave": + signal = self.get_wave_action(name, t) + elif self.current_mode == "sin": + # 原有正弦运动模式 + if "left" in name: + signal = math.sin(t * self.config.sim_frequency) * self.config.joint_amplitudes[name] + else: + signal = -math.sin(t * self.config.sim_frequency) * self.config.joint_amplitudes[name] elif self.current_mode == "random": - signal = (math.sin(t * freq * 0.5) * 0.5 + 0.5) * amplitude * 2 - amplitude + # 随机运动模式 + signal = (random.random() * 2 - 1) * self.config.joint_amplitudes[name] elif self.current_mode == "stop": + # 停止模式 signal = 0.0 else: signal = 0.0 # 平滑过渡 - smooth_factor = 0.1 + smooth_factor = 0.05 self.last_ctrl_signals[name] = (1 - smooth_factor) * self.last_ctrl_signals[name] + smooth_factor * signal + + # 限制信号范围在关节限位内 + joint_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, name) + if joint_id != -1: + jnt_range = self.model.jnt_range[joint_id] + self.last_ctrl_signals[name] = np.clip(self.last_ctrl_signals[name], jnt_range[0], jnt_range[1]) + return self.last_ctrl_signals[name] def update_joint_controls(self): @@ -233,9 +321,13 @@ def update_joint_controls(self): ctrl_id = self.joint_ctrl_ids[name] if ctrl_id == -1: continue - ctrl_signal = self.get_joint_ctrl_signal(name, t) + try: - self.data.ctrl[ctrl_id] = ctrl_signal + ctrl_signal = self.get_joint_ctrl_signal(name, t) + if 0 <= ctrl_id < len(self.data.ctrl): + self.data.ctrl[ctrl_id] = ctrl_signal + else: + print(f"⚠️ 关节 {name} 控制ID {ctrl_id} 超出范围(最大:{len(self.data.ctrl) - 1})") except Exception as e: print(f"⚠️ 关节 {name} 控制失败:{e}") @@ -253,11 +345,13 @@ def collect_plot_data(self): # 添加各关节角度数据 for name in self.joint_names: qpos_idx = self.joint_qpos_indices[name] - if qpos_idx != -1 and qpos_idx < len(self.data.qpos): + if qpos_idx != -1 and 0 <= qpos_idx < len(self.data.qpos): angle = self.data.qpos[qpos_idx] self.plot_data[name].append(angle) + else: + self.plot_data[name].append(0.0) - # 限制数据点数量,避免内存占用过大 + # 限制数据点数量 if len(self.time_data) > self.config.max_plot_points: self.time_data.pop(0) for name in self.joint_names: @@ -284,8 +378,6 @@ def init_plot(self): self.ax.legend(loc='upper right', fontsize=10) self.ax.grid(True, alpha=0.3) - - # 设置y轴范围 self.ax.set_ylim(-2, 2) plt.tight_layout() @@ -294,12 +386,10 @@ def init_plot(self): def update_plot(self, frame): """更新绘图(动画回调函数)""" with data_lock: - # 更新每条线的数据 for name, line in self.lines.items(): if len(self.plot_data[name]) > 0 and len(self.time_data) == len(self.plot_data[name]): line.set_data(self.time_data, self.plot_data[name]) - # 自动调整x轴范围 if len(self.time_data) > 0: self.ax.set_xlim(max(0, self.time_data[-1] - 10), self.time_data[-1] + 1) @@ -319,13 +409,23 @@ def print_robot_state(self): self.fps = self.frame_count / elapsed_time if current_time - self.last_print_time >= self.config.state_print_interval: - print(f"\n===== 机器人状态(时间:{current_time:.2f}s | 帧率:{self.fps:.1f} FPS)=====") + print( + f"\n===== 机器人状态(时间:{current_time:.2f}s | 帧率:{self.fps:.1f} FPS | 模式:{self.current_mode})=====") for name in self.joint_names: ctrl_id = self.joint_ctrl_ids[name] qpos_idx = self.joint_qpos_indices[name] - if ctrl_id != -1 and qpos_idx != -1 and qpos_idx < len(self.data.qpos): + + if ctrl_id != -1 and qpos_idx != -1 and qpos_idx < len(self.data.qpos) and ctrl_id < len( + self.data.ctrl): print( f"关节 {name}: 位置 = {self.data.qpos[qpos_idx]:.2f} rad, 控制信号 = {self.data.ctrl[ctrl_id]:.2f}") + elif ctrl_id == -1: + print(f"关节 {name}: 无控制ID") + elif qpos_idx == -1: + print(f"关节 {name}: 无位置索引") + else: + print(f"关节 {name}: 索引超出范围") + self.last_print_time = current_time def reset_robot(self): @@ -333,50 +433,69 @@ def reset_robot(self): with data_lock: mujoco.mj_resetData(self.model, self.data) self.data.qpos[0:7] = [0, 0, 1.0, 1, 0, 0, 0] - # 重置控制信号缓存 + + # 重置控制信号和动作状态 for name in self.joint_names: self.last_ctrl_signals[name] = 0.0 + ctrl_id = self.joint_ctrl_ids[name] + if ctrl_id != -1 and ctrl_id < len(self.data.ctrl): + self.data.ctrl[ctrl_id] = 0.0 + + self.walk_phase = 0.0 + self.wave_arm = "right" + # 清空绘图数据 self.plot_data = {name: [] for name in self.joint_names} self.time_data = [] self.frame_counter = 0 - print("\n🔄 机器人已重置到初始状态!") - def input_listener(self): - """后台线程:监听控制台输入""" - global sim_running - self.input_thread_running = True - timeout = 0.1 + print("\n🔄 机器人已重置到初始状态!") - while self.input_thread_running and sim_running: + def check_user_input(self): + """检查用户输入(Windows兼容)""" + if sys.platform == 'win32': try: - ready, _, _ = select.select([sys.stdin], [], [], timeout) - if ready: + import msvcrt + if msvcrt.kbhit(): user_input = sys.stdin.readline().strip().lower() - if user_input == 'r': - self.reset_robot() - elif user_input in ["sin", "random", "stop"]: - self.current_mode = user_input - print(f"\n🔄 运动模式已切换为:{user_input}") - elif user_input == 'q': - sim_running = False - print("\n📤 收到退出指令,仿真将结束...") - elif user_input == 'clear': - with data_lock: - self.plot_data = {name: [] for name in self.joint_names} - self.time_data = [] - print("\n🧹 绘图数据已清空!") - elif user_input: - print(f"\n❓ 未知指令:{user_input},支持的指令:") - print(" - r:重置机器人") - print(" - sin/random/stop:切换运动模式") - print(" - clear:清空绘图数据") - print(" - q:退出仿真") - except Exception as e: - print(f"\n⚠️ 输入处理失败:{e}") - break + return user_input + except: + return None + return None + + def process_user_input(self, user_input): + """处理用户输入指令""" + if not user_input: + return - print("\n🔌 输入监听线程已优雅退出") + if user_input == 'r': + self.reset_robot() + elif user_input in ["walk", "wave", "sin", "random", "stop"]: + self.current_mode = user_input + print(f"\n🔄 运动模式已切换为:{user_input}") + if user_input == "walk": + print("👣 行走模式:机器人将进行自然行走动作") + elif user_input == "wave": + print("✋ 挥手模式:机器人将交替挥动手臂") + elif user_input == 'q': + global sim_running + sim_running = False + print("\n📤 收到退出指令,仿真将结束...") + elif user_input == 'clear': + with data_lock: + self.plot_data = {name: [] for name in self.joint_names} + self.time_data = [] + print("\n🧹 绘图数据已清空!") + elif user_input: + print(f"\n❓ 未知指令:{user_input},支持的指令:") + print(" - r:重置机器人") + print(" - walk:行走模式(新增)") + print(" - wave:挥手模式(新增)") + print(" - sin:正弦运动模式") + print(" - random:随机运动模式") + print(" - stop:停止运动") + print(" - clear:清空绘图数据") + print(" - q:退出仿真") def run_simulation(self): """运行仿真主循环""" @@ -385,14 +504,10 @@ def run_simulation(self): # 初始化绘图 self.init_plot() - # 启动输入监听线程 - input_thread = threading.Thread(target=self.input_listener) - input_thread.start() - # 启动可视化动画 self.ani = FuncAnimation(self.fig, self.update_plot, interval=50, blit=True, cache_frame_data=False) - # 显示绘图窗口(非阻塞) + # 显示绘图窗口 plt.show(block=False) # 启动MuJoCo可视化 @@ -408,18 +523,27 @@ def run_simulation(self): # 打印操作提示 print("\n📌 仿真操作提示:") print(" - 输入 'r' 回车:重置机器人") - print(" - 输入 'sin'/'random'/'stop' 回车:切换运动模式") + print(" - 输入 'walk' 回车:行走模式(新增)") + print(" - 输入 'wave' 回车:挥手模式(新增)") + print(" - 输入 'sin' 回车:正弦运动模式") + print(" - 输入 'random' 回车:随机运动模式") + print(" - 输入 'stop' 回车:停止运动") print(" - 输入 'clear' 回车:清空绘图数据") print(" - 输入 'q' 回车:退出仿真") print(" - 按 Ctrl+C:强制退出仿真") - print("\n🚀 仿真开始...") + print("\n🚀 仿真开始(默认模式:行走)...") # 仿真主循环 - global sim_running last_step_time = time.perf_counter() while sim_running and v.is_running(): current_time = time.perf_counter() + + # 检查并处理用户输入 + user_input = self.check_user_input() + if user_input: + self.process_user_input(user_input) + if current_time - last_step_time >= self.config.timestep: # 更新关节控制 self.update_joint_controls() @@ -445,28 +569,45 @@ def run_simulation(self): # 处理matplotlib事件 plt.pause(0.001) - # 停止输入监听线程 - self.input_thread_running = False - input_thread.join(timeout=1.0) - - # 关闭绘图窗口 + # 清理资源 plt.close(self.fig) - print("\n🏁 仿真结束!") # ====================== 程序入口 ====================== if __name__ == "__main__": - # 设置matplotlib后端(避免显示问题) + # 设置matplotlib后端 import matplotlib matplotlib.use('TkAgg') + # Windows控制台编码修复 + if sys.platform == 'win32': + try: + # 设置控制台编码为UTF-8 + import subprocess + + subprocess.call('chcp 65001', shell=True) + except: + pass + # 初始化配置 config = SimConfig() # 创建仿真器并运行 simulator = HumanoidSimulator(config) - simulator.run_simulation() - sys.exit(0) \ No newline at end of file + try: + simulator.run_simulation() + except KeyboardInterrupt: + sim_running = False + print("\n⚠️ 用户中断,正在退出...") + except Exception as e: + print(f"\n❌ 程序异常:{e}") + import traceback + + traceback.print_exc() + finally: + # 确保资源正确释放 + plt.close('all') + sys.exit(0)