0%

Android + Cardboard + Leap Motion 解决方案

这学期做的 VR 大作业,想使用 Leap Motion 提供的手势识别进行操控,但是没有各种 Headset 可以用(对,说的就是你俩 HTC 和 Oculus)。然而人家 Leap Motion 目前只提供 PC 和上面两兄弟的支持。那我们只有 Cardboard 怎么办呢?这里稍微记录一下。

我们在 PC 上利用 Leap Motion SDK 获取手势数据,然后传给 Android。传的方式参考了这里的思路:用 USB 将 PC 与 Android 连接起来,然后在 PC 端用 adb 建立连接后,Android 只需作为服务器监听 localhost 的一个端口,同时 PC 端作为客户端同样访问 localhost 的一个端口传数据即可。

目前 PC 端使用 Python 语言实现,Android 端使用 Unity 即 C# 语言实现。注意这里的 Unity 没有 Leap Motion 支持。因而它获取手势数据完全依赖于 PC 端的传入。如何选择适当的数据传入,并重现手的模型看起来是一个问题。

用户体验方面,我们在某宝上买了一个 Leap Motion 支架,它本来是为两个 Headset 准备的,但我们将其如法炮制到 Cardboard 上。目前几可以假乱真。

基于 USB+Unity 实现 Android 与 PC 通信

我们这里先实现上述任务的一个简单版本,实现 Android 与 PC 的通信即可。

我们基于这份代码进行改编。由于这里重点不在于 Socket 的使用,而且我也没搞懂,因此并不打算详细介绍。

Android Server

在 Unity 新建一个 EmptyObject 名为 Server 并添加名为 ServerController 的脚本如下:

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
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;

public class ServerController : MonoBehaviour
{
private const int BUFFER_SIZE = 8192;
private byte[] receiveBuffer;
private void Start()
{
receiveBuffer = new byte[BUFFER_SIZE];

Socket serverSocket = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp
);
IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint Point = new IPEndPoint(iPAddress, 8888);

serverSocket.Bind(Point);

serverSocket.Listen(10);

Debug.Log("begin accepting!");
serverSocket.BeginAccept(AcceptCallBack, serverSocket);
}

private void AcceptCallBack(IAsyncResult ar)
{
Socket serverSocket = ar.AsyncState as Socket;
Socket clientSocket = serverSocket.EndAccept(ar);

string msgSend = "Hello world!";
byte[] sendBuffer = Encoding.ASCII.GetBytes(msgSend);
clientSocket.Send(sendBuffer);

clientSocket.BeginReceive(receiveBuffer, 0, 1024, SocketFlags.None, ReceiveCallBack, clientSocket);

serverSocket.BeginAccept(AcceptCallBack, serverSocket);
}

private void ReceiveCallBack(IAsyncResult ar)
{
Socket clientSocket = null;
try
{
clientSocket = ar.AsyncState as Socket;
int count = clientSocket.EndReceive(ar);
string msgReceive = Encoding.ASCII.GetString(receiveBuffer, 0, count);
Debug.Log("client message: " + msgReceive);
clientSocket.BeginReceive(receiveBuffer, 0, 1024, SocketFlags.None, ReceiveCallBack, clientSocket);
}
catch (Exception)
{
Debug.Log("Something wrong in ReceiveCallBack!");
}
}
}

这里是利用 Socket 实现了一个典型的监听在 127.0.0.1:8888 上的异步 TCP 服务器放在 Android 端。我们将其初始化流程放在 Start 中。希望 Unity 能与 C# 原版的 Socket 友好相处 ^_^ 。

我们只需实现两个回调函数 AcceptCallBack, ReceiveCallBack 即可。在接收到客户端发来的信息之后将其用 log 输出到 Unity 。

PC client

