LEAP MOTION Core Asstet 2.3 Full Analyse

本系列文章将会从一个初学者的脚步去讲解unity和leapmotion协同开发的一系列问题:



特:本人水平有限所以有错误请指正
本人也会引用一些代码 如果有所侵犯请联系删除

自2015.11.22开始周更

第一部分基-基础

首先,关于leapmotion 我想会看此类文章的应该都知道这是什么,无关将来这个产品的发展会如何,它作为虚拟现实或现实增强开发的练手是非常合适的。因为他有着非常舒适的开发环境,简单而成熟的SDK。最最重要的是结合unity/unreal,更是一种让你能在短时间之内通过引擎完成高质量手势识别应用的方法。

关于原理,和底层算法,因为代码封装完善,你根本不可能窥探到什么(当然肯定有人可以)。所以我的评价仅仅从实际效果出发先对leapmotion做出一个评价。

下图为leapmotion传感器的识别区域:

识别

我们可以看到在y轴也就是高度上 leapmotion的识别距离真的是小的可怜(根据我实际测试不会超过50cm)也就是说如果不和其他硬件搭配
leapmotion的使用范围很窄

单单的桌面使用的话,基本没有什么实用价值原因有二:

1.桌面使用场景下,手势识别很累低效

2.leap仅仅依靠景深画面和算法得出的手势识别精度高,但是精确度差。也就是说可以识别你手势动作很微小的变化,但是对这些变化的呈现又表现出很大的偏差。

在和oculus dk2结合之后,leapmotion的实用性总算提高了一些,但是效果仍然不理想。

因此,后续leap在不推出新硬件的条件下,我十分不看好这个传感器的发展,未来的leap,要么被OVR等厂家收购,要么倒闭。

ok 有点跑题,绕回来。虽然他这不好那不好,但这是我们在市场上能获取到的最优秀的性价比最高的手势识别设备。以他和oculus来学习虚拟现实开发,是最合适不过的了

那废话说够了,开始吧

first 基础


准备阶段:

https://developer.leapmotion.com/getting-started/unity

当然是download everything_
准备工具包括:

unity(最好5.0以上版本)

http://unity3d.com/cn/get-unity/download

leapmotion应用&SDK

https://developer.leapmotion.com/

leapmotion unity core asset

