可以把 MotionInput 理解成一个"统一插座",GMR不需要知道前面的动作数据到底来自:
- Axis Studio;
- Noitom MocapApi;
- UDP测试程序;
- 以后增加的BVH文件。
GMR只需要知道:
你给我一个格式正确的
NoitomFrame,我就把它转换成G1动作。
不是MotionInput亲自转换,而是MotionInput的两个具体实现分别负责转换,然后统一返回NoitomFrame。
先看三者关系
MotionInput 统一规定
├── XsensUdpSource 具体干活
└── NoitomMocapApiSource 具体干活
MotionInput只是规定:
无论你内部怎么接收数据,
最后都必须给我返回一个 NoitomFrame。
它类似一份工作要求,不是实际干活的人。
一、先理解原来的问题
系统有两种动作来源。
来源1:Xsens UDP测试输入
程序直接监听一个UDP端口:
测试程序/Xsens
↓
UDP数据包
↓
XsensUdpSource
它收到的是UDP格式的数据,需要自己解析。
来源2:Noitom正式输入
真实动捕服先连接Axis Studio:
动捕服
↓
Axis Studio
↓
TCP 127.0.0.1:7003
↓
Noitom MocapApi
↓
NoitomMocapApiSource
它收到的是Axis通过Noitom SDK提供的人体Avatar骨骼数据。
这两种输入方式完全不同:
| 项目 | Xsens UDP | Noitom MocapApi |
|---|---|---|
| 网络方式 | UDP | TCP或UDP |
| 数据格式 | 自定义UDP数据包 | Noitom SDK事件 |
| 解析方式 | 自己解析数据包 | 调用MocapApi |
| 主要用途 | 测试、兼容旧程序 | 正式动捕服 |
| 是否依赖Axis | 不一定 | 是 |
如果没有统一接口,GMR里面可能会出现这种代码:
if (使用Xsens) {
接收UDP();
解析UDP();
执行GMR();
}
if (使用Noitom) {
连接MocapApi();
读取Avatar();
执行GMR();
}
这样会造成:
- GMR和网络代码混在一起;
- 两条链路可能各写一套GMR;
- 修复GMR时要改两个地方;
- 以后增加第三种输入会越来越乱;
- 很难判断问题发生在输入还是GMR。
所以我们在输入和GMR之间加了一层统一接口。
二、MotionInput到底是什么
MotionInput不是一种新的网络协议,也不是新的动作格式。
它只是C++里的一个统一接口,大致相当于:
class MotionInput
{
public:
virtual void open() = 0;
virtual std::optional<NoitomFrame> pollFrame() = 0;
virtual void close() = 0;
};
它只规定三件事:
open() 打开动作数据源
pollFrame() 获取一帧人体动作
close() 关闭数据源
但它没有规定数据必须从哪里来。
这就像一个统一插座:
插座规定:
必须提供电
但不关心电来自:
火电、水电、风电还是太阳能
同样:
MotionInput规定:
必须提供NoitomFrame
但不关心数据来自:
UDP、Axis、文件还是其他设备
三、两个实现分别做什么
1. XsensUdpSource
它是 MotionInput 的一个实现。
class XsensUdpSource : public MotionInput
{
public:
void open() override;
std::optional<NoitomFrame> pollFrame() override;
void close() override;
};
它内部负责:
监听UDP端口
↓
收到UDP数据包
↓
解析人体关节
↓
整理成NoitomFrame
↓
交给GMR
虽然它叫 XsensUdpSource,但它交给后面的数据已经被整理成统一格式。
输出是:
NoitomFrame
2. NoitomMocapApiSource
它也是 MotionInput 的一个实现。
class NoitomMocapApiSource : public MotionInput
{
public:
void open() override;
std::optional<NoitomFrame> pollFrame() override;
void close() override;
};
它内部负责:
通过MocapApi连接Axis
↓
监听TCP 127.0.0.1:7003
↓
等待AvatarUpdated事件
↓
读取人体骨骼位置和旋转
↓
整理成NoitomFrame
↓
交给GMR
它的输出同样是:
NoitomFrame
四、最关键的地方:输入不同,输出相同
两条链路的原始数据不同:
XsensUdpSource
收到的是UDP数据包
NoitomMocapApiSource
收到的是MocapApi Avatar事件
但是经过各自解析以后,它们都必须生成:
NoitomFrame
例如,可以把统一后的 NoitomFrame 简单理解为:
struct NoitomFrame
{
uint64_t timestamp_ms;
uint32_t posture_index;
std::vector<BodyPose> bodies;
};
里面包含类似:
Hips
Spine
Chest
Neck
Head
LeftShoulder
LeftArm
LeftForeArm
LeftHand
RightShoulder
RightArm
RightForeArm
RightHand
LeftUpLeg
LeftLeg
LeftFoot
RightUpLeg
RightLeg
RightFoot
每个身体部位都有:
位置 position
旋转 rotation
因此,到达GMR时,两种输入已经没有区别了。
五、GMR看到的是什么
GMR不会看到:
TCP端口7003
UDP端口8000
MocapApi
Axis Studio
网络数据包
它只会收到:
NoitomFrame frame;
然后执行:
RobotFrame robot_frame =
retargeter.retarget(frame);
换成大白话就是:
输入层说:
"这是这一时刻的人体骨骼。"
GMR说:
"我不管它来自哪套动捕设备,
只要关节名称、位置和旋转正确,
我就把它变成G1动作。"
六、运行时怎么选择输入
程序启动时通过参数选择。
正式使用Noitom
实际链路是:
动捕服
↓
Axis Studio
↓
TCP 7003
↓
NoitomMocapApiSource
↓
NoitomFrame
↓
GMR
使用UDP测试
如果运行:
-MotionInput udp
程序内部相当于:
if (motion_input == "udp") {
input = std::make_unique<XsensUdpSource>();
}
实际链路变成:
UDP测试数据
↓
XsensUdpSource
↓
NoitomFrame
↓
GMR
注意:二者不会同时正式控制GMR。启动时选择谁,当前运行就使用谁。
七、用翻译员来理解
可以把两种输入理解成两个人:
Xsens说英语
Noitom说中文
GMR只懂一种"标准语言":
NoitomFrame
所以两名翻译员分别负责:
XsensUdpSource:
英语 → 标准语言
NoitomMocapApiSource:
中文 → 标准语言
翻译完成以后,GMR只处理标准语言。
Xsens原始数据 ─→ XsensUdpSource ─┐
├→ NoitomFrame → GMR
Noitom原始数据 ─→ NoitomSource ──┘
八、为什么统一格式叫NoitomFrame
这个名称确实容易让人误会。
看到:
NoitomFrame
容易认为:
它只能装Noitom的数据。
但在当前工程里,它实际上被当作:
统一的人体骨骼帧
也就是说,Xsens UDP解析完成后,也会转换成 NoitomFrame。
从架构命名角度,更理想的名字可能是:
HumanMotionFrame
或者:
SkeletonFrame
这样更容易理解:
Xsens → SkeletonFrame
Noitom → SkeletonFrame
但我们没有随意改名,因为这个结构已经被GMR等代码使用。改名会扩大修改范围,增加不必要的风险。
所以当前可以这样记:
NoitomFrame虽然名字里有Noitom,
但在本项目里承担的是"统一人体骨骼帧"的角色。
九、MotionInput解决了什么问题
没有MotionInput时
GMR
├── 自己监听UDP
├── 自己连接MocapApi
├── 自己判断输入类型
├── 自己解析网络数据
└── 再执行IK
GMR要做的事情太多。
有MotionInput以后
XsensUdpSource:负责UDP
NoitomMocapApiSource:负责Axis/MocapApi
GMR:只负责人体到机器人的转换
ZMQ:只负责发送转换后的动作
每个模块只负责一件事。
十、它和ZMQ有什么关系
MotionInput负责的是GMR之前:
动作从哪里来
ZeroMQ负责的是GMR之后:
GMR结果怎么发出去
两个模块位于系统的不同位置:
输入侧 输出侧
MotionInput ZMQ
↓ ↑
NoitomFrame → GMR → RobotFrame → protobuf
不要把它们混在一起。
MotionInput:电脑怎样获得人体动作。GMR:人体动作怎样变成G1动作。protobuf:G1动作怎样组织成数据。ZMQ:数据怎样传给MuJoCo或机器人。
十一、当前实际运行的正式链路
你刚才穿动捕服测试时,实际使用的是这一条:
```mermaid
flowchart LR
A["动捕服"] --> B["Axis Studio"]
B --> C["TCP 127.0.0.1:7003"]
C --> D["Noitom MocapApi"]
D --> E["NoitomMocapApiSource"]
E --> F["统一 NoitomFrame"]
F --> G["输入 FIFO"]
G --> H["C++ GMR IK"]
H --> I["G1 RobotFrame"]
I --> J["protobuf"]
J --> K["ZeroMQ"]
K --> L["MuJoCo"]
```
XsensUdpSource这次没有参与运行,它只是保留下来的测试入口:
```mermaid
flowchart LR
A["UDP测试数据"] --> B["XsensUdpSource"]
B --> C["统一 NoitomFrame"]
C --> D["同一套 C++ GMR"]
```
十二、最简单的记忆方法
只需要记住下面四句话:
MotionInput是统一入口。
XsensUdpSource负责解析UDP测试数据。
NoitomMocapApiSource负责读取Axis正式数据。
无论数据来自哪里,最终都变成NoitomFrame,再交给同一套GMR。
最终最简化结构就是:
不同的数据来源
↓
不同的输入适配器
↓
统一NoitomFrame
↓
同一个GMR
↓
同一个ZMQ发送链路
它的本质不是增加复杂度,而是把原来混在一起的事情分开。