2015-06-30 21:37:37 shadow_guo 阅读数 2365
  • Unity3D入门到精通-(3)Unity资源管理精讲

    本次系列课程的目标是让Unity3D初学者掌握Unity3d的资源管理技术进行了全面介绍,特别对AssetBundle资源如何进行更新,以及加载(依赖资源加载)进行了系统的介绍。 适合对象:Unity初学开发者,Unity中级开发者,网络程序开发者,所有对游戏开发有兴趣的人员。 学习条件:有一定的Unity3D基础,了解C#的基本开发知识。

    4645 人正在学习 去看看 张刚

Leap Motion Using Unity3D——环岛跑车篇

 一. Leap Motion简介

        Leap Motion作为一款体感传感器,通过立体视觉捕捉红外图像信息并产生手掌在三维空间中的相关信息。下面会结合示例来说明Leap Motion采集的模型数据在跑车游戏中的应用。翻译来自Pierre Semaan的博客“Leap Enabling the Unity3D Car Tutorial”,然后对有关联的代码和操作注意事项加以说明。

二. 软件要求

1. Unity3D

        官方给的选择为Unity 5 Pro或者Unity 5 Free,不过笔者用的是Unity 4.6;

2. 跑车教程

        在Unity3D的资源商店中搜索”Car Tutorial“,然后在Unity3D中免费下载并导入资源即可;

3. Leap SDK

        在Leap Motion的官网上下载最新版本的SDK。如果下载不了或者回到前一个界面,可以尝试换浏览器下载。

4. Leap Motion输入脚本

        选用Pierre Semaan提供的pxLeapInput.cs。

三. 操作步骤

1. 创建新的工程

        新建Unity3D工程时,导入Car Tutorial.unitypackage包。

2. 确保CompleteScene场景正常工作

        CompleteScene场景存放在Scenes文件夹下,双击打开场景后可以俯视看到环形跑道。不过由于Unity 3版本的跑车游戏转到Unity 5后提示shader有问题,于是岛外的背景颜色看起来有些违和。运行游戏,按上下左右键可以控制跑车则说明游戏正常。

3. 复制CompleteScene

CompleteScene复制后重命名为LeapCompleteScene,这样可以备份源文件并开始大胆地实验。

4. 在Assets目录下创建Plugins文件夹

5. 复制Leap文件至Plugins文件夹和游戏文件的根目录下

复制Leap.dll和LeapCSharp.dll至游戏文件夹的根目录下,注意两个文件是有不同操作系统的版本。然后复制LeapCSharp.NET3.5.dll至Plugins文件夹下。

6. 复制LeapUnityExtensions.cs至Plugins文件夹

        下载LeapUnityExtensions.cs然后复制进Plugins文件夹。

7. 复制pxsLeapInput.cs至Plugins文件夹

        下载pxsLeapinput.cs文件并复制到Plugins文件夹。脚本必须在Plugins文件夹下因为后面我们需要在javascript脚本中参考它,并且它需要先编译这样javascript引擎才能看见它。

a)(可选)检查pxsLeapInput.cs脚本

    它是一个单例并且包含易于从最近的Update中获取图像帧和手的属性。关键函数为GetHandAxisPrivate:
private static float GetHandAxisPrivate(string axisName, bool scaled)
{
	// Call Update so you can get the latest frame and hand
	Update();
	float ret = 0.0F;
	if (m_Hand != null)
	{
		Vector3 PalmPosition = new Vector3(0,0,0);
		Vector3 PalmNormal = new Vector3(0,0,0);
		Vector3 PalmDirection = new Vector3(0,0,0);
		if (scaled == true)
		{
			PalmPosition = m_Hand.PalmPosition.ToUnityTranslated();
			PalmNormal = m_Hand.PalmNormal.ToUnity();				
			PalmDirection = m_Hand.Direction.ToUnity();
		}
		else
		{
			PalmPosition = m_Hand.PalmPosition.ToUnity();
			PalmNormal = m_Hand.PalmPosition.ToUnity();
			PalmDirection = m_Hand.Direction.ToUnity();
		}
		switch (axisName)
		{
		case "Horizontal":
			ret = PalmPosition.x ;
			break;
		case "Vertical":
			ret = PalmPosition.y;
			break;
		case "Depth":
			ret = PalmPosition.z ;
			break;
		case "Rotation":
			ret = -2 * PalmNormal.x ;
			break;
		case "Tilt":
			ret = PalmNormal.z ;
			break;
		case "HorizontalDirection":
			ret = PalmDirection.x ;
			break;
		case "VericalDirection":
			ret = PalmDirection.y ;
			break;
		default:
			break;
		}
	}
	if (scaled == true)
	{
		if (ret > 1) {ret = 1;}
		if (ret < -1) {ret = -1;}
	}
	return ret;
}
        pxLeapInput被定义为一个静态类;定义了一个Leap名称空间中的空Controller对象m_Controller,空Frame对象m_Frame和空Hand对象m_Hand。
        构造函数中用new创建了一个Controller对象,如果创建失败,则返回Errors字符串提示。
        定义Frame函数返回m_Frame,定义Hand函数返回m_Hand。
        在Update函数中,首先判断m_Controller是否创建成功,如果创建成功,则将当前帧传给上一帧(这句没起作用),用Frame函数返回当前帧m_Frame;如果当前帧不为空,则说明检测到手,将当前帧的类中的Hands数组中索引为0的部分赋值给m_Hand(说明只是用到单手控制)。
        在GetHandAxisPrivate函数中,函数输入包括轴的名称axisName(字符串)和缩放标志位scaled(布尔型)。首先调用Update函数获取当前帧和当前帧中索引为0的手;如果布尔型变量scaled值为真,则掌心位置使用ToUnityTranslated函数获取,掌心方向量和掌心方向用ToUnity函数获取,否则掌心位置用ToUnity获取。

(1)如果轴名称为”水平“,则返回掌心位置的x轴值;
(2)如果轴名称为”垂直“,则返回掌心位置的y轴值;
(3)如果轴名称为”深度“,则返回掌心位置的z轴值;
(4)如果轴名称为”旋转“,则返回掌心法向量的x轴值的-2倍;
(5)如果轴名称为”倾斜“,则返回掌心法向量的z轴值;
(6)如果轴名称为”水平方向“,则返回掌心方向的x轴值;
(7)如果轴名称为”垂直方向“,则返回掌心方向的y轴值。

        如果scaled为真,将轴值截断至-1到1之间。

8. 在Car.js中调用pxsLeapInput

        GetHandAxis函数调用GetHandAxisPrivate函数时,scaled值置为真,所以返回值为1且掌心位置调用ToUnityTranslated函数。为了用Leap Motion输入覆盖键盘输入,需要在GetInput函数中修改。
if(LeapEnabled == true){
	throttle = pxsLeapInput.GetHandAxis("Depth") * forwardScale;
	steer = pxsLeapInput.GetHandAxis("Rotation");
	print("Throttle: " + throttle.ToString() + "Steer: " + steer.ToString());
}
else{
	throttle = Input.GetAxis("Vertical");
	steer = Input.GetAxis("Horizontal");
}
        和GetAxis类似,GetHandAxis的返回值变化范围为[-1,1]。同时为了避免直接覆盖键盘控制,添加LeapEnabled标志位选择Leap Motion输入是否使能。为了调整前进速度,添加forwardScale变量避免跑车加减速过快或过慢带来的不适。
public var LeapEnabled : boolean = false; 
public var forwardScale : float = 0.4;

9. 使能Leap Motion输入

       scaled变量创建时默认值为假,所以需要将Car物体右侧对应的脚本Car.js下的公有变量scaled打勾(置为真)。运行游戏即可用Leap Motion控制游戏。游戏操作的正确方式为:手心和手背以手臂为轴逆时针旋转为左转,反之顺时针旋转为右转,旋转时的静态角度代表跑车转动的幅值。手掌置于Leap Motion前方则第一视角与车头前进的方向相同,反之置于后方则第一视角与车头前进的方向相反(车尾前进,车体后退)。

10. Leap Motion输入介绍

        为了更好地用Leap Motion控制游戏,至少要知道我们用什么样的手势运动替换掉了水平轴和垂直轴的输入。所以,下面简单介绍用到的帧和手的数据结构以及手的数据结构对应的手的属性。

