使用手势控制Three.js
在本文中,我们将探讨如何将MediaPipe的手部追踪集成到Three.js项目中,以及为什么这种组合不仅令人兴奋,而且对于构建直观的无手3D交互来说是实用的。

想象一下,不用鼠标或键盘就能控制一个3D场景——只需你的手。
将Google的MediaPipe(手部模块)与Three.js结合,你将拥有《少数派报告》级别的交互体验——今天无需头显。这降低了硬件成本,提高了可访问性,并使你处于空间用户体验的最前沿。
以下是一些你可以应用的方法:
- 电子商务: 消费者可以旋转一双运动鞋,打开它并更改颜色,通过手指滑动来提升参与度和转化率。
- 数据可视化: 分析师可以从三维散点图中“抓住”一个数据点并将其拉向自己以揭示隐藏的维度。
- 无障碍: 无法握鼠标的人——比如有运动障碍或重复性劳损(RSI)的人——可以获得一个无需控制器的沉浸式内容入口。
- 游戏工作室: 设计师可以在午餐时阻断近战组合或舞蹈表情,并在当天下午进行测试。
- 健身应用: 教练一次记录锻炼;AI将动作重新定位到一个3D化身上,用户可以实时跟随。
- 雕塑: 用户用手指雕刻虚拟粘土。
在本文中,我们将探讨如何将MediaPipe的手部追踪集成到Three.js项目中,以及为什么这种组合不仅令人兴奋,而且对于构建直观的无手3D交互来说是实用的。
1、发现MediaPipe

大约三个月前,我偶然看到@measure_plan发布的看起来神秘而有趣的演示,并被这个奇怪的名字:“MediaPipe”(由Google开发)所吸引。
我决定联系他并提议做一个合作演示项目,我们可以一起开发一个基于手势的FPS(第一人称射击游戏)概念演示。我们最终制作了一个有趣的合拍演示,这激发了我深入研究官方文档的兴趣。
2、我们是怎么做到的?
我们将工作分为两个方面。
- 我会专注于FPS游戏本身(实现一个 KeyboardAndMouseInput 控制)
- 他会处理输入处理(实现一个 HandGesturesInput 控制)。
我们同意了一个简单的公共接口:
class Control {
/**
* 在Y轴上。这会旋转相机左右...
*/
get cameraPan() {
return 0;
};
/**
* 在X轴上。这会旋转相机左右...
*/
get cameraPitch() {
return 0;
};
/**
* 前后移动。目前Y轴不会使用...
*/
get direction() {
return new Vector3(0, 0, 0);
}
}
这让我可以专注于使用键盘开发FPS,而他则可以专注于完善和实现与MediaPipe的集成,读取手势,使其按照预期提供给我们的接口。
两位开发者,一个接口
这种方法在集成新的输入系统时效果最好。始终使用一个公共接口。这允许以简单的方式创建临时或新实现。想想看,对我来说实现键盘控制有多容易。
3、那么,什么是MediaPipe?
MediaPipe 是一套库和工具,让你快速在应用程序中应用人工智能(AI)和机器学习(ML)技术。你可以立即将这些解决方案插入到你的应用程序中,根据需要自定义它们,并在多个开发平台上使用它们。
“AI”这个吓人的词一开始让人感到不安,但抽象的魔力让可怕的的部分消失了……你只需要选择一个具有你想要从其中获取AI技能的模块,并将其作为普通的js模块安装。
4、探索相机控制
在阅读了所有文档之后,我独自完成的第一个演示是一个手控飞行相机应用,使用Three.js。因为我想要使用手控,所以我必须安装手部关键点检测器模块(实时检测一只手或多只手的关键点,提供它们在估计的3D空间中的位置)

4.1 手作为输入
手部模块 抽象了通过AI基于图像(或视频流,如网络摄像头)推断手和手指位置的机制。它提供了手的位置信息,以“3D”空间中的点形式呈现。(你永远不会自己处理AI 这是从你那里抽象出来的,即变得简单!)
相机的馈送(或单张图像)被输入到这个模块中,结果是我们得到一组预设的3D点(称为“关键点”)。每个关键点都有已知的ID,因此我们知道,例如索引8是食指的尖端…

