一、我为什么要做这个?
先说背景,我是一个本科毕设选手,2027 年就毕业了,学的软件工程技术。毕设课题是“手”。
要做就做个好的嘛,用市面上现成的那种 6DOF 或者 10DOF 的机械手,我觉得没有什么意义,要做就做一个没人做过的东西。
其实我的进度比文章的进度要快,在这个过程中我遇到了很多困难,哪怕借助 AI 去解决,仍然有很多问题。这个系列的文章是为了向后来者提供一些参考,也是为了鼓励自己持续完成分享。
当然,也是为了进入一家更好的公司,向自己的未来奋斗。
顺便一提,我是理科生嘛,文章写得不是很好,文章有很多是 AI 润色的,希望大家不要介意哈。
文章中的代码只是节选,全部代码我会开源到GitHub,等我做完毕设,整理一下就开源。到时候也会编辑一下文章,地址贴在这里。
这个灵巧手简单说就是:有一个机械手(五根手指、一堆关节),我要先做一个桌面软件,可以:
– 加载各零件的 STL 模型,在 3D 里组装成一只手
– 给关节插上旋转轴,拖动关节角,手指跟着弯
– 设置关节限位(比如食指只能弯 -120° 到 0°)
– 用姿势串驱动多个关节,做握拳、比 OK、张开等手势
– 后续还要接腱绳映射和简化动力学
听起来是不是像一个“小型 Gazebo”?差不多。但区别在于——现有的引擎一个都不用,全部自己做。
为什么?因为赵老师说:“我在软件、仿真、框架上偏弱”。
做着的时候我发现,这些其实是最好的架构师。它们逼我分层、逼我解耦、逼我理解每一个矩阵乘法在干什么。毫不夸张地说,这个项目中所学到的东西,可能比我大学四年中学到的东西都要多。
二、技术选型:为什么是 Qt + VTK + Eigen?
选型这一步,我在“用现成的”和“自己造”之间反复横跳了三天。最后的选择如下:
| 层 | 选型 | 为什么不用别的 |
| 数学 | Eigen 3.4 | 矩阵、四元数、最小二乘全齐了。不选 GLM(太图形向)、不选 Armadillo(太重)。Eigen 是头文件库,零链接,完美 |
| 渲染 | VTK 9.3 | STL 加载、三角网格渲染、拾取(ray casting)、相机交互全都有。不选 OpenGL 裸写(造轮子太苦)、不选 Qt3D(Qt 6.8 已弃用)、不选 Assimp+OpenGL(交互层全自己写) |
| 曲线 | QCustomPlot(后续接入) | 轻量、纯 Qt、不依赖 OpenGL |
| 构建 | CMake 3.30 + Ninja + MinGW 13.1 | 全家桶都在QT路径下,一条 PATH 搞定 |
为什么坚持 core/ 不碰 Qt 和 VTK?
这是整个项目最重要的一条架构纪律。我的 core/ 目录编译出来是一个纯 C++ 静态库,只依赖 Eigen:
// core/types.hpp —— 只有数据定义,没有一行 Qt / VTK
#include <Eigen/Core>
#include <Eigen/Geometry>
namespace hand_core {
using Vec3 = Eigen::Vector3d;
using Transform = Eigen::Isometry3d; // 刚体变换:旋转 + 平移
struct Joint { /* … */ };
struct Link { /* … */ };
// …
}
这意味着什么?意味着我可以“脱离界面”写一个命令行测试程序来验证运动学:
// _fk_test.cpp —— 无 GUI,纯数值验证
#include <core/kinematics.hpp>
#include <cstdio>
int main() {
hand_core::Tree tree;
// … 建几个节点,设关节角 …
auto poses = hand_core::forwardKinematics(tree);
std::printf("FK 计算完成,共 %zu 个连杆\\n", poses.size());
}
不用启动 Qt、不用开窗口、不用管 OpenGL 上下文,一行 g++ 编译就能跑。
测试速度快 100 倍,调试痛苦少 100000000 倍。
三、架构分层:五层蛋糕
项目长这样:
hand_sim/
├── core/ 纯 C++17 + Eigen:运动学、树、质量特性、几何包围体
├── model/ JSON 工程文件读写(nlohmann/json vendored)
├── render/ VTK 渲染:STL 加载、LinkActor、场景、相机交互
├── ui/ Qt Widgets:主窗口、属性面板、树控件、视口封装
├── plot/ 曲线显示(规划中)
├── 3rdparty/ Eigen 3.4.0 + nlohmann/json 3.11.3(vendored)
└── build/ 构建产物 + 测试程序
依赖方向严格单向:
ui → render → core
ui → model → core
core 不知道上层的存在。它只管算矩阵,不管矩阵算完后是画在屏幕上还是存进文件。
四、数据模型:一棵树统治一切
灵巧手的运动学本质是一棵树——手掌是根,手指是子树,指尖是叶子。每个节点由一个 Joint(关节)和一个 Link(连杆)组成:
// core/tree.hpp
struct TreeNode {
int id = -1;
int parent = -1; // -1 = 根;-2 = 已删除
Joint joint;
Link link;
};
其中 Joint 描述“这个零件怎么动”:
struct Joint {
JointType type = JointType::Fixed; // Fixed(焊死)或 Revolute(绕轴转)
Transform origin = Transform::Identity(); // joint frame 相对父连杆的固定变换
Vec3 axis = Vec3::UnitZ(); // 转轴方向(定义在 joint frame 里)
JointLimit limit; // 关节限位 [lower, upper]
JointMimic mimic; // 关节角联动
};
关键约定(跟 URDF 一模一样):
T_link = T_parent · joint.origin · Rot(axis, q)
翻译成人话:一个连杆的位置 = 爸爸的位置 × 我出生时的位移 × 我绕自己轴转了多少度。
就这么简单。但就是这么简单的公式,驱动了整只手的运动。
五、正向运动学:10 行代码让手动起来
FK(Forward Kinematics)是整个仿真器的心脏。它回答一个问题:“给定每个关节的旋转角度,每个零件在哪里?”
// core/kinematics.cpp —— 核心中的核心
Transform linkPose(const Tree& tree, int node_id) {
// 从当前节点一路爬到根节点
const std::vector<int> chain = tree.chainToRoot(node_id);
Transform T = Transform::Identity();
// 从根侧开始累乘
for (auto it = chain.rbegin(); it != chain.rend(); ++it) {
const TreeNode* n = tree.node(*it);
if (!n || n->parent == -2) break;
// T = T · (关节固定变换 · 关节旋转)
T = T * (n->joint.origin * jointMotion(*n, nodeQ(tree, n->id)));
}
return T;
}
其中 jointMotion 负责“绕轴转 q 度”:
Transform jointMotion(const TreeNode& n, double q) {
if (n.joint.type != JointType::Revolute) return Transform::Identity();
Transform R = Transform::Identity();
R.linear() = Eigen::AngleAxisd(q, n.joint.axis).toRotationMatrix();
return R;
}
注意一个设计红线:
限位(limit)不在 FK 里截断!FK 只管计算 T_parent · origin · Rot(axis, q),至于 q 该不该超限,那是积分器层的事。
为什么?因为如果你在 FK 里偷偷把 q 截断了,那么仿真步进、轨迹插值、逆运动学求解器……所有上游调用者都被你坑了。
六、配一种“绿色无污染”的构建方式
工具链安装完毕后,编译一行搞定:
# PowerShell(注意:Qt/MinGW 不在默认 PATH,要手动加)
$env:PATH = "C:\\Qt\\Tools\\mingw1310_64\\bin;C:\\Qt\\Tools\\CMake_64\\bin;C:\\Qt\\Tools\\Ninja;$env:PATH"
cd hand_sim/build
ninja hand_sim
测试也是一行:
.\\_fk_test.exe # 验证正向运动学
.\\_bind_test.exe # 验证几何绑定
.\\_mimic_test.exe # 验证关节联动
先测试,后界面。
每个数值测试都是独立的 .exe,跑一次不到 1 秒。
比起“打开软件、点击、截图、放大看像素”的传统验证方式,效率高到飞起。
咱们下篇见。👋


