实体与虚拟的 6D 状态同步
毕设的核心命题是”现实驱动虚拟,虚拟赋能现实”——实体机器人的位姿实时同步进 UE5 场景,UE5 里的高保真物理反过来给算法提供验证环境。这套闭环的技术难点不在渲染,而在两个引擎对空间的定义根本不一样。
先说清楚一件事:这个仓库里没有一个叫 rcia_ue5_bridge 的 ROS2 节点。vision_utils/src/test/RCIA_Vision/ 下只有两个 WebSocket 测试桩:
# websocket S.py —— 服务端,纯 echo
async def echo(websocket, path):
async for message in websocket:
print(f"Received message: {message}")
await websocket.send(f"Echo: {message}")
start_server = websockets.serve(echo, "172.20.153.18", 8080)
# websocket C.py —— 客户端,发一条收一条
async with websockets.connect("ws://192.168.0.2:8080") as websocket:
await websocket.send("Hello Server!")
greeting = await websocket.recv()
这两个脚本的作用是打通链路、量延迟,不是业务代码。真正的数据流转逻辑在 UE5 工程侧的 Blueprint 里,不在这个仓库。所以下面写的是设计与推导,代码侧只能给出 ROS2 这一端的事实。
1. 手性转换
两个引擎的约定:
| ROS2(REP-103) | UE5 | |
|---|---|---|
| 手性 | 右手系 | 左手系 |
| X | 前 | 前 |
| Y | 左 | 右 |
| Z | 上 | 上 |
| 单位 | 米 | 厘米 |
| 旋转正方向 | 逆时针(右手定则) | 顺时针 |
两者都是 Z-up,唯一的差别是 Y 轴反向——这正是左右手性的体现。所以位置映射是一次 Y 取反加一次单位缩放:
姿态跟着手性一起翻。UE5 用 FRotator(Pitch, Yaw, Roll),度制;ROS2 用四元数或 RPY,弧度制。绕 Y 轴取反的坐标变换会让另外两个轴的旋转方向反过来:
Roll 不取反是因为它绕 X 轴,而 X 轴在变换里没动。这条规律可以这样记:取反的轴自身不变号,另外两轴变号。
顺带纠正一个流传很广的写法。有资料把 ROS→UE5 的映射写成 x→x, y→-z, z→y,这个映射是把 Z-up 换成 Y-up,对应的是 Unity(左手 Y-up),不是 UE5。UE5 和 ROS2 一样是 Z-up,套这个公式会让整个场景躺倒 90°,而且这个错误在只看俯视图时看不出来——躺倒之后水平面内的运动依然正常,只有涉及高度时才暴露。调试时如果发现”平面运动对得上、上下就是不对”,先怀疑这里。
2. 上下行链路
上行(实体 → 虚拟):ROS2 侧把 TargetSpinTop 和云台 TF 序列化成 JSON 推给 UE5,驱动虚拟机器人跟随实体运动。这条链路的频率由视觉帧率决定,debug 模式 120 Hz、非 debug 模式可达 200 Hz,上限取决于相机曝光时间与处理管线瓶颈。
下行(虚拟 → 实体):UE5 里的 Chaos 物理引擎跑弹道仿真,把虚拟弹着点回传给 ROS2,与查表法算出的补偿角做交叉验证。这条链路不需要高频,事件驱动即可。
传输选 WebSocket 而不是 DDS,理由很简单:UE5 没有可靠的原生 DDS 支持,而 WebSocket 在两端都有成熟实现,JSON 也便于抓包排查。代价是序列化开销和文本冗余——一个 6D 位姿的 JSON 约 200 字节,二进制只要 48 字节。在 200 Hz 下这点带宽差异仍可接受,可读性更值钱。
论文实测数据传输延迟低于 5 ms。这个数字是用上面那两个 echo 脚本量出来的:客户端发一条带时间戳的消息,服务端原样返回,除以二得到单程延迟。测的是链路本身,不含序列化和 Blueprint 的处理时间——所以”5 ms”应该理解为传输通道的能力上界,不是端到端同步延迟。这一点在论文里没有写清楚,是个应该修正的表述。
3. simulate_mode 让这套东西能离线跑
串口驱动里那个开关在这里发挥了关键作用:
serial_driver_node:
ros__parameters:
simulate_mode: true
打开之后串口不真的开,视觉链路可以在没有实体机器人的情况下完整跑起来,云台姿态由 UE5 侧提供。这是虚实闭环成立的前提——否则调试仿真必须开着真车。
4. 时序与坑点
时间戳要用同一个时钟。 ROS2 用 rclcpp::Clock,UE5 用引擎自己的 GetWorld()->GetTimeSeconds(),两者起点不同。同步时应该传 ROS2 的绝对时间戳,UE5 侧记录首次收到的偏移量做换算,而不是各算各的。
JSON 浮点精度。 位姿用 float 序列化成十进制字符串时,默认精度可能只有 6 位有效数字。米制下 6 位约等于毫米级——够用;但如果先换算成厘米再序列化,精度会退化。先传米、UE5 侧再乘 100,不要在 ROS2 侧换算。
WebSocket 的粘包不存在,但队列积压存在。 WebSocket 是消息边界保留的,不用自己拆帧。但如果 UE5 侧渲染卡顿,未处理的消息会在接收缓冲里堆积,恢复后一次性涌入——虚拟机器人会”快进”一段。上行链路应该在发送端做丢弃:只保留最新状态,语义上和相机的 latest 抓帧策略完全一致。
5. 验证方法
- 静态标定点对齐:实体和虚拟场景里各放若干标记点,同步后比对坐标。这是标记配准机制要解决的问题
- 手性自检:让实体机器人向左平移,虚拟机器人也必须向左。这个测试极其简单但必须做——手性搞反的症状是镜像,静止时完全看不出来
- 延迟测量:带时间戳的回环包,统计 RTT 分布而不只看均值
- 高度一致性:让机器人上坡或抬升云台,专门检查 Z 轴——这是 Y-up/Z-up 混淆唯一会暴露的场景
下一篇是跨场景对齐的核心:低延迟标记配准。