具身智能仿真器收官:Gazebo与CoppeliaSim分工实践
写到第十篇这个系列该收口了。前面九篇我们把具身智能仿真器这条线从数据、传感器、机械臂一路捋到学习路线最后一篇落在两套最常被拿来对比的仿真环境上Gazebo 和 CoppeliaSim。说实话这两个名字在具身智能的讨论里几乎是绑定的——做机械臂强化学习的人离不开 CoppeliaSim做 ROS/ROS 2 整机集成的人离不开 Gazebo而真正做过完整项目的人往往两个都用过只是用在不同阶段。这篇不打算写成一个谁更好的口水仗。我更想讲的是当你的具身智能 agent 需要一个能跑通感知—决策—执行的闭环沙盒时Gazebo 和 CoppeliaSim 各自扮演什么角色、边界在哪里、模型怎么互通、踩过的坑怎么绕开。如果你手上有 URDF有机械臂或者小车有一台装着 Ubuntu 的机器并且想让训练出来的策略真的能跑起来那这篇东西就是给你写的。哪怕你只是刚开始搭 gazebo 仿真环境或者卡在 urdf 导入 coppeliasim 那一步也能从后面几节的排查表里直接抄作业。1. 收官篇为什么要把 Gazebo 和 CoppeliaSim 摆在同一张桌子上1.1 具身智能里仿真器到底承担了什么活很多人对仿真器的理解停留在画个三维图看机器人动一动。真做过具身智能项目就知道仿真器在整条链路里承担的是数据工厂 安全试验场这两个角色。前者要能高速批量产出带标注的感知数据RGB、深度、点云、关节状态、力矩后者要能让一个还没训练好的策略在里面乱撞而不砸坏任何硬件。这两件事对仿真器的要求是不一样的。批量产数据要求渲染吞吐高、支持多实例并行、支持光照和材质随机化安全试验场要求动力学足够真、接触解算不炸、时间步进可控可复现。Gazebo 长于前者加上完整的机器人中间件生态CoppeliaSim 长于后者的接触处理和脚本化快速原型。这也就解释了为什么在具身智能的工程实践里常见组合是CoppeliaSim 做算法快速迭代Gazebo 做 ROS 整机联调。还有一个经常被忽略的点仿真器输出的是具身智能数据集的原始素材而数据集质量要求里有一条很硬的指标是状态—动作—观测三者时间戳对齐。Gazebo 走的是 ROS 时间/clock话题 use_sim_timeCoppeliaSim 走的是自己的仿真步计数器加远端 API 回传。两套时间体系如果不做统一混着用的时候数据一定是错位的后面我会专门讲怎么对齐。1.2 两套环境的核心差异对照先把差异摊开后面所有实操都建立在这张表上。这不是官方的功能清单是我自己用下来最影响开发节奏的几个维度。维度GazeboClassic 11 / Gazebo SimCoppeliaSimV4.5定位ROS/ROS 2 原生仿真整机系统联调通用机器人仿真算法快速原型模型格式SDF 为主URDF 需转换自有.ttm场景格式支持 URDF 导入通信方式ROS 话题/服务/动作gz-transportZeroMQ Remote API、Legacy Remote API、内嵌 Lua物理引擎ODE、Bullet、DARTBullet、ODE、Newton、Vortex、MuJoCo脚本能力插件为主CPython 靠外部节点内嵌 Lua 外部 Python改逻辑不用重编译传感器相机、深度、激光、IMU、力/力矩插件齐全视觉传感器、力传感器、接近传感器、视觉传感器回调学习曲线陡坑集中在环境和版本匹配缓坑集中在导入和坐标系并行采样多实例 无头渲染可行多实例可行但 ZMQ 端口管理要自己管看这张表就能明白一件事它们不是替代关系。Gazebo 的强项是你的代码本来就在 ROS 2 里跑插上仿真就能验证CoppeliaSim 的强项是我想十分钟内看到机械臂按我的逻辑动起来而且我不想重新编译任何东西。1.3 选型逻辑先问三个问题再决定我在带新人的时候一般让人先回答三个问题答案基本就决定了先用哪个。第一个问题你的算法最终要落到 ROS/ROS 2 的节点图里吗如果是直接上 Gazebo别犹豫因为ros2_control那一套控制器接口、TF 树、传感器话题在 Gazebo 里是天然打通的在 CoppeliaSim 里得自己搭桥。第二个问题你现在主要在调的是控制逻辑还是感知数据调控制逻辑逆运动学、轨迹规划、抓取策略用 CoppeliaSim改脚本即时生效反馈循环短调感知相机标定、点云配准、SLAM用 Gazebo因为它的相机插件和 ROS 消息类型可以直接喂给你现有的感知栈。第三个问题你需要几个并行实例做强化学习要知道一个仿真实例一小时能产多少 transition 直接决定实验能不能做完。Gazebo Sim 支持--headless-rendering加多实例CoppeliaSim 也支持无界面模式但端口和场景状态隔离要自己写脚本管。提示如果你的目标是把具身智能 agent 从仿真推到真机建议一开始就用 Gazebo 做最终验证环境因为它和真机上运行的 ROS 2 节点图是一致的。CoppeliaSim 更适合当作草稿纸。2. 两套仿真器的核心概念与文件体系拆解2.1 URDF、SDF、场景文件一份模型三种命运具身智能项目里最绕不开的就是模型描述文件。URDF 是 ROS 世界的通用语言描述的是连杆树 关节 视觉/碰撞几何SDF 是 Gazebo 的原生格式除了这些还描述世界、光照、物理参数、插件CoppeliaSim 则把它全部内化成自己的场景对象树。这里有个非常容易踩的坑URDF 里的inertial经常是随手填的。很多开源机械臂模型包括一些桌面级六轴臂的 URDF 是从 CAD 导出的惯量矩阵要么缺失要么用了一个默认值。Gazebo 遇到缺失惯量会给你一个很小的默认值结果就是机械臂在仿真里轻得像纸片一碰就飞CoppeliaSim 的 URDF 导入插件则会根据质量自动生成一个近似惯量看起来正常但真机上完全不是那么回事。我的做法是惯量必须手算或者从 CAD 导出后核对。对于规则几何体惯量有解析解比如一个质量 m、长 l 的均匀细杆绕质心垂直轴的转动惯量是I m * l² / 12。你可以先用这个把量级估出来再看 URDF 里填的值是不是差了三个数量级。这一步花十分钟能省掉后面几天的为什么策略在仿真里学会了到真机就废。!-- URDF 中一个常被忽略但很关键的部分 -- link namelink2 inertial origin xyz0 0 0.15 rpy0 0 0/ mass value1.2/ !-- 单位是 kg·m²不是 kg·mm²也不是 g·cm² -- inertia ixx0.0043 ixy0 ixz0 iyy0.0043 iyz0 izz0.0009/ /inertial collision geometrycylinder radius0.04 length0.3//geometry /collision /link顺便说一句collision几何和visual几何要分开处理。视觉几何可以精细碰撞几何必须简化——用圆柱代替复杂网格用凸包代替凹形物理引擎才不会在接触解算上耗光算力。CoppeliaSim 导入 URDF 时会把两者都变成 shape你得手动把碰撞体替换成简化版本否则一个机械臂场景跑起来实时率能掉到 0.2 以下。2.2 Gazebo 的插件体系与它为什么这么重Gazebo 的能力几乎全部来自插件。传感器是插件控制器接口是插件世界物理参数是插件连加载一个模型都要通过spawn_entity服务。这种设计的代价是启动链路长、依赖多好处是每一样都能换成你自己的实现。以机械臂为例一个完整可用的 Gazebo 仿真链路大概是这样的robot_state_publisher发布 TF →gz_ros2_control插件加载硬件接口 →controller_manager加载joint_state_broadcaster和joint_trajectory_controller→ 你发一条轨迹话题 → 胖子控制器算出力矩/位置 → 插件写进 Gazebo 的关节 → 物理引擎步进 → 传感器插件回传。这条链上任一环断掉现象都是机器人不动但原因完全不同。我见过最常见的是controller_manager起来了但 controller 没激活ros2 control list_controllers一看状态是unconfigured这时候去翻spawner的日志多半是 YAML 里的 controller 名字和实际对不上。Gazebo Classic 和 Gazebo Sim原来叫 Ignition的插件命名还不一样Classic 用libgazebo_ros_camera.so这种新的是gz-sim的 system plugin 加gz_ros2_control。Ubuntu 22.04 上跑 ROS 2 Humble官方配套是 Gazebo FortressUbuntu 24.04 上跑 ROS 2 Jazzy配套是 Gazebo Harmonic。版本别乱配这是我在 gazebo安装ros环境ubuntu22 这类问题上见过最多的翻车点。2.3 CoppeliaSim 的场景树、脚本分层与 API 调用CoppeliaSim前身 V-REP的思路完全不同它是一个自带脚本引擎的场景编辑器。场景里每个对象都有一个句柄handle脚本分四层主脚本main script整个场景一份负责仿真步进的整体流程一般不动子脚本child script挂在对象上分线程型和非线程型线程型可以写阻塞循环非线程型必须写成状态机定制脚本customization script挂在对象上但不参与仿真循环处理 UI 交互、属性面板附加组件add-on全局工具脚本用来做批量操作。这套分层的价值在于改逻辑不用重编译。你改一行 Lua保存仿真立刻反映。相比之下 Gazebo 里改一个插件的参数都得重启整条 launch。所以在做抓取策略的早期迭代时我在 CoppeliaSim 里一天能试几十种参数组合在 Gazebo 里可能只能试几种。外部控制走 ZeroMQ Remote APIPython 端大概长这样from coppeliasim_zmqremoteapi_client import RemoteAPIClient client RemoteAPIClient(localhost, 23000) sim client.require(sim) joint_handles [sim.getObject(f/UR5/joint{i}) for i in range(1, 7)] sim.setStepping(True) sim.startSimulation() for i in range(500): t sim.getSimulationTime() target [0.3 * (1 - (i % 100) / 100.0)] * 6 for h, q in zip(joint_handles, target): sim.setJointTargetPosition(h, q) sim.step() sim.stopSimulation()注意sim.setStepping(True)这一行。CoppeliaSim 默认是自由运行仿真时间和真实时间会漂开了步进模式之后只有你调用sim.step()它才走一步这对做强化学习的同步采样是必须的否则你的 agent 和仿真器会各跑各的。2.4 坐标约定、单位与旋转表示三个最容易翻车的地方这一节单独拎出来说因为它踩过太多次。第一单位。URDF 用米和弧度CoppeliaSim 也默认米和弧度Gazebo 用米和弧度。看起来一致但 URDF 导入 CoppeliaSim 时有个选项是缩放因子默认值不一定是 1。如果你的模型导进去像一个玩具先去看这个。第二旋转表示。URDF 用 rpy固定轴 XYZ 欧拉角Gazebo 的 SDF 用四元数或者 rpyCoppeliaSim 的 API 大量使用欧拉角且可能是内旋顺序。同一个姿态三种表示转换时符号错了你还看不出来——模型朝向反了 180 度但关节还能动只有在你发现末端执行器总是抓反方向时才意识到。第三坐标轴朝向。ROS/URDF 约定是 X 前、Y 左、Z 上。很多从 Blender 导出的模型是 Z 前、Y 上导入时要么在导出环节转好要么在仿真器里加一层包装的 dummy 对象做旋转。用 Blender 导出 gazebo 模型的时候导出 DAE 记得选 Y-up并且检查 scale 是不是 1.0。注意导入模型之后的第一件事不是马上跑控制而是用仿真器自带的位姿显示工具把每个连杆的坐标系和真机 CAD 对照一遍。这十分钟的检查能挡掉后面八成控制逻辑没问题但结果不对的问题。3. 环境搭建实操从零到能跑起来3.1 Gazebo 侧Ubuntu 22.04 ROS 2 Humble 的完整准备这套组合是目前最省心的因为官方有配套的二进制包。装完之后要验证三件事Gazebo 能起来、ROS 2 能起来、两者能对话。# 基础 ROS 2 环境按官方文档装好之后 sudo apt install ros-humble-gazebo-ros-pkgs sudo apt install ros-humble-ros2-control ros-humble-ros2-controllers sudo apt install ros-humble-gz-ros2-control # Gazebo Sim 用这个 # 快速验证 Gazebo 本体 gz sim shapes.sdf # Gazebo SimHarmonic/Fortress gazebo # Gazebo Classic # 验证 ROS 2 能拿到仿真时间 ros2 topic echo /clock --onceros2 topic echo /clock --once这条命令很关键。它验证的是仿真时钟有没有通过 ROS 暴露出来。如果这里收不到消息后面所有跟use_sim_time相关的节点都会用墙上时间TF 会乱成一团。解决方法是 launch 文件里确保gzserver带-r参数自动开始仿真或者用ros_gz_bridge做时钟桥接。关于gazebo ros pkgs包它是个元包里面包含gazebo_ros、gazebo_plugins、gazebo_msgs等。Gazebo Classic 时代大家都在用它切到 Gazebo Sim 之后对应的是ros_gz系列包ros_gz_sim、ros_gz_bridge、ros_gz_image。这两套不要混用混用的症状是插件加载了但话题发不出来。还有一个经常被问到的组合Ubuntu 24.04 ROS 2 Jazzy Gazebo Harmonic。这条线更新的方式是apt install ros-jazzy-ros-gzGazebo 本体则通过apt install gz-harmonic。装的时候注意ros_gz的版本要和 Gazebo 主版本号对上Harmonic 对应gz-sim 8。3.2 CoppeliaSim 侧跨平台安装与远端 API 准备CoppeliaSim 的安装简单得多官网下压缩包解压就能跑不需要系统级依赖。真正要配的是远端 API 的客户端。Legacy Remote API 时代需要手动编译remoteApi.so并放到正确路径比较麻烦现在推荐用 ZeroMQ Remote APIPython 端一行装pip install coppeliasim-zmqremoteapi-client启动 CoppeliaSim 之后场景里要确认simRemoteApi相关的附加组件已经加载。ZMQ API 默认监听23000端口可以用sim.setNamedStringParam改也可以在启动配置里改。做多实例并行的时候每个实例给一个独立端口./coppeliaSim.sh -GzmqRemoteApi.rpcPort23001 -GzmqRemoteApi.pubPort23002-G是覆盖全局参数的写法后面的参数名要和你装的版本对上。我一般会写一个 shell 脚本按序号自动生成端口从 23000 开始每实例加 10。CoppeliaSim 的 Edu 版本免费功能上对具身智能研究够用唯一要注意的是它和 Pro 版在某些传感器和引擎上有限制。做小车或者六轴臂的常规仿真Edu 完全够。3.3 URDF 导入 CoppeliaSim 的完整流程与三个开关urdf导入coppeliasim这个操作看起来是一键实际上有三个开关决定了成败。流程是这样菜单里找到 URDF 导入功能不同版本位置略有差别通常在 Modules 或 Plugins 菜单下选择你的.urdf文件然后会弹一个对话框。对话框里的关键选项关节模式每个关节可以选择 dynamic动力学驱动或 kinematic运动学驱动。做控制算法验证选 kinematic让关节严格跟随你的目标值做动力学研究选 dynamic让它受重力、惯量、接触影响。基座处理如果你的 URDF 根连杆需要固定在世界坐标上勾选固定选项否则它会掉下去。几何合并可以把多个视觉 shape 合并成一个减少场景对象数加快渲染。导入完成后第一件事是检查对象树。正常的结构应该是一个根 dummy 下面挂着一串 shape 和 joint每个 joint 的父子关系对应 URDF 的连杆树。如果看到一堆散开的 shape、没有 joint 连接说明 URDF 本身有问题——最常见是link没有通过joint串联或者 joint 的parent/child名字写错。导入后的三个必改项第一碰撞体简化。把复杂网格的碰撞体替换成基本几何形状方法是选中对象用形状替换功能或者手动加一个不可见的简化 shape 作为碰撞体。第二质量与惯量核对。对着上一节算出来的值在对象属性里核对一遍。第三关节限制。URDF 里的 joint limit 有时候导过来会丢尤其是速度限制。在属性面板里手动补上否则关节会以无限速度冲到底。3.4 GPU 加速与渲染无头环境下的正确姿势做具身智能训练无头渲染几乎是刚需。你不可能给每个并行实例配一块显示器和桌面环境。Gazebo Sim 的做法是加--headless-rendering然后配合ogre2渲染引擎。传感器相机会走 GPU 离屏渲染需要显卡驱动支持 EGL 或者 GLX。容器里跑的话用 NVIDIA 容器工具链把 GPU 透进去然后# 无头渲染 指定渲染引擎 gz sim -s -r --headless-rendering --render-engine ogre2 world.sdf # 容器内验证 GPU 是否真的被用上 nvidia-smi glxinfo -B | grep -i OpenGL renderer如果glxinfo里显示的是llvmpipe那说明你在用软件渲染速度会差一个数量级这时候去查驱动和 EGL 的环境变量。CoppeliaSim 的无头模式用命令行参数启动视觉传感器的采集需要显式开启。要注意的是无头模式下视觉传感器的帧率会和物理步进解耦如果你做的是视觉伺服得自己加同步逻辑等一帧渲染完成再读数据。关于gazebo使用gpu加速还有个隐形坑是混合显卡笔记本。带独显的机器上OpenGL 请求可能被集显接走表现为 Gazebo 起来很卡、实时率上不去。Ubuntu 上可以用prime-select或者环境变量强制指定具体方式取决于你的驱动版本。4. 把同一个机械臂同时跑在两套仿真器里的完整流程4.1 统一模型源一份 URDF两处分发我的做法是以 URDF 为唯一真源所有改动先改 URDF再分发到两边。这样能避免Gazebo 里改了一个参数CoppeliaSim 忘了同步这种慢性病。具体做法是用一个xacro文件生成 URDF再用它生成 Gazebo 需要的 SDF 和 CoppeliaSim 需要的导入文件。xacro的好处是可以用宏和参数把几何、惯量、关节限制集中管理xacro:macro namearm_link paramsname mass length radius link name${name} inertial mass value${mass}/ inertia ixx${mass*length*length/12} ixy0 ixz0 iyy${mass*length*length/12} iyz0 izz${mass*radius*radius/2}/ /inertial /link /xacro:macro这样惯量是算出来的不是抄来的。后面不管换多长的连杆数值自动跟着变。分发流程# 生成完整 URDF xacro robot.urdf.xacro robot.urdf # Gazebo 侧直接用 URDFGazebo 会自动转 SDF # CoppeliaSim 侧用同一个 robot.urdf 走导入流程关键点在于两边用的是同一个文件。这样当你在 CoppeliaSim 里发现关节限制不对时改的是 URDFGazebo 那边自动跟着对。4.2 Gazebo 侧的控制器配置与启动机械臂在 Gazebo 里要动核心是ros2_control的配置。配置文件分两部分硬件接口描述和控制器参数。# robot_controllers.yaml controller_manager: ros__parameters: update_rate: 100 # Hz和物理步长配合 joint_state_broadcaster: type: joint_state_broadcaster/JointStateBroadcaster arm_controller: type: joint_trajectory_controller/JointTrajectoryController arm_controller: ros__parameters: joints: - joint1 - joint2 - joint3 - joint4 - joint5 - joint6 command_interfaces: - position state_interfaces: - position - velocity constraints: stopped_velocity_tolerance: 0.05 goal_time: 0.5update_rate和 Gazebo 的物理步长关系要算清楚。Gazebo 默认物理步长是 1 ms1000 Hz控制器 100 Hz意味着控制器每 10 个物理步更新一次。如果步长设成 10 ms 而控制器还是 100 Hz两者频率一样控制会变得很脆稍微加点负载就抖。启动顺序不能乱# 1. 起 Gazebo 世界 gz sim -r empty_world.sdf # 2. 把机器人 spawn 进去 ros2 run ros_gz_sim create -topic robot_description # 3. 加载并激活控制器 ros2 run controller_manager spawner joint_state_broadcaster ros2 run controller_manager spawner arm_controller # 4. 验证 ros2 control list_controllerslist_controllers的输出里状态应该是active。如果是inactive或者unconfigured去ros2 control list_hardware_interfaces看接口有没有起来。这一步是排障的分水岭接口没起来是插件问题接口起来了但控制器没激活是参数问题。4.3 CoppeliaSim 侧的关节驱动与数据回传CoppeliaSim 那边不用搞这么复杂的中间件直接对着关节句柄写目标值就行。但有两个细节决定了仿真结果可不可信。第一控制模式要和算法假设一致。如果你的算法假设的是位置控制就在 CoppeliaSim 里把关节设成位置控制模式如果假设的是力矩控制就得同时设置目标力矩和关节的动力学参数。混用的话仿真里跑出来的策略到真机上会完全失效因为真机的关节既不是理想位置源也不是理想力矩源。第二仿真时间步要和你的控制周期匹配。CoppeliaSim 默认的仿真步长是 50 ms20 Hz这个值对控制器来说太粗了。做机械臂控制建议改到 5 ms 或者更小在仿真设置里改。步长缩小之后计算量上升但至少物理是可信的。# 关节位置控制 回读状态 for h in joint_handles: sim.setJointMode(h, sim.jointmode_kinematic, 0) sim.setJointTargetPosition(h, 0.0) sim.setStepping(True) sim.startSimulation() for step in range(2000): q_des compute_action() # 你的策略 for h, q in zip(joint_handles, q_des): sim.setJointTargetPosition(h, q) sim.step() q_act [sim.getJointPosition(h) for h in joint_handles] t sim.getSimulationTime() buffer.append((t, q_act, q_des))注意jointmode_kinematic。这个模式下关节严格跟随目标位置不走动力学解算适合验证运动学层面的逻辑。要做接触力、抓取稳定性这类研究得换成 dynamic 模式并且给关节配 PID。数据回传这块CoppeliaSim 的getSimulationTime给的是仿真时间不是墙上时间。这一点和 Gazebo 的/clock是一致的都遵守仿真时间的语义。做数据集的时候时间戳一定要用仿真时间用墙上时间会导致时序错乱尤其在实时率不是 1.0 的时候。4.4 传感器对齐与数据校验怎么确认两边看到的是同一个世界传感器对齐是双仿真器并用时最容易被跳过、后果最严重的一步。需要对齐的传感器至少包括这几类传感器Gazebo 侧接口CoppeliaSim 侧接口对齐要点RGB 相机sensor_msgs/Image视觉传感器回调 /getVisionSensorImg内参、图像上下翻转深度相机sensor_msgs/Image(32FC1)深度图 /getVisionSensorDepthBuffer深度单位米 vs 归一化IMUsensor_msgs/Imu加速度计 / 陀螺仪对象坐标系朝向、单位关节编码器sensor_msgs/JointStategetJointPosition零位定义六维力/力矩geometry_msgs/WrenchStamped力传感器对象参考坐标系传感器系 vs 世界系两处最容易出问题的地方图像上下翻转和深度归一化。CoppeliaSim 的视觉传感器取出的原始图像缓冲区行序和 ROS 图像消息的行序是反的直接转成 ROS 消息喂给感知节点图是倒的你会在为什么检测框位置全错上浪费一整下午。解决办法是在缓冲区和消息之间加一次行翻转写在转接代码里。深度那块更隐蔽CoppeliaSim 的getVisionSensorDepthBuffer返回的是归一化深度近裁剪面为 0远裁剪面为 1而 ROS 的深度图约定是米。转换公式涉及近远裁剪面near, far 0.01, 5.0 depth_m near * far / (far - (far - near) * depth_norm)这个公式我贴在代码注释里很多年了因为每次换场景都会忘。力/力矩传感器的对齐要点是参考坐标系。ROS 的WrenchStamped有header.frame_id明确说这个力是在哪个坐标系下表达的。CoppeliaSim 的力传感器默认可能返回世界坐标系下的值直接对比两边数据会差一个旋转变换。做抓取力控的时候这个差异足以让结论完全反过来。校验方法很朴素但有效让机械臂在两个仿真器里做同一个动作序列录下所有传感器数据画在同一张图上看。曲线形状应该一致幅值允许有差异物理引擎不同。如果形状都不一样说明有一个环节的坐标系或者单位错了。5. 常见问题与排查速查表5.1 Gazebo 界面一直闪、卡顿、加载失败为什么gazebo界面一直在闪这个问题我见过太多次答案基本集中在三类原因。虚拟机里的显卡问题。VMware 或 VirtualBox 里跑 Gazebo Classic界面闪、花屏、窗口撕裂最常见的原因是虚拟显卡的 OpenGL 支持不全。老办法是设一个环境变量把虚拟显卡的 3D 加速特性关掉export SVGA_VGPU100 gazebo这个变量是 VMware 特有的在物理机上设了没坏处也没用。Wayland 与 X11 的问题。Ubuntu 22.04 之后默认会话是 WaylandOGRE 渲染在某些驱动上表现异常。切换回 X11 会话或者在登录界面选 X11通常能解决闪烁。另一个思路是改 OGRE 的渲染目标模式export OGRE_RTT_MODECopy显卡驱动与混合显卡。NVIDIA 专有驱动加上 Intel 集显的机器Gazebo 可能被分配到集显上渲染。检查方法是看glxinfo的 renderer 字段。如果不是独显用__NV_PRIME_RENDER_OFFLOAD1 __GLX_VENDOR_LIBRARY_NAMEnvidia前缀强制走独显。加载失败的话先看终端输出。gz sim起不来一般是 SDF 语法错误或者资源路径找不到模型加载不出来一般是GAZEBO_MODEL_PATH没配或者模型目录里缺model.config。Gazebo Sim 用GZ_SIM_RESOURCE_PATH这个变量名和 Classic 时代的GAZEBO_MODEL_PATH不一样从旧版本迁移时特别容易漏。5.2 导入 URDF 之后模型散架、坐标系错乱urdf导入coppeliasim之后最常见的三类现象对应的原因和处理方式是这样的现象可能原因处理方式模型散成一堆 shape没有关节连接URDF 里 link 未通过 joint 串联或 joint 名字不匹配用 URDF 校验工具先检查树结构完整性模型尺寸小得离谱或大得离谱导入缩放因子不是 1或 URDF 用了非米单位在导入对话框里改缩放或修正 URDF 单位模型朝向不对视觉网格本身坐标系与 URDF 约定不一致在网格层面修正或在导入后加一层旋转 dummy关节能动但末端位置全错关节零位定义不一致或关节轴方向反了逐个关节手动核对零位和轴向改 URDF 的axis有个细节值得单独说URDF 里fixed类型的关节在 CoppeliaSim 里会被合并成一个刚体而在 Gazebo 里如果不显式指定可能保留成一个固定的 joint 对象。当你的模型里有大量用 fixed joint 连接的传感器或者末端工具时两边的对象结构会不一样这时候按对象名去取句柄的代码就会挂。解决办法是在两边的脚本里都用语义化的命名并且写一层封装去适配不同的对象结构。5.3 关节不动、抖动、穿透物理层面的排查顺序机械臂在仿真里不动排查顺序应该是从上层往物理层走不要一上来就调物理参数。第一步看指令有没有到。Gazebo 侧ros2 topic echo /arm_controller/controller_state看有没有反馈CoppeliaSim 侧直接打印你设的目标值确认不是空数组。这一步能挡掉一半问题。第二步看关节限制。如果目标值超出了关节上下限控制器可能会拒绝执行或者卡在边界。Gazebo 的joint_trajectory_controller在超限时会报错但不会让关节动日志里能看到。第三步看控制模式和增益。动态模式下如果 PID 增益太小关节会像没吃饭一样软绵绵地慢慢挪增益太大则抖。经验值是先把 P 从 10 开始往上加加到头开始抖就退回来一半再补一点 D 抑制振荡。这个和真机调参的思路一样只不过仿真是零成本试错多试几组没关系。第四步才是物理引擎参数。接触刚度、摩擦系数、求解器迭代次数这些在最后调。Gazebo 里改这些是改 SDF 的physics段和表面参数CoppeliaSim 里是在对象属性和全局设置里改。穿透问题基本都指向碰撞体简化不足或者步长太大。两个高速运动的物体如果步长是 10 ms一个步内移动的距离可能超过物体厚度求解器来不及检测碰撞就穿过去了。缩小步长或者开启连续碰撞检测能解决。5.4 时间同步、实时率与强化学习采样的关系这一条是我最想强调的因为它直接决定你的具身智能 agent 训练能不能复现。Gazebo 里有个实时率RTF指标1.0 表示仿真时间和真实时间等速。RTF 小于 1 意味着仿真跑得比真实慢这在复杂场景里很常见。问题是很多人写控制代码时用了墙上时间做周期判断结果 RTF 一变控制周期就跟着变。正确的做法是用仿真时间。ROS 2 里把节点的use_sim_time参数设为true节点就会订阅/clockCoppeliaSim 里用sim.getSimulationTime()。还有一个隐蔽的坑物理步长和渲染步长的分离。Gazebo Sim 里物理步长可以设得很小比如 1 ms保证物理可信但渲染可以只按 30 Hz 出图。如果你在按时序采集图像数据就会看到时间戳不均匀分布。做数据集的时候要在后处理阶段做时间对齐用插值把状态和图像对齐到统一的时间栅格上。强化学习采样那边CoppeliaSim 的步进模式给了你完全确定性的时间推进——调用一次sim.step()推进一个固定步长这个特性在做算法对比实验时非常宝贵因为它排除了实时率波动带来的噪声。Gazebo 里想做到同样的确定性就得用gz sim的单步命令加同步模式配置起来麻烦一些。5.5 无头环境、二维码加载与视觉场景随机化ign gazebo加载二维码这个需求背后其实是一个很实的问题做具身智能的视觉任务经常需要在场景里放标记物来验证定位或者位姿估计算法。Gazebo 里给一个平面贴纹理做法是给材质指定一张图片路径要在资源目录里能找到。如果图片加载不出来画面显示为纯色多半是路径没进GZ_SIM_RESOURCE_PATH。更工程化的做法是用脚本生成随机纹理每轮 episode 换一批材质和光照这就是视觉域的随机化。做 sim-to-real 的时候这一步能显著提升策略的泛化性。CoppeliaSim 那边对应的是动态修改的纹理和视觉传感器的图像处理参数。无头环境下的视觉传感器还有一个陷阱某些渲染后端在无头模式下的光照计算结果和带界面时不一致尤其是环境光遮蔽和阴影。如果你的数据集是在带界面时调好的光照参数下采集的切到无头批量采集时颜色分布会漂移。建议采集前先跑一小批和带界面的结果对比一下直方图。ros2 gazebo slam这类组合里激光雷达的仿真也有类似问题Gazebo 的 GPU 激光插件用离屏渲染算距离无头模式下要确保渲染上下文正常创建否则话题有发布但数据全零或者全满量程。验证方法是把点云可视化出来看是不是一个正常的房间形状。6. 写在系列收口处这两套环境我到底怎么分工用十篇下来如果只能留一句话给还在纠结选哪个的人我的答案是别选分工用。我自己现在的固定做法是这样的。算法设计和快速验证阶段在 CoppeliaSim 里做因为改一行脚本立刻见效一个下午能试几十组抓取参数而且它的接触解算对抓取这类任务比 Gazebo 顺手。等到算法基本成型要把感知、规划、控制串成完整的节点图就搬到 Gazebo 里用ros2_control那一套把整条链路跑通顺便把真机上要用的 launch 文件在这里调好之后迁到真机几乎不用改结构。最后做批量数据生成和策略的泛化测试回到无头多实例的 Gazebo 或者 CoppeliaSim取决于任务更偏感知还是更偏控制。中间有一件事必须做统一模型源和统一时间语义。所有模型从同一份 xacro 出所有时间戳用仿真时间所有传感器在两边都做一次对照校验。这三条做到了两个仿真器之间切换的成本就只是重跑一遍导入而不是重写一遍代码。最后分享一个我踩过好几次才形成习惯的小操作每次换仿真环境或者升级版本之后先跑一个五分钟的最小闭环验证——一条关节轨迹、一次相机取图、一次关节状态回读三个都通过再动正式的实验。这个习惯帮我挡掉了大量跑了一晚上发现数据全是错的这种事故。具身智能的仿真实验动辄几个小时前面这五分钟真的不算什么。