使用 MediaPipe 等基于关键点的姿态估计系统,可以从图像中获取一组人体关键点。然而,这些关键点本身无法直接驱动 3D 角色,还需要将其转换为骨骼的旋转。本文将在 Unity 中对这一过程进行逐步推导。在开始之前,让我们先梳理姿态估计方案的两种主要类型,并分析不同输出形式对后续处理的影响。
两类姿态估计方案
基于关键点
这类方案输出人体各关节的二维或三维坐标,通常包含 17~33 个点。代表性的方案有,
- OpenPose CMU 开源的经典方案,支持多人姿态估计,输出 25 个身体关键点
- MediaPipe Pose Google 开源方案,支持 33 个全身关键点,可在移动端实时运行
- VideoPose3D 从 2D 关键点序列中恢复 3D 坐标,利用时序信息提升深度估计精度
关键点方案的输出是一组坐标,要驱动骨骼就需要自行将这些坐标转换为旋转。这正是本文要解决的问题。
基于参数化人体模型(SMPL)
这类方案直接输出人体模型的参数,包括每个关节的旋转和体型参数。代表性的方案有,
- 4DHumans 单帧输入即可回归 SMPL 参数,速度快,适合实时场景,但缺少帧间连续性,动作可能逐帧抖动
- WHAM 利用视频时序信息,输出时间上连贯的 SMPL 序列,动作比单帧方法更平滑,同时估计人体在世界坐标中的全局位移
- GVHMR 在 WHAM 的基础上进一步提升全局轨迹的精度,尤其擅长处理快速移动和大范围位移的场景
SMPL 方案的输出已经包含了关节旋转,可以直接映射到骨骼系统,不需要额外的转换计算。值得注意的是,SMPL 背后的 Meshcapade 近期已被 Epic Games 收购,未来有望在虚幻引擎中得到更深入的整合。
两者的比较
| 基于关键点 | 基于 SMPL | |
|---|---|---|
| 输出 | 关节坐标 | 关节旋转 + 体型参数 |
| 驱动骨骼 | 需要自行计算旋转 | 可直接映射 |
| 实时性 | 轻量,适合端侧实时 | 较重,多数需要 GPU |
| 身体接触 | 仅有关节位置,难以判断身体部位间的接触关系 | 包含体型参数,对接地、坐姿等身体部位接触的场景还原更好 |
| 灵活性 | 不依赖特定人体模型 | 绑定 SMPL 拓扑 |
| 商业授权 | 部分方案开源且支持免费商用 | SMPL 模型本身用于商业用途需向 Meshcapade 获取授权 |
从关键点到骨骼旋转
MediaPipe 等系统给出了三维空间中的关键点坐标,而 Unity 骨骼系统需要的是每根骨骼的旋转。我们的任务就是从前者推出后者。
这之所以可行,是因为骨骼动画中动作由旋转定义。不同身高的人在做同样动作时,各关节的空间位置会不同,但骨骼之间的相对旋转是一致的。只要每根骨骼的旋转正确,无论角色身材比例如何,最终表现出的动作都会一致。
在这种情况下,一个自然的想法是直接使用 IK 来拟合这些关键点。但这种方法在实际应用中存在限制。原因在于,MediaPipe 的 3D 关键点经过归一化处理,其数值与真实物理长度无关。这意味着关键点之间的距离并不可靠,难以作为 IK 的有效约束。
另一方面,MediaPipe 给出的 3D 关键点坐标已经很好地描述了人体动作,所以更直接的做法是从这些关键点中提取方向信息,推算各骨骼的旋转,从而复原动作。
本文假设读者已经了解 Unity 的 Humanoid 骨骼系统以及向量叉乘的基本概念。示例中将使用 MediaPipeUnityPlugin,
https://github.com/homuler/MediaPipeUnityPlugin
完整示例代码可在本文末尾获取。
躯干
为了将关键点与模型骨骼放入同一坐标系中直接比较,我们先将模型放置在 Unity 世界原点,并保持其旋转为 identity(即为 0),同时约定 Unity 的 X 轴正方向为角色右侧。

