我最近做了一个名为 Everyone Is Spider-Man 的浏览器实时动作捕捉项目。
用户授权摄像头后,系统会识别人脸、上半身、手腕和手指动作,并使用识别结果驱动蜘蛛侠 3D 模型。用户转头、抬手、弯曲手肘或活动手指时,模型都会尽可能同步做出相应动作。
这个项目的核心目标是实现一个浏览器中的实时“蜘蛛侠镜子”:
项目地址: https://github.com/MartinDelophy/everyone-is-spiderman
项目主要使用以下技术:
项目的实时处理链路如下:
浏览器摄像头
↓
MediaPipe 识别人脸、身体和手部关键点
↓
坐标转换、镜像处理和置信度过滤
↓
Kalidokit 计算人体姿态
↓
转换为标准化人形骨骼旋转
↓
Three-VRM 驱动角色骨骼
↓
Three.js 实时渲染摄像头画面不会上传到服务端,视觉推理、姿态计算和角色渲染全部在用户浏览器中完成。
这种架构具有两个明显优势:
同时,它也对用户设备性能提出了更高要求。浏览器需要同时完成视频解码、AI 推理、姿态计算和 3D 渲染。
MediaPipe Tasks Vision 可以在浏览器中直接运行视觉模型。
项目需要处理三类数据。
人脸关键点主要用于计算:
身体关键点负责驱动:
手部关键点用于控制:
手部追踪也是整个项目中最容易出现抖动和识别错误的部分。
MediaPipe 返回的数据主要是关键点坐标,而 3D 角色需要的是骨骼旋转。
例如,当用户抬起右手时,模型并不是简单地把手移动到某个坐标,而是需要依次调整:
因此,需要先把关键点转换成人体姿态,再把姿态映射到角色骨骼。
项目使用 Kalidokit 处理这一转换。简化后的代码如下:
const poseResult = Kalidokit.Pose.solve(
poseLandmarks,
worldLandmarks,
{
runtime: "mediapipe",
video
}
);得到姿态结果后,再将手臂、头部和身体的旋转应用到对应骨骼。
不同 3D 模型的骨骼命名方式可能完全不同。
例如,左上臂可能叫:
LeftArm
upper_arm.L
mixamorigLeftArm
J_Bip_L_UpperArm如果业务逻辑直接依赖模型原始骨骼名称,更换模型后往往需要重写大量映射代码。
Three-VRM 提供了标准化的人形骨骼接口:
const leftUpperArm =
vrm.humanoid.getNormalizedBoneNode("leftUpperArm");
const rightHand =
vrm.humanoid.getNormalizedBoneNode("rightHand");应用层只需要操作统一的骨骼名称,不需要关心模型内部原始命名。
项目使用固定的蜘蛛侠 GLB 模型作为可见角色资产,并将其映射到标准人形骨骼系统中。
视觉识别结果会随着光线、遮挡和运动速度不断变化。如果把每一帧的识别结果直接赋值给模型,角色会出现明显抖动。
对于骨骼旋转,可以使用四元数球面插值:
bone.quaternion.slerp(targetQuaternion, smoothingFactor);其中,smoothingFactor 决定模型追随目标姿态的速度。
数值过小时:
数值过大时:
更合适的方案是采用自适应平滑:
这样可以同时兼顾稳定性和响应速度。
自拍摄像头通常会以镜像形式显示,而视觉模型输出的坐标不一定已经镜像。
常见问题包括:
基础的横向镜像可以写成:
const mirroredX = 1 - landmark.x;但实际项目中不能简单地对所有横坐标执行相同操作。
系统中至少存在三套坐标:
摄像头原始坐标
屏幕显示坐标
Three.js 场景坐标正确的处理方式是建立统一的中间坐标层:
这样可以避免不同模块分别进行镜像,导致坐标被重复翻转。
相比头部和身体,手部追踪更容易受到遮挡和运动速度影响。
当两只手交叉时,MediaPipe 可能在连续帧中交换左右手结果。
可以根据以下信息维持手部身份:
不能只依赖单帧的左右手分类结果。
握拳或做蜘蛛侠手势时,部分手指关键点可能被遮挡。
这时可以利用关键点置信度:
const weight = confidence > threshold ? 1 : 0.2;置信度较低时,不要立即采用新的旋转结果,而是保留上一帧姿态或缓慢恢复到默认状态。
如果只判断当前帧是否满足条件,一个手势可能在几十帧内被连续触发。
可以为手势建立状态机:
IDLE
↓
CANDIDATE
↓
HOLDING
↓
TRIGGERED
↓
COOLDOWN
↓
IDLE只有手势连续保持一定时间后才触发,触发后进入冷却状态,可以显著降低误操作。
蜘蛛侠经典的蛛丝发射手势通常具有以下特征:
可以根据指尖到手掌的距离判断手指是否伸直:
function isFingerExtended(tip, pip, wrist) {
const tipDistance = distance(tip, wrist);
const jointDistance = distance(pip, wrist);
return tipDistance > jointDistance * 1.15;
}然后组合多个条件:
const isWebGesture =
indexExtended &&
pinkyExtended &&
!middleExtended &&
!ringExtended;为了减少误识别,还可以增加:
后续还可以加入双手蛛丝发射、瞄准、握拳和连续动作组合。
项目运行时需要同时进行 AI 推理和 3D 渲染,因此性能优化非常重要。
Three.js 可以以 60 FPS 渲染,但 MediaPipe 不一定需要每帧执行。
例如:
3D 渲染:60 FPS
视觉推理:20~30 FPS
骨骼更新:使用插值补齐这样可以减少视觉模型占用,同时保持画面流畅。
人体关键点每秒可能更新几十次。如果全部写入 React State,会触发大量组件重新渲染。
实时追踪数据更适合存入 ref:
const trackingRef = useRef(null);Three.js 渲染循环直接读取 trackingRef.current。只有权限状态、语言选择和界面提示等低频数据需要存入 State。
可以根据设备性能调整:
在移动设备上,稳定的 30 FPS 通常比不稳定的高画质更重要。
项目中的摄像头画面和动作识别结果都在浏览器本地处理,不会上传到服务器。
摄像头应用需要清楚地向用户说明:
此外,浏览器摄像头 API 通常只允许在以下环境中使用:
localhost因此,生产环境部署时必须启用 HTTPS。
开发这个项目后,我总结了几条比较重要的经验。
AI 模型能够输出关键点,只代表功能可以运行。真正决定体验的是:
摄像头追踪是一个连续过程,应当结合历史帧判断身份、速度和手势状态。
如果 UI、手势识别和角色渲染分别处理镜像,很容易出现左右方向不一致的问题。
关键点短暂丢失时,让模型逐渐恢复到中立姿势,会比瞬间归零自然得多。
降低动作延迟通常比增加光影效果更能提升沉浸感。
接下来准备重点推进以下功能。
这个项目打通了一条完整的浏览器实时动作捕捉链路:
摄像头画面
→ 人体关键点
→ 姿态计算
→ 骨骼重定向
→ 蜘蛛侠模型
→ 实时渲染MediaPipe 负责感知用户动作,Kalidokit 负责计算人体姿态,Three-VRM 提供标准化人形骨骼接口,Three.js 则负责最终的 3D 渲染。
整个方案不需要专业动捕设备,也不需要把视频上传到服务器。用户只需一台带摄像头的电脑,就可以在浏览器中获得实时角色驱动体验。
后续最值得投入的方向,是手指控制、遮挡恢复、低延迟优化和蜘蛛侠特色手势。只有把这些细节处理好,角色才能从“跟着用户移动”进一步提升到“像用户一样自然地移动”。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。