你可以在这里查看官方实时演示。你可以在这里查看这个工作代码示例来查看一些代码的实际运行情况。
4.2 网络摄像头的3D点?
其实不是你理解的真正的3D。该模块使用AI来推断每个关键点的位置。它会给你屏幕空间中的一个值和一个Z值。每个关键点都有这个“Z”组件,但这个值是一个近似值(值是噪声的*;使用* 1-Euro过滤器来限制或平滑它。)根据我的测试,我们可以通过调整它来获得手在空间中的伪3D定位。虽然不精确,但如果调整得当,它适用于手势分析。
4.3 那你是怎么控制相机的呢?
我意识到,在所有设置之后,我唯一能使用的只是数组中的3D点(类似Vector3的点,{x,y,z}),所以从那以后,我又回到了我的领域:原生Three.js。
开始看着我的手,思考如何用它来控制相机,拥有3D点作为输入数据?于是我想出了以下解释:
- 俯仰 → 上下倾斜(绕X轴旋转)我用了地标12和9之间的Y差。
- 偏航 → 左右转动(绕Y轴旋转)我用了地标12和9之间的X差
- 翻滚 → 侧倾(绕Z轴旋转)我用了地标8和12之间的
Math.atan2
。
如你所见,没有什么是固定的。你可以找到许多其他方法来做同样的事情。
现在我将解释如何开始使用和试验MediaPipe进行手势识别:
5、设置
在我们开始之前,我们需要安装这个库:
npm install @mediapipe/tasks-vision
并且,由于这使用了AI,而每个AI都带有模型,我们需要下载模型。
//
// 这将返回我们神奇的手势识别器...
//
const createHandLandmarker = async () => {
const vision = await FilesetResolver.forVisionTasks(
`https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm`
);
return await HandLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`,
delegate: "GPU" //[!] GPU委托仅限Chrome;Safari回退到WASM(2–3倍慢)
},
runningMode: "VIDEO", // 可以是IMAGE | VIDEO | LIVE_STREAM
numHands: 1 // 为了一个简单的演示,我们只扫描一只手
});
};
5.1 检查可用性…
记得首先检查用户是否已经授予了网络摄像头访问权限…
//
// 在你的代码中,如果为false则显示一条消息,并且只有在此返回true时才启动你的应用。
//
const webcamIsAvailable = () => !!navigator.mediaDevices?.getUserMedia;
5.2 网络摄像头
为了获得网络摄像头的馈送,以便能够读取你在相机上的手的位置,我们需要先设置摄像头:
//
// HTML Video元素: <video id="webcam" autoplay playsinline></video>
//
const video = document.getElementById("webcam") as HTMLVideoElement;
//
// CANVAS: 用于绘制关键点以进行调试
// (这样你可以看到模型看到的内容)
// <canvas class="output_canvas" id="output_canvas"></canvas>
//
const canvasElement = document.getElementById( "output_canvas" ) as HTMLCanvasElement;
const canvasCtx = canvasElement.getContext("2d");
5.3 翻转网络摄像头的图像
你可能需要翻转来自网络摄像头的视频,你可以通过CSS来实现:
video#webcam,
canvas#output_canvas {
transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
-moz-transform: rotateY(180deg);
}
5.4 视频和画布定位
如果你想要在画布上渲染调试线,这将是可选的。为了让画布堆叠在视频上方,你可能希望用position: absolute
样式化画布,这样它就会在顶部渲染(确保两者都被包裹在一个 position:relative
父级中)

5.5 启动馈送
为了启动网络摄像头,它必须由用户点击某个按钮来启动。由于安全限制,你不能自动启动它。所以添加一个带有点击监听器的按钮...
document.getElementById("webcamButton").addEventListener("click", startWebcam );
let handLandmarker = null;
function startWebcam()
{
// 延迟初始化处理程序...
if( !handLandmarker )
{
handLandmarker = createHandLandmarker()
}
handLandmarker.then( handReader => {
//
// 要求网络摄像头馈送
//
return navigator.mediaDevices.getUserMedia({ video:true }).then((stream) => {
// 让视频对象显示网络摄像头馈送...
video.srcObject = stream;
// 当视频有数据要显示时,动作开始。
video.addEventListener("loadeddata", predictWebcam);
});
});
}
6、MediaPipe 实际应用!
这是将网络摄像头与MediaPipe连接起来扫描图像中手的部分...
function predictWebcam() {
//
// [!] 如果场景是60 fps,建议将推理限制为30 fps以避免笔记本电脑的热节流。
// 你可以使用“timeSinceLastPrediction”来跳过以避免过于激烈...
//
//
// *** 警告:诡异的AI函数调用 ***
// 在这个调用中完成了AI魔法... 这是AI在后台运行的地方... 这是昂贵的计算。
//
const results = handLandmarker.detectForVideo(video, performance.now());
//
// 这里是关键点的位置... 就是这样!很难吗?
//
const landmarks = results.landmarks;
}
7、调试手(可选)
要查看调试线,为了绘制辅助工具(在演示中看起来很炫酷),你需要安装这些模块:
npm install @mediapipe/drawing_utils @mediapipe/hands
然后导入:
import { drawConnectors, drawLandmarks } from "@mediapipe/drawing_utils";
import { HAND_CONNECTIONS } from "@mediapipe/hands"; // 常量
并在你的predictWebcam
函数中调用:
// 绘制段(关节到关节的线)
drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, { color: '#00FF00', lineWidth: 2 });
// 在每个关节上绘制点
drawLandmarks(canvasCtx, landmarks, { color: '#FF0000', lineWidth: 1 });
8、与Three.js集成
现在,你可以用这些关键点做很多事情,predictWebcam
是你将landmarks
重定向到你的应用程序中某处的地方,你在那儿对它们做些什么。
可能性仅受你想象力的限制。 每个关键点都会给你手部关键点的坐标,允许你实时跟踪手指、手势和整体手的方向。你可以使用这些数据来操作3D对象,触发动画,控制相机,甚至在VR或AR中创建全新的交互方法。除了实际应用外,这些关键点还邀请了富有创意的实验:设计基于手势的游戏、互动艺术,或沉浸式装置,让用户的手成为自然的输入设备。这就是创造力真正主宰的地方,将原始的数据点转化为动态、引人入胜的体验。
8.1 示例:控制相机
在这个示例代码中,你可以通过手的左右移动水平移动相机,并通过让食指靠近或远离相机来放大或缩小。
// 假设你已经设置了handLandmarker并有一个Three.js场景
function updateCameraFromLandmarks(landmarks, camera) {
const wrist = landmarks[0]; // 关键点0
const indexTip = landmarks[8]; // 关键点8
// 将归一化坐标转换为屏幕/世界值
const panX = (wrist.x - 0.5) * 10; // 根据你的场景调整比例
const panY = -(wrist.y - 0.5) * 10;
const zoomZ = 10 + (0.5 - indexTip.y) * 20; // 沿z轴移动相机
// 应用于相机
camera.position.x = panX;
camera.position.y = panY;
camera.position.z = zoomZ;
camera.lookAt(0, 0, 0);
}
8.2 示例:在场景中渲染手
你可能想与场景中的物体互动,这时你可能希望在场景中添加每个手的关键点对象,以便可能用作碰撞器?
// 在你的场景设置中...
//
const landmarkSpheres = [];
const sphereGeometry = new THREE.SphereGeometry(0.02, 8, 8);
const sphereMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
// 为每个21个已知关键点创建一个球体...
for (let i = 0; i < 21; i++) {
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);
landmarkSpheres.push(sphere);
}
function convertLandmarkToThreeJS(lm, scale = 1, offset = { x: 0, y: 0, z: 0 }) {
// lm: {x, y, z} 从MediaPipe世界坐标
return {
x: lm.x * scale + offset.x,
y: lm.y * scale + offset.y,
z: lm.z * scale + offset.z
};
}
//
// 每帧更新球体位置
//
function updateLandmarks(landmarks) {
if (!landmarks || landmarks.length !== 21) return;
for (let i = 0; i < 21; i++) {
const lm = landmarks[i];
const lm = convertLandmarkToThreeJS(landmarks[i], 1, undefined);
landmarkSpheres[i].position.set(lm.x, lm.y, lm.z);
}
}
8.3 示例:检测捏合手势
你可能想要检测用户是否正在捏合并对此手势做出反应:
// 关键点索引
const THUMB_TIP = 4;
const INDEX_TIP = 8;
// 捏合的阈值距离(以世界坐标为单位)
const PINCH_THRESHOLD = 0.03;
let isPinching = false;
function checkPinch(landmarks) {
if (!landmarks || landmarks.length < 9) return false;
const thumb = landmarks[THUMB_TIP];
const index = landmarks[INDEX_TIP];
// 欧几里得距离
const dx = thumb.x - index.x;
const dy = thumb.y - index.y;
const dz = thumb.z - index.z;
const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
return distance < PINCH_THRESHOLD; // true = 手正在捏合
}
8.4 最基本的用途
一旦你知道了关键点,它们只是“3D”点,你可以从中驱动值:
- 某些关键点之间的距离:音量、缩放、如果小于某个阈值则抓取,如果大于某个阈值则释放等。
- 段之间的角度:根据某些关键点形成的矢量方向旋转某些东西...
- 缩放:使用Z值或关键点之间的相对距离来推断大小的变化。例如:如果你将手靠近摄像头,所有相对距离都会增加,用这个来创建表示某物缩放程度的delta值等...
- 绘画:你可以使用食指来驱动画布或表面上的笔刷的2D位置,并用它来绘画。也许使用z组件作为笔刷的压力。
记住,关键点只是“3D”点
MediaPipe 抽象了所有困难的AI部分,只留下一个漂亮的3D点数组。。不用担心。一旦你有了关键点,你就告别MediaPipe,你的Three.js之旅继续!
原文链接:Use Hand Gestures to control Three.js
汇智网翻译整理,转载请标明出处