(1)数值单位

        距离:毫米;时间:毫秒;速度:毫米/秒;角度:弧度。

(2)帧

        帧的基本实体包括Hand,fingers和tools。controller.frame()获取当前帧;controller.frame(n)获取上n帧;frame.id()获取当前帧的整数型ID;Leap::Listener类中检测新帧到来的回调函数为onFrame(controller);frame.hand(id),frame.finger(id)和frame.tool(id)为获取当前ID的手,手指和工具数据。

(3)手

        获取手的方向时也用到x,y和z轴坐标,虽然经过ToUnity函数的转换(不清楚什么意思),但是我猜得到的还是roll,pitch和yaw值。不过用到手的方向时还是调用官方提供的roll(),pitch()和yaw()比较妥。手掌和手指的位置是相对于Leap Motion中心的全局坐标系的位置,每个地方有不同的RPY方向。有时虽然调用的函数相同,但继承函数所在的基类的对象不同,比如direction函数。P.S.下图为C++中的函数,仅供说明使用。

结语

        本篇主要描述如果用Leap Motion的输入来替换键盘的方向键,并没有深入Unity3D生成的游戏的代码,不过应该很多人会比笔者更了解Unity3D。鼠标和键盘对于二维平面操作要优于Leap Motion,因为Leap Motion在控制时很难令其中一维坐标保持不变,对于手来说很容易疲惫;而且二维操作在空中显然费力不讨好;但是对于三维平面操作,鼠标和键盘一定要结合起来用才行,Leap Motion只是提供了一种更加自然的操作方式。所以,在虚拟环境中绘制手模型,并以人眼为反馈作三维空间操作是可选的比较合理又自然的交互方式。

2013-09-15 17:23:51 dachang221 阅读数 19984
  • Unity3D入门到精通-(3)Unity资源管理精讲

    本次系列课程的目标是让Unity3D初学者掌握Unity3d的资源管理技术进行了全面介绍,特别对AssetBundle资源如何进行更新,以及加载(依赖资源加载)进行了系统的介绍。 适合对象:Unity初学开发者,Unity中级开发者,网络程序开发者,所有对游戏开发有兴趣的人员。 学习条件:有一定的Unity3D基础,了解C#的基本开发知识。

    4645 人正在学习 去看看 张刚

昨天Leap Motion到货,setup十分简单,玩了几个商店里免费的应用,操作还挺流畅。对开发者,关于C#和unity3D 官网从overview到api的具体介绍都很详细,于是简单上手试了试,主要是利用手势控制gameobject的变换(移动、旋转等)等。


需要预先说明,leap SDK for unity是作为plugin library获取leap数据的,而免费版的U3D不支持native plugins。对此开发者中心也有办法能够使免费版的U3D使用leap SDK,原文猛击:https://developer.leapmotion.com/articles/creating-leap-apps-with-unity-standard-license,本文中U3D为Pro版本。

1.首先新建unity工程

