一、系统总体结构

当前目标结构是:

Noitom动捕服
    ↓
Noitom接收器/Hub
    ↓
Axis Studio / Axis Neuron
    ↓
MotionInput统一接口
    ├── NoitomMocapApiSource(正式)
    └── XsensUdpSource(测试)
    ↓
NoitomFrame
    ↓
GMR IK重定向
    ↓
RobotFrame
    ↓
MotionStreamInterpolator
    ↓
MocapFrame protobuf
    ↓
MotionFrameHeader + CRC32
    ↓
ZeroMQ PUSH
    ↓ TCP 5555
ZeroMQ PULL
    ↓
MocapFrame protobuf
    ↓
MuJoCo qpos
    ↓
G1 29DOF可视化

这里存在两个网络段,不能混为一个:

第一段:Axis → xsens_core
第二段:xsens_core → MuJoCo Viewer

正式配置下:

Axis → MocapApi TCP → xsens_core
xsens_core → ZMQ TCP → MuJoCo

测试配置下:

Axis/测试工具 → UDP 8000 → xsens_core
xsens_core → ZMQ TCP 5555 → MuJoCo

二、物理层发生了什么

1. 动捕服传感器

人体穿戴多个惯性传感器。传感器测量的主要是:

  • 加速度
  • 角速度
  • 磁场
  • 传感器姿态
  • 时间信息

这些还不是最终人体骨骼位置。

2. Noitom接收器

传感器通过无线方式将数据送到Noitom接收器或Hub。

电脑一般通过:

  • USB
  • 网线
  • 专用接收器

访问这些数据。

3. Axis Studio / Axis Neuron

Axis软件负责:

  • 识别传感器
  • 标定人体
  • 建立Avatar
  • 姿态融合
  • 骨骼约束
  • 计算每个关节的姿态
  • 计算骨骼位置
  • 输出结构化人体动作

到这里,数据从原始IMU变成了:

Avatar
├── Hips
├── Spine
├── Spine1
├── Spine2
├── Neck
├── Head
├── LeftArm
├── LeftForeArm
├── LeftHand
├── RightArm
├── ...
├── LeftFoot
└── RightFoot

我们的程序没有自己实现IMU解算,而是使用Axis已经解算好的骨骼。

这很重要:

GMR输入不是原始惯导数据,而是Axis生成的人体骨骼世界姿态。


三、MotionInput统一输入层

核心定义:

class MotionInput
{
public:
    virtual void open() = 0;

    virtual void close() noexcept = 0;

    virtual std::optional<NoitomFrame>
    pollFrame() = 0;

    virtual const char*
    name() const noexcept = 0;
};

这个接口的意义是:

上层程序不关心数据来自哪里

上层只做:

input->open();

while (running)
{
    auto frame = input->pollFrame();

    if (!frame)
    {
        continue;
    }

    retargeter.retarget(*frame);
}

四、正式输入:NoitomMocapApiSource

它内部封装:

NoitomClient

然后使用 Noitom SDK:

MocapApi.h
MocapApi.lib
MocapApi.dll

1. 程序启动

默认:

--motion-input noitom
--noitom-transport tcp
--noitom-ip 127.0.0.1
--noitom-port 7001

含义是:

Axis与xsens_core在同一台电脑
Axis开放TCP Server端口7001
xsens_core通过MocapApi连接127.0.0.1:7001

如果Axis运行在另一台电脑:

Axis电脑:192.168.10.2
MuJoCo电脑:192.168.10.3

则参数应该是:

--noitom-ip 192.168.10.2
--noitom-port Axis实际端口

2. 初始化SDK

Noitom客户端先通过:

MCPGetGenericInterface(...)

取得多个SDK接口:

IMCPSettings
IMCPRenderSettings
IMCPApplication
IMCPAvatar
IMCPJoint

它们分别负责:

  • IMCPSettings:网络、BVH格式配置
  • IMCPRenderSettings:单位和坐标输出
  • IMCPApplication:连接、事件轮询
  • IMCPAvatar:Avatar信息
  • IMCPJoint:读取骨骼节点

3. 配置TCP

正式模式调用:

SetSettingsTCP(
    server_ip,
    server_port,
    settings_handle
);

这里的关系是:

Axis = TCP Server
MocapApi客户端 = TCP Client