将 pose landmark 显示在屏幕上,并将左肩与左髋标记为红色,便于识别。

同时给出关键点索引,方便后续引用。

https://github.com/google-ai-edge/mediapipe/blob/master/docs/solutions/pose.md
通过观察可以发现,Hips 的朝向可以由四个关键点确定,X 轴对应左髋指向右髋的方向,Y 轴对应髋部中心指向肩部中心的方向。

据此可以从关键点中计算出两个方向,
Vector3 hipXDir = (landmark[24] - landmark[23]).normalized;
Vector3 hipCenter = (landmark[23] + landmark[24]) * 0.5f;
Vector3 shoulderCenter = (landmark[11] + landmark[12]) * 0.5f;
Vector3 hipYDir = (shoulderCenter - hipCenter).normalized;C#接下来需要将这两个方向转换为骨骼旋转。Unity 提供的 Quaternion.LookRotation(forward, up) 可用于构造旋转,其中 Z 轴对齐 forward,Y 轴尽量靠近 up。
由于当前已知的是 X 与 Y 方向,可以通过叉乘补出 Z 轴,并传入 LookRotation,
Vector3 hipZDir = Vector3.Cross(hipXDir, hipYDir).normalized;
_hips.rotation = Quaternion.LookRotation(hipZDir, hipYDir);C#这里需要注意,LookRotation 会精确对齐 Z 轴(forward)。虽然我们最初是从 X 和 Y 出发,但通过叉乘构造出的 hipZDir 已经与 hipXDir、hipYDir 共同组成一组正交基。
这样,Hips 的 Z 轴垂直于躯干平面,Y 轴对齐髋部中心指向肩部中心的方向,X 轴由正交关系自动确定,从而唯一确定骨骼的空间朝向。


总结一下,这里我们做了四件事,
- 将关键点表示在 Unity 的空间内,并且确保上下、左右、前后方向与 Unity 世界坐标一致
- 对于 Hips 骨骼,选取双肩和双髋构成的平面来描述其朝向
- 从这些关键点中提取 Z 轴朝向(forward)和 Y 轴朝向(up)
- 将 forward 和 up 传入
LookRotation得到 Hips 骨骼的旋转
其中后三步是后面所有骨骼的通用模式。不同骨骼之间的差异,只体现在关键点的选择以及方向的构造方式上。
接下来用同样的方法控制 Chest。与 Hips 类似,都是通过“肩部与髋部”之间的空间关系来确定朝向,只是参考点的组合略有不同。Hips 由双髋和肩部中心确定,而 Chest 则由双肩和髋部中心确定。两者的 Y 轴一致,均为髋部中心指向肩部中心,而 X 轴则改为由左肩指向右肩。
Vector3 chestXDir = (landmark[12] - landmark[11]).normalized;
Vector3 chestYDir = (shoulderCenter - hipCenter).normalized; // 与 Hips 共享同一个 Y
Vector3 chestZDir = Vector3.Cross(chestXDir, chestYDir).normalized;
_chest.rotation = Quaternion.LookRotation(chestZDir, chestYDir);C#四肢
MediaPipe 的四肢关键点在每一段上只提供两个点,只能确定骨骼的延伸方向。而 LookRotation 需要两个方向来唯一确定旋转。仅有一条方向时,骨骼仍可以绕该轴自由旋转,其姿态并不唯一。
为了解决这一问题,需要额外引入一个参考方向,并与延伸方向做叉乘,构造出第二个轴,从而确定完整的旋转。
右手臂
观察模型可以发现,右侧 UpperArm 骨骼沿本地 X 轴正方向延伸。

在关键点中,上臂的延伸方向由 12(肩)指向 14(肘)确定,将其作为 rUpperArmXDir。再引入世界坐标的 up 作为参考方向,通过两次叉乘构造出完整的旋转值,