2.在项目根目录下新建Plugins文件夹,从下载好的SDK(https://developer.leapmotion.com/downloads)中找到 ~/LeapSDK/lib/UnityAssets/Plugins,将里面的内容放到Plugins文件夹中。之后找到 ~/Examples/UnitySandbox/Assets/Scripts/Leap/LeapUnityExtensions.cs,同样复制到Plugins文件夹。这时plugins内应有LeapCSharp.bundle,LeapCSharp.NET3.5.dll,以及LeapUnityExtensions.cs,至此SDK就算搞定,十分方便。

3.场景内创建一个plain和一个cube(用以移动)

4.在plugins中新建cs脚本,命名LDCLeapControl.cs,在IDE中添加代码如下:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Leap;

public static class LDCLeapControl
{
	public static float ROTATE_MOD = 3.14F;
	
	//member variables;
	static Leap.Controller _controller;
	static Leap.Frame _frame;
	static Leap.Hand _hand;
	
	static LDCLeapControl()
	{
		_controller = new Leap.Controller();
	}
	
	//getters
	public static Leap.Frame Frame
	{
		get
		{
			return _frame;
		}
	}
	public static Leap.Hand Hand
	{
		get
		{
			return _hand;
		}
	}
	//get latest frame called each second
	public static void Update () 
	{
		if(_controller != null)
		{
			Frame lastFrame = _frame == null ? Frame.Invalid : _frame;
			_frame = _controller.Frame();
			if(_frame != null)
			{
				if(_frame.Hands.Count > 0)
				{
					_hand = _frame.Hands[0];
				}
			}
		}
	}
	
	public static float getHandInput(string gesture)
	{
		float leapData = getLeapData(gesture);
		return leapData;
	}
	
	private static float getLeapData(string gesture)
	{
		Update();
		float leapData = 0.0F;
		
		if(_hand != null)
		{
			Vector3 PalmPosition = new Vector3(0,0,0);
			Vector3 PalmNormal = new Vector3(0,0,0);
			Vector3 PalmDirection = new Vector3(0,0,0);
			PalmPosition = _hand.PalmPosition.ToUnityTranslated();
			PalmNormal = _hand.PalmNormal.ToUnity();
			PalmDirection = _hand.PalmPosition.ToUnity();
			
			if(gesture == "Rotation")
			{
				leapData = -ROTATE_MOD * PalmNormal.x;
			}
		}
		return leapData;
	}
}

首先引用leap命名空间,其中controller作为主接口,用以取得leap的跟踪数据,调用frame()方法可实时获取最新帧。Frame表示每一帧中探测到的一系列手以及手指的数据,包含了Hands,Fingers,Pointables以及Tools等属性。而本文中使用的Hand,表示探测到用户的手及其各种信息,包括手掌方向向量Direction,手掌法向量PalmNormal,手掌距离leap motion远点的距离PalmPosition,手掌移动的速率PalmVelocity...等等。

回顾LDCLeapControl这个单例,Update()方法用以取得最新的帧数据,用以控制object的手被设定为出现在frame中handlist内的第一只出现的手(ID=0)。在getLeapData()方法中,获取hand的属性PalmNormal(这里仅以旋转为例),其在x方向的分量校准后作为我们对应旋转"Rotation"获取到的leap数据。

这时,LDCLeapControl作为一个native plugin已经可以为我们所用了,在Assets中新建脚本:moveBehaviour.cs,并添加代码如下:

using UnityEngine;
using System.Collections;

public class moveBehaviour : MonoBehaviour {
	
	public float smooth = 2.0f;
	public float tiltAngle = 60.0f;
	public bool leapIsEnabled = false;

	void Update () 
	{
		if(leapIsEnabled)
		{
			float rotate = LDCLeapControl.getHandInput("Rotation");
			float tiltAroundY = rotate * tiltAngle;
			Quaternion target = Quaternion.Euler(0, tiltAroundY, 0);
			transform.rotation = Quaternion.Slerp(transform.rotation, target, Time.deltaTime * smooth);
		}

	}
}
在这里利用取得的leapData作为object旋转的quaternion值(position移动等变换同理,略),将该行为付给cube(打开leapIsEnabled开关),运行,现在随着你手腕的左右旋转,方块也会在y方向上旋转了。





2018-11-20 10:24:05 Rick__ 阅读数 794
  • Unity3D入门到精通-(3)Unity资源管理精讲

    本次系列课程的目标是让Unity3D初学者掌握Unity3d的资源管理技术进行了全面介绍,特别对AssetBundle资源如何进行更新,以及加载(依赖资源加载)进行了系统的介绍。 适合对象:Unity初学开发者,Unity中级开发者,网络程序开发者,所有对游戏开发有兴趣的人员。 学习条件:有一定的Unity3D基础,了解C#的基本开发知识。

    4645 人正在学习 去看看 张刚

1.Leap Motion 官网下载SDK并导入Unity3D,官网地址:https://developer.leapmotion.com/  本次测试适用unity版本为2017.1以上

下载内容:

2.Leap Motion 获取手部关节点两种方式:

(1)脚本:AttachmentPointBehaviour 中:可以获取手部各关节的position和rotation,使用时加自己的判断逻辑即可。

(2)示例场景一中:可以获取手部各关节的position,可另外编写脚本,将关节的根节点拖拽使用即可,

如:

3.获取关节点的位置及旋转信息后,可以通过rotation来得到关节的旋转度,从而判断每只手是否弯曲,每根手指是否弯曲;

也可通过位置信息得到向量方向变化算出角度变化来判断。

这里我们使用向量计算角度:

我们需要在手指默认的伸直状态来一个作为基准的向量,以食指举例,食指伸直时指尖与指跟方向的向量为a,实时状态下指尖与指跟的变化中的向量为b,求a与b之间的变化角度,来判断弯曲程度,可以自行添加合适的阈值。

4.向量之间求角度

//向量a,b的夹角,得到的值为 弧度转换为角度值  
        float angle = Mathf.Acos (Vector3.Dot (初始.normalized, 变化.normalized)) * Mathf.Rad2Deg;  

5.由此可得到食指是否弯曲以及弯曲程度,以此来适用其余手指,进而可以自行定义手势。 

2015-02-10 20:24:51 book_longssl 阅读数 823
  • Unity3D入门到精通-(3)Unity资源管理精讲

    本次系列课程的目标是让Unity3D初学者掌握Unity3d的资源管理技术进行了全面介绍,特别对AssetBundle资源如何进行更新,以及加载(依赖资源加载)进行了系统的介绍。 适合对象:Unity初学开发者,Unity中级开发者,网络程序开发者,所有对游戏开发有兴趣的人员。 学习条件:有一定的Unity3D基础,了解C#的基本开发知识。

    4645 人正在学习 去看看 张刚

 

           最近仔细比较了目前版本中的两套动画系统:Legacy和Mecanim。Mecanim系统功能较之Legacy要强大很多,但是使用AnimatorController着实不方便(尽管使用AnimatorOverrideController可以避免重复编辑状态机),是因为游戏逻辑层面往往要用一个状态机或者类似的机制来控制角色的状态,而角色层面的状态逻辑和动画层面是无法一一对应的,两套复杂的状态机要配合起来。。。想想就觉得蛋疼啊!难怪很多朋友现在还在使用Legacy动画系统。Legacy动画系统其实功能也很全面了,包括Layer、过渡混合、上下身混合之类的功能完全能够胜任,而且控制起来就直接的多了。唯独Root Motion这个我很需要特性没有支持,本文就探讨一下如何在Legacy动画系统之上附加Root Motion功能,其实很简单 。


何谓Root Motion

在不使用Root Motion的情况下,类似走、跑这样的位移控制是这样的:

1.    请美术在导出动画时把位移去掉;
2.    在程序代码里控制角色移动的速度,在播放动画的同时,计算其位移。
这种做法其实挺不科学的,程序控制的角色,只能当做一个质点来处理,并且大多数时候都是匀速运动,而动画中的角色的移动往往很难跟这个匹配。所以需要比较良好的计算和比较好的美术技巧才能避免角色“滑步”的现象。在“跑”这种快速移动这,滑步还比较好处理,如果是慢速移动。。。。再厉害的美术也爱莫能助了。这种情况下,最好还是使用Root Motion:
1.    美术在导出动画的时候是附带位移的;
2.    程序把动画的每一帧的位移是从动画中读取出来,再应用到角色上的,这样就能达到动画和位移的完美匹配了。
在Legacy中添加Root Motion功能
了解了Root Motion的概念之后,在Unity3D引擎中我们很简单就可以实现此功能了。Unity3D有一个统一的对象层次结构设计,这点非常赞,我们可以很简单找到角色的根骨骼,然后把其中的Transform变换读取出来,请见以下示例代码:


//-- 计算当前帧的Root Motion
    Vector3 rootPos = m_rootBone.localPosition;
    m_rootMotion = rootPos - m_lastRootPos;
    m_lastRootPos = rootPos;
    rootPos.x = 0;
    rootPos.z = 0;
    m_rootMotion.y = 0;
    m_rootBone.localPosition = rootPos;
请注意,我们在后续的代码中要把m_rootMotion附加的角色对象上,所以m_rootBone的postion被reset了。
在读取了此帧的Root Motion,在可以把它应用到当前对象之上了:
//-- Apply Root Motion
  Vector3 nextPos = this.transform.position + m_rootMotion;
        this.transform.position = nextPos;
另外,一个细节需要处理一下,在动画循环的那一帧,需要特殊处理一下。好的,看一下完整的源代码吧:
using UnityEngine;
using System.Collections;
public class ApplyRootMotion : MonoBehaviour
{
  public Transform m_flagObject;   // 用来测试位置的一个对象
  //-- Root Motion 控制变量
  Transform m_rootBone;
    Vector3 m_lastRootPos;
    Vector3 m_rootMotion;
    int m_lastAnimTime;
  void Start ()   {
    //-- 从SkinnedMeshRenderer中读取Root Bone
    SkinnedMeshRenderer skinMesh = this.gameObject.GetComponentInChildren<SkinnedMeshRenderer>();
  m_rootBone = skinMesh.rootBone;
  //-- 变量初始化
  m_rootMotion = Vector3.zero;
  m_lastRootPos = m_rootBone.localPosition;
  m_lastAnimTime = 0;
  }  
  void Update ()   {
    //-- Apply Root Motion
    Vector3 nextPos = this.transform.position + m_rootMotion;
  this.transform.position = nextPos;

  //-- 测试代码:更新测试物体的位置
  Vector3 flagPos = m_flagObject.position;
  flagPos.x = nextPos.x;
  flagPos.z = nextPos.z;
  m_flagObject.position = flagPos;
  //-- 测试代码:更新摄像机
  Camera.main.transform.LookAt(this.transform);
  }
  void LateUpdate()    {
    AnimationState animState = this.animation["walking"];
    if ((int)animState.normalizedTime > m_lastAnimTime)    {
    //-- 动画循环处理
    m_lastRootPos = m_rootBone.localPosition;
    m_rootMotion = Vector3.zero;
  }  else  {
    //-- 计算当前帧的Root Motion
    Vector3 rootPos = m_rootBone.localPosition;
    m_rootMotion = rootPos - m_lastRootPos;
    m_lastRootPos = rootPos;
    rootPos.x = 0;
    rootPos.z = 0;
    m_rootMotion.y = 0;
    m_rootBone.localPosition = rootPos;
  }
  m_lastAnimTime = (int)animState.normalizedTime;
    }
}

最后是截图。。。好吧,静态图片看不出效果,可以下载完整Demo(请使用Unity 4.6版本打开),角色移动非常平滑.



QQ截图20150210202215.png

2015-07-14 23:48:06 u012289636 阅读数 10796
  • Unity3D入门到精通-(3)Unity资源管理精讲

    本次系列课程的目标是让Unity3D初学者掌握Unity3d的资源管理技术进行了全面介绍,特别对AssetBundle资源如何进行更新,以及加载(依赖资源加载)进行了系统的介绍。 适合对象:Unity初学开发者,Unity中级开发者,网络程序开发者,所有对游戏开发有兴趣的人员。 学习条件:有一定的Unity3D基础,了解C#的基本开发知识。

    4645 人正在学习 去看看 张刚

Leap Motion作为一款手势识别设备,相比于Kniect,优点在于精确度。

在我的毕业设计《场景漫游器》的开发中,Leap Motion的手势控制作为重要的一个环节。以此,谈谈开发中使用Leap Motion进行手势识别的实现方式以及需要注意的地方。


一、对Leap Motion的能力进行评估

在设定手势之前,我们必须知道Leap Motion能做到哪种程度,以免在设定方案之后发现很难实现。这个评估依靠实际对设备的使用体验,主要从三个方面:

1.Leap Motion提供的可视化的手势识别界面

2.SDK文档说明

3.Leap商店中的APP

基本可以的得出:

1.Leap Motion的识别对于水平方向或者以水平方向为基础手势能够较好的识别。

2.对于握拳或者垂直的行为识别会出现误差,这种误差和具体的手势行为有关。

3.不应该过分依赖高精确度,Leap Motion能检测到毫米级别是没错的,但是有时候会把你伸直的手指识别成弯曲的,所以要做好最坏的打算。


二、实际的需要

移动、旋转、点击按钮、缩放和旋转物体、关闭程序、暂停,基本的功能需求是这样。

有一些原则:

1.相同环境下的手势应该接近和方便的转换。旋转和移动的之间的转换应该设计的很自然。

2.手势避免冲突,手势过于相似不是什么好事。比如三个伸直的手指和四个伸直的手指不应该被设计成两个手势。当然这不是绝对的,如果你进行一个缓慢的动作并且动作是面向Leap Motion的摄像头,这时候应该相信它,至少要针对这个手势做一个单独的测试。


三、考虑基本的数据结构和算法的轮廓

Leap Motion的SDK在第一部分的时候已经浏览过,最起码能知道Leap Motion可以包含的信息,从SDK看来这是非常丰富的,既然设计自己的手势,那么最好不要依赖于SKD开发包的炫酷的手势。很可能,这些手势只是官方用来演示或者炫耀的。自己设计手势的基本数据结构也有另外的好处,比如更换了体感设备,但是功能是相似的,这时候只需要更改获取数据的方式就可以了(从一个SDK更换到另一个SDK),而不要修改算法。

算法的轮廓与基本数据有很大的关系。所以数据结构一定要尽量的精简并且允许修改(可能某个算法占据了决定性因素,但是开始没考虑到)。

public class HandAndFingersPoint : MonoBehaviour 
{
	const int BUFFER_MAX=5;
	Controller m_LeapCtrl;

    <span style="white-space:pre">	</span>public E_HandInAboveView m_AboveView = E_HandInAboveView.None;
    
	//手指-数据 ,[0]表示左手,[1]表示右手
	private Dictionary<Finger.FingerType,FingerData>[] m_FingerDatas = new Dictionary<Finger.FingerType, FingerData>[2];
	//buffer,[0]表示左手,[1]表示右手,[,n](n属于0,3,表示第n次缓存)
	private Dictionary<Finger.FingerType,FingerData>[,] m_FingerDatasBuffer=new Dictionary<Finger.FingerType, FingerData>[2,BUFFER_MAX];
	private int m_CurBufIndex=0;
	//palm 0:左手 和1:右手
	private PointData[] m_PalmDatas = new PointData[2];
	
	private readonly PointData m_DefaultPointData = new PointData(Vector.Zero, Vector.Zero);
        private readonly FingerData m_DefaultFingerData = new FingerData(Vector.Zero,Vector.Zero,Vector.Zero);
HandAndFingersPoint类中剩下的部分是对数据的填充、清除、刷新等方法。E_HandInAboveView记录哪只手先进入Leap Motion的视野,用于设定优先级。
另外两个基本的数据结构PointData和FingerData:

//一个手指的数据包含一个指尖点数据和手指根骨的位置数据
public struct FingerData
{
    public PointData m_Point;//指尖的位置和指向
    public Vector m_Position;//手指根骨的位置,对于拇指来说是Proximal phalanges近端指骨的位置

    public FingerData(PointData pointData, Vector pos)
    {
        m_Point = pointData;
        m_Position = pos;
    }

    public FingerData(Vector pointPos, Vector pointDir, Vector pos)
    {
        m_Point.m_Position = pointPos;
        m_Point.m_Direction = pointDir;
        m_Position = pos;
    }

    public void Set(FingerData fd)
    {
	m_Point = fd.m_Point;
	m_Position = fd.m_Position;
    }
}
//一个点的数据,包括方向和位置
public struct PointData
{
    public Vector m_Position;//位置
    public Vector m_Direction;//方向

	public PointData(Vector pos,Vector dir)
	{
		m_Position = pos;
		m_Direction = dir;
	}

	public void Set(PointData pd)
	{
		m_Position = pd.m_Position;
		m_Direction = pd.m_Direction;
	}

	public void Set(Vector pos,Vector dir)
	{
		m_Position = pos;
		m_Direction = dir;
	}
}

//先被看到的手
public enum E_HandInAboveView
{
    None,
    Left,
    Right
}

基本数据定义好之后,最好确认数据的填充是没问题的,实际通过Frame frame = Leap.Controller.Frame();来获取最新的数据。这时候并不急着写完和基本数据相关的方法,现在最终要的是手势算法的合理性。要判断是否合理,最好先写一个算法。

最简单的是伸掌手势,在控制中水平的伸掌用于漫游,垂直的伸掌用于暂停。我发现手掌依赖于手指,而手指包括两个状态——伸直和弯曲。另外,其他的手势,也都是手指的伸直或者弯曲,外加方向的判定累积出各种效果。理所当然的,应该单独写出手指的弯曲和伸直判定算法:

/// <summary>
/// 该方法提供对于单个手指匹配的算法,如伸直,弯曲
/// 以后可能的改变:对于不同的场景可能要求有所不同,这里的阈值也许会随之改变
/// </summary>
public class FingerMatch
{
	//弯曲状态的角度阈值
	static readonly float FingerBendState_Radian = Mathf.PI*4f / 18 ;//40度
	//伸直状态的角度阈值
	static readonly float FingerStrightState_Radian = Mathf.PI/12;//15度

	/// <summary>
	/// 手指伸直的状态,当根骨-指尖的方向和指向的偏差小于阀值时,判定手指为伸直状态。
	/// 注意无效的方向为零向量,先判定是零向量
	/// </summary>
	/// <param name="adjustBorder">对阈值做的微调</param>
	/// <returns></returns>
	public static bool StrightState(FingerData fingerData, float adjustBorder=0f)
	{
		bool isStright =false;
		Vector disalDir = fingerData.m_Point.m_Direction;
		//如果指尖方向为0向量,表示无效的数据
		if (!disalDir.Equals(Vector.Zero)) 
		{
			Vector fingerDir = fingerData.m_Point.m_Position - fingerData.m_Position;//指尖位置减去指根位置,由指根指向指尖的向量	        
			float radian = fingerDir.AngleTo(disalDir);
			
			if (radian < FingerStrightState_Radian + adjustBorder)
			{
				isStright = true;
			}
		}
		return isStright;
	}

	/// <summary>
	/// 判断一根手指是否处于弯曲状态
	/// </summary>
	/// <param name="fingerData">需要判定的手指数据</param>
	/// <param name="bandBorder">弯曲的阈值</param>
	/// <returns></returns>
	public static bool BendState(FingerData fingerData, float adjustBorder=0f)//,out float eulerAugle)
	{
		bool isBend = false;

		//eulerAugle = -1f;
		Vector disalDir = fingerData.m_Point.m_Direction;
		if( !disalDir.Equals(Vector.Zero) )
		{
			Vector fingerDir = fingerData.m_Point.m_Position - fingerData.m_Position;//指尖位置减去指根位置,指跟到指尖的向量

			float radian = fingerDir.AngleTo(disalDir);
			//eulerAugle = radian*180/Mathf.PI;	
			//夹角超过定义的阈值时,认定为弯曲状态
			if (radian > FingerBendState_Radian + adjustBorder)
			{
				isBend = true;
			}
		}

		return isBend;
	}

}

上面包含了一个重要的概念——阈值。它是描述到底何种程度算是伸直,何种程度算是弯曲。阈值的确定是需要实际测试来决定的。写到这里也是时候进行一次简单的测试了,毕竟算法的轮廓已经确定。我甚至没写出手掌伸直的判定算法,就确定是可行的。

基本数据结构相关的操作——HandAndFingersPoint类:源代码GitHub链接

该类使用基本数据,在Unity Editor中运行会展示了一个手掌的轮廓,蓝色表示手指的方向,红色表示手指骨根到掌心和指尖的连线,黄色表示掌心到指尖的连线:



四、手势实现中简要的概括

其他代码都可以在我的GitHub:Leap Motion In Unity3D仓库中获取,在手势的实现中,也包含了一些小的技巧,比如对于动作的匹配要防止手指的颤抖引起的误差,采用离散的数据取样——每隔一定时间做一次取样。

使用和观察这些脚本的方式:可以把这些脚本放在一个GameObject中,通过Leap Motion会看到脚本的属性在匹配成功时会发生变化。另外,脚本中包含了事件的注册功能,换句话说,外部可以向任意的手势注册一个事件,以便手势完成匹配或者到达某种匹配状态时做一些额外的处理。这些脚本现在并不能直接完成我们的需求,如暂停。我们需要在这些手势状态或者动作上做进一步的限定,如根据掌心的方向设定垂直向前的手掌为暂停,水平的手掌为平移之类的。


没有更多推荐了,返回首页