不是我们的程序监听7001,而是我们的程序主动连接Axis的7001。

4. 配置骨骼格式

SDK配置:

SetSettingsBvhRotation(BvhRotation_XYZ)
SetSettingsBvhTransformation(BvhTransformation_Enable)
SetSettingsBvhData(BvhDataType_Binary)

同时默认Render Settings把位置单位设置为:

meter

这避免把毫米直接送进MuJoCo。

如果单位不正确,典型表现是:

机器人根位置移动1000倍
模型瞬间飞走

5. 打开Application

执行:

CreateApplication()
SetApplicationSettings()
SetApplicationRenderSettings()
OpenApplication()
EnableApplicationCacheEvents()

其中 EnableApplicationCacheEvents() 很重要。

它表示SDK应缓存事件,让程序逐个读取,而不是只提供最后一次状态。

这更符合目前:

可以慢一点,但希望拿到完整数据

的要求。

6. 读取事件

程序循环调用:

PollApplicationNextEvent()

事件可能包括:

AvatarUpdated
RigidBodyUpdated
SensorModulesUpdated
Error
None

我们只处理:

MCPEvent_AvatarUpdated

每次Avatar更新后,读取:

avatarHandle
timestamp
postureIndex
PTP time

postureIndex 是Noitom动作帧序号。

程序会统计:

上一帧:100
当前帧:101 → 正常

上一帧:100
当前帧:104 → 缺少101、102、103

日志可能输出:

[NoitomJump]
prev_posture_index=100
posture_index=104
missing_posture_indices=[101,102,103]

这让我们可以判断数据是否在Noitom入口就已经缺失。

7. 读取Avatar关节

通过:

GetAvatarJoints()
GetJointTag()
GetJointGlobalPosition()
GetJointGlobalRotation()

获得每个关节的:

世界位置 x y z
世界四元数 x y z w

SDK返回四元数顺序是:

x, y, z, w

内部转换成GMR使用的:

w, x, y, z

8. Noitom节点映射

Noitom SDK节点被转换成统一名称,例如:

JointTag_Hips         → Hips
JointTag_Spine        → Spine
JointTag_Spine1       → Spine1
JointTag_Spine2       → Spine2
JointTag_Spine3       → Spine3
JointTag_Neck         → Neck
JointTag_Head         → Head

JointTag_LeftArm      → LeftArm
JointTag_LeftForeArm  → LeftForeArm
JointTag_LeftHand     → LeftHand

JointTag_RightArm     → RightArm
...

Noitom没有单独的Toe节点时,会使用Foot作为Toe回退:

LeftToe  ← LeftFoot
RightToe ← RightFoot

如果节点缺失,程序会:

  • 输出警告
  • 填入单位姿态
  • 保持统一的数据结构长度

9. 坐标系转换

Noitom SDK输出的数据经过:

位置:(x, y, z) → (x, -z, y)

四元数也进行对应的坐标旋转。

目的是把Axis坐标系统一到现有GMR/Lafan风格坐标系。

这里是第一次实际穿动捕服测试时必须重点观察的地方。

若转换错误,会出现:

  • 人向前,模型横向移动
  • 抬手变成手臂前后旋转
  • 人站立,模型躺倒
  • 左右动作相反

代码结构可行,但坐标转换必须通过实际设备确认。


五、测试输入:XsensUdpSource

文件:

  • [xsens_udp_source.hpp (line 1)](C:/Users/RX01296/Desktop/全身控制7_28/gmr-master_PN - 副本/cpp_xsens_g1/include/xsens_udp_source.hpp:1)
  • [xsens_udp_source.cpp (line 1)](C:/Users/RX01296/Desktop/全身控制7_28/gmr-master_PN - 副本/cpp_xsens_g1/src/xsens_udp_source.cpp:1)

启动:

--motion-input udp
--ip 0.0.0.0
--port 8000

这条路径:

UDP 8000
  ↓
XsensUdpParser
  ↓
坐标转换
  ↓
四元数连续性处理
  ↓
NoitomFrame

它期待固定格式:

24字节Header
+
23节点 × 8个大端float

它不是通用Noitom UDP解析器。

因此它的用途是:

  • 保留旧输入方式
  • 回滚测试
  • 使用已知格式的录制/发生器测试
  • Noitom SDK暂时不可用时排查GMR