Vector3 rUpperArmXDir = (landmark[14] - landmark[12]).normalized; // 肩→肘
Vector3 rUpperArmZDir = Vector3.Cross(rUpperArmXDir, Vector3.up).normalized;
Vector3 rUpperArmYDir = Vector3.Cross(rUpperArmZDir, rUpperArmXDir).normalized;
_rightUpperArm.rotation = Quaternion.LookRotation(rUpperArmZDir, rUpperArmYDir);C#不过,当 rUpperArmXDir 与 Vector3.up 接近平行时,叉乘会退化为零向量。这种情况通常出现在手臂上举或下垂时。
为避免退化,需要在接近竖直时替换参考方向。通过观察模型可以发现,
手臂上举(rUpperArmXDir ≈ Vector3.up)时,骨骼的 Y 轴指向世界 -X(身体左侧),

手臂下垂(rUpperArmXDir ≈ -Vector3.up)时,骨骼的 Y 轴指向世界 +X(身体右侧),

因此,可以通过点积判断当前方向是否接近与 Vector3.up 平行;若是,则根据手臂的朝向,将参考方向替换为 -Vector3.right 或 +Vector3.right,以避免叉乘退化。
Vector3 aux = Mathf.Abs(Vector3.Dot(rUpperArmXDir, Vector3.up)) < 0.99f
? Vector3.up : (rUpperArmXDir.y > 0 ? -Vector3.right : Vector3.right);
Vector3 rUpperArmZDir = Vector3.Cross(rUpperArmXDir, aux).normalized;
Vector3 rUpperArmYDir = Vector3.Cross(rUpperArmZDir, rUpperArmXDir).normalized;C#右下臂由肘(14)→腕(16)确定方向,逻辑和上臂完全一样,只换关键点。
Vector3 rLowerArmXDir = (landmark[16] - landmark[14]).normalized; // 肘→腕
Vector3 aux = Mathf.Abs(Vector3.Dot(rLowerArmXDir, Vector3.up)) < 0.99f
? Vector3.up : (rLowerArmXDir.y > 0 ? -Vector3.right : Vector3.right);
Vector3 rLowerArmZDir = Vector3.Cross(rLowerArmXDir, aux).normalized;
Vector3 rLowerArmYDir = Vector3.Cross(rLowerArmZDir, rLowerArmXDir).normalized;
_rightLowerArm.rotation = Quaternion.LookRotation(rLowerArmZDir, rLowerArmYDir);C#左手臂
观察左上臂的 Transform Gizmo 可以发现,其 X 轴正方向指向左侧,与骨骼的延伸方向(肩→肘)相反。因此在构造 xDir 时,需要将方向反转,即从肘指向肩,

// 左上臂: 肘(13)→肩(11)
Vector3 lUpperArmXDir = (landmark[11] - landmark[13]).normalized;
// 左下臂: 腕(15)→肘(13)
Vector3 lLowerArmXDir = (landmark[13] - landmark[15]).normalized;C#参考方向同样使用世界空间的 up。在接近竖直时,退化处理与右手臂保持一致。观察可知,
- 左臂上举时,lUpperArmXDir 接近 -Vector3.up,骨骼的 Y 轴指向世界 +X(身体右侧)
- 左臂下垂时,lUpperArmXDir 接近 +Vector3.up,骨骼的 Y 轴指向世界 -X(身体左侧)
lUpperArmXDir.y > 0 ? -Vector3.right : Vector3.right 自动覆盖了这两种情况。后续的 aux、zDir、yDir 以及 LookRotation 计算流程与右手臂完全一致,无需做额外修改。
右腿
腿部和手臂的处理方式不同。观察右大腿骨骼可以发现,骨骼沿本地 Y 轴负方向延伸(从髋向膝盖,向下),同时本地 X 轴指向身体右侧。

