なんかいろいろしてみます

Nov 22, 2017 - 3 minute read - HoloLens

HoloLensでSharingする方法(TCP編)

HoloLens同士のオブジェクト共有で利用されるSharing機能の仕組みを解説して,TCPによる実装方法を紹介します.

HoloLensのSharingとは

複数のHoloLensが同一の位置に同一オブジェクトを表示して共有する機能をSharingといいます. Sharing機能はHoloLensをネットワークに接続することで各デバイスの位置と行動を共有しています. しかし,HoloLensはUnityアプリ起動時の位置を原点(0,0,0)として各オブジェクトの配置を行っているためそれぞれ固有の基準座標を持っています. そのため複数のHoloLensがSharingを行うためには以下の機能が必要になります.

  • 複数のHoloLensの基準座標をそろえるための位置合わせ機能
  • 各HoloLensの位置を行動を共有するためのネットワーク機能

今回はSharingの基本となるネットワーク通信の方法について紹介します.

Sharingのネットワーク通信方法

ネットワーク通信を行う場合

  • ローカルネットワークによるLAN内で通信を行う方法
  • インターネットを経由した遠隔地で通信を行う方法

があります. 今回は遅延が少なく同一環境でのSharingを行う想定でローカルネットワークでのSharing方法を紹介します.

Sharingサーバーを利用

初期のMixedRealityToolkit(旧HoloToolkit)から同梱されているSharingサーバーを利用した通信方法です. MixedRealityToolkit(MRTK)のSource codeのzipファイルに同梱されています(githubのreleaseからダウンロードできるunitypackageには入っていません). MixedRealityToolkit-Unity-1.2017.2.0\External\HoloToolkit\Sharing\Serverの中にSharingService.exeが入っており,これをUnityEditorまたは直接起動することで動作させることができます.

SharingService.exeを直接起動する場合はコマンドラインより

SharingService.exe -local
として起動する必要があります.

SharingサーバーはWindows環境でのみでしか動作しませんが,クラウド(Azure)などのWindowsのあるサーバー環境であればインターネット経由でのShringも可能となっています. ただしSharingサーバーが必ず必要になるためサーバーレス環境では動作できません. またHoloLensとUnityEditor上でのみ動作するため他のデバイスとの連携は難しいかもしれません. 詳しい利用方法に関してはMRTKのSharingのサンプルシーンを確認してください.

UNETを利用

Unityが提供しているネットワーク機能であるUNETもHoloLensで利用できます. 複数台からのネットワーク通信やサーバーレス機能など便利な機能が利用できます. またUnityが対応している様々なデバイスで動作できるので,HoloLensのほかにAndroidなどともSharingができます. ただしUnityが提供している機能なのでUnity以外の実行環境とのSharing,連携は難しいかもしれません. 詳しい利用方法に関してはMRTKのSharingWithUNETのサンプルシーンを確認してください.

自前でTCP通信を行う

Unity以外の実行環境との通信やUnityでは行えないネットワーク処理を行わせたい場合には自前で通信処理を実装する必要があります. ここではTCPによるネットワーク通信を行います. しかしHoloLensの実行環境であるUWPが利用しているC#関数とUnityが利用できるC#関数が異なっているため,デバイスによる処理の切り分けを行う必要があります.

Unity編

主にUnityEditor上,Standalone環境などUWP環境以外で動作します. ネットワークには非同期処理を行いますが,Unity上ではバージョンによってはTaskが利用できない場合があるため,ThreadTcpClientTcpListenerで実装していきます.

  • サーバー側
 1using System;
 2using System.Collections.Generic;
 3using System.Net;
 4using System.Threading;
 5using System.Net.Sockets;
 6using System.Text;
 7
 8public class TcpNetworkServerManager
 9{
10    public TcpNetworkServerManager(int port)
11    {
12        TcpListener tcpserver = new TcpListener(IPAddress.Any, port);
13        tcpserver.Start();
14        Thread thread = new Thread(() =>
15        {
16            TcpClient tcpclient = tcpserver.AcceptTcpClient();
17            NetworkStream stream = tcpclient.GetStream();
18            try
19            {
20                byte[] bytes = new byte[tcpclient.ReceiveBufferSize];
21                stream.Read(bytes, 0, bytes.Length);
22                stream.Write(bytes, 0, bytes.Length);
23            }
24            catch (Exception) { }
25            stream.Close();
26            tcpclient.Close();
27            tcpserver.Stop();
28        });
29        thread.Start();
30    }
31}
  1. サーバーとしてportを指定して接続を待ちます.
  2. threadを開始しクライアントが接続を行ってきたらAcceptTcpClient()で接続を許可します.
  3. クライアントからのデータ送信を受け取ってそのままクライアントへ返します.
  4. 処理が終了したのでthreadとサーバーを停止します.
  • クライアント側
 1using System;
 2using System.Text;
 3using System.Threading;
 4using System.Net.Sockets;
 5
 6public class TcpNetworkClientManager
 7{
 8    private NetworkStream stream = null;
 9
10    public TcpNetworkClientManager(string IP,int port)
11    {
12        Thread thread = new Thread(()=> {
13            TcpClient tcp = new TcpClient(IP, port);
14            stream = tcp.GetStream();
15            try
16            {
17                byte[] bytes = new byte[tcp.ReceiveBufferSize];
18                stream.Read(bytes, 0, bytes.Length);
19                string ms = Encoding.UTF8.GetString(bytes);
20            }
21            catch (Exception) { }
22            stream.Close();
23            tcp.Close();
24        });
25        thread.Start();
26    }
27
28    public void SendMessage(string data)
29    {
30        if (stream != null)
31        {
32            byte[] bytes = Encoding.UTF8.GetBytes(data);
33            Thread sendthread = new Thread(()=> { stream.Write(bytes, 0, bytes.Length); });
34            sendthread.Start();
35        }
36    }
37}
  1. Thread内で接続先サーバーのIPアドレスとport番号を指定してサーバーに接続します.
  2. サーバーとの接続に成功したらSendMessage(string data)よりデータを送信します.
  3. サーバーからデータが送信されるとデータを受け取りstring msで文字列に変換します.
  4. データの送受信に成功すると接続を停止します.