不建议把它作为最终Noitom正式入口。


六、统一数据:NoitomFrame

两个输入源最后都输出:

NoitomFrame
{
    timestamp_ms;
    posture_index;
    bodies[];
}

每个Body大致是:

BodyPose
{
    name;
    position;
    rotation;
}

例如:

NoitomFrame
├── timestamp_ms = 123456
├── posture_index = 1001
└── bodies
    ├── Hips
    │   ├── position
    │   └── rotation
    ├── Spine2
    ├── Head
    ├── LeftArm
    ├── LeftForeArm
    ├── LeftHand
    ├── RightArm
    ├── LeftUpLeg
    └── ...

这就是输入层与GMR之间的边界。

只要两种输入源能产生正确的 NoitomFrame,GMR完全不需要知道数据来源。


七、GMR IK做了什么

调用点:

[xsens_core_main.cpp (line 460)](C:/Users/RX01296/Desktop/全身控制7_28/gmr-master_PN - 副本/cpp_xsens_g1/src/xsens_core_main.cpp:460)

auto robot_frame =
    retargeter.retarget(*frame);

完整实现来自:

[cpp_gmr_retargeter.cpp (line 1)](C:/Users/RX01296/Desktop/全身控制7_28/gmr-master_PN_xsenscpp最终版/gmr-master_PN/cpp_noitom_g1/src/cpp_gmr_retargeter.cpp:1)

它使用:

  • MuJoCo模型
  • Jacobian
  • Forward Kinematics
  • DAQP约束QP
  • 关节角限制
  • 两阶段IK任务
  • 人体身高缩放

机器人模型:

[g1_mocap_29dof.xml (line 1)](C:/Users/RX01296/Desktop/全身控制7_28/gmr-master_PN - 副本/assets/unitree_g1/g1_mocap_29dof.xml:1)

IK配置:

[bvh_xsens_to_g1.json (line 1)](C:/Users/RX01296/Desktop/全身控制7_28/gmr-master_PN - 副本/general_motion_retargeting/ik_configs/bvh_xsens_to_g1.json:1)

GMR匹配关系

例如:

人体Hips
    ↓
机器人pelvis

人体Spine2
    ↓
机器人torso_link

人体LeftArm
    ↓
机器人left_shoulder_yaw_link

人体LeftForeArm
    ↓
机器人left_elbow_link

人体LeftHand
    ↓
机器人left_wrist_yaw_link

人体LeftUpLeg
    ↓
机器人left_hip_yaw_link

人体LeftLeg
    ↓
机器人left_knee

人体LeftFoot
    ↓
机器人left_ankle

GMR求解的目标不是“人体关节角直接复制给机器人”。

而是:

让G1的关键身体部位
尽量跟随人体关键身体部位的位置和旋转
同时满足G1关节角限制

输出为 RobotFrame,主要包含:

  • 根位置
  • 根姿态
  • 29个关节位置
  • 相关身体状态

八、插值层

人体帧率和输出帧率可能不同,例如:

Noitom输入:60Hz
MuJoCo输出:60Hz

或者:

Noitom输入:30Hz
输出:50Hz

程序通过:

MotionStreamInterpolator

RobotFrame 转为连续的 MotionFrame

这里必须设置真实帧率:

--input-fps
--output-fps

如果Axis实际60Hz,但配置30Hz,可能导致:

  • 速度计算错误
  • 插值时间错误
  • 动作节奏不正确
  • GMR输出不连续
  • 机器人动作过快或过慢

建议首次测试:

InputFps = Axis实际帧率
OutputFps = Axis实际帧率

先用:

60 → 60

确认正确后,再测试其他输出频率。


九、protobuf数据层

GMR输出被转换成:

MocapFrame

主要字段:

index
body_pos_w       3
body_lin_vel_w   3
body_quat_w      4
body_ang_vel_w   3
joint_pos        29
joint_vel        29
left_hand        7
right_hand       7

当前MuJoCo主要使用:

  • body_pos_w
  • body_quat_w
  • joint_pos

Viewer会严格检查:

body_pos_w_size == 3
body_quat_w_size == 4
joint_pos_size == 29
joint_vel_size == 29

字段长度错误时会拒绝该帧,避免数组越界。


十、ZMQ传输层

当前不是PUB/SUB,而是根据“不能主动丢帧”的新要求改为:

PUSH/PULL

PC端:

xsens_core
ZMQ_PUSH
bind tcp://*:5555

MuJoCo端:

ZMQ_PULL
connect tcp://127.0.0.1:5555

如果以后MuJoCo在另一台电脑:

connect tcp://<xsens_core电脑IP>:5555

为什么改成PUSH/PULL

PUB/SUB的特点是:

  • 适合实时广播
  • 订阅者慢时可能丢数据
  • 新订阅者看不到历史数据
  • CONFLATE会主动覆盖旧帧

现在的要求是:

允许等待,但不主动丢任何帧

所以当前采用:

  • 阻塞发送
  • SNDHWM = 10000
  • RCVHWM = 10000
  • LINGER = -1
  • 不使用 DONTWAIT
  • 不使用 CONFLATE

当接收端变慢时:

不会主动覆盖旧帧
而是缓存并最终阻塞发送端

代价是:

延迟可能逐步升高

这符合目前“完整性优先于实时性”的要求。


十一、传输包格式

每个ZMQ消息结构:

"MOCAP_FRAME"
+
44字节 MotionFrameHeader
+
protobuf MocapFrame

Header包含:

magic
version
message_type
frame_id
capture_timestamp_us
send_timestamp_us
payload_size
joint_count
reserved
crc32

接收端检查:

  • magic正确
  • 版本正确
  • 类型正确
  • 关节数量为29
  • payload长度匹配
  • CRC32匹配
  • frame ID是否连续

因此可以识别:

损坏数据
协议版本错误
关节数量错误
protobuf不完整
frame ID跳变

十二、MuJoCo Viewer

文件:

[xsens_zmq_mujoco_viewer_main.cpp (line 1)](C:/Users/RX01296/Desktop/全身控制7_28/gmr-master_PN - 副本/cpp_xsens_g1/src/xsens_zmq_mujoco_viewer_main.cpp:1)

接收流程:

ZMQ PULL
  ↓
Header/CRC校验
  ↓
protobuf反序列化
  ↓
字段长度检查
  ↓
FIFO pending队列
  ↓
逐帧取出
  ↓
写入MuJoCo qpos
  ↓
mj_forward
  ↓
渲染

MuJoCo qpos 写入:

qpos[0:3]    根位置
qpos[3:7]    根四元数
qpos[7:36]   29个关节位置

这里使用:

mj_forward(model, data);

而不是物理控制仿真的:

mj_step(model, data);

所以当前Viewer属于:

运动学动作可视化

它验证的是:

  • 人体动作是否被正确读取
  • GMR输出是否合理
  • 29DOF是否正确映射
  • 姿态是否连续
  • 坐标系是否正确
  • 网络帧是否完整

它暂时不验证:

  • G1动力学平衡
  • 地面接触控制
  • 电机扭矩
  • ONNX控制策略
  • LowCmd
  • 机器人是否会摔倒

这正适合当前第一阶段。


十三、一键启动过程

执行:

.\run_zmq_mujoco.ps1 `
  -MotionInput noitom `
  -NoitomTransport tcp `
  -NoitomIp 127.0.0.1 `
  -NoitomPort 7001 `
  -InputFps 60 `
  -OutputFps 60 `
  -HumanHeight 1.75

脚本会:

  1. 检查 xsens_core.exe
  2. 检查 xsens_zmq_mujoco_viewer.exe
  3. 检查 MocapApi.dll
  4. 检查G1 XML。
  5. 检查IK JSON。
  6. 启动MuJoCo Viewer。
  7. Viewer连接 tcp://127.0.0.1:5555
  8. 启动 xsens_core.exe
  9. xsens_core连接Axis TCP端口。
  10. 收到Avatar事件。
  11. 生成 NoitomFrame
  12. 执行GMR IK。
  13. protobuf序列化。
  14. ZMQ发送。
  15. MuJoCo逐帧显示。

十四、完整性能够保证到什么程度

需要区分三个等级。

程序不主动丢帧

当前已经做到:

  • Noitom事件缓存
  • GMR输出FIFO
  • 阻塞ZMQ发送
  • MuJoCo输入FIFO
  • 未启用CONFLATE
  • 未使用latest-frame覆盖
  • frame ID缺口统计

正常连接期间顺序完整

TCP和ZMQ在进程持续运行、连接正常时,可以保证字节按序可靠传输。

断线/崩溃后绝对恢复

当前不能绝对保证。

如果发生:

  • 网线拔掉
  • Axis崩溃
  • xsens_core崩溃
  • MuJoCo进程崩溃
  • 电脑断电

内存队列会丢失。

严格持久化零丢帧还需要:

原始动作落盘
frame ID确认
接收端ACK
未确认帧重传
断点续传

但对于当前“穿动捕服做MuJoCo模拟”的阶段,现有机制足够用于正常在线验证。


十五、软件层面可行性判断

Noitom输入层:高可行

理由:

  • SDK文件完整。
  • DLL约2.9 MB,不是空文件。
  • import library存在。
  • 已有完整 NoitomClient 实现。
  • 代码已经实现TCP/UDP模式。
  • Avatar、Joint、Posture Index接口齐全。
  • 新增适配器源码通过语法检查。

主要风险:

  • Axis实际端口不一定是7001。
  • Axis需要启用对应网络输出。
  • Axis版本与SDK r70必须兼容。
  • TCP Server和UDP输出设置方式需要以实际Axis界面为准。

判断:约85%可行,剩余必须通过实际Axis连接验证。

MotionInput抽象:高可行

  • 接口简单。
  • 两条输入路径完全隔离。
  • 同一时间只实例化一种输入。
  • 输出类型统一。
  • 不修改GMR接口。

判断:约95%可行。

GMR IK:高可行

  • 完整源码存在。
  • MuJoCo Jacobian存在。
  • DAQP求解器存在。
  • XML和IK JSON存在。
  • 源码集成验证已通过。

主要风险:

  • 人体身高参数。
  • Noitom坐标系。
  • 初始站姿。
  • 关节方向。
  • 人体比例缩放。

判断:算法和源码约90%可行;实际动作质量必须靠设备校准。

ZMQ传输:高可行

  • protobuf字段匹配。
  • Header编解码测试通过。
  • CRC校验存在。
  • FIFO存在。
  • PUSH/PULL配置符合完整性优先要求。
  • 本机回环TCP结构简单。

判断:约95%可行。

MuJoCo显示:高可行

  • Viewer源码已经完成。
  • 29DOF映射清晰。
  • 使用现有G1 XML。
  • qpos结构与模型预期一致。
  • FIFO显示逻辑存在。

主要风险:

  • 依赖是否正确链接。
  • XML中的关节顺序是否与GMR输出顺序完全一致。
  • 根四元数和坐标方向。
  • GLFW运行库部署。

判断:约90%可行。

当前整体可行性

单看软件设计:

总体可行性:高

目前更准确的判断是:

源码结构完成度:约90%
构建环境完成度:取决于当前安装
无设备静态验证:通过
实际Noitom联机验证:尚未进行
实际MuJoCo窗口验证:尚未进行

十六、第一次运行最可能出现的问题

按概率排序:

  1. Axis TCP端口设置不一致。
  2. Axis没有启用BVH/TCP输出。
  3. MocapApi.dll找不到或版本不兼容。
  4. 防火墙阻止Axis TCP连接。
  5. Noitom坐标系方向不正确。
  6. Axis实际帧率与 InputFps 不一致。
  7. 人体身高配置不正确。
  8. MuJoCo/GLFW DLL没有复制到EXE目录。
  9. GMR初始姿态与穿戴标定姿态不一致。
  10. Viewer队列持续增长,说明GMR输出快于显示消费速度。

十七、首次联调建议

不要一开始就做复杂动作。

依次做:

  1. 自然站立。
  2. 左手慢慢抬起。
  3. 右手慢慢抬起。
  4. 左腿小幅抬起。
  5. 右腿小幅抬起。
  6. 身体向前倾。
  7. 身体向左转。
  8. 缓慢下蹲。

每一步检查:

人体左 → 模型左
人体前 → 模型前
抬手 → 对应肩肘腕
抬腿 → 对应髋膝踝
站立 → 模型没有横躺
静止 → 关节没有明显抖动

第一阶段成功标准是:

Axis收到动捕
Noitom posture index连续
GMR持续输出
ZMQ frame ID连续
CRC invalid=0
MuJoCo动作方向正确
pending队列不持续增长

达到这些标准后,软件链路才算完成第一轮闭环验证。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