据此可以从关键点中提取两个方向,
- landmark 23→24(左髋到右髋),作为 X 方向
- landmark 26→24(膝盖到髋),作为 Y 方向
Vector3 legXDir = (landmark[24] - landmark[23]).normalized; // 左髋→右髋(左右腿共用)
Vector3 rLegYDir = (landmark[24] - landmark[26]).normalized; // 膝→髋
Vector3 rLegZDir = Vector3.Cross(legXDir, rLegYDir).normalized;
_rightUpperLeg.rotation = Quaternion.LookRotation(rLegZDir, rLegYDir);C#这里没有像手臂那样处理退化情况,是因为 legXDir(水平左右)与 rLegYDir(竖直向上)天然接近正交。只有当腿完全水平指向正左或正右时才可能接近平行,而在实际捕捉中几乎不会出现。
右小腿的处理方式相同,由膝(26)指向踝(28)确定延伸方向,X 轴继续沿用 legXDir,
Vector3 rShinYDir = (landmark[26] - landmark[28]).normalized; // 踝→膝
Vector3 rShinZDir = Vector3.Cross(legXDir, rShinYDir).normalized;
_rightLowerLeg.rotation = Quaternion.LookRotation(rShinZDir, rShinYDir);C#右脚的处理方式不同。观察右脚可以看到,骨骼沿本地 Z 轴正方向延伸(踝指向脚尖,向前),而不是像大腿小腿那样沿本地 Y 轴。

所以 zDir 直接来自关键点,配合 legXDir,就能叉乘得到 yDir。
Vector3 rFootZDir = (landmark[32] - landmark[28]).normalized; // 踝→脚尖
Vector3 rFootYDir = Vector3.Cross(rFootZDir, legXDir).normalized;
_rightFoot.rotation = Quaternion.LookRotation(rFootZDir, rFootYDir);C#左腿
左腿和右腿完全对称,X 方向继续复用右腿中定义的 legXDir。唯一的区别是关键点索引,从 24/26/28/32 换成了 23/25/27/31。
- 左大腿,髋(23)→膝(25),Y 正向 = 膝→髋 =
landmark[23] - landmark[25] - 左小腿,膝(25)→踝(27),Y 正向 = 踝→膝 =
landmark[25] - landmark[27] - 左脚,踝(27)→脚尖(31),Z 正向 =
landmark[31] - landmark[27]
// 左大腿
Vector3 lLegYDir = (landmark[23] - landmark[25]).normalized;
Vector3 lLegZDir = Vector3.Cross(legXDir, lLegYDir).normalized;
_leftUpperLeg.rotation = Quaternion.LookRotation(lLegZDir, lLegYDir);
// 左小腿
Vector3 lShinYDir = (landmark[25] - landmark[27]).normalized;
Vector3 lShinZDir = Vector3.Cross(legXDir, lShinYDir).normalized;
_leftLowerLeg.rotation = Quaternion.LookRotation(lShinZDir, lShinYDir);
// 左脚
Vector3 lFootZDir = (landmark[31] - landmark[27]).normalized;
Vector3 lFootYDir = Vector3.Cross(lFootZDir, legXDir).normalized;
_leftFoot.rotation = Quaternion.LookRotation(lFootZDir, lFootYDir);C#头部
我们将 face landmark 显示在 Unity 空间中。和 pose landmark 部分一致,将部分点标记为红色,用于辅助区分上下与左右关系。
通过实际转动头部可以观察到,相比 pose landmark,face landmark 在姿态变化时响应更充分且更贴近真实运动。因此,这里使用 face landmark 来驱动头部旋转。
观察模型头部骨骼可以发现,其本地 X 轴指向身体右侧,本地 Y 轴指向上方。与 Hips 的处理方式类似,可以从面部关键点中选取四个点来构造局部坐标系,
- 前额(10,图中脸部上方的黄色点)与下巴(152,图中脸部下方的红色点)用于确定纵向
- 右眼外角(33,图中脸部右侧的黄色点)与左眼外角(263,图中脸部左侧的红色点)用于确定横向

