这几年一直在折腾虚拟形象和动作捕捉这摊事,试过光学动捕、惯性动捕,最后发现日常做原型和轻量级项目,最顺手的一套反而是“普通摄像头 + MediaPipe + Unity”的组合。这个方案没有专业动捕设备的成本压力,不需要穿动捕服,一台带摄像头的笔记本就能把真人动作映射到3D角色上。这次把这套流程里踩过的坑、绕过的弯、优化过的细节全部整理出来,给准备入坑动作捕捉和Unity角色驱动的人一个可以直接落地的参考。
1. 整体思路与方案选型:为什么选MediaPipe而不是惯性/光学动捕
1.1 项目需求拆解
先说说这个项目到底要解决什么问题。需求听起来简单:用摄像头捕捉人的动作,把动作同步到Unity里的3D人物模型上,让模型跟着真人动。但真做起来,里面有几个核心问题需要回答。
第一,用什么方式捕捉动作?市面上主流有光学动捕(比如Vicon、OptiTrack)、惯性动捕(比如Xsens、诺亦腾)、还有基于视觉的动捕(OpenPose、MediaPipe、MoveNet)。前两种精度高但成本也高,光学动捕需要多台红外相机和反光标记点,惯性动捕需要穿一身传感器,都不是拿来快速做原型的首选。
第二,数据怎么处理?摄像头捕捉到的是2D画面,但MediaPipe Pose能直接输出33个关键点的3D坐标(包括深度估计)。这个输出能不能直接用,需不需要转换,是驱动Unity模型的关键。
第三,数据怎么送到Unity?常见做法是走网络协议(UDP)在本地进程间通信,Python负责捕捉,Unity负责接收和驱动。这个链路要稳定、延迟要低,还得处理坐标系不一致的问题。
我对这个项目的定位是“轻量级实时动捕原型”,目标是能在普通笔记本上跑通,延迟控制在100ms以内,效果足够用来做虚拟主播、数字人演示、动作风格迁移测试之类的事。MediaPipe正好满足这些要求:单目摄像头就能跑,CPU也能实时,开源免费,而且输出的是归一化坐标和三维世界坐标,配合Unity做骨骼驱动非常顺手。
1.2 方案对比:为什么不用现成的动捕插件或硬件设备
很多人会问,Unity商店里也有不少动捕方案,为什么还要自己造轮子?我的看法是:如果要快速验证效果,可以先用现成方案;但如果想把动作捕捉和角色驱动这条链路彻底吃透,自己组合MediaPipe和Unity是性价比最高的一条路。
| 方案 | 成本 | 精度 | 上手难度 | 适用场景 |
|---|---|---|---|---|
| 光学动捕 | 很高(设备+场地) | 高 | 高 | 影视、游戏动画制作 |
| 惯性动捕 | 中高(动捕服) | 中高 | 中 | 现场表演、多人捕捉 |
| 深度相机方案(Azure Kinect) | 中 | 中高 | 中 | 交互装置、体感应用 |
| MediaPipe + 普通摄像头 | 几乎为零 | 中(可接受) | 低 | 原型验证、虚拟主播、轻量交互 |
MediaPipe这条路最大的优势在于“门槛低、链路透明”。门槛低是指只要有一台带摄像头的电脑,装好Python和Unity就能开始做;链路透明是指整条pipeline都是自己写的,从关键点坐标到骨骼旋转,出了问题能一步步排查,而不是拿着黑盒插件只能干瞪眼。
当然它也有短板。单目视觉方案对遮挡很敏感,侧身动作、手臂交叉时容易丢失关键点;深度估计是相对值,不是真实的厘米级距离;对于手指级精细化捕捉,Pose关键点做不了,得配合Hand Landmarks单独做手势识别。这些短处后面会在“常见问题”部分展开讲。
1.3 核心链路预览
整个项目的数据流可以拆成四个环节:
- 摄像头采集视频帧,MediaPipe Pose检测每一帧里的人体,输出33个关键点的坐标;
- Python端把人体的骨骼关键点整理成结构化数据(比如JSON),打包后通过UDP发送到Unity;
- Unity使用C#接收数据,把关键点坐标转换成骨骼旋转;
- Unity把计算出的旋转应用到角色的骨架上,驱动模型动作。
这条链路里最关键的技术点有三个:MediaPipe的pose landmark数据解读、关键点坐标到骨骼旋转的数学转换、Unity端的平滑与镜像处理。后面的章节会逐个展开。
2. 技术原理:从摄像头到骨骼旋转
2.1 MediaPipe Pose到底输出了什么
MediaPipe Pose是Google开源的一个单目姿态估计方案,相比OpenPose,它在移动端和CPU上有更好的实时性。它通过神经网络检测人体的33个关键点,每个关键点包含三类信息:
- x、y:归一化后的图像坐标,范围是0到1,以图像左上角为原点;
- z:关键点相对于臀部中心(大致是髋关节中点)的深度值。这个值是估算的,不是绝对深度,但能用来计算关节在空间中的相对位置;
- visibility:可见度,0到1之间,表示该关键点被模型“看到”的置信度。
除了pose_landmarks,MediaPipe还提供world_landmarks。它同样是33个关键点的三维坐标,但单位是米,坐标原点在人体的臀部中心,X轴朝人的左边,Y轴朝上,Z轴朝人背后。这个world_landmarks对Unity驱动特别重要,因为Unity的三维世界也是以米为单位的,直接用world_landmarks做比例换算,比用归一化坐标省事很多。
在代码层面,获取这些数据只需要几行:
import cv2 import mediapipe as mp mp_pose = mp.solutions.pose pose = mp_pose.Pose( static_image_mode=False, model_complexity=1, min_detection_confidence=0.5, min_tracking_confidence=0.5 ) cap = cv2.VideoCapture(0) while cap.isOpened(): success, frame = cap.read() if not success: break frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = pose.process(frame_rgb) if results.pose_landmarks: for idx, lm in enumerate(results.pose_landmarks.landmark): x, y, z = lm.x, lm.y, lm.z注意,这里传给pose.process()的是RGB格式,不是BGR,否则检测效果会受影响。后面做方向判断、关节夹角计算时,也是基于这33个点。
2.2 坐标系和骨骼旋转:整个项目最关键的一座桥
拿到MediaPipe的坐标后,第一件事不是把它们一股脑塞给Unity,而是先做坐标系的“翻译”。
MediaPipe里的坐标是“右手系”:X朝人的左边,Y朝上,Z朝背后(正方向是背离摄像头/指向屏幕外)。Unity是世界空间里的“左手系”:X朝右,Y朝上,Z朝屏幕里(摄像头方向)。如果直接把两个坐标系统硬套,角色大概率会左右反转、前后颠倒,甚至整个拧成麻花。
一个稳妥的转换做法是把Y轴保持不变,X和Z取相反方向。对应到代码:
# 把mediapipe的world_landmark转成unity空间的方向 unity_x = -world_x unity_y = world_y unity_z = -world_z这只是最基础的坐标映射。光有“点”的坐标还不够,驱动骨骼需要的是“旋转”。
人体骨骼可以看成一条条刚体链:大臂是从肩膀到肘部的一段,小臂是从肘部到手腕的一段,大腿是从髋部到膝盖的段,小腿是从膝盖到脚踝的段。每一段都有一个方向向量,方向向量可以由两个关键点相减得到:
shoulder = np.array([lx, ly, lz]) elbow = np.array([lx, ly, lz]) upper_arm_dir = elbow - shoulder # 大臂方向有了方向向量,下一步是求四元数旋转。在Unity里,有两种典型做法:
一是Quaternion.FromToRotation。它接收两个向量,返回从第一个方向旋转到第二个方向的四元数。如果我们知道骨骼在玩偶或角色模型上的“初始方向”(T-Pose下的大臂方向),就能用FromToRotation算出目标旋转。
二是Quaternion.LookRotation。它根据一个“朝向”和一个“上方向”构造旋转。适合躯干、头部这类需要同时约束朝向和轴向的部位。
实际用的时候,我会对每个骨骼段定义一个“父节点-子节点”对,然后从MediaPipe的关键点数据里构造出目标方向向量。比如:
// 一个大臂目标的伪代码 Vector3 shoulder = GetLandmarkPos(11); // 左肩 Vector3 elbow = GetLandmarkPos(13); // 左肘 Vector3 targetDir = elbow - shoulder;接下来要把这个方向向量和模型骨骼自带的初始方向做旋转匹配。这一步看似简单,但坑不少,最关键的问题是:MediaPipe的world_landmarks是以人体臀部中心为原点的“世界方向”,而Unity模型里每根骨骼的旋转是相对于父骨骼的。直接赋值会带来旋转层级错乱。
2.3 为什么是33个点就够了
很多人觉得动作捕捉至少得上百个标记点才够精确,但MediaPipe只用33个点就能驱动一个完整的类人体角色。这背后的原因是:人体关节本身就简化成了33个关键位置,其中每个关节的旋转自由度是有限的(Joint limits)。所以只要关键点位置不出大偏差,骨骼方向就能基本还原。
33个点的分布大致是:脸部区域10个(鼻子、眼睛、耳朵等),躯干和四肢23个。对动作捕捉来说,真正决定姿势的是四肢关节:左右肩膀、肘部、手腕、髋部、膝盖、脚踝。其他点大多是辅助判断朝向和稳定识别。
这意味着在Unity中,我们实际只需要关注大概10到12根骨骼:左右大臂、左右小臂、左右大腿、左右小腿、头颈方向、躯干方向。这一简化大幅降低了代码复杂度和计算量。如果连手指都要驱动,就得另接Hand Landmarks,那就是另一个项目了。
3. Python端:MediaPipe动作捕捉实现
3.1 环境准备:Python版本与依赖安装
我建议直接用Python 3.8到3.10之间的版本,太新的Python版本有时会遇到MediaPipe没有预编译wheel包的问题。安装依赖:
pip install opencv-python mediapipe numpy做一个快速验证,看MediaPipe能不能正常加载模型:
import mediapipe as mp print(mp.__version__)如果这一步报错,大多数情况是Python版本不匹配或者网络问题。换版本后基本能解决。装好之后,连接摄像头时注意:笔记本内置摄像头一般是0,外接摄像头可能是1或2。OpenCV打不开摄像头时,可以检查cap.isOpened()返回值,而不是盲目往下跑。
3.2 动作捕捉脚本的核心逻辑:从视频帧到关键点结构
捕捉脚本的循环结构大概是这样的:
import cv2 import mediapipe as mp import json import numpy as np mp_pose = mp.solutions.pose pose = mp_pose.Pose( static_image_mode=False, model_complexity=1, smooth_landmarks=True, enable_segmentation=False, min_detection_confidence=0.5, min_tracking_confidence=0.5 ) cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break # 水平翻转,让画面变成“镜像”效果,操作者看着更自然 frame = cv2.flip(frame, 1) rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = pose.process(rgb) if results.pose_landmarks: landmarks = results.pose_landmarks.landmark world_landmarks = results.pose_world_landmarks.landmark frame_data = { "timestamp": time.time(), "landmarks_2d": [ {"x": lm.x, "y": lm.y, "z": lm.z, "visibility": lm.visibility} for lm in landmarks ], "landmarks_3d": [ {"x": lm.x, "y": lm.y, "z": lm.z} for lm in world_landmarks ] } # 把frame_data发送出去(后面补UDP发送) # 实时可视化 mp_drawing = mp.solutions.drawing_utils mp_drawing.draw_landmarks( frame, results.pose_landmarks, mp_pose.POSE_CONNECTIONS ) cv2.imshow("MediaPipe Pose", frame)这里有两个细节值得注意。第一个是cv2.flip(frame, 1),水平翻转之后,跟真人左右一致,否则你做“抬左手”,画面里检测到的是你视觉上的右手,模型会跟着乱。第二个是model_complexity参数,它影响模型精度和速度,默认是1,数值越大精度越高但越慢。如果机器性能一般,可以降到0,速度明显提升,精度损失在可接受范围内。
3.3 数据发送:为什么选UDP而不是TCP
Python端和Unity端的通信方案,我一开始用的是TCP,因为TCP有确认机制,不会丢包。但实际跑起来发现延迟不稳定,TCP的粘包和重传机制在实时动捕场景里反而碍事。动作捕捉的帧率在20到30帧左右,就算偶尔丢一两帧,角色只是轻微卡顿,无伤大雅;但延迟忽高忽低是绝对不能接受的。所以我后来换成了UDP,走本机localhost,延迟能稳定在几毫秒。
发送端代码:
import socket import json UDP_IP = "127.0.0.1" UDP_PORT = 9000 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 在每一帧拿到data之后 message = json.dumps(frame_data).encode("utf-8") sock.sendto(message, (UDP_IP, UDP_PORT))这里有个大坑:数据包大小。一帧33个关键点,每个关键点带x/y/z和visibility,数据量很小,不到2KB。但如果你后面加了Hand Landmarks或Face Mesh,数据量会涨到十几KB,UDP单个数据报在IPv4下最大只有65507字节,超出就得分包,Unity接收端也要处理分片。直接用TCP反而省事。所以“用UDP还是TCP”不能一刀切,要看实际数据量。做纯Pose捕捉,UDP绝对够用。
3.4 可视化调试:别急着让角色动,先看骨架准不准
我吃过一个亏:一开始没做可视化,直接拿着坐标往Unity里灌,结果角色肢体扭曲,根本分不清是MediaPipe检测错了,还是Unity骨骼驱动错了。后来花十分钟在Python端画出骨架,问题立刻定位。
MediaPipe自带draw_landmarks可以把骨架画在图像上,这是第一层验证。第二层验证是在Unity里也用LineRenderer把骨骼画一遍,这样能把“捕捉端”和“驱动端”彻底分开排查。只要Python端画出的骨架跟真人动作一致,Unity端画出的骨架也一致,就能确定驱动逻辑没问题,剩下的只是模型骨骼绑定的艺术问题。
4. Unity端:人物模型驱动实现
4.1 接收端与数据协议:用C#解析JSON
Unity端需要写一个接收脚本。最基础的做法是让MonoBehaviour里的Update()每帧检查UDP接收队列。不要让UDP接收阻塞主线程,否则帧率会被拉低。我采用的是在Start()里开一个异步方法,或者用async void,把接收到的JSON字符串缓存到一个变量里,然后主线程在Update()里消费最新的一帧数据,舍弃积压的旧数据。这样做能保证角色永远使用最新动作,不会越积越卡。
数据解析方面,JSON用JsonUtility就够了,但需要注意JsonUtility不能直接序列化List<Vector3>,所以要先定义对应的数据类:
[System.Serializable] public class LandmarkData { public float x; public float y; public float z; } [System.Serializable] public class PoseFrame { public double timestamp; public LandmarkData[] landmarks_3d; }收到字符串后反序列化成PoseFrame,再转成内部用到的Vector3[]数组。注意Unity的Vector3的Y轴和Z轴需要做一次坐标变换,这个在后面的坐标落地部分详细说。
4.2 骨骼映射与旋转计算:从目标方向到四元数
到了这一节,就是整个项目最难也最核心的部分:把33个点变成模型骨骼的旋转。
先说模型准备。我建议用Unity的Humanoid角色模型,这样可以用Animator.GetBoneTransform(HumanBodyBones.RightUpperArm)拿到指定骨骼,不用手动去层级里找名字。前提是模型必须正确配置了Humanoid Avatar。
接着,定义关键的骨骼段。每个骨骼段的“起点”和“终点”对应MediaPipe关键点的索引。下面是MediaPipe Pose的常用索引对照:
| MediaPipe索引 | 关节名称 | 说明 |
|---|---|---|
| 11 | Left Shoulder | 左肩 |
| 13 | Left Elbow | 左肘 |
| 15 | Left Wrist | 左手腕 |
| 12 | Right Shoulder | 右肩 |
| 14 | Right Elbow | 右肘 |
| 16 | Right Wrist | 右手腕 |
| 23 | Left Hip | 左髋 |
| 25 | Left Knee | 左膝 |
| 27 | Left Ankle | 左踝 |
| 24 | Right Hip | 右髋 |
| 26 | Right Knee | 右膝 |
| 28 | Right Ankle | 右踝 |
有了对应关系,接下来计算旋转。理论上一根骨骼,比如“右上臂”,它的方向等于worldLandmark[14] - worldLandmark[12]。但Unity里的骨骼是世界旋转的,想设置骨骼旋转不能直接拿这个方向量赋值,要算出四元数。
我用的方法是:在模型处于T-Pose时记录每根骨骼的初始世界方向(或者用模型的默认方向)。运行时,用Quaternion.FromToRotation在“初始方向”和“目标方向”之间求差值旋转,然后把差值旋转应用给骨骼。
// 假设boneInitialDir是模型T-Pose下该骨骼的世界方向 // targetDir是MediaPipe计算出的目标方向 Quaternion targetRotation = Quaternion.FromToRotation(boneInitialDir, targetDir); // 根旋转修正 bone.rotation = rootTransform.rotation * targetRotation;这里rootTransform是模型的根Transform(通常是人物的骨盆或Hips),用来把MediaPipe坐标系里的方向正确映射到Unity世界空间。如果模型初始方向不是标准T-Pose,建议先调整好模型姿态,否则旋转基准就是错的。
大臂旋转搞定后,小臂、大腿、小腿都是同一个套路,不做赘述。但有一个重要顺序:必须从父骨骼往子骨骼更新,也就是先设置躯干/颈椎,再设置大臂、大腿,最后设置小臂、小腿。因为子骨骼的旋转是相对于父骨骼的,父骨骼动了,子骨骼的世界方向如果不重新计算,会被覆盖。
4.3 平滑、镜像与偏移处理:让动作看起来自然
实时动捕里最影响观感的是“抖动”。MediaPipe单帧检测会有噪声,直接把原始四元数赋给骨骼,角色会像帕金森一样抖。解决抖动有两个思路,一是让MediaPipe的smooth_landmarks打开,二是在Unity端做时间上的平滑插值。
我习惯在Unity端做双保险:
// 使用Slerp做旋转平滑,speed在10~20之间 transform.rotation = Quaternion.Slerp( transform.rotation, targetRotation, Time.deltaTime * rotationSmoothSpeed );注意rotationSmoothSpeed要按骨骼不同调参。肘关节扭曲比肩关节小,可以设快一点;肩关节的抖动更明显,就设慢一点。这个值不能全局通用,必须逐骨骼调。
然后是镜像问题。摄像头画面经过cv2.flip后,MediaPipe拿到的关键点已经和真人视觉方向一致,但到了Unity里,还要做一次符号处理。如果在Unity里看到角色抬的是相反的手,最直接的做法是在收到数据后对所有x取反:
landmark.x = -landmark.x;如果你用的是world_landmarks,也会遇到同样问题,要做Z轴和X轴的手性转换。建议把所有坐标转换统一封装在一个函数里,不要让脏数据到处传。
最后是偏移处理。MediaPipeworld_landmarks的坐标原点是人体臀部中心,而Unity模型根节点位置不一定对齐到原点。为了保证角色不“飞走”,要把整个坐标系平移到模型Root的位置。一般做法是,在设置骨骼旋转时,直接修改animator.bodyPosition或把根骨骼的position设为当前臀部中心位置加一个固定偏移。
这里提一个非常基础的注意点:如果模型是Generic模式,GetBoneTransform配合Humanoid骨骼枚举是不好用的,建议统一用Humanoid模式,除非你有特殊需求,否则不要选Generic。
4.4 一个可以直接抄的C#驱动脚本骨架
给一个最简版本的C#脚本结构:
using System; using System.Net; using System.Net.Sockets; using System.Text; using UnityEngine; [System.Serializable] public class LandmarkData { public float x; public float y; public float z; } [System.Serializable] public class PoseFrame { public LandmarkData[] landmarks_3d; } public class MotionCaptureReceiver : MonoBehaviour { public int port = 9000; public Animator animator; private UdpClient udpClient; private string latestData; private Vector3[] landmarkPositions = new Vector3[33]; void Start() { udpClient = new UdpClient(port); udpClient.BeginReceive(ReceiveCallback, null); } private void ReceiveCallback(IAsyncResult ar) { IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); byte[] data = udpClient.EndReceive(ar, ref remoteEndPoint); latestData = Encoding.UTF8.GetString(data); udpClient.BeginReceive(ReceiveCallback, null); } void Update() { if (string.IsNullOrEmpty(latestData)) return; PoseFrame frame = JsonUtility.FromJson<PoseFrame>(latestData); if (frame != null && frame.landmarks_3d != null) { for (int i = 0; i < frame.landmarks_3d.Length; i++) { landmarkPositions[i] = new Vector3( -frame.landmarks_3d[i].x, frame.landmarks_3d[i].y, -frame.landmarks_3d[i].z ); } // 把landmarkPositions转成骨骼旋转 ApplyPoseToBones(landmarkPositions); } } private void ApplyPoseToBones(Vector3[] positions) { // 核心旋转计算,参考上面“骨骼映射与旋转计算”一节 } void OnDestroy() { udpClient?.Close(); } }这个脚本只负责接收和存储数据,真正的旋转计算需要配合具体的模型骨骼定义来做。我建议把“坐标转换”“旋转计算”“平滑”拆成三个独立的类,这样后续换模型、换捕捉源时改动最小。
5. 常见问题与排查实录
5.1 坐标乱飞:模型突然转到奇怪的角度
症状:角色在某个角度下四肢拧成麻花,或者大幅旋转。
原因:大多数情况是坐标系转反了,或者用Quaternion.FromToRotation时初始方向不对。MediaPipe的world_landmarks是右手系,Unity是左手系,我只取反X和Z是不够的,有些骨骼段(比如肩到肘的方向)还依赖模型本身的初始骨骼方向。如果你发现“躺下能对上,站起来就乱”,多半是初始方向向量没校准。
排查顺序:先在Python端固定一个T-Pose,打印出world_landmarks里的肩、肘、腕坐标;再到Unity里打印对应的骨骼世界方向,手动比对数值,确认转换公式没问题。
5.2 角色抖动:动作看起来像在“打冷颤”
症状:骨骼旋转一直在微小抖动。
原因:MediaPipe单帧检测有噪声,加上手部快速移动时关键点置信度下降,导致旋转目标值不断跳变。
解决办法分三层:一是调大min_detection_confidence和min_tracking_confidence,过滤掉置信度过低的关键点;二是打开smooth_landmarks;三是在Unity端调整平滑参数。如果抖动仍然严重,可以对3D坐标先做一阶低通滤波,再拿去算旋转。
值得提醒的是,过度平滑会带来动作延迟,角色看起来像在“太空步”。调参时要平衡,我建议平滑速度常数在10到15之间,延迟体感比较小。
5.3 Unity收不到数据:端口、防火墙和编码的坑
症状:Python端一直在sendto,但Unity端UdpClient收不到任何数据。
常见原因:
- 端口被占用或被防火墙拦截。本机回环
127.0.0.1一般不触发防火墙,但如果绑定的IP是局域网地址,Win/Mac防火墙会拦。解决:只监听127.0.0.1。 - Python端没有真正执行
sendto,只是打印了frame_data但没发送。 - JSON编码问题。
JsonUtility.FromJson要求字段名完全匹配,大小写都要对上,landmarks_3d和LandmarkData[]如果不一致,反序列化后数组长度会是0。
排查方法:先在Unity端写一个测试按钮,手工发一段固定JSON,看能不能正确解析;再把Python端的发送频率降到每秒1帧,确认网络链路通不通。
5.4 MediaPipe安装与性能问题
症状:import mediapipe报错,或者运行很卡。
原因:很可能是Python版本和MediaPipe的预编译包不匹配。MediaPipe在不同Python版本下支持的包不一样,太新的Python版本往往没有对应wheel。
建议使用Python 3.9或3.10,如果还报错,去PyPI或者官方GitHub看对应版本要求,手动下载安装,不要死磕pip install。
性能方面,如果CPU跑不太动,优先级调整顺序是:把model_complexity降到0、把输入图像缩小(用cv2.resize把帧缩到480甚至360宽度)、把检测帧率从30帧降到15帧(跳帧)。注意MediaPipe本身要求传入的图像RGB通道,缩放时别把通道顺序改错。
6. 进阶扩展方向:从“能动”到“好用”
这套方案稳定跑通之后,能扩展的方向非常多。我列几个亲测有效的方向,供你决定下一步怎么走。
第一,增加手势识别。MediaPipe不仅能做Pose,还能做Hand Landmarks。把手势识别结果叠加到角色手上,可以让角色做手指动作,比如比个心、竖个拇指。但注意,Pose和Hands是两个不同模型,不能在同一帧里直接复用,需要分别推理,性能消耗会翻倍。
第二,结合面部驱动。MediaPipe Face Mesh能输出468个面部关键点,可以驱动模型表情。比如提取眼睛睁开程度、嘴角上扬程度,映射到Unity的BlendShape上,让虚拟角色有表情变化。这样一套摄像头就能同时驱动身体和脸部,做虚拟主播够用了。
第三,模型驱动从“旋转对齐”升级到“IK解算”。我现在做的旋转对齐方案,直接把方向向量转成四元数,对模型的骨骼长度比例没有要求,比较简单。但如果模型比例和真人差异大,可以改用Unity的IK系统:先把骨盆、手、脚目标点计算出来,再用IK解算驱动四肢。IK方案的优点是模型不容易穿模,缺点是计算量更大,对关节限制处理也更复杂。
第四,加一点数据后处理。MediaPipe的关键点数据可以做姿态滤波,比如卡尔曼滤波或者基于历史帧的平滑,能进一步减少抖动。这块做深了,就是商业动捕方案的底子了。
7. 写在最后:一些个人体会
做了这个项目之后,最大的感受是,动作捕捉没有想象中那么遥不可及。专业动捕设备贵,但摄像头加开源的MediaPipe,再加Unity的骨骼驱动能力,一套亲手搭起来的轻量动捕系统,足够支撑很多创意原型。
如果让我给一个学习路径的建议:先不要急着直接驱动一个完整的人形模型,先用LineRenderer画骨架把基础验证做通,再一步步加上旋转计算、平滑、镜像、模型绑定。每一步都单独验证,出问题了也容易定位。这套方案虽然精度比不上专业设备,但胜在链路透明、可控性强、成本为零,作为原型验证和入门学习的切入点,价值很高。后面再想往深了做,无非就是换更好的摄像头、多加几个视角、上深度相机而已,核心的数据链路和思维模型不会变。