接下来是 PC 客户端。

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
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace client
{
class Program
{
static void Main(string[] args)
{
Socket clientSocket = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
clientSocket.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 7777));

byte[] dataReceive = new byte[8192];
int count = clientSocket.Receive(dataReceive);
string msgReceive = Encoding.ASCII.GetString(dataReceive, 0, count);
Console.WriteLine("received from server: " + msgReceive);

string str = Console.ReadLine();
while (true)
{
byte[] dataSend = Encoding.ASCII.GetBytes(str);
clientSocket.Send(dataSend);
str = Console.ReadLine();
if (str.Equals("exit"))
{
break;
}
}

clientSocket.Close();
Console.ReadLine();
}
}
}

简单的 TCP 客户端,连接到服务器 127.0.0.1:7777 。两个端口号是由下面的 adb forward 命令来设定的。这里为了方便直接写死。

流程

  1. 使用 USB 线连接 Android 和 PC ,打开调试模式

  2. 使用 adb 输入在命令行中输入

    1
    adb forward tcp:7777 tcp:8888
  3. 使用 adb 监控 Android 的调试信息,我们只关注 Unity 的输出

    1
    adb logcat -s Unity
  4. 在 Android 上运行 Unity 程序

  5. 在 PC 上运行客户端

我们惊喜的看到,Android server 已经收到了 PC client 传过来的消息!

Json 解析

我们已经将 IDE 换成了 Rider ,就很舒服,可以直接用 Nuget 安装 Json.NET 了。

从工具栏中的 Tools -> NuGet -> Manage Nuget Packages For Solution ,随后直接安装最热门的 Newtonsoft.JSON 即可。

使用这里的代码 ,解析功能正常工作。

Multithreading in Unity

今天一整天都在纠结于 Unity 的线程安全问题(以及看一些奇怪的东西,虽然我已经将他们全部删掉了)

找了很多资料,被大家一致认同的说法是:

  • Unity 在有些地方会使用多线程充分利用多核 CPU

  • 但是这个细节并不暴露给开发者,你所写的那些基于 Event System 被 Unity 在合适的时机调用的代码全部在 Main thread 上运行

    比如 Update, FixedUpdate 那些…

  • 由于 Unity 是这样使用这些你写的代码的,因此你所能够在代码中调用到的 Unity API 都不是线程安全的,甚至为了保证安全性,如果你另开一个线程在其中比方说修改一个 GameObject 的 Position ,Unity 都会抛出一个 Exception

因此对开发者来说应该可以认为 Unity 所做的事情仅仅是单线程的。

后来我将上面的代码改了一下,在异步回调接收到客户端传来的消息后将一个 Text Label 的文字修改,但是并未触发 Exception 。(后来修改一个3D Text 的文字就闪退了…说明我们还是别在这里做一些危险的事情)

今天看了一下异步的机制,在这里比较详细的介绍了异步的底层实现。作者的主要观点是在异步机制中,并不会多出一个“牺牲品”线程被阻塞。而是通过中断,由 OS 来提醒一个 thread pool 线程去调用异步回调函数。不知道中间又经历了怎样复杂的过程,从结果来看,Unity 认为异步回调函数依旧是在 Main thread 上运行的。

我们暂时忽略 Unity 的 Job System 那一套理论,而是绝不吝惜开新线程,反正这种规模又不会带来什么上下文切换开销。

从目前看来,最简单的实现方式当属沿用上面的异步回调方式,在里面,每读到 PC 传来的一帧,就将其保存下来;随后在主线程的 Main thread 中,在 Update 中,读取保存的那一帧并进行相应的修改。

如果上面这种方法没有线程安全问题(我现在仍在怀疑)那可再好不过了。但是如果出了问题,我们仍可以中规中矩地新加一个服务器线程,并通过锁的方式与主线程安全地共享数据。

对于一个懒人来讲, Unity 认为安全我们就认为安全,因此姑且先按照异步的方式实现试一试。

记录一下这些可能有用的回答:

1

2

LeapMotion Rendering

从 head mounted 视角来看,$x$ 轴正方向为左,$y$ 轴正方向为前,$z$ 轴正方向为下。同时,其单位为毫米 (mm) 。