据此提取两个方向,
- headYDir,下巴指向前额
- headXDir,左眼外角指向右眼外角
再通过叉乘得到第三个轴,并传入 LookRotation。
Vector3 headYDir = (faceLandmarks[10] - faceLandmarks[152]).normalized;
Vector3 headXDir = (faceLandmarks[33] - faceLandmarks[263]).normalized;
Vector3 headZDir = Vector3.Cross(headXDir, headYDir).normalized;
_head.rotation = Quaternion.LookRotation(headZDir, headYDir);C#这样即可得到头部的完整朝向。
在实际应用中,也可以将部分旋转分配到 neck 骨骼上,以获得更自然的过渡效果,这里不再展开。
手部
我们将 hand landmark 显示在 Unity 空间中,并将左手标记为红色,方便区分左右。
可以看到,与 pose landmark 相比,hand landmark 在手掌朝向上的追踪更加准确,同时还提供完整的手指关节信息。因此,我们使用 hand landmark 数据来驱动手部姿态。
为便于后续说明,下图给出了手部关键点的索引。

https://github.com/google-ai-edge/mediapipe/blob/master/docs/solutions/hands.md
右手掌
观察模型可以发现,右手掌与手臂一致,沿本地 X 轴正方向延伸。

从手部关键点的分布可以看到,腕部(0)、食指根(5)和小指根(17)三个点构成一个三角形,基本覆盖了整个手掌平面。
基于这三个点,先构造两个方向,
- toIndex,腕→食指根
- toPinky,腕→小指根
由这两个方向可以进一步得到,
handYDir,为 toIndex 和 toPinky 的叉乘结果,垂直于手掌平面,指向手背方向。
另外,我们使用手腕指向中指根(0→9)的方向作为手掌的延伸方向
Vector3 rToIndex = (handLandmarks[5] - handLandmarks[0]).normalized;
Vector3 rToPinky = (handLandmarks[17] - handLandmarks[0]).normalized;
Vector3 rHandYDir = Vector3.Cross(rToIndex, rToPinky).normalized;
Vector3 rHandXDir = (handLandmarks[9] - handLandmarks[0]).normalized; // 腕→中指根C#再由 rHandXDir 与 rHandYDir 叉乘得到第三个轴,并构造旋转,
Vector3 rHandZDir = Vector3.Cross(rHandXDir, rHandYDir).normalized;
_rightHand.rotation = Quaternion.LookRotation(rHandZDir, rHandYDir);C#这样我们便能控制手掌朝向。
右手四指
观察可以发现,除拇指外的四根手指,其骨骼与手掌一致,均沿本地 X 轴方向延伸。每一段骨骼都可以通过相邻两个关键点确定其延伸方向。

在确定了手指的延伸方向 rFingerXDir 之后,还需要再确定另外两个正交方向,才能构造完整的旋转。
一种自然的思路是直接复用手掌的坐标系。
首先可以考虑使用手掌的 Y 轴,即掌面指向掌背的方向。但在手指弯曲幅度较大时,手指的局部朝向会逐渐偏离手掌平面,此时手指的 Y 方向可能与手掌 Y 轴不再一致,甚至出现反转,从而导致姿态不稳定。

另一种思路是使用手掌的 Z 轴,即将 rHandZDir 作为手指的 Z 方向。该方向在手部运动过程中相对稳定,但如果直接使用,由于 LookRotation 会严格对齐 forward,当所有手指共用同一个 rHandZDir 时,其 Z 轴会被锁定,无法表达手指之间的张开角度。例如下图中,关键点描绘的手指是张开的,但模型中的手指却保持并拢。

为了解决这个问题,需要为每根手指单独计算其 Z 轴方向。
这里利用叉乘的一个重要性质,结果向量同时垂直于两个输入向量。
当前我们已知两个方向,
- 手掌提供的 rHandZDir
- 手指延伸方向 rFingerXDir

第一步,rHandZDir 和 rFingerXDir 叉乘出 rFingerYDir,它同时垂直于两者。

第二步,rFingerXDir 和 rFingerYDir 叉乘出 rFingerZDir,这样构造出的三轴两两正交。

把 rFingerZDir 和 rFingerYDir 传入 LookRotation,此时,骨骼的 X 轴会精确对齐 rFingerXDir。

