这篇博客一方面算是学习笔记,另一方面主要讲述自己是如何通过看官方文档,以及通过GPT来帮助写出脚本实现需求的。
此外打个广告,推荐我以往的博客,我接下来都会出一些系列,主要是给大家一个低门槛高效率开发unity功能模块的方法。
1.问题描述
首先,按惯例,先描述我现在要解决的问题:通过手部做一个动作触发某方法,做另一个动作触发另一个方法。所以我现在需要实现手势识别,或者说手部跟踪。手势设计方面,我的思路就是获取手部关节点位置,然后判断两个关节是否靠近,如果靠近则判定手势触发。这样我认为是比较容易实现的,基于此,我设计了两个手势:
一个是判断左右手食指指尖接触,大拇指指尖接触。另一个是判断左右手小指指尖接触。
这两个手势一方面在一般操作过程中不会误触发,另一方面手势不复杂。
2.官方文档
手部跟踪 - MRTK3 | Microsoft Learn
上面是这次我用的官方文档,其实感觉这个网站很多技术我都用到在了我的项目里,很实用,不过这一次写博客的主要目的是这一功能的官方介绍很“复杂”,说实话,看得我绕绕的。
因为没找到官方案例,所以这一功能属实难懂,而且给了一堆莫名其妙的介绍,但是他有代码行的应用,这算是一个突破口。
就是说如果英语能力好的人,耐心看是不错啦,但是用GPT帮助阅读官方文档我觉得是挺方便的,此外你将官方文档喂给GPT之后,方便它接下来辅助你写脚本。
3.GPT辅助阅读文档以及帮写脚本
下面是我给定的prompt角色设定:
你是Unity3D和Hololens2专家。用中文回答问题,并且注意我没有手,
请回答完整的答案,以便我直接粘贴使用。
下面给出我的向GPT的提问:
// Get a reference to the aggregator.
var aggregator = XRSubsystemHelpers.GetFirstRunningSubsystem<HandsAggregatorSubsystem>();
// Wait until an aggregator is available.
IEnumerator EnableWhenSubsystemAvailable()
{
yield return new WaitUntil(() => XRSubsystemHelpers.GetFirstRunningSubsystem<HandsAggregatorSubsystem>() != null);
GoAhead();
}
// Get a single joint (Index tip, on left hand, for example)
bool jointIsValid = aggregator.TryGetJoint(TrackedHandJoint.IndexTip, XRNode.LeftHand, out HandJointPose jointPose);
// Get an entire hand's worth of joints from the left hand.
bool allJointsAreValid = aggregator.TryGetEntireHand(XRNode.LeftHand, out IReadOnlyList<HandJointPose> joints)
// Check whether the user's left hand is facing away (commonly used to check "aim" intent)
// This is adjustable with the HandFacingAwayTolerance option in the Aggregator configuration.
// "handIsValid" represents whether there was valid hand data in the first place!
bool handIsValid = aggregator.TryGetPalmFacingAway(XRNode.LeftHand, out bool isLeftPalmFacingAway)
// Query pinch characteristics from the left hand.
// pinchAmount is [0,1], normalized to the open/closed thresholds specified in the Aggregator configuration.
// "isReadyToPinch" is adjusted with the HandRaiseCameraFOV and HandFacingAwayTolerance settings in the configuration.
bool handIsValid = aggregator.TryGetPinchProgress(XRNode.LeftHand, out bool isReadyToPinch, out bool isPinching, out float pinchAmount)
这是关于手部跟踪的官方文档内容,请帮我解释相关内容的功能。
可以通过上述提问快速了解代码结构。接着提问:
我现在需要获取右手指尖的关节点IndexTip的位置位置信息,并实时打印出来。请帮我写一个脚本,实现以上功能。
这么问主要是来测试GPT是否可以完成这部分简单的功能。如果你直接让他输出一个代码,你自己可能都不知道它的运行机制,而且如果运行失败了你也不知道哪里出错了【血泪教训】。所以我喜欢一步步来给出自己的需求。先一步步提问,保证每次GPT给出的答案的正确性,这样一来可以及时发现是哪一个功能添加时出了问题,针对性的让GPT来debug,是的没错,这个逻辑就是让GPT来debug。
注意:关于上面代码可能会出现诸如没有该域名的using,直接自动化using就行。
然后在接着问GPT之前,先上一张我在其他博客看到的MRTK2的手部关节图。
这个关节图其实MRTK2和MRTK3有细微差别,但是其实问题不大,如下面这行代码
aggregator.TryGetJoint(TrackedHandJoint.IndexTip, XRNode.RightHand, out HandJointPose rightIndexPose)
需要自己注意的地方就是IndexTip这个索引的使用,在不确定GPT能否正确说出索引的情况下,我选择将这个索引直接告诉它,比如提问GPT:我需要获取左右手ThumbTip位置,IndexTip位置,以及PinkyTip位置。但是你会发现代码报错,说PinkyTip没找到,其实是小指这个索引变了的缘故,直接在输入TrackedHandJoint.后,后面会自动弹出所有关节点索引,我看到LittleTip,猜测应该就是小指尖,结果测试果然没错。
最后接着提问,内容就大概关于:你希望提取_______哪几个关节点位置,然后判定____和____的距离达到某一阈值后,触发相关功能函数。
以下是我的代码,供大家直接拿去使用。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
using Microsoft.MixedReality.Toolkit.Subsystems;
using Microsoft.MixedReality.Toolkit;
using UnityEngine.Events;
public class HandTipTracker : MonoBehaviour
{
private HandsAggregatorSubsystem aggregator;
private float lastGestureTime = 0.0f;
public UnityEvent start;
public UnityEvent close;
public UnityEvent onGesture_Close;
// 状态符号:0代表上次触发关闭功能,1代表上次触发开启功能
private int gestureState = 0;
void Start()
{
StartCoroutine(EnableWhenSubsystemAvailable());
}
IEnumerator EnableWhenSubsystemAvailable()
{
yield return new WaitUntil(() => XRSubsystemHelpers.GetFirstRunningSubsystem<HandsAggregatorSubsystem>() != null);
aggregator = XRSubsystemHelpers.GetFirstRunningSubsystem<HandsAggregatorSubsystem>();
}
void Update()
{
if (aggregator != null)
{
if (aggregator.TryGetJoint(TrackedHandJoint.IndexTip, XRNode.RightHand, out HandJointPose rightIndexPose))
{
//Debug.Log($"右手指尖位置: {rightIndexPose.Position}");
}
if (aggregator.TryGetJoint(TrackedHandJoint.IndexTip, XRNode.LeftHand, out HandJointPose leftIndexPose))
{
//Debug.Log($"左手食指尖位置: {leftIndexPose.Position}");
}
if (aggregator.TryGetJoint(TrackedHandJoint.ThumbTip, XRNode.LeftHand, out HandJointPose leftThumbPose))
{
// Debug.Log($"左手大拇指尖位置: {leftThumbPose.Position}");
}
if (aggregator.TryGetJoint(TrackedHandJoint.ThumbTip, XRNode.RightHand, out HandJointPose rightThumbPose))
{
// Debug.Log($"右手大拇指尖位置: {rightThumbPose.Position}");
}
if (aggregator.TryGetJoint(TrackedHandJoint.LittleTip, XRNode.LeftHand, out HandJointPose leftLittlePose)) { }
if (aggregator.TryGetJoint(TrackedHandJoint.LittleTip, XRNode.RightHand, out HandJointPose rightLittlePose)) { }
CheckGesture_Open(leftIndexPose, rightIndexPose, leftThumbPose, rightThumbPose);
CheckGesture_Close(rightLittlePose, leftLittlePose);
}
}
private void CheckGesture_Open(HandJointPose leftIndexPose, HandJointPose rightIndexPose, HandJointPose leftThumbPose, HandJointPose rightThumbPose)
{
// 判定指尖接触的阈值(需要根据实际情况调整)
float touchThreshold = 0.05f;
// 判断食指尖是否接触
bool isIndexTipsTouching = Vector3.Distance(leftIndexPose.Position, rightIndexPose.Position) < touchThreshold;
// 判断大拇指尖是否接触
bool isThumbTipsTouching = Vector3.Distance(leftThumbPose.Position, rightThumbPose.Position) < touchThreshold;
// 如果食指尖和大拇指尖都接触
if (isIndexTipsTouching && isThumbTipsTouching)
{
// 当前时间
float currentTime = Time.time;
// 如果当前时间与上次手势检测时间的差大于2秒
if (currentTime - lastGestureTime > 2.0f)
{
// 更新上次手势检测时间
lastGestureTime = currentTime;
// 触发相应的功能
ToggleGestureState();
}
}
}
private void CheckGesture_Close(HandJointPose leftLittlePose, HandJointPose rightLittlePose)
{
// 判定指尖接触的阈值
float touchThreshold = 0.05f;
bool isPinkyTipsTouching = Vector3.Distance(leftLittlePose.Position, rightLittlePose.Position) < touchThreshold;
// 如果小拇指尖靠近
if (isPinkyTipsTouching)
{
// 当前时间
float currentTime = Time.time;
// 如果当前时间与上次手势检测时间的差大于2秒
if (currentTime - lastGestureTime > 2.0f)
{
// 更新上次手势检测时间
lastGestureTime = currentTime;
// 触发相应的功能
OnGesture_Close();
}
}
}
private void ToggleGestureState()
{
if (gestureState == 0)
{
gestureState = 1;
Debug.Log("开启功能触发");
start?.Invoke();
}
else
{
gestureState = 0;
Debug.Log("关闭功能触发");
close?.Invoke();
}
}
private void OnGesture_Close()
{
Debug.Log("检测到手势:左右手小拇指尖接触");
// 触发UnityEvent
onGesture_Close?.Invoke();
}
}
这个代码需要注意的点是,我才用的是UnityEvent事件,从Unity界面中添加事件,这样的好处就是更加灵活,而且后期阅读代码时更直观的知道这个脚本是干嘛的。毕竟Unity就是实时判断各种状态机的状态来判断是否调用其他函数的过程。只要知道状态的定义以及函数对象是谁,基本这个代码就知道咋回事了。
而至于这个gestureState符号主要是实现一个手势控制功能的开关。
4.问题总结,小tip
这个脚本的测试环节:
- 平时习惯按空格键操控一只手,这两只手的情况就是按下ctrl键和shift键,单独移动左手实现的。
- 此外还要注意,在电脑上运行代码时,一定要从一开始就将虚拟手显示出来,因为系统在start时,没看到手的情况下会默认它满足所有关节点的接触的。只要显示一只手后,这个bug就会消除。用头显设备时我也建议在刚开始时保证手在视线内。