https://developer.leapmotion.com/downloads/unity
(有朋友反应找不到leap core asset2.3下载地址在这里

以上下载完毕之后接入leap使用visualizer检视和利用控制面板校准的过程我就不再赘述了,都有详细的引导。

那么既然是unity开发,c#自然是非常合适的了。所以对于刚刚接触unity的同学。了解unity的构成,以及c#脚本语言是非常重要的。

但是相比起来c#脚本教学可就多了去了,因此我只是做归纳总结。不作深入探究

下面我们简单的从leap的视角来看看unity/c#的一些特性:

头文件篇:

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

命名空间类似其他语言中的库,所有脚本代码都要写在命名空间的类中。命名空间leap就是整个leapmotion脚本运作的核心。但是这和传统的c#有一些不同,例如在vs环境中的c#新建后
会产生以下引用:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

从两段引用中我们也可以看出,c#语言在库上和某些早期的语言思路不太一致。至于详细的应用我们会在以后的更新中完整的介绍

正式代码篇:

unity:

public class NewBehaviourScript : MonoBehaviour {


//初始化函数,在游戏开始时系统自动调用。一般用来创建变量之类的东西。
void Awake () {

}
// 在所有Update函数前系统自动条用。一般用来给变量赋值。
void Start () {

}

// 每帧调用一次
void Update () {

}

//固定时间间隔调用一次
void FixedUpdate()
{

}

//每一个Update执行之后执行一次

void LateUpdate()
{

}

}

vs:

namespace ConsoleApplication1
{
class Program
{
    static void Main(string[] args)//相当于c++中等main函数
    {
       //____code___//
    }
}
}

我们看到 vs中需要自己定义命名空间 再以类的形式组织代码
而unity cs中只能继承于 MonoBehaviour这个命名空间,至于mono~是一种框架,鉴于初学贴的定位我们就不深入了。在此原则之下,只要没有碰到的C#script和传统的c#的区别我就一概不讲了。以免喧宾夺主。

至于unity的界面以及使用的教程有人写的水平比我高很多于是我就转载啦:

http://www.cnblogs.com/fortomorrow/archive/2012/10/28/unity01.html

总的来说,如果你有其他面对对象语言的基础。那么学习c#的成本将会非常低。如果你以前熟悉javascript/java,那么C#特性的异同在大多数情况下甚至可以忽略。

unity开发如果对实现效果不是极为苛刻那么开发的效率是非常高的。

如果你是个初学者,我会贴一段代码供大家熟悉c#特性的代码。供大家调试,修改。当然这些特性很多不会直接出现在脚本的编写中,但熟悉他们对于初学者来说对之后的工作有着非常大的好处

1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace _11
{
class Program
{
static void Main(string[] args)
{
Person p1 = new Person();
p1.Hello();
p1.Age = 21;
p1.Name ="Kungge";
//p1.Nickname = "Dogegg";// 注意这个外部不能使用
Console.WriteLine(p1.Name );
//p1.Hello2();//这个外部也不可使用
p1.GiveNn("Gumzhep");
Console.ReadKey();
}
class Person
{
public int Age;
public string Name;
private string Nickname;
public void GiveNn(string nickname)
{
if (nickname == "Dogegg")
{
return;
}
this.Nickname = nickname;
}
private void Hello2()
{
}
public void Hello()
{
Console.WriteLine("HelloWorld");
Console.WriteLine("{0} {1} {2}",this.Age,this.Name,this.Nickname);//this.是我自己的
}
}
}
}

2.属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace _11
{
class Program
{
static void Main(string[] args)
{
Person p = new Person();
p.Age = 20;
Console.WriteLine(p.Age);
p.Age = -1;//非法值 无用
Console.WriteLine(p.Age);
p.Nage = 20;
Console.WriteLine(p.Nage);
/*p.Dage = 20;
Console.WriteLine(p.Dage );*/
//这将出现死循环
/* Person2 p2 = new Person2();
p2.Age = 20;//其是只读的
Console.WriteLine(p2 .Age);*/
Person3 p3 = new Person3();
p3.Age = 21;
Console.WriteLine(p3.Age);
Console.ReadKey();
}
class Person
{
private int age;
public int Age
{
set//set用来赋值
{
if (value < 0)//public字段和属性的区别 :属性可以进行非法设置值的判断
{
return;
}
this.age = value;//value是用户赋过来的值
}
get//get用来取值
{
return this.age;
}
}
private int nage;
public int Nage
{
set
{
this.nage = value;
}
get
{
return 18;//返回值是多少就是多少
}
}
private int dage;
public int Dage
{
set
{
this.Dage = value;//将出现死循环
}
get
{
return this.Dage;
}
}
}
/* class Person2
{
public int Age
{
get//这是只读的
{
}
}
}*/
class Person3
{
public int Age//编译器自动生成私有字段和set get代码块
{
get;
set;
}
}
}
}

3.练习一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace _11
{
class Program
{
static void Main(string[] args)
{
Robot r1 = new Robot();
r1.Name = "K1";
Robot r2 = new Robot();
r2.Name = "K2";
Robot r;
Console.WriteLine("请选择1:r1,2:r2");
string s = Console.ReadLine();
if (s == "1")
{
r = r1;
}
else
{
r = r2;
}
Console.WriteLine("Hello,I'm robot.");
r.Eat(5);
while (true)
{
string str = Console.ReadLine();
r.Speak(str);
}
Console.ReadKey();
}
}
class Robot
{
public string Name { set; get; }
private int full { set; get; }
public void SayHello()
{
Console.WriteLine("Hello,my name is {0}", Name);
}
public void Speak(string str)
{
if (str.Contains("你") && (str.Contains("名字") || str.Contains("姓名")))
{
this.SayHello();
}
else if (str.Contains("你") && str.Contains("女朋友"))
{
Console.WriteLine("我还没有了,要不你给我介绍一个");
}
else if (str.Contains("今天") && str.Contains("天气"))
{
Console.WriteLine("天气很好");
}
else if (str.Contains("你") && str.Contains("吃饭"))
{
Console.WriteLine("我已经吃过了");
}
else if (str.Contains("再见") || str.Contains("拜") || str.Contains("8"))
{
Console.WriteLine("拜,下次聊");
return;
}
else if (full <= 0)
{
Console.WriteLine("I'm hungry! Give some food.Please input the number.");
string str1 = Console.ReadLine();
this.Eat(Convert.ToInt32(str1));
}
else
{
Console.WriteLine("听不懂");
}
full--;
}
public void Eat(int food)
{
full = full + food;
}
}
}

4.对象的引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace _11
{
class Program
{
static void Main(string[] args)
{
int i = 10;//int,char,bool,datetime等类型都是值类型(value type),赋值的时候是传递拷贝
int j = i;
i++;
Console.WriteLine(j);
Person p1 = new Person(10);//普通的对象则是引用类型,赋值的时候是传递引用
Person p2 = p1;
p1.Age++;
Console.WriteLine(p2.Age);
IncAge(p2);
Console.WriteLine(p2.Age);//传递给函数也是引用传递
static void IncAge(Person p)
{
p.Age++;
}
Console.ReadKey();
}
}
class Person
{
public int Age
{
get;
set;
}
public Person(int age)
{
this.Age = age;
}
}
}

5.构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace _11
{
class Program
{
static void Main(string[] args)
{
Person p1 = new Person();
Person p2 = new Person("K1");
Person p3 = new Person("K2",20);
Console.WriteLine("{0} {1}",p1 .Name,p1 .Age);
Console.WriteLine("{0} {1}", p2.Name, p2.Age);
Console.WriteLine("{0} {1}", p3.Name, p3.Age);
Console.ReadKey();
}
}
class Person
{
public int Age { set; get; }
public string Name { set; get; }
public Person()//构造函数和类名相同 可以重载
{
Name = "unknown";
Age = 1;
}
public Person(string name)
{
this.Name = name;
}
public Person(string name, int age)
{
this.Name = name;
this.Age = age;
}
}
}

6.继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace 继承
{
class Program
{
static void Main(string[] args)
{
Chinese c1 = new Chinese();
c1.Name = "张三";
c1 .Age =21;
c1.Say();
c1.HaveTea();
American a1 = new American();
a1.Name = "Gump";
Japaniese j1=new Japaniese ();
//American a4=new Person ();//错误
Person p1 = c1;//这是正确的
//American a2=p1;//错误的
//American a3 =(American) p1;//强制转换错误
Person p2 = j1;
Person p3 = new Person();
Chinese c2 = (Chinese)p3;//想想为什么强制转换没有成功
Console.ReadKey();
}
}
class Person//所有的类都直接或间接继承Object类 其是所有类的基类
{
public int Age { set; get; }
public string Name { set; get; }
public void Say()
{
Console.WriteLine("{0}",Name);
}
}
class Chinese : Person
{
public string Tea { set; get; }
public void HaveTea()
{
Console.WriteLine("Chinese Tea is good!");
}
}
class American : Person
{
public void Westmeal()
{
Console.WriteLine("Make a difference.");
}
}
class Japaniese : Person
{
}
}

7.异常及异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace 异常
{
class Program
{
static void Main(string[] args)
{
try
{
int i = Convert.ToInt32("asd");
Console.WriteLine("Convert 之后");
}
catch(Exception ex)
{
Console.WriteLine("数据错误:"+ex.Message +ex.StackTrace);
Console.WriteLine("数据错误之后");
}
try
{
string str = DescAge(200);
}
catch(Exception ex)
{
Console.WriteLine("数据错误:"+ex.Message);
}
Console.ReadKey();
}
static string DescAge(int age)
{
if (age >= 0 && age <= 150)
{
return "正常";
}
else if (age > 150)
{
throw new Exception("神人啊");
}
else
{
throw new Exception("不正常");
}
}
}
}

8.常量,变量及static成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace 常量,变量及static成员
{
class Program
{
public const int Week = 7;//const常量 常量名要大写
static void Main(string[] args)
{
const int Pi = 3;//不可变的量 注意这种用法
// Pi = 4;//
Console.WriteLine(3*Pi);
Person.height = 172;//注意
Person.Say();//不用new就能使用的方法static方法
Horse.Run();
//Person.Sing();//
Person p = new Person();
p.Sing();
p.Age =20;
//Rabbit r = new Rabbit();//
Rabbit .Weigth =8;
//Rabbit.Walk();
Rabbit.Eat();
Console.ReadKey();
}
}
public class Person
{
public static int height;
public int Age;
public static void Say()
{
Console.WriteLine(height);
//Console.WriteLine(Age);//在static成员中不能直接调用非static成员
//Person.Sing();
}
public void Sing()
{
Console.WriteLine(height);
Console.WriteLine(Age);
Person.Say();//在非static方法中可以调用static方法中的字段 方法
}
}
public class Horse
{
public static int Size;
public int Ability;
public static void Run()
{
Console.WriteLine(Person.height);
// Console.WriteLine(Person.Age);//
Person.Say();//在static方法中可以调用其它static方法的字段,属性,但不能调用非static的
//Person.Sing();
}
public void Leap()
{
Console.WriteLine(Person.height);
// Console.WriteLine(Person.Age);
Person.Say();
//Person.Sing();
}
}
static class Rabbit//这是一个静态类
{
public static int Weigth;
public string Name;
public static void Eat()
{
Console.WriteLine(Horse.Size);
// Console.WriteLine(Horse.Ability);
}
public void Walk()
{
Console.WriteLine(Horse.Size);
}
}
}

9.命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using 命名空间.hr;
namespace 命名空间
{
class Program
{
static void Main(string[] args)
{
Person p = new Person();//这是本命名空间的类
命名空间.wk.Person p2 = new 命名空间.wk.Person();
p2.Say();
Dog d = new Dog();//using 命名空间.hr
Convert.ToInt32("12");//using System
Console.ReadKey();
}
}
class Person
{
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace 命名空间.wk
{
class Person
{
public void Say()
{
Console.WriteLine("Hello");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace 命名空间.hr
{
class Dog
{
}
}

10.索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace 索引
{
class Program
{
static void Main(string[] args)
{
Person p = new Person();
p[1]=172;
Console.WriteLine("{0} {1}",p[1],p[2]);
Console.WriteLine(p["Tom",1,2]);
Console.ReadKey();
}
}
class Person
{
public int Num1=168;
public int Num2=173;
public int this[int index]
{
set
{
if (index == 1)
{
Num1 = value;
}
else if (index == 2)
{
Num2 = value;
}
else
{
throw new Exception ("数据错误");
}
}
get
{
if (index == 1)
{
return Num1;
}
else if (index == 2)
{
return Num2;
}
else
{
throw new Exception("数据错误");
}
}
}
public string this[string name,int i1,int i2]
{
get
{
return name + i1 + i2;
}
}
}
}

动手是最重要的,在调试这些代码的过程中。你会深入的了解c#中的一些特性。

Now,just do it.

goodbye next week~

第二部分:环境搭建

本周我们将着重介绍unity&leapmotion协同开发中的环境搭建,场景创建问题

这章内容和核心组件使用有着很强的关系所以作为先导请务必认真阅读

首先我们需要认识一下(xxxx.unitypackage)文件的应用

unitypackage格式的文件可以直接import到unity中,记住最好是在打开unity的情况下,在project面板里右击,然后在import packages->custom package中选中你要导入的资源。

那么我们先来看看 core Asset中都有些什么吧

asset

可以说 leap对这个SDK做了很好的梳理Gizmos中仅存储着传感器在场景中的贴图

而leapmotion下的东西才是我们真正要用到的东西

lp

也就是说如果你只进行桌面的应用开发的话 那么leapmotion+ovr还有OVR这两个文件夹几乎可以删除了。

那么我们怎么才在场景中看到手呢?

此时unity强大之处就来了 接下来跟我做:

为了让大家能尽快的获取(运行)一些能看得东西

now 我们在文件视图中看到了搜索框是吧在里面搜索 HandController

将蓝色的prefab(预制体)拖入Hierarchy

于是我们可以看到一个这样的场景

sennce

现在可以运行啦!!!(不要告诉我不知道怎么运行)

是不是看到了两个小小的手!!!!

是这样 unity和各个三维文件之间的单位换算有着区别因此我们如果想看见一双正常的手 要么把摄像机拉近拉的离控制器更近
要么把 控制器的scale参数调大 (为了方便计算,十的整数倍的倍率比较好)

OK我们终于能看到了写一个unity程序的入门之法

</id=3>

sandbox

在unity的core Asset下我们可以看到很多的demo 。这些demo给我们提供了很多思路,其下的脚本也很有参考性。
其中某些写法对我们构建程序来说,有着很强大的指导意义。

甚至对于一些代码经验贫乏的童鞋来说 根本不用自己写代码 仅仅靠ctrl+c&ctrl+v就可以创建一个demo来完成某些功能

但是说实在的我并不鼓励这样做 毕竟我们的目标是学习一些技能 一些知识来为以后的虚拟现实开发奠基

下周我们将正式开始讲解unity&leap的程序开发的初章 因此我们需要知道unity中一些重要的事情

  • 预制体(prefab)model

prefab

上图中我们可以看到 handcontroller 就是一个预制体 它包含了脚本 预设置 模型等等 可以说成是一个集合 里面装着这个元素在场景中得以运行所需要的所有文件。可以被复制,重用,使得游戏的编写,场景创建变得高效。比如我们可以把一栋房子,他所有的组件,脚本,模型等等打包为预制体 然后在场景中复制粘贴为更多的房子 成为一个小区等等

此外我们看到handcontroller sandbox当我们把他点开以后发现它是由一个个平面拼接而成的

当我们点进prefab下更多的预制体 会发现更多手的模型 其中文件夹的命名也让你能清楚地理解内容

而在scenes文件夹下你也可以找到更多的工程从而进入更多的脚本去预先了解一些代码的写法。

  • 坐标系

你可能会想 这有什么问题 ?

但是这说起来是个大问题,因为unity中存在着多种坐标判断体系

——视口坐标

——世界坐标

——屏幕坐标

——GUI坐标

1、World Space(世界坐标):我们在场景中添加物体(如:Cube),他们都是以世界坐标显示在场景中的。transform.position可以获得该位置坐标。

2、Screen Space(屏幕坐标):以像素来定义的,以屏幕的左下角为(0,0)点,右上角为(Screen.width,Screen.height),Z的位置是以相机的世界单位来衡量的。注:鼠标位置坐标属于屏幕坐标,Input.mousePosition可以获得该位置坐标,手指触摸屏幕也为屏幕坐标,

Input.GetTouch(0).position//可以获得单个手指触摸屏幕坐标。
Screen.width = Camera.pixelWidth
Screen.height = Camera.pixelHeigth

3、ViewPort Space(视口坐标):视口坐标是标准的和相对于相机的。相机的左下角为(0,0)点,右上角为(1,1)点,Z的位置是以相机的世界单位来衡量的。

4、绘制GUI界面的坐标系:这个坐标系与屏幕坐标系相似,不同的是该坐标系以屏幕的左上角为(0,0)点,右下角为

(Screen.width,Screen.height)

在此仅仅是粗略介绍, 今后在具体问题下 我们才能更好地认识和学习

此周务必多进行探索,下周将进行大量内容的讲解。

1,核心组件的使用。

在unity的官方文档中中很明显并没有给出如何去怎样很好的去使用给出的资源那我们先来明确一下 core asset中的资源都是用来干什么的

(1).handcontroller(以下简化为hc)

这个组件的目的就是在空间具象化出传感器检测到的手,以及传感器本身的检测范围,我们可以在inspector 面板中清楚地看到他的构成
这里写图片描述

这里的命名十分清楚,在hc的脚本中包含了左右手的模型,包括

图像模型
物理模型
工具模型
是否头戴的选项(主要用于协同oculus的开发)
以及其他一些可供调整的选项

让我们点进图像模型去看又会发现一些更有意思的问题
prefab

首先我们会发现这些图像模型都是一些预制体他们由统一的skeletalhand脚本控制 这是他们的灵魂所在

是这个脚本把一些零散的模型组合在一起成为一个手 统一之下既然会有skeletalhand 那么skeletalfinger 这些脚本共同控制着在场景中手的运动是符合人体规律,也符合数据的(虽然在具体过程中还是会出现偏差和bug)

了解了由骨骼拼成的图像化手的模型,再来了解物理模型
与图像模型进行比较
phy
rigi

我们会发现从中发现很大的不同 物理模型的网格为了减少计算提高性能
仅仅用一些粗略的网格去勾绘触发区域 但是 这也基本足够用 因为触发本身就是不精确的 且此种网格因为涉及了很多计算本身就是极为耗费性能的。

在这套物理系统中 我们可以轻松方便的设计触发 ,碰撞
当你新建了一个预设的cube或者其他物体且使用他们的collider的话可以用下面一段代码轻松地检测碰撞以及触发

1
2
3
4
5
void OnTriggerEnter(Collider collider)
{
print(collider.gameObject.name + ":" + Time.time);
}

当然你也可能发现 我们在选择图形模型的时候出现了很多预制体,实际上这里出现的预制体也就都是手的不同的图形模型 我们可以根据左右手分别选择左右手的图形模型 其中包括了机器,仿真,纯白,为了达到更高的调试效果 我们推荐使用白色基础模型 因为他的渲染需求小很多 能够更清晰的反馈问题
window

此外工具模式可用性极低 在我的测试中即使工具异常明显 识别率成功率几乎是不可用的 因此我们在程序中几乎不需要考虑工具模式。

(2).SCENCE

在scence文件夹中 存在着很多leap用作演示的工程 他们统一基于core asset中的公用资源编写 所以所有文件都是可见的 都是可重用的 他们为我们的开发带来了很多思路 以及方法
比如我们以其中的imagine hand 就可以清晰的看出 leapmotion的工作进程以及识别算法的思路
:在正常的识别模式之下 采用了高噪点的模式同时蓝色边沿就是景深图像经过算法处理后 形成单帧的手势运算图形

imagine


我们如果直接采集图像和图形化检视面板作比较的话 就不难发现 这种描边+景深处理的形式


这里写图片描述


在这写预置的场景中甚至有一些解决了我们无从下手的设计问题

这里写图片描述
这里写图片描述

在这些设计工作上我们可以根据这些例子 这些demo直接去设计从而省去了很多的探索过程

鉴于下一章直接要讲一些功能的实现方式 我们在此直接贴出一些unity中物体操作的代码供大家调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
using UnityEngine;
using System.Collections;
public class mover : MonoBehaviour {
// Use this for initialization
void Start () {
//x轴坐标
//float xPostion = -5f;
//z轴坐标
//float zPostion = 10f;
//直接将当前物体移动到x轴为xPostion,y轴为0,z轴为zPostion的三维空间位置。
transform.position = new Vector3(0.1f,0.1f,0.1f);//初始化坐标
}
// Update is called once per frame
/*
void Update () {
/*
//移动速度
float TranslateSpeed = 0.1f;
//Vector3.forward 表示“向前”
transform.Translate(Vector3.forward *TranslateSpeed);
//单方向移动*/
/*
//轴移动速度移动速度
float xSpeed = 0.01f;
//z轴移动速度移动速度
float zSpeed = 0.01f;
float ySpeed = 0.01f;
//向x轴移动xSpeed,同时想z轴移动zSpeed,y轴不动
transform.Translate(xSpeed,ySpeed,zSpeed);
*/
/*
//按下键盘“上方向键”控制cube的位置
if(Input.GetKey(KeyCode.UpArrow))
{
Debug.Log ("up");
transform.Translate(0.1f,0f,0f);}
if(Input.GetKey(KeyCode.LeftArrow))
{
Debug.Log("left");
transform.Translate(0f,0.1f,0f);}
if(Input.GetKey(KeyCode.DownArrow))
{
Debug.Log("down");
transform.Translate(-0.1f,0f,0f);}
if(Input.GetKey(KeyCode.RightArrow))
{
Debug.Log("right");
transform.Translate(0f,-0.1f,0f);}
//按下鼠标左键(0对应左键 , 1对应右键 , 2对应中键)
/*
if(Input.GetMouseButton(0))
Debug.Log ("mouse down");
Input.GetAxis("Mouse X");//鼠标横向增量(横向移动)
Input.GetAxis("Mouse Y");//鼠标纵向增量(纵向移动)
*/
/*
//旋转
this.transform.Rotate (Vector3.up, 0.3f);
this.transform.Rotate (Vector3.right, 0.3f);
this.transform.Rotate (Vector3.forward, 0.3F);
*/
/*
//缩放
float speed = 0.1f;
float x;
float z;
x = Input.GetAxis("Horizontal") * Time.deltaTime * speed; //水平
z = Input.GetAxis("Vertical") * Time.deltaTime * speed; //垂直//"Fire1","Fine2","Fine3"映射到Ctrl,Alt,Cmd键和鼠标的三键或腰杆按钮。新的输入轴可以在Input Manager中添加。
transform.localScale += new Vector3(x, 0, z);
*/
//鼠标按着左键移动
/*
void Update()
{
move ();
}
void move()//move函数可复用
{ float speed = 0.1f;
float y;
float x;
if (Input.GetMouseButton (0)){
y = Input.GetAxis ("Mouse X") * Time.deltaTime * speed;
x = Input.GetAxis ("Mouse Y") * Time.deltaTime * speed;
transform.Translate(x*5,y*5,0);
Debug.Log("x="+x);
Debug.Log("y="+y);
}
}
*/
/*
float speed = 100.0f;
float x;
float z;
float y;
//鼠标控制旋转
void Update () {
if(Input.GetMouseButton(0)){//鼠标按着左键移动
y = Input.GetAxis("Mouse X") * Time.deltaTime * speed;
x = Input.GetAxis("Mouse Y") * Time.deltaTime * speed;
Debug.Log("X="+x);
Debug.Log("y="+y);
}
else{x = y = 0 ;}//检测鼠标如果没有在按压状态下移动的话,返回坐标归零
transform.Rotate(new Vector3(x,y,0));
/**---------------其它旋转方式----------------**///transform.Rotate(Vector3.up *Time.deltaTime * speed);//绕Y轴 旋转//用于平滑旋转至自定义目标
/* pinghuaxuanzhuan();}
//平滑旋转至自定义角度
void OnGUI(){if(GUI.Button(new Rect(Screen.width - 110,10,100,50),"set Rotation")){//自定义角度
targetRotation = Quaternion.Euler(45.0f,45.0f,45.0f);// 直接设置旋转角度
iszhuan = true;}
}
bool iszhuan= false;
Quaternion targetRotation;
void pinghuaxuanzhuan(){if(iszhuan){
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 3);//平缓旋转函数可复用
}
}
*/
/*
* 脚本bug处理总结:
* 1,情况一:官方样板如果出现bug,则为编译框架问题改正方法:project->assembly csoption->build/general->target framework由3.5改为4.5
* 2,情况二:如果提示符号问题,很可能是空格所带来的问题,mono对编辑区空格宽容度较低,提示脚本中某行的问题,如果确定语法没错,很可能是因为空格的问题
* 3,情况三:单精度后必须加f 1.0 错 1.0f对
* 4,情况四:复制粘贴的代码需要大量精力来重新对格式进行debug,因此如果不是两三百行以上,尽量自己重写
* 5,情况五:如果编译错误很可能是因为空白区域留有字符
* /
*/
}

注:以上代码均为不同功能片段凑成 不是直接复制粘贴运行就能完成功能 至于怎么修改和去除注释 我想就不用我教了

此外我们需要对摄像机的位置进行调整的话 可以用以下代码 用以保证摄像机永远对着物体拍摄
以下代码的脚本需附在摄像机上 以此摄像机的轨迹就是以物体为球心的球面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using UnityEngine;
using System.Collections;
public class camral : MonoBehaviour {
public Vector3 v1, v2;
public GameObject cube;
// Use this for initialization
void Start () {
cube = GameObject.Find("House N251015");//house xxxx为物体的名字
}
// Update is called once per frame
void Update () {
if (Input.GetKey (KeyCode.UpArrow))
this.transform.RotateAround (cube.transform.position, Vector3.right, Time.deltaTime*10f);
if (Input.GetKey (KeyCode.DownArrow))
this.transform.RotateAround (cube.transform.position, Vector3.left, Time.deltaTime * 10f);
if (Input.GetKey (KeyCode.LeftArrow))
this.transform.RotateAround (cube.transform.position, Vector3.down, Time.deltaTime * 10f);
if (Input.GetKey (KeyCode.RightArrow))
this.transform.RotateAround (cube.transform.position, Vector3.up, Time.deltaTime * 10f);
}
}

在后期的开发中我们还需要对物体的坐标进行世界坐标化 以免旋转过后因为自身坐标的变化而引起方向的混乱 因此 我们物体的操作 都要有基于世界坐标的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
using UnityEngine;
using System.Collections;
public class world_v : MonoBehaviour {
public GameObject cube;
// Use this for initialization
void Start () {
this.cube.transform.position = new Vector3 (0f, 0f, 0f);
}
// Update is called once per frame
void Update () {
if(Input.GetKey(KeyCode.UpArrow))
{
Debug.Log ("up");
this.cube.transform.Translate(Vector3.up * Time.deltaTime, Space.World);
// this.cube.transform.Translate(0f,0.1f,0f);
}
if(Input.GetKey(KeyCode.LeftArrow))
{
Debug.Log("left");
this.cube.transform.Translate(Vector3.left * Time.deltaTime, Space.World);
}
//this.cube.transform.Translate(-0.1f,0f,0f);}
if(Input.GetKey(KeyCode.DownArrow))
{
Debug.Log("down");
this.cube.transform.Translate(Vector3.down * Time.deltaTime, Space.World);
//this.cube.transform.Translate(0f,-0.1f,0f);
}
if(Input.GetKey(KeyCode.RightArrow))
{
Debug.Log("right");
this.cube.transform.Translate(Vector3.right * Time.deltaTime, Space.World);
//this.cube.transform.Translate(0.1f,0f,0f);
}
if (Input.GetKey (KeyCode.W)) {
Debug.Log ("z+");
this.cube.transform.Translate(Vector3.forward * Time.deltaTime, Space.World);
// this.cube.transform.Translate (0f, 0f, 0.1f);
}
if (Input.GetKey (KeyCode.S)) {
Debug.Log("z-");
this.cube.transform.Translate(Vector3.back* Time.deltaTime, Space.World);
//this.cube.transform.Translate(0f,0f,-0.1f);
//________________________________________________________________
}
if (Input.GetKey (KeyCode.Y)) {
//this.cube.transform.Rotate(new Vector3(1,0,0));
this.cube.transform.Rotate(Vector3.up *1f, Space.World);
}
if(Input.GetKey(KeyCode.U))
{
//this.cube.transform.Rotate(new Vector3(0,1,0));
this.cube.transform.Rotate(Vector3.left *1f, Space.World);
}
if(Input.GetKey(KeyCode.I))
{
//this.cube.transform.Rotate(new Vector3(0,0,1));
this.cube.transform.Rotate(Vector3.forward *1f, Space.World);
}
if (Input.GetKey (KeyCode.H)) {
//this.cube.transform.Rotate(new Vector3(-1,0,0));
this.cube.transform.Rotate(Vector3.down *1f, Space.World);
}
if (Input.GetKey (KeyCode.J)) {
//this.cube.transform.Rotate(new Vector3(0,-1,0));
this.cube.transform.Rotate(Vector3.right *1f, Space.World);
}
if (Input.GetKey (KeyCode.K)) {
//this.cube.transform.Rotate(new Vector3(0,0,-1));
this.cube.transform.Rotate(Vector3.back *1f, Space.World);
}
}
}

描述了以上一些操作 在unity中你就可以让物体动起来了并且按照一定的路线去规定物体怎么去运动 也为我们的进一步交互打好了基础

下周的内容将是真正意义上开发的开始 请仔细完成以上代码的功能

本周着重开始讲解leap开发中的实际问题


首先我们必须对一些核心的脚本进行熟悉 明白他们的工作原理 :

1.重中之重simple.cs

作为官方编写的文档 几乎出现了所有的api 函数所有的返回数据 所有的方法 除去一些需要自己经验的写法 基本全部囊括其中了 但是 以下代码 基本见不到了simple.cs的影子

鉴于是教程 那我的定位是代码讲解 采取分立介绍最后汇总 不是铺陈代码
tip :

以下代码片段都不是直接复制粘贴就能运行的 小白还是回去看我之前的内容
以下代码片段都不是直接复制粘贴就能运行的 小白还是回去看我之前的内容
以下代码片段都不是直接复制粘贴就能运行的 小白还是回去看我之前的内容

头文件部分 因为设计到了一些list 或是一些UI绘制 所以头文件显得多了一些(当然以后会更多)

1
2
3
4
5
6
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System;
using Leap;
using System.Collections.Generic;

在脚本类中的定义工作 (以后的脚本基本基于以下定义工作,如不作特殊说明则默认为以下定义)

1
2
3
4
5
6
7
public HandController hc; //控制器
private HandModel hm; //手部模型
public GameObject cubePrefab; //可不定义
public Vector squizehand; //紧握得手vector
private GameObject cube = null;
Frame currentFrame = null;//定义当前帧
Frame lastFrame = null;//上一帧

函数部分
借助返回的数据 我们可以设计更多的手势 比如在大量测试后 我们发现35左右的值在leap设计中是关键的参数所以:

1
2
3
4
5
6
7
bool squize(float radius)//判断手势是否为握持 阀值为35
{
if (radius < 37)
return true;
else
return false;
}

另外 如果你有自己的想法 或是对返回值有所要求or不想用官方给的东西 那么可以自己写一个判断双手距离的函数:

1
2
3
4
5
6
7
double distance(Vector a,Vector b)//双手距离判断 退出机制触发阀值为35
{
double c=Math.Sqrt (Math.Pow ((a.x-b.x), 2) + Math.Pow ((a.y-b.y), 2) + Math.Pow ((a.z-b.z), 2));
Debug.Log ("TWO hand distance="+c);
return c;
}

以上两个非常简单的函数共同使用 就诞生了更多的用法 我们后续会介绍 并且经过测试 其稳定性甚至要高于某些官方的预制手势
那好既然说道预制手势 下面我们就来讨论一下 预制手势的用法:
首先需要在start()中定义 打开预制手势:

1
2
3
4
5
hc.GetLeapController().EnableGesture(Gesture.GestureType.TYPECIRCLE); //环绕
hc.GetLeapController().EnableGesture(Gesture.GestureType.TYPESWIPE); //挥动
hc.GetLeapController().EnableGesture(Gesture.GestureType.TYPE_SCREEN_TAP); // ?
hc.GetLeapController ().EnableGesture (Gesture.GestureType.TYPEKEYTAP); //点击
hc.GetLeapController ().EnableGesture (Gesture.GestureType.TYPEINVALID); //无效

在此之后我们就可以通过foreach来调用各种预制手势 但说实话 相比起来 预制手势能完成的工作实在是太少了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
foreach (Gesture g in gestures) {
Debug.Log (g.Type);
//----code----//
//此中循环为调取手势列表中预制的手势
//leap在API中预置了手势即:
//Gesture.GestureType.TYPESWIPE
//Gesture.GestureType.TYPE_SCREEN_TAP
//Gesture.GestureType.TYPEKEYTAP
//Gesture.GestureType.TYPECIRCLE
//等
//以此预制手势来实现操作则在此编码
}

以上为预制手势使用的基本框架 下面就是预制手势完成的小demo : 返回发生keytap手势时手的位置

1
2
3
4
5
6
7
8
9
foreach (Hand hand in frame.Hands) {
foreach (Gesture g in gestures) {
Debug.Log (g.Type);
if (g.Type == Gesture.GestureType.TYPEKEYTAP) {
Debug.Log("tip position"+hand.PalmPosition);
}
}
}

(其中值得注意的是: swipe 和keytap这两个手势互相干扰非常严重 所以我们需要注意 这两个预制手势的共同使用是相当糟糕的


那么讲完了预制手势,我们来讲讲返回值。首先,既然要完成手势的设计,能获取各种方法的返回值才是最重要的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void OnFrame (HandController controller)
{
//获取基本信息
Frame frame = controller.GetFrame(); //当前帧
Frame lastframe = controller.getlastframe ();//之前帧 星标1后续会重点讲这里记住
Debug.Log ("Frame id: " + frame.Id //帧ID
+ ", timestamp: " + frame.Timestamp //帧时间戳:从捕捉开始经过了多少毫秒
+ ", hands: " + frame.Hands.Count //有几只手
+ ", fingers: " + frame.Fingers.Count //有几只手指
+ ", tools: " + frame.Tools.Count //有几个工具
+ ", gestures: " + frame.Gestures ().Count); //手势计数
Debug.Log ("lastFrame id: " + lastframe.Id
+ ", lasttimestamp: " + lastframe.Timestamp
+ ", lasthands: " + lastframe.Hands.Count
+ ", lastfingers: " + lastframe.Fingers.Count
+ ", lasttools: " + lastframe.Tools.Count
+ ", lastgestures: " + lastframe.Gestures ().Count);
}

以上这个方法清楚的显示了leap代码构建的逻辑:

控制器下有一个记录每帧数据的容器 我们从每个数据帧中获得我们想要的信息 通过和之前帧对比来达到我们的目的
如果我们想启用以上函数打印一些基本信息

1
OnFrame(hc);//平时不必调用

(至于怎么修改获取容器中的值 我们稍后就会讲到 所以大家先看我讲的东西)

frame :就是我们所说的帧 之下的方法也就是在一帧之内我们能得到的 所有工作基本基于 frame展开 那么就是说 基本工作的展开都是从定义帧开始

1
2
Frame frame = hc.GetFrame ();
Frame lastframe = hc.getlastframe ();
1
2
this.currentFrame = hc.GetFrame ();
GestureList gestures = this.currentFrame.Gestures ();

在此基础上我们后续的工作才能展开 以下为在左手右手基础上的 代码框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Vector lefthand = null;
Vector righthand = null;
bool lefthandexist = false;//判断左右手是否在场景中存在
bool righthandexist = false;
foreach (var h in hc.GetFrame().Hands) {
if (h.IsLeft) {
lefthandexist=true;
Debug.Log ("lefthand exist? =" + lefthandexist);
lefthand = h.PalmPosition;
//____code____//
/*
此中h即为左手,可按照手的一切操作方式来编码
*/
}
if (h.IsRight) {
righthandexist=true;
Debug.Log ("righthandexist? =" + righthandexist);
righthand=h.PalmPosition;
foreach (Finger finger in righthand.Fingers) {
Finger.FingerType type=finger.Type();
Debug.Log (" Finger id: " + finger.Id
+ ", " + finger.Type().ToString()
+ ", length: " + finger.Length
+ "mm, width: " + finger.Width + "mm");
if(type==Finger.FingerType.TYPE_INDEX)// 手指类型的判断
Debug.Log("tip position="+finger.TipPosition+
"tip direction="+finger.Direction);
//____code____//
}
if(lefthandexist&&righthandexist)
{
//____code____//
//如果左右手同时存在情况下的代码放在如下
}
}

以上基本为手势部分代码设计框架
还有很多内容没法全部细讲 那么我就直接贴上来代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/*控制台输出debug。log与using system之中 console。writeline用法相同
* 代码使用foreach来循环以达到返回所有手部数据的要求因此设计中可以带来更多设计
* keytap可用来点选
* type circle可用来旋转目标
* 挥舞可用来滑动菜单
* UI检测方式可用跟踪手的位置 如果:palm_position<e(UI模块范围)&&gesture==keytap 则生成交互
*
* 在此基础上改变交互规则:
*
* 以下为可收集数据中的注释 全可在以上对帧的循环下执行
* WriteLine ("Frame id: " + frame.Id
+ ", timestamp: " + frame.Timestamp
+ ", hands: " + frame.Hands.Count
+ ", fingers: " + frame.Fingers.Count
+ ", tools: " + frame.Tools.Count
+ ", gestures: " + frame.Gestures ().Count);
* //pass
* WriteLine (" Hand id: " + hand.Id
+ ", palm position: " + hand.PalmPosition);
// Get the hand's normal vector and direction
Vector normal = hand.PalmNormal;
Vector direction = hand.Direction;
// 计算手的角度 翻滚,偏离角
WriteLine (" Hand pitch: " + direction.Pitch * 180.0f / (float)Math.PI + " degrees, "
+ "roll: " + normal.Roll * 180.0f / (float)Math.PI + " degrees, "
+ "yaw: " + direction.Yaw * 180.0f / (float)Math.PI + " degrees");
// 获得胳膊的骨骼
Arm arm = hand.Arm;
WriteLine (" Arm direction: " + arm.Direction
+ ", wrist position: " + arm.WristPosition
+ ", elbow position: " + arm.ElbowPosition);
// 获得手指 foreach也可嵌套
foreach (Finger finger in hand.Fingers) {
SafeWriteLine (" Finger id: " + finger.Id
+ ", " + finger.Type().ToString()
+ ", length: " + finger.Length
+ "mm, width: " + finger.Width + "mm");
// 获得手指的骨骼
Bone bone;
foreach (Bone.BoneType boneType in (Bone.BoneType[]) Enum.GetValues(typeof(Bone.BoneType)))
{
bone = finger.Bone(boneType);
SafeWriteLine(" Bone: " + boneType
+ ", start: " + bone.PrevJoint
+ ", end: " + bone.NextJoint
+ ", direction: " + bone.Direction);
}
}
*
*
*
* ——————————————————————————————————————————————————————————————————
* 以下为prefab手势的扩展与详解
*
* 旋转手势 顺时针与逆时针的判断规则:
* case Gesture.GestureType.TYPE_CIRCLE:
CircleGesture circle = new CircleGesture (gesture);
// Calculate clock direction using the angle between circle normal and pointable
String clockwiseness;
if (circle.Pointable.Direction.AngleTo (circle.Normal) <= Math.PI / 2) {
//Clockwise if angle is less than 90 degrees
clockwiseness = "clockwise";
} else {
clockwiseness = "counterclockwise";
}
* 计算旋转手势转过的角度:
* float sweptAngle = 0;//初始化角度为零
// 计算从上一帧到这一帧的角度
if (circle.State != Gesture.GestureState.STATE_START) {
CircleGesture previousUpdate = new CircleGesture (controller.Frame (1).Gesture (circle.Id));
sweptAngle = (circle.Progress - previousUpdate.Progress) * 360;
}
WriteLine (" Circle id: " + circle.Id
+ ", " + circle.State
+ ", progress: " + circle.Progress
+ ", radius: " + circle.Radius
+ ", angle: " + sweptAngle
+ ", " + clockwiseness);
*
*
*
*/

手势脚本部分引路工作基本完成

下面介绍另一个比较重要的脚本

2.HandController.cs
这个脚本是leapmotion核心之一 上面脚本中多数方法 从handcontroller这个脚本中调用所以
为了方便与满足我们的开发需求 对这个脚本的改动是最方便的 但是鉴于接近500line的体量 我不可能一一讲请 所以挑重点:也就是我们刚刚所提到的 frame

1
2
3
4
5
6
public Frame GetFrame() {
if (enableRecordPlayback && recorder_.state == RecorderState.Playing)
return recorder_.GetCurrentFrame();
return leap_controller_.Frame();
}

也就是说 我们使用的get frame() 原型在这里 所做的也就是从更深层的leap_controller()中获取
但是 我们细心就会发现 这是个方法 所以 我们可以举一反三

1
public Frame getlastframe(int i){return leap_controller_.Frame (i);}

通过这个定义 我们就可以在今后的脚本中直接使用自己的getlastframe()获取自己想要的帧
如果想要上一帧 那就hc.getlastframe(1)就好了

tips: 如果你想设计效果更好的手势建议去通读handcontroller 以及与之相关有继承关系的脚本


基于本节所讲的设计框架:
我们可以根据所给代码设计一系列的动作 去做一些触发 模拟 控制
至于剩下的内容 多着眼于 代码优化 动作设计
更多的是一些关于渲染器 着色器 触发器 等等的内容
本周内容较多 请多做练习

good bye next week (or next year)

本周 我们着重将讲解代码的优化问题

首先
经过了上周我们 讲解的代码框架 以及设计规则 我们都有了足够的手法去设计一些自己想要的动作 但是 在一番编写之后 你肯定会发现 有很多方面并不能达到你设想的那样 其中涉及的一些很隐性的问题 表述起来比较繁杂 因此 我将会更多的以图片来展示问题 那些比较动态的问题 将会很难理解 所以 你应该尝试着跟着我的描述去做


其次
一些有问题的代码 因为我们的开发过程没有版本更替 所以一些有问题的代码没有保存 我只能从比较显眼的问题入手


第一点:

世界坐标
我们之前提到过的世界坐标的问题 我也附上了代码 想了解的可以去看之前的博客 但是没有细讲 下面着重来说这个问题

根据上次提供的脚本 新建一个脚本将代码复制附在摄像机上 然后新建一个cube 将物体与代码重定义的物体绑定
我们将两个方块一个初始化在(000)点,一个初始化在(020)点
reset


下面我们修改脚本

1
2
3
4
5
6
7
8
if(Input.GetKey(KeyCode.UpArrow))
{
Debug.Log ("up");
//this.cube.transform.Translate(0,0.1f,0,Space.World);
this.cube.transform.Translate(0f,0.1f,0f);
}

以此类推 将带space.world的脚本注释 将不带spac.world的语句取消注释

下面我们根据所定义的按键来操作 发现上下左右都没问题 下面来旋转

同样的旋转系数 同样的操作

cube
cube1

他们的坐标系根本不一致!!!这时候 我们再去操纵他们前后左右

看

我们分别点选坐标系就能看出端倪 世界坐标的作用是统一采用世界的坐标系来规定物体的旋转 如果不指定 物体的变换则是根据自身的坐标来的 所以经过旋转的物体 他们的运动在自己的坐标系内 很可能出现和你的坐标系相反 你输入的指令 是左 他却往右跑 正所以 世界坐标是规范物体运动的最好方法

第二点:

awake

这点其实没什么好展示 因为这是unity开发中常会遇到的问题
脚本执行顺序说明:
先执行所有子脚本中的awake();
在执行所有脚本中的start();
然后执行所有update();
当所有脚本中的update();执行一遍之后,则执行fixupdate();和lateupdate();
即unity中没有多线程概念
且如果在awake中掺杂了关于获取对象的定义
那么脚本之间执行顺序不同就有可能出现空指针

第三点 :

触发网格

middle
我们从图片上可知 仅仅是触发网格 也是有一个一个简单的网格拼凑而成


与之相比较我们再来看看 模型的网格的结构


mesh

所以当我们设计捏取这个动作的时候 触发网格的误差足以让你发疯 所以 如果你想设计捏取动作 那么最好不要牵扯捏取的手指对物体做误差触发
否则 就和官方程序里 那个下国际象棋的demo和 把头拼在机器人身上那个demo体验一样差

我的做法是 每当捏取的两个手指 发生捏取手势的时候 在捏取点生成一个半径固定(不能太大)球体触发网格 每当物体与球体发生触发 求出触发列表 把列表中每一个物体的触发点到球心的距离 求出
最后把最距离最小的那个 物体设为 捏取物体
至于代码 :

nothing ~ i can’t show that

第三点:

UI

相信这点困扰了一些人 因为鼠标键盘 体系的UI 乃至触屏体系的UI 都是直接集成在unity的
我们可以方便的调用 但是 如果设计这种三维空间内的交互设计还是比较复杂的 如果没有相关文档支持 或者例子来借鉴 那么设计工作确实有些 难以展开

但是我们自己探索中就会发现wight这个东西:
在搜索框中搜索 wight然后加载其中的设置
当我们运行了wight之后就可以发现两种可手势交互的组件 一个是按钮 一个是滑动条


wight


我们仔细观察他们的组织结构 就能清晰的发现 手势设计和 UI的组织结构 设计方法 组织方法

**组织结构

经过观察

1
2
3
using UnityEngine;
using System.Collections;
using LMWidgets;

我们可以首先发现 这个系列的脚本有着多层的继承关系

1
2
3
4
5
6
7
8
9
10
11
protected override void Start()
{
base.Start();
}
protected override void FixedUpdate()
{
base.FixedUpdate();
UpdateGraphics();
}

再往源头去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;
using System;
using System.Collections;
namespace LMWidgets
{
public abstract class SliderBase : LeapPhysicsSpring, AnalogInteractionHandler<float>, IDataBoundWidget<SliderBase, float>
{
protected DataBinderSlider m_dataBinder;
// Binary Interaction Handler - Fires when interaction with the widget starts.
public event EventHandler<LMWidgets.EventArg<float>> StartHandler;
// Analog Interaction Handler - Fires while widget is being interacted with.
public event EventHandler<LMWidgets.EventArg<float>> ChangeHandler;
// Binary Interaction Handler - Fires when interaction with the widget ends.
public event EventHandler<LMWidgets.EventArg<float>> EndHandler;
public GameObject upperLimit;
public GameObject lowerLimit;
protected float m_localTriggerDistance;

源头上LMxxxx的这些代码 的含义其实我们不必去细究 如果你没有涉及高层次的变动 复制粘贴 就好了 这种UI设计工作 说起来是个繁杂的工作 因此往往脚本的编写更加要求成体系 结构明了 所以要比功能编写付出充分的耐心 这些问题 不难 但是极其细致 繁复

我们要掌握的是设计思想 即 我们需要 熟悉button/slider的脚本 以及相关UI元素的使用

我们以button为例看一下这个设计该怎么展开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class ButtonDemoGraphics : MonoBehaviour
{
public void SetActive(bool status)
{
Renderer[] renderers = GetComponentsInChildren<Renderer>();
Text[] texts = GetComponentsInChildren<Text>();
Image[] GUIimages = GetComponentsInChildren<Image>();
foreach (Renderer renderer in renderers)
{
renderer.enabled = status;
}
foreach(Text text in texts){
text.enabled = status;
}
foreach(Image image in GUIimages){
image.enabled = status;
}
}
public void SetColor(Color color)//这个不用我说了吧
{
Renderer[] renderers = GetComponentsInChildren<Renderer>();
Text[] texts = GetComponentsInChildren<Text>();
Image[] GUIimages = GetComponentsInChildren<Image>();
//获取组件
foreach (Renderer renderer in renderers)
{
renderer.material.color = color;
}
foreach (Text text in texts){
text.color = color;
}
foreach(Image image in GUIimages){
image.color = color;
}
}
}

在以上这个命名空间里 我们可以看出 他的思路是 这个脚本中完成和定义的工作 都是为了 button的绘图做准备 设置颜色 设置样式如何如何
再看下一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using UnityEngine;
using System.Collections;
using LMWidgets;
public class ButtonDemoToggle : ButtonToggleBase
{
public ButtonDemoGraphics onGraphics;
public ButtonDemoGraphics offGraphics;
public ButtonDemoGraphics midGraphics;
public ButtonDemoGraphics botGraphics;
public Color MidGraphicsOnColor = new Color(0.0f, 0.5f, 0.5f, 1.0f);
public Color BotGraphicsOnColor = new Color(0.0f, 1.0f, 1.0f, 1.0f);
public Color MidGraphicsOffColor = new Color(0.0f, 0.5f, 0.5f, 0.1f);
public Color BotGraphicsOffColor = new Color(0.0f, 0.25f, 0.25f, 1.0f);
public override void ButtonTurnsOn()
{
TurnsOnGraphics();
}
public override void ButtonTurnsOff()
{
TurnsOffGraphics();
}
private void TurnsOnGraphics()
{
onGraphics.SetActive(true);
offGraphics.SetActive(false);
midGraphics.SetColor(MidGraphicsOnColor);
botGraphics.SetColor(BotGraphicsOnColor);
}
private void TurnsOffGraphics()
{
onGraphics.SetActive(false);
offGraphics.SetActive(true);
midGraphics.SetColor(MidGraphicsOffColor);
botGraphics.SetColor(BotGraphicsOffColor);
}
private void UpdateGraphics()
{
Vector3 position = transform.localPosition;
position.z = Mathf.Min(position.z, m_localTriggerDistance);
onGraphics.transform.localPosition = position;
offGraphics.transform.localPosition = position;
Vector3 bot_position = position;
bot_position.z = Mathf.Max(bot_position.z, m_localTriggerDistance - m_localCushionThickness);
botGraphics.transform.localPosition = bot_position;
Vector3 mid_position = position;
mid_position.z = (position.z + bot_position.z) / 2.0f;
midGraphics.transform.localPosition = mid_position;
}
protected override void Start()
{
base.Start();//
}
protected override void FixedUpdate()
{
base.FixedUpdate();//
UpdateGraphics();//
}
}

button1

我们通过鼠标停留就可以发现 这些继承关系的源头

1
2
3
4
5
6
7
8
protected override void Start() {
if ( m_dataBinder != null ) {
setButtonState(m_dataBinder.GetCurrentData(), true); // Initilize widget value
}
else {
setButtonState(false, true);
}
}

所以我们可以用这种方法探究整个UIwight的工作机制 在这种情况下 我们可以清楚的认识到 整个UI的脚本资源之间的组织形式 和生动了解 总结编写方法 甚至官方这些 方法可以被我们复用以达到 真正的设计我们的新的 手势交互UI
这里写图片描述

以上图来看 在世界坐标视野中 我们可以看到 UI都被拥有box cllider 不同形式的UI设计 有着 不同的触发器样式 思路一下子就变得很清晰 在 三维的组织形式下 触发器的 工作形式需要好好调整 普遍是一个拥有冗余度的触发器 并且触发器的位置可以被推动 很好的模拟了现实世界的情况


在2016年三月 左右 我会写一套 手势识别 UI设计框架 所以一些 代码能力稍弱的童鞋 尽请期待

因为这个问题是个可以无限被讨论的问题 所以剩下的 更多的探索工作留给读者 我只是 指引一条方向供大家参考因此 如果有更好的方式也请加入qq群我们一起讨论

343074971

第四点:

缩放手势

我对这个功能的编写出现了很多延伸版本
一开始的想法是 调用上一帧 和这一帧的双手位置进行计算 差值为正则放大 差值为负则缩小
但是我显然高估了我对执行顺序的把控能力 基本上这个思路需要脚本的控制能力达到炉火纯青的地步才会写对 后期 对数据的检查发现 这个差距实在是太小以及上一帧的数据重复性导致的判断进程根本无法控制

后来又想了一个双手向量相加取模的类似的想法 但是也因为精度等等 原因 失败

经过多次的 实验 测试 记录 我发现 270是一个比较合适的 取值
因此 针对双手的绝对距离 来设计这个缩放手势

最后又发现 这个方式不符合人的操作 又返回对比相对值的思路 最后 更改了 leap的传值方式 在当前帧 计算 前一帧得手的距离 和当前帧 手的距离 进行对比 最后实现了一个 比较好的结果

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Hand lefthand=null;
Hand righthand = null;
Hand last_lefthand = null;
Hand last_righthand = null;
bool lefthandexist = false;//判断左右手是否在场景中存在
bool righthandexist = false;
double scale=0f;
double dis=0f;
double last_dis= 0f;
double handdis(Hand a,Hand b)
{
double c=Math.Sqrt (Math.Pow ((a.PalmPosition.x-b.PalmPosition.x), 2) + Math.Pow ((a.PalmPosition.y-b.PalmPosition.y), 2) + Math.Pow ((a.PalmPosition.z-b.PalmPosition.z), 2));
return c;
}

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//左右手模块_________________________________________________________________________________________________________
foreach (var h in hc.GetFrame().Hands) {
if (h.IsLeft) {
lefthandexist = true;
lefthand = h;
Debug.Log ("lefthand exist? =" + lefthandexist);
Debug.Log ("lefthand position=" + lefthand.PalmPosition);
Debug.Log ("lefthand squize?=" + squize (lefthand.SphereRadius));
}
if (h.IsRight) {
righthandexist = true;
righthand = h;
Debug.Log ("righthandexist? =" + righthandexist);
Debug.Log ("righthand position=" + righthand.PalmPosition);
Debug.Log ("righthand squize?=" + squize (righthand.SphereRadius));
}
}
if((lefthandexist)&&(righthandexist))
{
if((squize(lefthand.SphereRadius))&&(squize(righthand.SphereRadius)))
{
foreach (Hand hand in frame.Hands)//当前帧的手
{
foreach(Hand lastframe_hand in lastframe.Hands)
{
if(hand.IsLeft){lefthand=hand;}
if(hand.IsRight){righthand=hand;}
if(lastframe_hand.IsLeft){last_lefthand=lastframe_hand;}
if(lastframe_hand.IsRight){last_righthand=lastframe_hand;}
}
}
dis=handdis(lefthand,righthand);
last_dis=handdis(last_lefthand,last_righthand);
if(dis>last_dis)
{Debug.Log("++++++++++");
this.cube.transform.localScale+=(new Vector3(1f,1f,1f));}
if(dis<last_dis)
{Debug.Log("__________");
this.cube.transform.localScale+=(new Vector3(-1f,-1f,-1f));}
}
}

最后:

我想说一些总结的设计原则 能保证你 做出来的东西至少是有实用性的:

  1. 设计动作最好 符合直觉
  2. 如果你有着较好的 代码能力 尽量不要使用预制手势
  3. 不要使用识别有冲突的手势
  4. 不要使用工具(tool模式)
  5. 不要相信官方宣传的精度 shit~

抱歉的是,之前说的LEAP/UI框架设计可能只有两篇 因为个人时间实在是不允许 这个问题如果展开去写的话 那么说写本书都是不为过的 且因为内容修改很是杂乱 所以我第一篇文章用来介绍LEAP预置UI的结构第二篇用来讲How to design&build~


鉴于直接涉及交互问题 因此这篇文章的受众显得很尴尬 但是相信 认真按照我之前博客学习的同学都能够理解其中的意思

关于leap这个东西 我在第一篇文章中就提到过 ——just a toy.
所以只是用来开发的练手,根本别指望交互效果能够很好


根据我的经验来讲 手势识别的交互UI根据交互方式大概分为三种

1,触发式操作

2,手势操作(下一期)

3,映射性操作(下一期)

所以 我从最基本的触发操作ui开始入手


预警:这篇文章very very long~~

入门:

从leap coreAsset当中找到

widght这个文件夹
widght
那先找个demo运行一下吧

SENCES

下的运行一下咯~

效果

好 我们就由这个开始 unity UI和leap交互的前导
首先 我们来熟悉 官方预设的四种UI样式 分别是:

dial\滚轮菜单

scrolltext\可滑动的字体

slider\滑动条

toggle button\按钮


sence


当我们把这四种prefab拖入场景加以调整

再加上控制器 就算已经完整地展示了 所有的 官方预制形式

那么我们就来一个一个说吧

1.最简单的scrolltext\可滑动的字体

这个组件的主要控制过程在这里
scrollhandle
scroll


我们可以看到他的结构相当简单 boxcollider检测触发 文本上下边界 和显示框上下边界 当text的上边界高于显示框的时候会以一个速度贴合回来 并且因为使用localposition 这个组件的工作不会因为坐标的颠倒而出错

1
2
3
4
5
6
protected virtual void ResetPivots()
{
m_pivot = transform.localPosition;
if (m_target != null)
m_targetPivot = transform.parent.InverseTransformPoint(m_target.transform.position);
}

text


针对 TEXT组件的修改 我们可以直接在inspectors中修改文字等等操作 而直接生成一个类似阅读器之类的应用

在脚本中我们可以看到来龙去脉

首先是这个类LeapPhysicsBase

相信有一定基础的小伙伴都知道 既然 设置了触发器 那么肯定会有 检测函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected virtual void OnTriggerEnter(Collider collider)
{
//检测是否是手
if (m_target == null && IsHand(collider) && State != LeapPhysicsState.Disabled)
{
State = LeapPhysicsState.Interacting;
m_target = collider.gameObject;
ResetPivots();
}
}
protected virtual void OnTriggerExit(Collider collider)
{
if (collider.gameObject == m_target)
{
State = LeapPhysicsState.Reflecting;
m_target = null;
}
}

以上两个函数清楚的写了触发执行的过程 那既然是检测不只是要判断是否出发 还要对造成出发的对象进行判断 是否为手于是调用了以下这个函数

1
2
3
4
private bool IsHand(Collider collider)
{
return collider.transform.parent && collider.transform.parent.parent && collider.transform.parent.parent.GetComponent<HandModel>();
}

以上三个部分共同工作,就生成了最基本的触发事件

而整个组建的状态更改 控制 识别都放在一个FixedUpdate();里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected virtual void FixedUpdate()
{
if (m_target == null && State == LeapPhysicsState.Interacting)
{
State = LeapPhysicsState.Reflecting;
}
switch (State)
{
case LeapPhysicsState.Interacting://交互
ApplyInteractions();
break;
case LeapPhysicsState.Reflecting://反映
ApplyPhysics();
break;
case LeapPhysicsState.Disabled://无
break;
default:
break;
}
ApplyConstraints();
}

代码清晰易懂 对组件的3种状态做了规整的编写 代码结构十分清晰

1
2
3
4
5
6
public enum LeapPhysicsState
{
Interacting, // Responsible for moving the widgets with the fingers
Reflecting, // Responsible for reflecting widget information and simulating the physics
Disabled // State in which the widget is disabled
}

在不同的状态下 对应不同的执行过程 非常规整、 值得一提的是 leap官方的代码风格很适合我们去仔细钻研 其中不乏一些亮点 对于我们来说很值得借鉴 对于没有形成良好代码风格的新手来说十分值得学习

1
2
3
4
5
6
7
protected virtual void Awake()
{
if (GetComponent<Collider>() == null)
{
Debug.LogWarning("This Widget lacks a collider. Will not function as expected.");
}//碰撞其检测与error/warning输出
}

在这个基类的脚本中 我们基本了解了这个组件的运行机理下面就来看第二个脚本ScrollBase

这个脚本中有几个重要的常数
1,弹簧力(SnapSpringForce);
2,阻力(drag);
3,交互比例(InteractionScale);


这几个参数决定着你出发这个组件并滑动之后 多久能停下来 惯性运行的时间 等等效果
其中甚至运用到了一些公式运算

m_dampingForce = Mathf.Sqrt(4.0f * SnapSpringForce);

(阻尼/力)
类似这种运算 但最重要的还是定义了文本组件的上下界和显示框的上下界
text

我们可以清晰地看到 在这个组件的下方 文明本下界要长的多


特效t

文本的属性来看 我们也可以清楚地看到纵横比也可以修改文字

而从脚本层面我们可以看到更多的这种继承关系

这是基类中的

1
2
3
4
5
6
protected virtual void ResetPivots()
{
m_pivot = transform.localPosition;
if (m_target != null)
m_targetPivot = transform.parent.InverseTransformPoint(m_target.transform.position);
}

这是scrollbase中的

1
2
3
4
protected override void ResetPivots() {
base.ResetPivots();
m_contentPivot = ContentTransform.localPosition;
}

最后不得不佩服的是 为了实现 惯性这种细微的操作感代码的复杂程度高了不少 以至于催生了很多的计算方法

1
2
3
4
5
6
//计算一维弹簧力
protected float calculate1DSpringForce(float offsetVector) {
float springForce = offsetVector * SnapSpringForce;
float dampingForce = m_dampingForce * (m_velocity);
return springForce - dampingForce;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected float calculateOverrunMagnitude() {
//计算超过量(文本边界超过显示边界的量)
float overrunDistance = 0.0f;
// Put all positions in object space.
Vector3 localContentTop = transform.InverseTransformPoint(ContentTopBound.position);
Vector3 localContentBottom = transform.InverseTransformPoint(ContentBottomBound.position);
Vector3 localContainerTop = transform.InverseTransformPoint(ContainerTopBound.position);
Vector3 localContainerBottom = transform.InverseTransformPoint(ContainerBottomBound.position);
if (localContentTop.y < localContainerTop.y) {
overrunDistance = localContainerTop.y - localContentTop.y;
}
else if (localContentBottom.y > localContainerBottom.y) {
overrunDistance = localContainerBottom.y - localContentBottom.y;
}
return overrunDistance;
}

至于一些 基类中的状态判断循环所采用的应用方法 我只贴一个例子

ApplyInteractions();

基类中定义了它的抽象方法在状态循环中 当状态为

case LeapPhysicsState.Interacting:

时调用了ApplyInteractions(); 而在scrollbase类中 整个重载了这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected override void ApplyInteractions() {
Vector3 targetInteractorPositionChange = transform.parent.InverseTransformPoint(m_target.transform.position) - m_targetPivot;
targetInteractorPositionChange *= InteractionScale;
targetInteractorPositionChange.x = 0.0f;
targetInteractorPositionChange.z = 0.0f;
Vector3 contentCurrentPosition = ContentTransform.localPosition;
Vector3 newContentPosition = m_contentPivot + targetInteractorPositionChange;
Vector3 velocity = (newContentPosition - contentCurrentPosition) / Time.deltaTime;
m_velocity = velocity.y;
ContentTransform.localPosition = newContentPosition;
}

在这种结构下  我们其实如果只是想实现一个交互  那么还是很简单的  但是如果想加强操控感受 改善交互效果 可以说难度非常大 已经超出了新手的能力范围  因为其中涉及到了太多的 UI交互设计技巧  经验和逻辑。


OK~ NEXT

2.按钮

button
照旧 我们先看他的构成 非常清晰有没有
四种图像分别对应

打开状态
关闭状态
转移状态
通用元素

接下来我们抛开他的素材去看代码
这两个类的规模就要小很多了 因为没有很复杂的交互优化 整个组件非常清爽
ButtonDemoGraphics脚本中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void SetActive(bool status)
{
Renderer[] renderers = GetComponentsInChildren<Renderer>();
Text[] texts = GetComponentsInChildren<Text>();
Image[] GUIimages = GetComponentsInChildren<Image>();
foreach (Renderer renderer in renderers)
{
renderer.enabled = status;
}
foreach(Text text in texts){
text.enabled = status;
}
foreach(Image image in GUIimages){
image.enabled = status;
}
}

设置激活的方法显得非常干练包括对材质,图片,文字的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void SetColor(Color color)
{
Renderer[] renderers = GetComponentsInChildren<Renderer>();
Text[] texts = GetComponentsInChildren<Text>();
Image[] GUIimages = GetComponentsInChildren<Image>();
foreach (Renderer renderer in renderers)
{
renderer.material.color = color;
}
foreach (Text text in texts){
text.color = color;
}
foreach(Image image in GUIimages){
image.color = color;
}
}

包括对颜色更改也是

而在对此基类调用的时候ButtonDemoToggle类这种代码结构异常的清晰明了(真的好棒!!啊)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void TurnsOnGraphics()
{
onGraphics.SetActive(true);
offGraphics.SetActive(false);
midGraphics.SetColor(MidGraphicsOnColor);
botGraphics.SetColor(BotGraphicsOnColor);
}
private void TurnsOffGraphics()
{
onGraphics.SetActive(false);
offGraphics.SetActive(true);
midGraphics.SetColor(MidGraphicsOffColor);
botGraphics.SetColor(BotGraphicsOffColor);
}

如果你想使用这个按钮 做一个状态判断的话

1
2
3
4
5
6
7
8
9
10
public override void ButtonTurnsOn()
{
TurnsOnGraphics();
}
public override void ButtonTurnsOff()
{
TurnsOffGraphics();
}

写在这两个方法下 的话 就能达成你的目的啦
比如 我想按钮显示关闭的时候的时候程序暂停运行 那就

1
2
3
4
5
6
7
public override void ButtonTurnsOff()
{
TurnsOffGraphics();
Debug.Break();
}

至于替换原本的ON/OFF的UI纹理 那就不用我教了 相信 能看到这里的都会操作 我们去如何设计按键触发的过程
就有了一个清晰的方案

自定义按钮样式替换元素 ->继承以重用脚本设计->自定义触发操作->完成

总结:

按钮为什么这里就总结了?
答:按钮的类继承关系非常复杂 但是 组件本身有着很好的可修改性 重用性 因此 对于这种良心 组件 我们也不用想着从 脚本角度去修改 替换按钮的外观 和样式 保留官方这种成熟的继承风格 对于我们生成 的软件的稳定性至关重要;

public abstract class ButtonToggleBase : ButtonBase, BinaryInteractionHandler < bool > , IDataBoundWidget < ButtonToggleBase, bool>

随意感受一下这个继承

3.滑动条(slider)

先看图
slider


我们可以看到 为了达成滑动条这个组件 所需要的 步骤 就多得多了
虽然在hierarchy里面全部展开以后很吓人的样子但是其实 只有三类

1.top
2.line
3.dot

我们先从top开始看
其实 他就是按键的马甲而已,SliderDemoGraphics负责控制这个按键的图像
top

top的结构从这个拆分能清楚的看清每个部分
toplayer:圆点按钮中心
midlayer:填充材质(slidersecondary)
botlayer:选定高亮边框

所以在这个类当中 我们会看到按钮中出现过的套路

1
2
3
4
5
6
7
8
public void SetActive(bool status)
{
Renderer[] renderers = GetComponentsInChildren<Renderer>();
foreach (Renderer renderer in renderers)
{
renderer.enabled = status;
}
}
1
2
3
4
5
6
7
8
public void SetColor(Color color)
{
Renderer[] renderers = GetComponentsInChildren<Renderer>();
foreach (Renderer renderer in renderers)
{
renderer.material.color = color;
}
}

Top的构成很简单 但是整个滑动条还是比较复杂的 就是因为这个类SliderDemo的继承SliderBase又继承于 LeapPhysicsSpring 这种继承关系导致我们想从自上而下的修改功能变得不是那么容易
而我们只能从底层向上寻找
Sliderdemo:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected override void sliderPressed()
{
//按下
base.sliderPressed();
PressedGraphics();
}
protected override void sliderReleased()
{
//释放
base.sliderReleased();
ReleasedGraphics();
}

这两个函数描述了按下后和释放后的指令 所以从这里来看 我们可以把一切我们想要的滑动条的按钮触发释放 来激活的事件 写在这两个方法里

最重要的检查触发被放在了高一级的类Sliderbase中的CheckTrigger()方法:

1
2
3
4
5
6
7
8
9
10
private void CheckTrigger()
{
if (State == LeapPhysicsState.Interacting) {
//状态确定
fireSliderChanged (GetSliderFraction ());
if (m_dataBinder != null) {
m_dataBinder.SetCurrentData (GetSliderFraction ());
}
}
}

而最根本的监听来自于一个诡异的方法

1
2
3
4
5
6
7
8
private void onStateChanged(object sender, EventArg<LeapPhysicsState> arg) {
if ( arg.CurrentValue == LeapPhysicsState.Interacting ) {
sliderPressed();//按下
}
else if ( arg.CurrentValue == LeapPhysicsState.Reflecting ) {
sliderReleased();//释放
}
}

EventArg是包含事件数据的类的基类,而onStateChanged()方法中前者是一个对象(其实这里传递的是对象的引用,如button的click事件则sender就是button,相信有过c#/xaml/winfrom/编程经验得同学都见到过这个用法),后面是包含事件数据的类的基类。
而在这个代码中sender就是leap中的一个对象 后面的基类将状态参数CurrentValue 表达出来


讲完了触发那么现在该进一步了
arg.CurrentValue同时将修改State的值 这个值相当于整个类中的状态参量 代表按钮是否被按下

而在这个脚本中 还要根据state的值来进行更多的操作

1
2
3
4
5
6
public enum LeapPhysicsState
{
Interacting, //手指等 触发按钮
Reflecting, //模拟物理特性 从触发被改变回到预置位置
Disabled // 关闭(正常)状态
}

.Enum 类型是所有枚举类型的抽象基类(它是一种与枚举类型的基础类型不同的独特类型)
这里用到enum来准确描述状态 使得代码清晰易懂 易于维护

所以 当我们找到FixedUpdate中的UpdateGraphics()方法后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void UpdateGraphics()
{
float handleFraction = GetHandleFraction();
Vector3 topPosition = transform.localPosition;
topPosition.x = 0f;
topPosition.y = 0f;
topPosition.z -= (1.0f - handleFraction) * 0.25f;
topPosition.z = Mathf.Min(topPosition.z, -0.003f); // -0.003 为保证dots和top永不相交的中间层
topLayer.transform.localPosition = topPosition;
Vector3 botPosition = transform.localPosition;
botPosition.x = 0f;
topPosition.y = 0f;
botPosition.z = -0.001f;
botLayer.transform.localPosition = botPosition;
midLayer.transform.localPosition = (topPosition + botPosition) / 2.0f;
//___________________________________________________________________
if (activeBar)
{
UpdateActiveBar();//激活
}
//______________________________________________________________________
if (numberOfDots > 0)
{
UpdateDots();//根据bot的位置判断
}
}


在此我们只从Dot展开去讲 因为其他的过程基本上是 八九不离十

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void UpdateDots()
{
for (int i = 0; i < dots.Count; ++i)
{//dot的数量 根据此位置dot的x坐标 和 top的x轴自身坐标对比判断 来逐个绘制 小于高亮 大于常亮
if (dots[i].transform.localPosition.x < transform.localPosition.x)
{
Renderer[] renderers = dots[i].GetComponentsInChildren<Renderer>();
foreach (Renderer renderer in renderers)
{
renderer.material.color = DotsOnColor;
renderer.material.SetFloat("_Gain", 3.0f);//高亮
}
}
else
{
Renderer[] renderers = dots[i].GetComponentsInChildren<Renderer>();
foreach (Renderer renderer in renderers)
{
renderer.material.color = DotsOffColor;
renderer.material.SetFloat("_Gain", 1.0f);//常亮
}
}
}
}

而我们在先前的inspector中也看到了DotsOnColor/DotsOffColor
dot

在其他部分的调用中 和Dots的绘制如出一辙 基本都是结合状态参量来进行判断 例如

1
2
3
4
public void SetWidgetValue(float value) {
if ( State == LeapPhysicsState.Interacting || State == LeapPhysicsState.Disabled ) { return; } // 使得状态在交互过程中稳定
SetPositionFromFraction (value);
}

那么最后来看这个LeapPhysicsBase类中state终极目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected virtual void FixedUpdate()
{
if (m_target == null && State == LeapPhysicsState.Interacting)
{
State = LeapPhysicsState.Reflecting;
}
switch (State)
{
case LeapPhysicsState.Interacting://交互状态
ApplyInteractions();
break;
case LeapPhysicsState.Reflecting://从交互状态返回正常状态
ApplyPhysics();
break;
case LeapPhysicsState.Disabled://关闭(正常)状态
break;
default:
break;
}
ApplyConstraints();
}

这是一个大写的清晰明了 之前对State枚举类型在这里一下就亮了
用Switch来确定状态执行相应状态的方法集

至此 总分总式的把Solider说完了 已经 洋洋洒洒的说了1w字了 那么 我就继续吧

滚轮/表盘(Dial)

照例先看图:
dial


从trigger的网格来看就非常的复杂 所以要是自己想设计一个这种手势操作的UI组件 可以说难度非常
并且 坦白说 这个组件如果不经过自定义或者修改非常的华而不实 因为 他的选项实在是太多了 多到你手退出操作区域的时候 都会产生误操作

那么我们就先从可见外观结构说起

dial2


1.picker//选择器
2.maskpanel//荫罩面
3.backpanel //背景面

更进一步:

dial3

从这开始就开始有意思了起来

首先 这是两个collider 小的这个被设置为滚轮上的字在这个区域被显示为高亮(HighLight)
HilightTextVolume这个类中 清晰的表明了这一点 通过 触发器trigger的三个函数 巧妙的控制了字体的高亮显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//进入
void OnTriggerEnter(Collider other) {
Text text = other.GetComponentInChildren<Text>();
if (text == null) { return; }
text.color = Color.white;
}
//保持
void OnTriggerStay(Collider other){
Text text = other.GetComponentInChildren<Text>();
if (text == null) { return; }
text.color = Color.white;
CurrentHilightValue = text.text;
}
//离开
void OnTriggerExit(Collider other) {
Text text = other.GetComponentInChildren<Text> ();
if (text == null) { return; }
text.color = textColor;
}

这给我们提供了一个设计技巧 对于UI设计提升期的童鞋来说 是个相当有用的模式
进入->保持->离开
|__|

而之后的用于隐藏的collider原理几乎一致在DatePickerHideVolume类中:

1
2
3
4
5
6
7
8
9
10
11
12
void OnTriggerEnter(Collider other) {
Text text = other.GetComponentInChildren<Text> ();
if (text == null) { return; }
text.enabled = false;//设置隐藏
}
void OnTriggerExit(Collider other) {
Text text = other.GetComponentInChildren<Text> ();
if (text == null) { return; }
text.enabled = true;//设置显示
}

那个大的collider 被放在转盘的圆心位置 所以转盘总有一半是被隐藏的 这样就保证了前视的时候后面的文字不会对前面的造成干扰

都是利用触发器来实现 但是还有个

比较有趣

的问题 再来看图
fade

我们发现 从下到上 文字的透明度越高这是怎么做到的呢?

首先 定义了一条曲线

public AnimationCurve FadeCurve;

curve


在曲线之后通过曲线来计算 透明值

1
2
float opacityMod = FadeCurve.Evaluate(referenceDotDirection);//计算不透明度模
float goalOpacity = m_originalLabelOpacity * opacityMod;//目标透明度
1
2
3
4
5
6
foreach(Text textComponent in m_textLabels) {
//遍历设定
Color textColor = textComponent.color;
textColor.a = goalOpacity;//对文字颜色的Alpha通道进行修改
textComponent.color = textColor;
}

讲完了这个外在 再来讲内在的控制部分

准备好受虐吧 这部分我也不太懂了 所以我尽量说我的理解 有错误请指出

先是生成label

public List< string > DialLabels;

labe


根据字符串List的数量来生成label 这个数量我们当然可以确定 所以 我们想使用这个控件 的话
当然 应该使得这个list.count的数量趋近合理 这样不仅使得控件简洁明了 还是得交互变得更容易 准确性更高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void generateAndLayoutLabels() {
float currentLayoutXAngle = LabelAngleRangeStart;
for( int i=1; i<=DialLabels.Count; i++ ) {
Transform labelPrefab = Instantiate(LabelPrefab, DialCenter.transform.position, transform.rotation) as Transform;
//生成
labelPrefab.Rotate(currentLayoutXAngle, 0f, 0f);
LabelAngles.Add (-currentLayoutXAngle);
labelPrefab.parent = DialCenter;
labelPrefab.localScale = new Vector3(1f, 1f, 1f);
Text labelText = labelPrefab.GetComponentInChildren<Text>();
labelText.text = DialLabels[i - 1];
DialLabelAngles.Add(DialLabels[i - 1], -currentLayoutXAngle);
labelText.transform.localPosition = new Vector3(0f, 0f, -DialRadius);
currentLayoutXAngle = ((Mathf.Abs(LabelAngleRangeStart) + Mathf.Abs(LabelAngleRangeEnd))/(DialLabels.Count)) * -i;
//调整
}
LabelPrefab.gameObject.SetActive(false); // Turn off the original prefab that was copied.
}

开始句柄
更改句柄
结束句柄

1
2
3
4
//模拟交互的句柄
public event EventHandler<EventArg<int>> ChangeHandler;
public event EventHandler<EventArg<int>> StartHandler;
public event EventHandler<EventArg<int>> EndHandler;

当然触发检测是必不可少的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void OnTriggerEnter (Collider other)
{
if (target_ == null && IsHand (other)) {
//像之前一样确定触发体是手
target_ = other.gameObject;//旋转角
pivot_ = transform.InverseTransformPoint (target_.transform.position) - transform.localPosition;//旋转轴心
if (GetComponent<Rigidbody>().isKinematic == false)
transform.GetComponent<Rigidbody>().angularVelocity = Vector3.zero;
interacting_ = true;
if (dialGraphics)
dialGraphics.HilightDial ();
}
}
void OnTriggerExit (Collider other)
{
if (other.gameObject == target_) {
EndInteraction ();//结束交互
}
}

从触发检测部分计算出的目标角度和旋转轴心 然后我们找到应用的方法

1
2
3
4
5
6
7
protected virtual void ApplyRotations ()
{
//旋转至目标角度
Vector3 curr_direction = transform.InverseTransformPoint (target_.transform.position) - transform.localPosition;
//轴心
transform.localRotation = Quaternion.FromToRotation (pivot_, curr_direction) * transform.localRotation;
}

而这一切都在基类的FixedUpdate()中运行

1
2
3
4
5
6
7
8
9
10
11
12
void FixedUpdate ()
{
if (target_ == null && interacting_) {
// 当交互时手已经被销毁
EndInteraction ();//结束交互
}
if (target_ != null) {
ApplyRotations ();//应用旋转
}
ApplyConstraints ();//应用约束以保证交互结束后 返回相应位置
}

至此 一些零碎的约束我已经无力再写下去了 就来归纳一下组件共同拥有的一些特性:

1.触发检测用于检测手和组件的交互 同时一般返回 :
触发初始信息
触发过程信息
触发结束信息

2.约束条件:
一般含有交互产生的改变量
交互后根据规则的对改变量的判断
对判断结果的执行

3.静止状态:
一般含有系统用于初始化的初始量
对于不产生交互状态下的静态量
一般为产生触发后约束条件的结果


至此

我们讲完了所有四个预置控件的结构与应用方法
为我们在unity3D引擎中设计 VR/AR环境下的交互逻辑铺平了道路;
但是很明显的是 在VR环境中设计交互操作明显有一定的方法论 触发设计,特效设计,逻辑设计每个部分单独拿出来都可以写一本书 并且在讲求经验的情况下 这和以前的UI设计相去甚远 平面UI的难度因为操控体系的健全而变得较为容易 但在三维环境下 无论是对手的模拟 还是对现实环境的模拟 都使得UI设计变得没有统一规则
你可以虚拟出一个平面实现来类触屏操作而套用平面UI的设计方法
更可以通过虚拟物体来体现虚拟现实技术的优势 这就使得设计变得无限可能
这两种方式并没有优劣之分 各个应用环境之下 恰当的采用 效果才是最佳的。


最后 因为这系列文章过长 因此下次更新可能会在较长之后了(都是眼泪);

Bye~ See you next month~

过了这么久才来更这篇实在是因为项目工程量实在是不允许

首先声明我并不是专业的UI设计人员 我们所有的leap UI设计全部来源与项目需求 且因为项目不是商业项目 所以设计方法看起来有一种“邪门歪道”的既视感 但是为VR/AR环境中的交互设计提供了一种思路

特别提示:以下内容并不是针对初学者而言的

如果你是个unity/leap开发的双面小白 那么基本可以不用看这篇文章了

整个项目的效果可以去这里看视频

OK~

言归正传

我们先来看看效果:

sence2


这是一个用手势控制选择的界面选择的时候会根据手势的挥动方向来进行左右切换
也就是我们上一篇文章中提到的手势操作交互,这类操作不与UI元素进行交运算(触发)
所以设计起来相对独立 也就是说 这类UI元素的制作可以按照普通的UI制作方式来进行

所以 在整个软件中 这个UI显得最普通 但是就是普通依旧不是那么好做

想实现一个类似IPhone音乐的选择界面并不是那么容易
像这样



这种动态效果通过静态图片无法很好地展示 但是想试想一个细微的效果实际上需要大量的工作


这个UI整个比较复杂所以我只能不太完整的去讲解
目录
这是它的目录结构 在某一个版本中 我曾经写了动态生成这些展示标记 但是由于数量变化会引起后面的缩放系数bug 我就改回了手动添加
text
这是每个UI元素的组成 其中有些组件是不必要的主要是为了原先鼠标操作而设计
( 后来整体取消了软件鼠标操作的功能 但保留了这些组件 比如碰撞器 对于一个手势操纵的UI类型来说完全是没必要的)

所以

针对这个UI元素来说 可讲的就只剩下

1.怎么细致的实现元素转动效果
2.怎么用手势控制旋转


一,转动效果 这其实很多demo里面都会出现 而这个实现的那我们来看看主要的结构
这里写图片描述
命名清晰明了 enhance控制器 控制 gameobject(UItexture)
UItexture下放了一个canvas 然后canvas上有三个text组件放置三组文字
sprite是背景控制(这里的sprite其实就是截取图形的插件)
有了六七列表我们就可以在控制器里通过代码来控制了

这里写图片描述

有两个类一个 EnhancelScrollView一个 EnhanceItem
首先定义 EnhanceItem脚本附在每个UItexture上面 设置flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
using UnityEngine;
using System.Collections;
public class EnhanceItem : MonoBehaviour
{
// 在ScrollViewitem中的索引
internal int scrollViewItemIndex = 0;
// 夹角大小
internal float angla = 0f;
// 动画时间值
internal float dValueTime = 0f;
// 前后项
internal EnhanceItem front, back;
/*
*
*
*
*
* internal关键字是类型和类型成员的访问修饰符。只有在同一个程序集的文件中,内部类型或者是成员才可以访问。
* 这是msdn上对internal的描述。
* 类型就是enum(枚举类型),class(类),interface(接口),struct(结构)等类型。
* 类型成员如函数,成员变量等。
*
* 一个完整的.exe或者是.dll文件就是一个程序集,一般伴随着exe程序集产生的还有一个程序集清单
* ,.exe.config文件。下面我就用一个例子来说明“internal关键字是类型和类型成员的访问修饰符。
* 只有在同一个程序集的文件中,内部类型或者是成员才可以访问”。
*
*
*/
public int flag = 777;//flag
private Vector3 targetPos = Vector3.one;
private Vector3 targetScale = Vector3.one;
private Transform mTrs;
private UITexture mTexture;
void Awake()
{
mTrs = this.transform;
mTexture = this.GetComponent<UITexture>();
}
void Start()
{
UIEventListener.Get(this.gameObject).onClick = OnClickScrollViewItem;
}
// 当点击Item,将该item移动到中间位置
private void OnClickScrollViewItem(GameObject obj)
{
EnhancelScrollView.GetInstance().SetHorizontalTargetItemIndex(scrollViewItemIndex);
}
/// <summary>
/// 更新该Item的缩放和位移
/// </summary>
public void UpdateScrollViewItems(float xValue, float yValue, float scaleValue)
{
targetPos.x = xValue;
targetPos.y = yValue;
targetScale.x = targetScale.y = scaleValue;
mTrs.localPosition = targetPos;
mTrs.localScale = targetScale;
}
public void SetSelectColor(bool isCenter)
{
if (mTexture == null)
mTexture = this.GetComponent<UITexture>();
if (isCenter)
mTexture.color = Color.white;
else
mTexture.color = Color.gray;
}
}

EnhancelScrollView脚本作为控制脚本附着在控制物体上
他就相当于每个物体

那么手势控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Leap;
// [ExecuteInEditMode]
public class EnhancelScrollView : MonoBehaviour
{
// 含有滑动项目的面板
public GameObject enhanceScrollView;
// 缩放曲线
public AnimationCurve scaleCurve;
// 位移曲线
public AnimationCurve positionCurve;
// 动画时间
public float duration = 0.2f;
// 宽度
public float width = 800.0f;
// y轴坐标固定值(所有的item的y坐标一致)
public float yPositionValue = 46.0f;
// 中间显示目标时间线(0显示第一个,0.5显示中间一个)
public float horizontalTargetValue = 0.0f;
// 滑动起始力
public float touchStartPower = 0.5f;
// 滑动阻力
public int touchForce = 120;
// 目标对象列表
private List<EnhanceItem> scrollViewItems;
// 目标对象Widget脚本,用于depth排序
private List<UITexture> textureTargets;
// 开始X坐标
private float startXPos = 0f;
// 当前处于中间的item
public EnhanceItem centerItem;
// 当前出移动中,不能进行点击切换
private bool isInChange = false;
// 位置动画的中间位置时间
private float positionCenterTime = 0.5f;
// 当前精度小数位
private int curACC = 4;
// 横向变量值
private float horizontalValue = 0.0f;
// 移动动画参数
private float originHorizontalValue = 0.0f;
private float currentDuration = 0.0f;
private static EnhancelScrollView instance;
internal static EnhancelScrollView GetInstance()
{
return instance;
}
//内部类型或成员才是可访问的
//__________________________________________________________
// 动作识别部分定义
public HandController hc;
Hand lefthand=null;
Hand righthand = null;
Hand last_lefthand = null;
Hand last_righthand = null;
Frame currentFrame = null;//定义当前帧
bool lefthandexist = false;//判断左右手是否在场景中存在
bool righthandexist = false;
float sweptAngle = 0;//初始化角度为零
int mark=0;//标记 与下面的函数作用
// 0代表不转动,-1代表向左转,1代表向右转
//____________________________________________________________
void checkmark(int sign)
{
if (sign == 1) {
SetHorizontalTargetItemIndex(centerItem.front.scrollViewItemIndex);
}
if (sign == -1) {
SetHorizontalTargetItemIndex(centerItem.back.scrollViewItemIndex);
}
mark = 0;
}
//执行转动的真正操作 检查标记从而确保只执行一次否则一次挥动将转动多次
void Awake()
{
instance = this;
}
void Start()
{
hc.GetLeapController().EnableGesture(Gesture.GestureType.TYPECIRCLE);
hc.GetLeapController().EnableGesture(Gesture.GestureType.TYPESWIPE);
hc.GetLeapController().EnableGesture(Gesture.GestureType.TYPE_SCREEN_TAP);
hc.GetLeapController ().EnableGesture (Gesture.GestureType.TYPEKEYTAP);
hc.GetLeapController ().EnableGesture (Gesture.GestureType.TYPEINVALID);
//开启所有手势 其实在这个脚本中只开启typeswipe就够了
// ____________________________________________
InitData();//初始化数据
// 设置第一个为选中状态
SetHorizontalTargetItemIndex(0);
}
/// <summary>
/// 初始化数据
/// </summary>
private void InitData()
{
startXPos = -(width / 2);
scrollViewItems = new List<EnhanceItem>();
scrollViewItems.AddRange(enhanceScrollView.GetComponentsInChildren<EnhanceItem>());
if (textureTargets == null)
textureTargets = new List<UITexture>();
float anglaDValue = 360 / scrollViewItems.Count;
int centerIndex = scrollViewItems.Count / 2;
for (int i = 0; i < scrollViewItems.Count; i++)
{
scrollViewItems[i].scrollViewItemIndex = i;
scrollViewItems[i].angla = anglaDValue * i;
scrollViewItems[i].dValueTime = GetCurveTimePos(scrollViewItems[i].angla);
// 构造环形链
scrollViewItems[i].front = i == 0 ? scrollViewItems[scrollViewItems.Count - 1] : scrollViewItems[i - 1];
scrollViewItems[i].back = i == (scrollViewItems.Count - 1) ? scrollViewItems[0] : scrollViewItems[i + 1];
UITexture tmpTexture = scrollViewItems[i].gameObject.GetComponent<UITexture>();
textureTargets.Add(tmpTexture);
scrollViewItems[i].SetSelectColor(false);//设为选中状态 调用了enhanceitem中的方法将mtexture中的混合颜色设为白色以显示高亮
}
}
//_____________________________________________________________________________________________
void Update()
{
if (!isInChange)
{
touch();
return;
}
currentDuration += Time.deltaTime;
float percent = currentDuration / duration;
horizontalValue = Mathf.Lerp(originHorizontalValue, horizontalTargetValue, percent);
UpdateEnhanceScrollView(horizontalValue);
SortDepth();
if (currentDuration > duration)
{
centerItem = textureTargets[textureTargets.Count - 1].gameObject.GetComponent<EnhanceItem>();
centerItem.SetSelectColor(true);
isInChange = false;
}
}
/// <summary>
/// 更新水平滚动
/// </summary>
/// <param name="fValue"></param>
private void UpdateEnhanceScrollView(float fValue)
{
for (int i = 0; i < scrollViewItems.Count; i++)
{
EnhanceItem itemScript = scrollViewItems[i];
float xValue = GetXPosValue(fValue, itemScript.dValueTime);
float scaleValue = GetScaleValue(fValue, itemScript.dValueTime);
itemScript.UpdateScrollViewItems(xValue, yPositionValue, scaleValue);
}
}
//滑动X轴增量位置
float xMoved;
private void touch()
{
// 记录滑动位置
if (Input.touchCount == 1 && Input.GetTouch(0).phase == TouchPhase.Moved)
{
//获取手指自最后一帧的移动
float x = Input.GetTouch(0).deltaPosition.x;
xMoved = x;
}
// 滑动结束时判断故事翻页
if (Input.touchCount == 1 && Input.GetTouch(0).phase == TouchPhase.Ended)
{
if (centerItem == null || Mathf.Abs(xMoved) < touchStartPower)
return;
int count = (int)(Mathf.Abs(xMoved * scrollViewItems.Count/ touchForce)) + 1;
int minHalfCount = Mathf.CeilToInt((float)scrollViewItems.Count / 2) - 1;
if (count > minHalfCount)
{
count = minHalfCount;
}
if(xMoved > 0)
{
SetHorizontalTargetItemIndex(GetMoveIndex(centerItem, -count));
}
else if (xMoved < 0)
{
SetHorizontalTargetItemIndex(GetMoveIndex(centerItem, count));
}
xMoved = 0;
}
}
/// <summary>
/// 缩放曲线模拟当前缩放值
/// </summary>
private float GetScaleValue(float sliderValue, float added)
{
float scaleValue = scaleCurve.Evaluate(positionCenterTime + sliderValue + added);
return scaleValue;
}
/// <summary>
/// 位置曲线模拟当前x轴位置
/// </summary>
private float GetXPosValue(float sliderValue, float added)
{
float evaluateValue = startXPos + positionCurve.Evaluate(positionCenterTime + sliderValue + added) * width;
return evaluateValue;
}
/// <summary>
/// 计算位置动画中的时间点
/// </summary>
/// <param name="anga">角度值,360度=1</param>
/// <returns></returns>
private float GetCurveTimePos(float anga)
{
// 设定0.5为位置中间
return Round(anga / 360f, curACC);
}
// 获取项目A到项目B之间最小的时间差值(圆形角度计算,1=360度)
private float GetCurveTimeDValue(EnhanceItem itemA, EnhanceItem itemB)
{
return Round((Mathf.DeltaAngle(itemA.angla, itemB.angla)) / 360f, curACC);
}
private void SortDepth()
{
textureTargets.Sort(new CompareDepthMethod());
for (int i = 0; i < textureTargets.Count; i++)
textureTargets[i].depth = i;
}
/// <summary>
/// 用于层级对比接口
/// </summary>
private class CompareDepthMethod : IComparer<UITexture>
{
public int Compare(UITexture left, UITexture right)
{
if (left.transform.localScale.x > right.transform.localScale.x)
return 1;
else if (left.transform.localScale.x < right.transform.localScale.x)
return -1;
else
return 0;
}
}
//核心滚动函数
/// <summary>
/// 设置横向轴参数,根据缩放曲线和位移曲线更新缩放和位置
/// </summary>
internal void SetHorizontalTargetItemIndex(int itemIndex)
{
if (isInChange)
return;
EnhanceItem item = scrollViewItems[itemIndex];//_____________新场景中根据这个来判断
if (centerItem == item)
return;
Debug.Log ("item = " + item.name);
if (item.name == "Texture01") {
//Debug.Log("YES");
}
// _________________________________________________________________________________________
float dvalue = centerItem == null ? 0 : GetCurveTimeDValue(centerItem, item);
// 更改target数值,平滑移动,设负数倒着转
horizontalTargetValue += -dvalue;
beginScroll(horizontalValue, horizontalTargetValue);
}
void FixedUpdate()
{
// ___________________________________________________________________________________手势swipe模块
this.currentFrame = hc.GetFrame ();
Frame frame = hc.GetFrame ();
Frame lastframe = hc.getlastframe ();
GestureList gestures = this.currentFrame.Gestures ();
Vector swipedirection=null;
foreach (Gesture g in gestures) {
if(g.Type==Gesture.GestureType.TYPE_SWIPE)
{
SwipeGesture swipe=new SwipeGesture(g);
swipedirection=swipe.Direction;
//Debug.Log("direction is "+swipedirection);
}
}
if (swipedirection.x > 0) {//判断手势向左还是向右参数向左则小于0向右则大于0
Debug.Log("right");
mark=1;
}
if (swipedirection.x < 0) {
Debug.Log("left");
mark=-1;
}
checkmark (mark);//检查参数以完成UI的旋转
// ————————————————————————————————————————————————————————————————————————————————————————————————————————————
}
/// <summary>
/// 开始滚动
/// </summary>
/// <param name="startTime"></param>
/// <param name="endTime"></param>
private void beginScroll(float startTime, float endTime)
{
if (isInChange)
return;
foreach (EnhanceItem item in scrollViewItems)
{
item.SetSelectColor(false);
}
originHorizontalValue = Round(startTime, curACC);
horizontalTargetValue = Round(endTime, curACC);
currentDuration = 0.0f;
isInChange = true;
}
/// <summary>
/// 向右选择角色按钮
/// </summary>
public void OnBtnRightClick()
{
if (isInChange)
return;
SetHorizontalTargetItemIndex(centerItem.back.scrollViewItemIndex);
}
/// <summary>
/// 向左选择按钮
/// </summary>
public void OnBtnLeftClick()
{
if (isInChange)
return;
SetHorizontalTargetItemIndex(centerItem.front.scrollViewItemIndex);
}
/// <summary>
/// 获取移动后的项目索引
/// </summary>
/// <param name="item">当前项目</param>
/// <param name="count">移动位数,负数表示倒移</param>
/// <returns></returns>
private int GetMoveIndex(EnhanceItem item, int count)
{
EnhanceItem curItem = item;
for (int i = 0; i < Mathf.Abs(count); i++)
{
curItem = count > 0 ? curItem.back : curItem.front;
}
return curItem.scrollViewItemIndex;
}
/// <summary>
/// 按指定小数位舍入
/// </summary>
/// <param name="f"></param>
/// <param name="acc"></param>
/// <returns></returns>
private float Round(float f, int acc)
{
float temp = f * Mathf.Pow(10, acc);
return Mathf.Round(temp) / Mathf.Pow(10, acc);
}
/// <summary>
/// 截取小数
/// </summary>
/// <param name="f"></param>
/// <returns></returns>
private float CutDecimal(float f) {
return f - (int)f;
}
}

以缩放曲线来决定 元素在位置发生转动后 大小的变化规则
以位移曲线来规定 元素的运动位置轨迹
两条曲线分别为:
scaleposition

可见一个小功能的实现竟然用了500+line
虽然不难 但是这个排除bug 保证逻辑正确的过程是在太令人头疼 构造环形链的代码也比较抽象

1
2
scrollViewItems = new List<EnhanceItem>();
scrollViewItems.AddRange(enhanceScrollView.GetComponentsInChildren<EnhanceItem>());

而之后的代码将手势识别的代码也包括进去了 主要思路就是判断挥动手势的方向 根据这个参数来确定UItexture整体转动的方向

至此我们的这个类型的UI代码讲解也结束了
总结一下手势控制类UI:

1,此类UI构建方式和普通UI区别较小(几乎没区别)无论是平面,立体。
2,此类UI的控制方式最好生成一个统一的操作函数例如turnleft(),turnright();之后直接用手势触发就好
3,如果使用单一手势触发的话最好设置一定的时延 否则 误操作将会非常多

最后

也就是这个系列的最后一些内容
我来讲讲印射式UI(或者说印射操作)

这里用到的就是

射线

leap和射线可以说是非常蛋疼的组合 我本身对这个功能的探索时间长达一个月
我就细细说一下这个奇葩的功能的由来:

首先

面对一个模型我们想把它散开 再在每个子物体上加上盒触发器 这种算法在unity里简直好像一坨屎
结果就如下图:
这里写图片描述
boxcollider不能很好的契合模型
Meshcollider又因为面数限制而不能使用
那只能用box凑活了 这就导致一个问题用手来触发这一坨屎一样的东西自然是很不精确的
并且一旦触及百万面的工业模型 性能就迅速下降到崩溃边缘:


这里写图片描述


像这样 于是我们怎么才能解决这个精确触发的问题呢?这是问题一

其二

leapmotion的手的大小和识别区域是绑定的 所以放大识别区域的结果是必然要放大手的模型
比如我想增大在空间内控制器的操作范围就会产生这种画面
hand


手的模型变大严重阻碍了软件的使用,所以又要保证手的大小适中作指示
而手的操作范围被限制在控制器识别范围之内。这就诞生了一个设计上的矛盾。
画面上要求手变小 操作范围上要求手变大。
于是
就有了射线

初次写射线功能其实和leap并不能很好的一起工作,因为有太多的隐含参数数值需要去探索

ray


用这种方法可以进行精确化的触发,选择。在这个基础上进行设计可以很多leap官方demo完成不了的操作
那下面我们先来看看代码 这个脚本挂camera上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
using UnityEngine;
using System.Collections;
using Leap;
public class ray : MonoBehaviour {
public hand_script2 control_script;//控制脚本定义
public HandController hc=null;
public RaycastHit hit;
public Vector3 handdir;
public Vector3 handpos;
//public GameObject cube = null;
public GameObject highlight=null;
float hx, hy, hz=0;
float dx, dy, dz = 0;
public GameObject particle_light=null;
float xr,yr,zr;
private float index=0;
Hand lefthand=null;
Hand righthand=null;
public GameObject color_apply=null;
Color origin=Color.black;
public Color apply_color;
// ______________________________________________________________
bool squize(float radius)//判断手势是否为握持 阀值为35
{
if (radius < 37)
return true;
else
return false;
}
// ---------------------------------------------------------------------------
private bool IsHand(Collider collider)
{
return collider.transform.parent && collider.transform.parent.parent && collider.transform.parent.parent.GetComponent<HandModel>();
}//判断触发器是否是手
void Awake()
{
}
// Use this for initialization
void Start () {
Debug.Log ("Start");
//空间两点距离计算:sqrt(pow(x1-x2,2)+pow(y1-y2,2)+pow(z1-z2,2))
index = hc.transform.localScale.x / 1000;//计算后面的缩放指数
}
// Update is called once per frame
void Update () {
bool lefthandexit = false;
bool righthandexit = false;
//Debug.Log ("Update");
//Vector3 fwd = new Vector3 (0, 0,10 );
Frame frame = hc.GetFrame ();
Frame lastframe = hc.getlastframe ();
apply_color = color_apply.gameObject.GetComponent<RGB> ().rgb;//获取color混合界面的颜色值可去掉
foreach (Hand hand in frame.Hands) {
if (hand.IsLeft) {
lefthandexit = true;
lefthand = hand;
}
if (hand.IsRight) {
righthandexit = true;
righthand = hand;
foreach (Finger finger in righthand.Fingers) {
Finger.FingerType type=finger.Type();
if (type == Finger.FingerType.TYPE_INDEX) {
dx = finger.Direction.x;
dy = finger.Direction.y;
dz = finger.Direction.z;
hx = finger.TipPosition.x;
hy = finger.TipPosition.y;
hz = finger.TipPosition.z;
}
//获取中指的信息finger_INDEX
}
//dx = hand.Direction.x;
//dy = hand.Direction.y;
//dz = hand.Direction.z;
handdir = new Vector3 (dx*index,dy*index,-dz*index);//必须乘指数
// ----------------------------------------------------------------------
//hx = hand.PalmPosition.x;
//hy = hand.PalmPosition.y;
//hz = hand.PalmPosition.z;
handpos = new Vector3 (hx*index, hy*index,-hz*index);//同样乘指数
// ----------------------------------------------------------
//Debug.Log ("righthand_position is " + handpos + "righthand dir is " + handdir);
//Debug.Log ("reall date is" + hand.PalmPosition + " /// " + hand.Direction);
xr = hand.RotationAngle (lastframe, Vector.XAxis);
yr = hand.RotationAngle (lastframe, Vector.YAxis);
zr = hand.RotationAngle (lastframe, Vector.ZAxis);
}
}
//cube.transform.Rotate (new Vector3(xr*index,yr*index,-zr*index),Space.World);
//原先用cube表示手的信息与射线做对照
/*
float x = this.transform.rotation.x;
float y = this.transform.rotation.y;
float z = this.transform.rotation.z;
Vector3 rota = new Vector3 (x, y, z);
Debug.Log ("this.position=" + this.transform.position);
dx = this.transform.position.x;
dy = this.transform.position.y;
dz = this.transform.position.z;
Vector3 position = new Vector3 (dx,dy+100,dz);
*/
if (squize(lefthand.SphereRadius) && (!squize (righthand.SphereRadius))) {
//如果左手握持且右手不握持
//bool Physics.Raycast(Ray ray, Vector3 direction, RaycastHit out hit, float distance, int layerMask)
if (Physics.Raycast(handpos,handdir,out hit,10000,1)) {
//(Physics.Raycast(射出点,射出方向向量,输出触发信息存储点,长度,1))
particle_light.transform.position =new Vector3(hit.point.x,hit.point.y,hit.point.z-2);//此处为粒子系统 用粒子系统跟随触发点以显示触发位置
if ((highlight.gameObject.name.ToString() != hit.collider.gameObject.name.ToString()) &&
origin == Color.black&&
(!IsHand(hit.collider))&&
(hit.collider.gameObject.GetComponent<Renderer>()!=null)) {
//判断是否第一次触发
Debug.Log ("first");
highlight = hit.collider.gameObject;
control_script.main_son = hit.collider.gameObject;
origin = highlight.gameObject.GetComponent<Renderer> ().material.color;
highlight.gameObject.GetComponent<Renderer> ().material.color = Color.gray;
}
if ((highlight.gameObject.name.ToString () != hit.collider.gameObject.name.ToString ()) &&
origin != Color.black&&
(!IsHand(hit.collider))&&
((hit.collider.gameObject.GetComponent<Renderer>()!=null))) {
//判断是否变更触发物体
Debug.Log ("change");
control_script.main_son = hit.collider.gameObject;
highlight.gameObject.GetComponent<Renderer> ().material.color = origin;
highlight = hit.collider.gameObject;
origin = highlight.gameObject.GetComponent<Renderer> ().material.color;
highlight.gameObject.GetComponent<Renderer> ().material.color = Color.gray;
}
//以上用来交换信息 坐到触发高亮
/*
if((highlight.gameObject.name.ToString()==hit.collider.gameObject.name.ToString())&&origin!=Color.black)
{
highlight = hit.collider.gameObject;
highlight.gameObject.GetComponent<Renderer> ().material.color = Color.red;
}
*/
//hit.collider.gameObject.GetComponent<Renderer> ().material.color = Color.red;
Debug.Log ("OBJ-name is " + hit.collider.gameObject);
//Debug.Log ("hit point is " + hit.point + " distance = " + hit.distance);
//Debug.Log ("Success");
Debug.DrawLine (handpos, hit.point, Color.red);
//在sence中显示射线并设置为红色
//Debug.Break ();
}
if (lefthandexit==true) {
Debug.Log ("________________");
//Debug.Break();
}
}
}
}
//射线脚本bug汇总:
/*
* 1.发射点以世界坐标为准则:
* 如handcontroller的放大倍率为100则数乘index指数为0.1
* 2.必须将控制器的位置考虑进去
* 3.射线距离尽可能大
*/

脚本毕来说一些特殊的地方
那些隐含的值最后确定为

index = hc.transform.localScale.x / 1000;//计算后面的缩放指数

而以上都基于 控制器在原点也就是(0,0,0)
而针对 非原点的情况

要把控制器的坐标也算进去才能保证你的射线是从指尖发出去的

如果想更改射线发射的位置 那么判断进其他的手指类型就好了 代码里面非常清楚

同时 在射线的位置用一个粒子特效做一个触发位置提示 效果像这样


light


在这个体系之下 你就可以把手指当做一个三维环境里的鼠标 对任意一个点进行触发操作以进一步的执行其他部分

所以我们的触发UI可以不用再设计在控制器的范围之内 我们可以用射线功能触发几乎无限远的元素

比如在一个环境中我们通过一定映射关系去触发区域之外的按钮
不同
如图所示


至此

所有LeapUI设计专题所有内容都讲完了
洋洋洒洒的九万多字
再次:

本人水平有限,有错误请联系qq:785929784
也欢迎有兴趣的开发者加入qq群:343074971
共同交流共同进步。

感谢群友对本博客的支持

后续将更新

欢迎支持

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2017 Winshare All Rights Reserved.

访客数 : | 访问量 :