几何上,新算的 rFingerZDir 相当于把 rHandZDir 投影到以 rFingerXDir 为法线的平面上。rFingerZDir 会随手指张开角度同步变化。
下面的代码以食指根关节为例(5→6)。
Vector3 rFingerXDir = (handLandmarks[6] - handLandmarks[5]).normalized;
Vector3 rFingerYDir = Vector3.Cross(rHandZDir, rFingerXDir).normalized;
Vector3 rFingerZDir = Vector3.Cross(rFingerXDir, rFingerYDir).normalized;
fingerBone.rotation = Quaternion.LookRotation(rFingerZDir, rFingerYDir);C#如下图所示,在为每根手指分别计算 Z 轴后,可以正确表现手指张开的角度。

右手拇指
拇指的情况较为特殊。
从模型结构可以看出,拇指骨骼的本地三轴与模型网格的实际延伸方向并不对齐。也就是说,本地坐标系中没有任何一根轴可以直接作为“骨骼延伸方向”。因此,如果直接沿用四指的构造方式,最终结果会出现明显偏移。

进一步观察可以发现,将拇指骨骼绕世界 Y 轴旋转约 -40 度后,其本地 +X 轴能够与拇指网格的实际延伸方向对齐。

基于这一点,计算流程可以分为两步。首先按照四指的方法,通过关键点构造拇指的局部坐标系,得到理想状态下的旋转。然后在此基础上叠加一个绕 Y 轴 +40 度的补偿,将结果映射回模型实际使用的坐标系。
// 右手拇指三段共用(idx = 1, 2, 3 对应近端、中节、末节)
Vector3 rThumbXDir = (handLandmarks[idx + 1] - handLandmarks[idx]).normalized;
Vector3 rThumbYDir = Vector3.Cross(rHandZDir, rThumbXDir).normalized;
Vector3 rThumbZDir = Vector3.Cross(rThumbXDir, rThumbYDir).normalized;
thumbBone.rotation = Quaternion.LookRotation(rThumbZDir, rThumbYDir) * Quaternion.Euler(0f, 40f, 0f);C#这里的“绕 Y 轴 +40 度”先作为一个固定补偿使用,后文会给出更通用的求解方法。
左手
和左右手臂一样,左手的 lHandXDir 和每段 lFingerXDir 需要取反。此外,左手的 lHandYDir 叉乘顺序也要反转,从 Cross(rToIndex, rToPinky) 变为 Cross(lToPinky, lToIndex),因为左右手是镜像关系,食指和小指的空间排列相反,同样的叉乘顺序会得到反方向的法线。
至此我们已经完成了从关键点到骨骼旋转的全部映射。

小结
可以看到,所有骨骼都遵循同一套思路。先观察骨骼在模型中的轴向,确定其沿哪一条本地轴延伸,例如右臂沿 X 正方向,腿部沿 Y 负方向,脚部沿 Z 正方向。随后围绕该延伸方向,通过叉乘构造其余两个正交轴,并最终得到对应的旋转。
这一方法能够成立,依赖两个前提条件。其一,骨骼坐标轴与 Unity 世界坐标轴对齐。其二,模型网格的延伸方向与某一条本地轴一致。拇指只满足前者而不满足后者,因此需要额外补偿。而在实际项目中,模型往往无法满足这两个条件,直接套用上述方法就可能得到错误结果。

