引言
XR 交互的终极形态是「没有控制器」:手就是输入设备,眼睛就是光标。这条路在硬件上已经走通(Quest 3、Vision Pro 都支持手部与眼动追踪),但在交互设计上远未成熟,因为手和眼睛的物理特性与鼠标键盘完全不同。
工程上的三个核心矛盾:手部追踪精度不足以做精细操作(抖动、遮挡、深度歧义)、注视不等于意图(Midas Touch 问题)、长时间举手会疲劳(Gorilla Arm)。任何忽略这三点的设计,演示时惊艳,实际使用十分钟就会被用户放弃。
本文按「手部原理 → 数据模型 → 手势识别 → 手部限制 → 眼动输入化 → 注视加捏合 → 消歧 → 协同设计 → 疲劳与无障碍」的顺序展开,重点给出可实现的判定逻辑与调参经验。手部与眼动的硬件原理见 XR 显示光学与头部眼动追踪 ,接入 Unity 的具体组件见 Unity XR Interaction Toolkit 交互体系 。
目录
- 手部追踪的技术原理
- 手部数据模型与关节点
- 手势识别的两条路线
- 关键手势的实现
- 手部追踪的工程限制
- 眼动追踪的输入化
- 注视加捏合范式
- Midas Touch 与消歧
- 眼动的其他用途
- 手眼协同设计
- 疲劳与无障碍
- 评估与调参
- 权衡取舍
- 常见坑清单
- 小结
1. 手部追踪的技术原理
主流手部追踪是基于单目或多目相机的关节点回归,而不是戴手套的传感方案:
输入:头显侧向/下向摄像头图像(Quest 3 有 2 个手部相机)
模型:CNN 主干 + 关节点热力图回归(或直接回归 3D 坐标)
输出:21 个关节点的 3D 位置 + 置信度
频率:通常 30~60 Hz(低于头部追踪的 1000 Hz)
模型结构上,主流方案沿用了人体姿态估计的思路:用 CNN 提取特征,再回归关节热力图,最后通过 PnP 或直接回归得到 3D 坐标。单目方案的深度本质上是估计值,这正是手部追踪在 Z 方向精度差的根源。相关的视觉算法基础可参考 Vision Transformer 与视觉骨干 与 目标检测与 YOLO 系列工程落地 。
2. 手部数据模型与关节点
OpenXR 与各平台统一使用 21 关节点模型(每只手):
0 Wrist 手腕
1 ThumbMetacarpal
2 ThumbProximal
3 ThumbDistal
4 ThumbTip
5 IndexMetacarpal
6 IndexProximal
7 IndexIntermediate
8 IndexDistal
9 IndexTip
10 MiddleMetacarpal
11~14 Middle*
15 RingMetacarpal
16~19 Ring*
20 PinkyMetacarpal
21 Pinky* (不同实现略有差异,主流为 26 点含指尖)
Unity 侧通过 XRHandSubsystem 读取:
// 读取关节点位姿
var subsystem = hands[0];
if (subsystem.leftHand.isTracked) {
var joint = subsystem.leftHand.GetJoint(XRHandJointID.IndexTip);
if (joint.TryGetPose(out Pose pose)) {
Vector3 tipWorld = pose.position; // 世界坐标
Quaternion rot = pose.rotation;
}
}
工程要点:
- 必须检查
isTracked:手出画或被遮挡时位姿会变成零值或残留旧值。 - 置信度字段:部分平台提供每关节置信度,低置信度关节应做平滑而非直接使用。
- 坐标空间:位姿相对哪个参考空间(local / local-floor)要明确,否则手会出现在错误高度。
2.1 手部追踪的性能特征
| 指标 | 典型值 | 说明 |
|---|---|---|
| 更新频率 | 30~60 Hz | 低于头部追踪,快速挥动会「拖影」 |
| 端到端延迟 | 30~80 ms | 明显高于手柄 |
| 关节数 | 21~26 | 平台差异,注意统一 |
| 深度误差 | 1~5 cm | 单目估计,前后方向最差 |
| 视野覆盖 | 约 60° 锥体 | 头显正前方 |
| 算力占用 | 移动端 1~3 ms/帧 | 与渲染争抢预算 |
延迟 30 到 80 ms 意味着快速挥手时手的位置会明显滞后,因此手势判定不能只看瞬时姿态,要结合运动轨迹做预测或加宽判定窗口。
3. 手势识别的两条路线
从关节点到「捏合」「握拳」这类语义,有两条路线:
| 路线 | 方法 | 优点 | 缺点 |
|---|---|---|---|
| 几何规则 | 关节点距离/角度阈值 | 无训练、可解释、低延迟 | 泛化差、需手调 |
| 模板匹配 | 与录制姿态比对 | 实现简单 | 对速度与姿态敏感 |
| 分类器 | 特征 + 小型 MLP/SVM | 泛化好 | 需采集数据 |
| 端到端 | 图像直接输出手势 | 最鲁棒 | 算力与数据需求高 |
实践中 90% 的场景用几何规则就够了,因为 XR 需要的手势种类很少(捏合、握拳、指向、张开、点赞),而规则法的延迟最低、最可控。
// 几何规则:捏合判定
bool IsPinching(XRHand hand, float threshold = 0.02f) {
if (!TryTip(hand, XRHandJointID.ThumbTip, out var thumb)) return false;
if (!TryTip(hand, XRHandJointID.IndexTip, out var index)) return false;
float d = Vector3.Distance(thumb, index);
// 用相对手掌宽度的比例而非绝对值,适配不同手型
float scale = PalmWidth(hand);
return d / scale < threshold;
}
用相对比例而非绝对距离是关键:不同人的手大小差异可达 40%,用绝对阈值会导致「大手用户永远捏不上、小手用户一直触发」。
3.1 分类器路线的最小实现
当手势种类变多(超过 6 到 8 种)时,规则法会变得难以维护,此时改用小型分类器:
import numpy as np
def features(joints):
# joints: (21, 3) 关节点,以手腕为原点、手掌尺度归一化
wrist = joints[0]
local = joints - wrist
scale = np.linalg.norm(joints[9] - joints[0]) # 中指指尖距离
local = local / (scale + 1e-6)
# 归一化后展平成 63 维特征
return local.flatten()
分类器本身极小:63 维输入、隐藏层 64、输出 N 类,移动端单帧推理通常低于 0.2 ms,不会成为瓶颈。
要点:
- 必须归一化:以手腕为原点消除位置差异,以手掌尺度消除手型差异,否则分类器会把「大手张开」识别成「小手握拳」。
- 时序信息:单帧特征无法区分「正在捏合」与「已经捏住」,需叠加最近若干帧形成时序特征,或对分类结果做时间平滑(连续 N 帧同类才确认)。
- 类别不平衡:静态手势容易采集,动态手势难采集,训练时需按类加权。
4. 关键手势的实现
各手势的判定依据与阈值参考:
| 手势 | 判定依据 | 参考阈值 |
|---|---|---|
| 捏合(Pinch) | 拇指尖与食指尖距离 | 小于手掌宽度的 25% |
| 握拳(Fist) | 四指指尖到手腕距离 | 均小于手掌宽度的 60% |
| 指向(Point) | 食指伸展,其余弯曲 | 食指伸直度大于 0.8 |
| 张开(Open Palm) | 五指均伸展 | 五指伸直度均大于 0.7 |
| 捏合保持 | 捏合持续时长 | 大于 300 ms |
伸直度(Extension)定义:
对每个手指,计算指尖到手腕的距离与
「完全伸直时指尖到手腕的距离」之比
1.0 = 完全伸直,0.5 = 弯曲
4.1 抖动抑制
原始关节点抖动明显,直接用于交互会「跳」,必须做滤波:
// 指数平滑(一阶低通)
Vector3 Smooth(Vector3 current, ref Vector3 state, float alpha) {
state = Vector3.Lerp(state, current, alpha); // alpha 取 0.3~0.5
return state;
}
滤波的代价是延迟:alpha 越小越平滑但越滞后。交互关键点(指尖)用较大 alpha 保响应,非关键点用较小 alpha 保稳定,是常见的折中做法。
5. 手部追踪的工程限制
必须认清的四个限制:
- 深度精度差:单目估计的 Z 误差可达数厘米,前后移动的交互(如推拉)不可靠。
- 视野边界:手离开摄像头视野即丢失,头部正前方约 60 度范围外追踪质量骤降。
- 遮挡歧义:手指互相遮挡时关节点会「穿透」,表现为手指穿过手掌。
- 双手交叉:两手重叠时容易串台,左手关节点被识别为右手。
对应的设计对策:
深度差 → 避免依赖前后移动的交互,改用捏合/张开
视野边界 → 交互区域限制在头显正前方 45 度锥体内
遮挡歧义 → 关键手势(捏合)不依赖被遮挡的关节
双手交叉 → 避免设计需要双手重叠的操作
6. 眼动追踪的输入化
眼动的精度与延迟在近年已达到「可做输入」的水平:
| 指标 | 消费级 | 高端 |
|---|---|---|
| 精度 | 1.5~3° | 0.5~1.5° |
| 采样率 | 60~90 Hz | 90~120 Hz |
| 延迟 | 20~50 ms | 10~20 ms |
| 标定耗时 | 5~15 s | 自动,几乎无感 |
眼动作为输入的核心价值是「定位」而不是「确认」。人眼可以在 100 ms 内跳到目标,比手快得多;但眼睛无法表达「我要选它」,必须配合第二通道(捏合、停留、语音)确认。这一分工是理解所有眼动交互设计的基础。
眼动能力边界:
定位快 → 适合做「指向」
无法确认 → 必须配第二通道
会扫视 → 需要「停留时长」或意图预测过滤
精度有限 → 目标不能太小(视角 2° 以上)
6.1 眼动信号预处理
原始注视点必须过滤才能当输入用:
三类噪声:
微颤(Tremor) : 高频小幅抖动,1~2° 幅度,需低通滤除
微跳(Microsaccade): 快速小幅跳动,< 1° 但速度高
眨眼(Blink) : 数据中断,需插值或标记无效
处理链:
1. 无效帧剔除(置信度为 0 或数据中断)
2. 中值滤波(窗口 3~5 帧)去脉冲噪声
3. 一阶低通平滑(alpha 0.3~0.5)
4. 扫视检测(速度阈值 30~100 °/s),扫视期间不触发选择
扫视检测很关键:人眼在扫视(Saccade)期间几乎不获取视觉信息,此时不应触发选择。加入扫视过滤后,误触发率通常能下降一半以上。
7. 注视加捏合范式
Gaze + Pinch 是目前公认最优的组合,也是 Vision Pro 的默认交互:
工作流:
1. 眼睛看向目标 → 系统高亮候选
2. 手指轻轻捏合(手可以自然放在腿上)
3. 系统确认选择
为什么好:
手不需要抬起(避免 Gorilla Arm)
眼不需要精确停在目标上(捏合瞬间的注视点已足够)
两者时间上解耦(可以先看再捏)
实现要点:
// 选择时取「捏合发生前 N 帧」的注视点,而非捏合瞬间
// 因为捏合动作本身会分散注意力,注视点可能已偏移
Vector3 GetSelectionGaze() {
int idx = Mathf.Max(0, gazeHistory.Count - 5); // 前 5 帧
return gazeHistory[idx];
}
取历史注视点而非即时注视点是关键技术细节:捏合的物理动作会让人不自觉地移开视线,用即时注视点会导致选错目标。
8. Midas Touch 与消歧
Midas Touch 问题指「用户看向某物就被触发」,导致误操作。四种消歧策略:
| 策略 | 机制 | 优点 | 缺点 |
|---|---|---|---|
| 停留时长 | 注视超过 400~800 ms 触发 | 无需第二通道 | 慢,长列表累 |
| 第二通道确认 | 捏合/按键/语音 | 准确 | 需另一只手 |
| 意图预测 | 用眼动模式判断意图 | 快 | 准确率有限 |
| 显式激活 | 进入「选择模式」后才响应 | 可控 | 模式切换成本 |
工程推荐是「第二通道确认」为主、「停留时长」为兜底:有手时用捏合,手不可用时退化为停留。停留时长建议 600 ms 起步,且必须配合进度指示器(环形进度条),否则用户不知道系统在等什么。
停留触发的可用性要点:
必须显示进度反馈(否则用户会重复注视)
目标不能太小(视角 3° 以上,否则注视点抖动导致离开)
需要「离开取消」而非「一直累积」
相邻目标间距要大于注视精度(避免误触发邻居)
9. 眼动的其他用途
除了选择,眼动还有四类高价值用途:
- 注视点渲染:直接省 30% 到 50% GPU,见 一体机 XR 性能优化实战 。
- 注意力分析:统计用户在看什么,用于内容优化与广告效果评估(注意隐私合规)。
- 自动滚动:阅读长文本时按注视位置滚动,解放双手。
- 化身眼部动画:社交 VR 中用眼动驱动 Avatar 的眼球与眨眼,显著提升真实感。
隐私红线:
眼动数据属生物特征,多数司法辖区视为敏感信息
必须明示采集目的、提供关闭选项、做匿名化
不要将原始眼动轨迹上传云端
10. 手眼协同设计
把手和眼放在一起设计,而不是各自独立:
| 任务 | 主通道 | 辅通道 | 说明 |
|---|---|---|---|
| 菜单选择 | 眼 | 捏合 | 快速定位 + 确认 |
| 物体抓取 | 手 | 眼 | 手精确操作,眼提供目标 |
| 远距离移动 | 眼 | 手柄 | 注视目标点后传送 |
| 文本输入 | 眼 | 手/语音 | 眼选键位,手确认 |
| 精细调整 | 手 | 眼 | 手做微调,眼做校验 |
设计原则:用眼睛做「选哪里」,用手做「怎么做」。违反这条原则的设计(例如用手做长距离指向)会同时牺牲速度与舒适度。
10.1 交互区域的划分
把空间按舒适度分层,内容放置与交互设计都遵循这个分层:
┌─────────────────────────────────────┐
│ 近场(0.3~0.7 m) │
│ 手可及,直接抓取;视线自然聚焦 │
│ 适合:手持物体、精细操作 │
├─────────────────────────────────────┤
│ 中场(0.7~2.5 m) │
│ 射线可达,注视选择;无需抬臂 │
│ 适合:菜单、按钮、列表 │
├─────────────────────────────────────┤
│ 远场(> 2.5 m) │
│ 需注视或传送;手部追踪不可用 │
│ 适合:导航目标、环境内容 │
└─────────────────────────────────────┘
中场是最舒适的交互区:可以用眼加捏合完成,手不必抬起。因此菜单、按钮这类高频操作应放在 1 到 2.5 米处,而不是贴在手上或放到远处。
11. 疲劳与无障碍
Gorilla Arm(大猩猩手臂) 是指手臂长时间悬空导致的酸痛,是免手柄交互最大的敌人。量化参考:
手臂前伸持物 1 分钟 → 肩部开始疲劳
手臂前伸持物 5 分钟 → 明显酸痛,动作精度下降
手臂自然下垂 → 可持续数小时
因此设计上应尽量让手处于自然下垂或搭在腿上的位置(Vision Pro 的做法),只在需要时抬起到交互区。
无障碍考虑:
- 运动能力受限用户:必须提供不依赖精确手部动作的路径(语音、注视停留、头部指向)。
- 色觉障碍:交互反馈不能只靠颜色,要加形状与音效。
- 听力障碍:音效反馈必须有视觉替代。
- 单手用户:所有操作应能用单手完成,不做双手强制手势。
12. 评估与调参
交互参数不能拍脑袋,要实测:
关键指标:
首次成功率(First-try Success Rate)→ 目标 > 95%
选择耗时(Time to Select) → 目标 < 1.5 s
误触发率(False Trigger Rate) → 目标 < 2%
疲劳自评(Borg CR10) → 10 分钟使用后 < 3
学习成本(首次使用达到熟练的尝试次数)→ < 5 次
方法:
招募 8~12 名非开发者,做任务式测试
记录成功率与耗时,而非只问「好不好用」
经验上,手势阈值需要在至少 8 名不同手型的测试者上标定,否则会偏向开发者自己的手型。
13. 权衡取舍
- 几何规则与分类器:前者零训练、低延迟、可解释,后者泛化好但需数据,手势少时选前者。
- 停留触发与捏合确认:前者无需第二通道但慢,后者快但要求手可用,常两者共存。
- 高精度眼动与低成本眼动:前者支撑 DFR 与精细交互,后者只够做粗略注视,按场景选。
- 滤波强度:强滤波稳定但滞后,弱滤波响应快但抖动,按交互类型分别设置。
- 双手手势与单手设计:双手表达力强但要求双手可见,无障碍与稳定性都更差。
- 眼动数据采集与隐私:采集提升体验但增加合规成本,必须明示并提供关闭项。
14. 常见坑清单
- 用绝对距离判定捏合:不同手型差异大,必须用相对比例。
- 不检查 isTracked:手出画后用残留位姿,物体乱飞。
- 手部关键点不滤波:指尖抖动导致射线乱跳,体验崩坏。
- 依赖前后移动的交互:单目深度估计误差大,推拉不可靠。
- 注视即触发:Midas Touch 导致误操作,必须加确认或停留。
- 取捏合瞬间的注视点:动作本身使视线偏移,应取历史帧。
- 停留无进度反馈:用户不知系统在等待,重复注视造成抖动。
- 要求长时间举手:Gorilla Arm 导致 5 分钟内放弃,应支持手放腿上。
- 只用颜色做反馈:色觉障碍用户无法区分,需加形状与音效。
- 阈值只用开发者手型调:上线后大量用户无法触发,必须多手型标定。
15. 小结
手眼交互的工程主线是:手部用几何规则做少量高价值手势(捏合为主)→ 用相对比例与滤波保证稳定性 → 眼动负责快速定位 → 用捏合或停留做确认 → 让手保持自然下垂 → 多手型标定阈值。核心洞见是「眼睛选哪里、手决定怎么做」,把两者放在各自擅长的维度上。
三个最容易见效的改动:捏合判定改用相对比例、注视选择取历史帧、允许手自然下垂。这三项通常能把首次成功率从 80% 提升到 95% 以上。
交互的舒适度约束在 VR 舒适度与晕动症工程对抗 里进一步展开;若要做空间中的内容持久化,读 空间锚点与跨会话持久化 。
延伸阅读
- XR 显示光学与头部眼动追踪 — 眼动与手部追踪的硬件
- Unity XR Interaction Toolkit 交互体系 — XRI 中的手部接入
- VR 舒适度与晕动症工程对抗 — 疲劳与不适的工程对策
- Vision Transformer 与视觉骨干 — 关节点回归的模型基础
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。