通过Leap Motion(以下简称LM)进行手势识别,在Unity3D虚拟场景里实现场景的左旋、右旋、瞬移,以完成场景的漫游。
效果展示
竖起左手小拇指,场景左移
竖起右手小拇指,场景右移
左手点赞,则从手的方向发出射线与场景求焦,并在落点处产生黄色小球进行标识,当脱离左手点赞手势(即左手改变成任意手势),瞬移至射线的落点位置。此处进行一个判断,即只能在name为“Ground”的地面上进行瞬移。
讲解
场景层级结构
MainCamera和LMProvider
把Leap XR Service Provider挂载在Main Camera上(把LM装在VR头盔或者眼镜上使用,如果要在桌面使用LM,则新建空节点挂载LeapServiceProvider即可)。Advanced下Device Offset Mode设置为Transform,则LPProvider会随着相机的移动而移动,相机位置变化后,LPProvider的位置会跟着变化,如果选择默认,则当相机位置变化后,LPProvider还停留在相机原先的位置,识别并渲染出的手也停留在原地而不会出现在相机前。
DeviceOrigin可以在MainCamera下挂载一个Transform的子物体,实现一个Transform上的偏移(也可以不加,测试没啥事)。
HandModel
HandModelManager挂载HandModelManager脚本,HandModel选用LopolyRiggedHand。
场景旋转功能
在RotateHandCheck里,利用了LM为Unity提供的SDK中的ExtendedFingerDetector(通常使用ExtendedFingerDetector和FingerDirectionDetector和DetectorLogicGate共同控制识别手势,但这里手势比较简单,所以只用了ExtendedFingerDetector)。
ExtendedFingerDetector是检测五根手指是否伸直弯曲来判断一些简单的手势。
比如上图就是监听手势,当小拇指伸直而其它四指弯曲时候,触发OnActivate里的函数,当脱离此手势,触发OnDeactivate里的函数。
ExtendedFingerDetector继承自Detector(LM提供),在其中实现了虚函数Activate和Deactivate,可以用不同参数的同名参数重载,则在OnActivate和OnDeactivate可以实现参数调用。(但是自定义的数据类型比如Hand就不能作为参数传递,不知道为什么,可能在Detector源码里有定义)。
public virtual void Activate(){
if (!IsActive) {
_isActive = true;
OnActivate.Invoke();
}
}
public virtual void Activate(int x)
{
if (!IsActive)
{
_isActive = true;
OnActivate.Invoke();
}
}
// 行不通
public virtual void Activate(Hand hand)
{
if (!IsActive)
{
_isActive = true;
OnActivate.Invoke();
}
}
在RoateHandLogic里加两个ExtendedFingerDetector,分别监测左右手,监听到特定手势后调用自己编写的RotateLogic里的方法。
传入RotateActivate里的参数是1(左手)是2(右手),将RotateSpeed设置为负、正,控制相机左转右转。在RotateActivate里将StartRotate加入进程(一秒五十帧)。
public void RotateActivate(int index)
{
if (index == 1 && rotateSpeed>0)
{
rotateSpeed = -rotateSpeed;
}
if (index == 2 && rotateSpeed < 0)
{
rotateSpeed = -rotateSpeed;
}
Debug.Log("右手开始旋转:"+index);
InvokeRepeating("StartRotate", 0, 0.02f);
}
private void StartRotate()
{
camera.transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.Self);
}
public void RotateDeActivate()
{
Debug.Log("旋转停止");
CancelInvoke("StartRotate");
}
值得注意的是,在ExtendedFingerDetector里直接完成对手势的响应操作也是可以的(需要在Inspector里设置Period(检测周期)设置的较小,默认值是0.1,那么在这里直接完成对手势的响应,效果就会卡。
if (HandModel.IsTracked && fingerState)
{
Activate();
// 此函数里直接完成手势的响应 也是可以的
}
else if (!HandModel.IsTracked || !fingerState)
{
Deactivate();
// Debug.Log("手势结束");
}
此外,在代码里无需对手的左右手进行判断,因为已经在Inspector里指定了是哪只手。使用如下语句可以直接获取对应的手。在ExtendedFingerDetector里获取手了之后,在别的脚本里可以直接使用HandModelBase来获取手的位姿信息。
public HandModelBase HandModel = null;
if (HandModel != null && HandModel.IsTracked)
{
Hand hand = HandModel.GetLeapHand();
if (hand != null)
{
// 相应操作
}
}
场景中位移
经过测试之后,选用大拇指点赞手势来做位移的指令手势,其他稍微复杂的手势,比如六脉神剑或者手枪手势,这些复杂收拾都会在监听的时候出现突然因为自遮挡而错误判断脱离此手势的状况,那么场景位移就会不受控制地瞬移,所以如果采用对手势进行监听的机制,那么位移应该尽量选择简单的手势。
监听到左手出现点赞手势时,会调用此脚本里的方法TransActivate从手的方向发出一道射线,并在落点出出现小球标记位置,然后脱离点赞手势则会调用此脚本里的方法TransDeactivate实现相机的位移。
射线是通过LineRenderer组件创建的,由于不会使用,所以设置的颜色也没法显示,呈现出来是紫色的不带mesh的样子。
// 在此函数里将发出射线加入线程,重复调用
public void TrasActivate()
{
// 不需要判断是左手还是右手,因为Public HandModelBase HandModel被设置为左手!!!!
// 也就是说,通过HandModel获取不到右手,只能获取到左手
//for(int i = 0; i < 2; i++)
//{
// Frame curFrame = LeapProvider.CurrentFrame.TransformedCopy(LeapTransform.Identity);
//}
if (HandModel != null && HandModel.IsTracked)
{
hand = HandModel.GetLeapHand();
if (hand != null)
{
InvokeRepeating("CastRay", 0, 0.02f);
}
}
}
private void CastRay()
{
Destroy(rayPoint);
line.enabled = true;
// Ray ray = new Ray(hand.Fingers[1].Bone(Bone.BoneType.TYPE_METACARPAL).NextJoint.ToVector3(), hand.Fingers[1].Direction.ToVector3());
Ray ray = new Ray(hand.StabilizedPalmPosition.ToVector3(), hand.Direction.ToVector3());
RaycastHit hit;
Physics.Raycast(ray, out hit);
line.SetPositions(new Vector3[] { hand.Fingers[1].Bone(Bone.BoneType.TYPE_METACARPAL).NextJoint.ToVector3(), hit.point });
rayPoint = Object.Instantiate(_rayPoint);
rayPoint.transform.position = hit.point;
// 先判断hit.transform是否存在,才能判断信息
if (hit.transform!=null && hit.transform.name == "Ground")
{
Debug.Log("我指向地面了!");
transLoc = hit.point;
}
// OnRayCast(hand.StabilizedPalmPosition.ToVector3());
}
public void TransDeActivate()
{
if (rayPoint != null)
{
Destroy(rayPoint);
}
CancelInvoke("CastRay");
if (transLoc != Vector3.zero)
{
transLoc.y = 1.8f;
Vector3 transDis = transLoc - camera.transform.position;
camera.transform.position = transLoc;
// hand.SetTransform(transLoc, Vector3.zero);
Debug.Log("相机进行了一个位移,相机的位置是:" + camera.transform.position);
Debug.Log("手/leap provider的位置是:" + hand.PalmPosition);
// Debug.Log("Leap的位置是:" + leap.transform.position);
}
else
{
Debug.Log("先前没有指向地面");
}
}
BUG
小球会沿着射线移动向用户
场景中会出现断掉的线,而且线没办法被渲染,呈现紫色的不带mesh的样子(LineRenderer组件的使用不是很熟悉
注 在使用中,不知是不是因为软件版本太老还是硬件接口问题,会出现4.1.0版本软件连接失败而最新软件连接成功的情况,此时在本程序中无法连接Leap Motion,出现Leap Service not connected的错误(可能是因为版本太老的SDK中只支持老版本的连接)