接下来将进一步讨论,在这些前提不成立的情况下,如何将这一方法扩展到任意模型。
适配任意模型
在实际模型中,应用上述方法时需要解决“轴不对齐”和“模型网格不沿本地轴”两个问题,对应地引入两个补偿量。
轴不对齐
可以在初始化时记录每根骨骼的世界旋转 initRot。它描述了骨骼从本地空间到世界空间的固定变换。
Quaternion initRot = bone.rotation;C#运行时,通过 LookRotation(zDir, yDir) 得到的是世界空间中的目标旋转。将其与 initRot 相乘,就相当于把这个旋转叠加到 T-pose 上,得到当前骨骼的世界旋转。
bone.rotation = Quaternion.LookRotation(zDir, yDir) * initRot;C#模型网格不沿本地轴
这种情况在拇指中已经出现过。本质是骨骼的本地轴与模型网格的实际延伸方向存在一个固定偏差。解决方法是在初始化时构造一个参考旋转 initAxisRot,使骨骼轴对齐到网格方向。计算出骨骼轴朝向后,再将这段变换抵消,即可得到正确的网格朝向。
其中,
- 骨骼轴,根据骨骼的排列方向选取对齐轴。对于纵向排列的骨骼,如 Hips、Chest、腿部、头部,使用 Y 轴。对于横向排列的骨骼,如手臂、手指,使用 X 轴。对于深度方向排列的骨骼,如脚部,使用 Z 轴。
- 网格方向,对于存在子骨骼的情况,取当前骨骼指向子骨骼的方向。若不存在子骨骼,则假设其与上一节保持一致。
以右手拇指根关节为例,我们使用 RightThumbProximal→RightThumbIntermediate 的方向作为 X 轴的目标方向。对应的 initAxisRot 可以这样构造,
// Start() 中捕获一次
Transform thumbProx = animator.GetBoneTransform(HumanBodyBones.RightThumbProximal);
Transform thumbInt = animator.GetBoneTransform(HumanBodyBones.RightThumbIntermediate);
Vector3 rHandZDir_T = ...; // 初始化时的手掌法线,和运行时 rHandZDir 同一个构造
Vector3 rThumbXDir_T = (thumbInt.position - thumbProx.position).normalized; // mesh 方向
Vector3 rThumbYDir_T = Vector3.Cross(rHandZDir_T, rThumbXDir_T).normalized;
Vector3 rThumbZDir_T = Vector3.Cross(rThumbXDir_T, rThumbYDir_T).normalized;
Quaternion initAxisRot = Quaternion.LookRotation(rThumbZDir_T, rThumbYDir_T);C#运行时仍按常规方式计算目标旋转,但需要将这段变换抵消,也就是右乘其逆,
// LateUpdate 中,每帧
Vector3 rThumbXDir = (thumbLandmarkInt - thumbLandmarkProx).normalized;
Vector3 rThumbYDir = Vector3.Cross(rHandZDir, rThumbXDir).normalized;
Vector3 rThumbZDir = Vector3.Cross(rThumbXDir, rThumbYDir).normalized;
thumbProx.rotation =
Quaternion.LookRotation(rThumbZDir, rThumbYDir) *
Quaternion.Inverse(initAxisRot);C#这样,LookRotation 得到的是“理想骨骼轴”的朝向,而乘上 Inverse(initAxisRot) 后,就将其转换回与实际模型网格对齐的结果。
另外,按上述方法计算得到的右手拇指 initAxisRot 为(-1,-38,-11),与前文通过观察得到的结果基本一致。
合并
将“轴不对齐”和“模型网格不沿本地轴”这两个补偿统一起来,可以得到一条适用于任意模型、任意骨骼的通用公式,
bone.rotation =
Quaternion.LookRotation(zDir, yDir) *
Quaternion.Inverse(initAxisRot) * initRot;C#其中,
- initRot 用于补偿骨骼初始姿态与世界坐标轴不对齐的问题
- initAxisRot 用于补偿网格实际延伸方向与骨骼本地轴不一致的问题
在实现时可以进一步优化。由 Quaternion.Inverse(initAxisRot) * initRot 在初始化完成后对每根骨骼来说都是常量,因此可以预先计算并缓存,
Quaternion compensation = Quaternion.Inverse(initAxisRot) * initRot; // 预计算
bone.rotation = Quaternion.LookRotation(zDir, yDir) * compensation; // 每帧C#现在,我们的方法可以推广到任意模型与任意骨骼的旋转求解。