HoloLens編

HoloLensやImmersiveデバイスでは実行環境がUWPのためUnityのC#ネットワーク処理の関数が利用できません. UWPではTCPネットワーク通信用の関数としてStreamSocketStreamSocketListenerを利用します. また非同期処理のためTaskを利用します.

この時UWPで動作する関数はUnityEditor上では動作しない場合があるため

#if UNITY_UWP
#endif

を利用して実行デバイスの切り分けを行います.

  • サーバー側
 1using System;
 2using System.Collections.Generic;
 3#if UNITY_UWP
 4using System.IO;
 5using System.Threading.Tasks;
 6using Windows.Networking.Sockets;
 7using Windows.Storage.Streams;
 8#endif
 9
10public class TcpNetworkServerManager
11{
12    public TcpNetworkServerManager(int port)
13    {
14#if UNITY_UWP
15        Task.Run(async()=>{
16            StreamSocketListener streamsocketlistener = new StreamSocketListener();
17            streamsocketlistener.ConnectionReceived += ConnectionReceived;
18            await streamsocketlistener.BindServiceNameAsync(port.ToString());
19        });
20#endif
21    }
22#if UNITY_UWP
23    private async void ConnectionReceived(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args)
24    {
25        StreamReader reader = new StreamReader(args.Socket.InputStream.AsStreamForRead());
26        StreamWriter writer = new StreamWriter(args.Socket.OutputStream.AsStreamForWrite());
27        try
28        {
29            string data = await reader.ReadToEndAsync();
30            await writer.WriteAsync(data);
31            await writer.FlushAsync();
32        }
33        catch (Exception) { }
34    }
35#endif
36}
  1. Taskを実行し内部でクライアントが接続してきた時に呼ばれるイベントを登録します.
  2. 接続用のportを指定し非同期で接続を待ちます.
  3. クライアントが接続してきた場合,登録したイベント内でStreamReaderStreamWriterを作成しデータの送受信を行えるようにします.
  4. 非同期でクライアントから送信されたデータを受け取りそのままクライアントへ返します.
  • クライアント側
 1using System;
 2#if UNITY_UWP
 3using System.IO;
 4using System.Threading.Tasks;
 5using Windows.Networking;
 6using Windows.Networking.Sockets;
 7#endif
 8
 9public class TcpNetworkClientManager
10{
11#if UNITY_UWP
12    private StreamWriter writer = null;
13#endif
14
15    public TcpNetworkClientManager(string IP,int port)
16    {
17#if UNITY_UWP
18        Task.Run(async () => {
19            StreamSocket socket = new StreamSocket();
20            await socket.ConnectAsync(new HostName(IP),port.ToString());
21            writer = new StreamWriter(socket.OutputStream.AsStreamForWrite());
22            StreamReader reader = new StreamReader(socket.InputStream.AsStreamForRead());
23            try
24            {
25                string data = await reader.ReadToEndAsync();
26            }
27            catch (Exception) { }
28            writer = null;
29        });
30#endif
31    }
32
33    public void SendMessage(string data)
34    {
35#if UNITY_UWP
36        if (writer != null) Task.Run(async () =>
37        {
38            await writer.WriteAsync(data);
39            await writer.FlushAsync();
40        });
41#endif
42    }
43}
  1. Task内で接続先サーバーのIPアドレスとport番号を指定してサーバーに接続します.
  2. サーバーとの接続に成功したらSendMessage(string data)よりデータを送信します.
  3. サーバーからデータが送信されるとデータを受け取りstring dataで文字列に変換します.

加えてHoloLensでは動作させるUnityアプリの特定の機能に対して許可を設定する必要があります. UnityEditorのPlayerSettingsよりPublishing Settings内にCapabilitiesの項目があります. ネットワーク通信を行う場合にはCapabilities内の

  • InternetClient
  • InternetClientServer
  • PrivateNetworkClientServer

にチェックを入れてください

上記の設定項目はUWP用にUnityからプロジェクトを出力した後でもVisualStudio上で確認できます. プロジェクト内のPackage.appxmanifest内の機能にあります.

まとめ

  • 実行環境ごとの切り分けが大変
  • 自前で実装するのはデバイスの違いを吸収する必要があるので大変
  • サーバーが利用できるのならMRTKのSharingサーバーによるSharingがおすすめ
  • UnityのみでShringを行う場合はUNETによるShringが便利
  • 今回の実装に関しては自前のHoloLensModuleにて複数台接続に対応したものを入れています.