另一方面,由于 initAxisRot 和 initRot 都是初始化时从同一姿态下捕获的,两者随姿态同步变化,使得姿态相关的部分会自动抵消,只保留骨骼本身的几何偏差,因此模型在初始化时并不要求严格处于 T-pose。
处理初始旋转
在最初推导中,我们假设角色的初始旋转为 identity,即模型未发生旋转。而在实际项目中,角色通常会以特定朝向放置在场景中。如果直接使用上述公式,会使驱动结果仍处于世界坐标系下,偏离原设定朝向。
解决方法是在最终结果前引入角色的初始世界旋转。
在初始化时记录,
Quaternion rootInit = transform.rotation; // Start 时一次性捕获C#运行时将其作为前缀,
bone.rotation = rootInit * Quaternion.LookRotation(zDir, yDir) * compensation;C#其中,Quaternion.LookRotation(zDir, yDir) * compensation 表示在世界坐标系下计算得到的目标姿态,而 rootInit 将该姿态整体映射到角色初始朝向对应的坐标系中,使驱动结果与角色当前朝向保持一致。

至此,我们完成了从关键点到骨骼旋转的推导,并使其能够适配任意模型与任意初始朝向。
局限和改进
本文介绍的是基于关键点驱动骨骼旋转的基础方案。在实际应用中,仍有多个方向可以进一步改进,下面列举几个典型问题。
腰部位移
MediaPipe 的 pose world landmark 以双髋中点为原点,因此 Hips 的坐标始终为 (0, 0, 0),无法直接获得角色的绝对位移,
若需要恢复位移信息,可以通过其他线索进行估计,例如,
- 利用全身关键点的整体高度变化推断垂直位移
- 结合 2D 归一化坐标中 Hips 的图像位置变化估计移动
数据抖动
即使激活 MediaPipe 的数据平滑功能,实际使用中仍然可以观察到明显抖动。
常见的改进方式包括,
- 对关键点位置在计算旋转之前进行时间平滑,从输入侧降低噪声
- 在旋转空间中进行插值,使输出更加连续
四肢沿长轴的转动
在前文中,手臂和腿部的旋转通过辅助方向(如世界 up 或髋宽方向)来构造。这种方式能够确定骨骼的指向(例如肩→肘的方向),但无法表达骨骼绕自身长轴的转动。
同时,手掌或脚部的翻转被集中施加在末端骨骼(Hand 或 Foot)上,而下臂或小腿未参与分担旋转,容易导致末端骨骼出现过度扭转,视觉上不自然。
主要改进方向有,
- 使用更贴合局部运动的辅助方向,例如基于关节弯曲平面构造法线,从而更准确地反映骨骼的实际旋转
- 将末端的扭动按比例分配到相邻骨骼(如下臂或小腿),以获得更均匀、自然的变形效果
使用示例代码
示例项目地址,
https://github.com/SunnyViewTech/MoCapLite
步骤
1. 下载 MediaPipeUnity.0.16.3.unitypackage 并导入项目。
https://github.com/homuler/MediaPipeUnityPlugin/releases/tag/v0.16.3
2. 使用 MoCapLite 中的 HolisticTrackingSolution.cs 替换以下路径中的同名文件,
\Assets\MediaPipeUnity\Samples\Scenes\Legacy\Holistic
3. 打开示例场景,
\Assets\MediaPipeUnity\Samples\Scenes\Legacy\Holistic\Holistic.unity
4. 将 Running Mode 设置为 Sync。

5. 导入角色模型,并将其 Rig 类型设置为 Humanoid。

6. 为模型添加 AvatarController 组件(可选添加 LandmarkGizmos 以便调试)。

7. 运行场景,即可看到动捕驱动效果。

说明
该示例代码用于演示从关键点到骨骼旋转的基本流程。其中,关于初始旋转的处理采用了更通用的实现方式,而针对模型不沿本地轴的部分,仅提供了一种可行实现,并非最优方案。
资源
以下是与本文主题相关的一些开源项目,可供参考,
https://github.com/yeemachine/kalidokit
https://github.com/digital-standard/ThreeDPoseUnityBarracuda
文中使用到的视频资源
https://pexels.com/video/woman-in-black-activewear-doing-leg-and-hip-exercise-5510143
https://pexels.com/video/young-man-practicing-break-dance-5363330