たるこすの日記

たるこすの日記

リアルからバーチャルへ、バーチャルからリアルへ

HoloLens と Oculus Touch を組み合わせてみる(Holographic Remoting Player でのみ動作)

こんにちは、たるこすです。

ついに HoloLens が日本発売されました。
ラッキング性能は信じられないほど良く完成度はかなり高いのですが、 難点としては視野角が狭いこととハンドジェスチャの種類が少ないことが挙げられます。

ハンドジェスチャには手のひらを開く「Bloom」, 人差し指を倒して戻す「Air-Tap」, 人差し指を倒してから手を動かす「Navigation, Manipulation」の動作がありますが、 手でものを掴んだり置いたりするような直感的な動作はできません。

そこで、コントローラとして Oculus Rift 用の Oculus Touch を使えないかと思い、 Oculus Avatar SDK を使って手を表示できないか試してみました。

結論から言うと、HoloLens 上での動作はできていませんが、 Holographic Remoting Player で動作させたときは手を表示することができました。

全体構成

Oculus 用の Unity アプリケーションと HoloLens 用アプリケーションを作成します。
双方でOculus Avatar SDK を用います。
Avatar SDK では、顔と手を Avatar として表示することができます。

Oculus 側では OculusTouch によって操作される Avatar の状態を UNET を使って HoloLens 側に送信します。
HoloLens 側では受け取った情報に基づいて Avatar の手の位置や状態を更新します。

とはいっても、Avatar の状態を送信できる形で取得するところや、受け取った情報をもとに Avatar を更新するところは Avatar SDK がやってくれるので、 データを送信する部分を実装するだけで良いです。

Oculus 側アプリケーション

Unity でプロジェクトを作成し、以下のページでダウンロードできる Oculus Utilities for Unity5 と Oculus Avatar SDK をインポートします。
Developer Center — Downloads | Oculus

Assets > OvrAvatar > Content > Prefabs にある Local Avatar を Scene に表示させて実行し、 Oculus, Oculus Touch を動かすと以下の写真のように Avatar が表示されます。

f:id:tarukosu:20170129203323p:plain

データ送信は以下のようなスクリプトで行います。

using System.IO;
using UnityEngine;
using UnityEngine.Networking;

public class AvatarSender : MonoBehaviour {
    public OvrAvatar LocalAvatar;

    private bool connected = false;
    private int connectionId;
    private int myReliableChannelId;
    private int myUnreliableChannelId;
    private int hostId;

    int packetSequence = 0;

    // Use this for initialization
    void Start()
    {
        // Get Avatar Packets
        LocalAvatar.RecordPackets = true;
        LocalAvatar.PacketRecorded += OnLocalAvatarPacketRecorded;

        // Initializing the Transport Layer with no arguments (default settings)
        NetworkTransport.Init();
        ConnectionConfig config = new ConnectionConfig();
        myReliableChannelId = config.AddChannel(QosType.Reliable);
        myUnreliableChannelId = config.AddChannel(QosType.Unreliable);

        HostTopology topology = new HostTopology(config, 10);

        hostId = NetworkTransport.AddHost(topology);
        Connect();
    }

    void Update()
    {
        int recHostId;
        int connectionId;
        int channelId;
        byte[] recBuffer = new byte[1024];
        int bufferSize = 1024;
        int dataSize;
        byte error;
        NetworkEventType recData = NetworkTransport.Receive(out recHostId, out connectionId, out channelId, recBuffer, bufferSize, out dataSize, out error);
        switch (recData)
        {
            case NetworkEventType.Nothing:
                if (!connected)
                {
                    Connect();
                }
                break;
            case NetworkEventType.ConnectEvent:
                connected = true;
                break;
            case NetworkEventType.DataEvent:
                break;
            case NetworkEventType.DisconnectEvent:
                connected = false;
                break;
        }
    }

    void Connect()
    {
        byte error;
        string ip = "127.0.0.1";
        connectionId = NetworkTransport.Connect(hostId, ip, 8888, 0, out error);
        Debug.Log("Connected to server. ConnectionId: " + connectionId);
        LogNetworkError(error);
    }

    void OnLocalAvatarPacketRecorded(object sender, OvrAvatar.PacketEventArgs args)
    {
        if (connected)
        {
            using (MemoryStream outputStream = new MemoryStream())
            {
                BinaryWriter writer = new BinaryWriter(outputStream);
                writer.Write(packetSequence++);
                args.Packet.Write(outputStream);
                SendPacketData(outputStream.ToArray());
            }
        }
    }
    void SendPacketData(byte[] data)
    {
        byte error;
        NetworkTransport.Send(hostId, connectionId, myUnreliableChannelId, data, data.Length, out error);
        LogNetworkError(error);
    }

    void LogNetworkError(byte error)
    {
        if (error != (byte)NetworkError.Ok)
        {
            NetworkError nerror = (NetworkError)error;
            Debug.Log("Error: " + nerror.ToString());
        }
    }
}

HoloLens 側アプリケーション

Oculus 側と同じく、Oculus Utilities for Unity5 と Oculus Avatar SDK をインポートします。
HoloToolkit もインポートします。

今度は、Assets > OvrAvatar > Content > Prefabs にある Remote Avatar を Scene に表示させます。
また、Oculus 側からのデータの受信は以下のようなプログラムで行います。

using UnityEngine;
using System.IO;
using UnityEngine.Networking;

public class AvatarReceiver : MonoBehaviour
{
    public OvrAvatar Avatar;

    private int connectionId;
    private int myReliableChannelId;
    private int myUnreliableChannelId;
    private int hostId;
    // Use this for initialization
    void Start()
    {
        // Initializing the Transport Layer with no arguments (default settings)
        NetworkTransport.Init();

        ConnectionConfig config = new ConnectionConfig();
        myReliableChannelId = config.AddChannel(QosType.Reliable);
        myUnreliableChannelId = config.AddChannel(QosType.Unreliable);

        HostTopology topology = new HostTopology(config, 10);

        hostId = NetworkTransport.AddHost(topology, 8888);
    }

    void Update()
    {
        int recHostId;
        int connectionId;
        int channelId;
        byte[] recBuffer = new byte[8192];
        int bufferSize = 8192;
        int dataSize;
        byte error;
        NetworkEventType recData = NetworkTransport.Receive(out recHostId, out connectionId, out channelId, recBuffer, bufferSize, out dataSize, out error);
        switch (recData)
        {
            case NetworkEventType.Nothing:
                Debug.Log("Nothing");
                break;
            case NetworkEventType.ConnectEvent:
                Debug.Log("Connect");
                break;
            case NetworkEventType.DataEvent:
                Debug.Log("Received");
                ReceivePacketData(recBuffer);
                break;
            case NetworkEventType.DisconnectEvent:
                break;
        }
    }

    void LogNetworkError(byte error)
    {
        if (error != (byte)NetworkError.Ok)
        {
            NetworkError nerror = (NetworkError)error;
            Debug.Log("Error: " + nerror.ToString());
        }
    }

    void ReceivePacketData(byte[] data)
    {
        using (MemoryStream inputStream = new MemoryStream(data))
        {
            BinaryReader reader = new BinaryReader(inputStream);
            int sequence = reader.ReadInt32();
            OvrAvatarPacket packet = OvrAvatarPacket.Read(inputStream);
            Avatar.GetComponent<OvrAvatarRemoteDriver>().QueuePacket(sequence, packet);
        }
    }
}

実行

2つのアプリケーションを実行します。
ただし、HoloLens側は Holographic Remoting Player を使って、Unity 上で実行します。

うまくいけば、HoloLens 側に Avatar が表示されるはずです。 Remote Avatar の位置を変更し、HoloLens から手が見えるように調整します。


HoloLens + Oculus Touch

今回は適当に手で調整しましたが、実用のためには何らかの方法で Oculus 側の座標系と HoloLens 側の座標系をキャリブレーションする必要があります。
ただ、HoloLens はアプリ起動のたびに原点が変わってしまうので、World Anchor を置いてその位置と Oculus の機器とをキャリブレーションする形になると思います。

HoloLens 側アプリを HoloLens 上で実行

HoloLens 側アプリをビルドして HoloLens上で実行してみましたが、以下のようなエラーが出てしまいました。

DllNotFoundException: Unable to load DLL 'libovravatar': The specified module could not be found. (Exception from HRESULT: 0x8007007E)
   at Oculus.Avatar.CAPI.ovrAvatarMessage_Pop()
   at OvrAvatarSDKManager.Update()
   at OvrAvatarSDKManager.$Invoke8(Int64 instance, Int64* args)
   at UnityEngine.Internal.$MethodUtility.InvokeMethod(Int64 instance, Int64* args, IntPtr method) 
(Filename: <Unknown> Line: 0)

そこで、C:\Program Files\Oculus\Support\oculus-runtime\libovravatar.dll を Assets\Plugins に入れてみましたが、 今度は以下のようなエラーが出ます。

BadImageFormatException: An attempt was made to load a program with an incorrect format. (Exception from HRESULT: 0x8007000B)
   at Oculus.Avatar.CAPI.ovrAvatarMessage_Pop()
   at OvrAvatarSDKManager.Update()
   at OvrAvatarSDKManager.$Invoke8(Int64 instance, Int64* args)
   at UnityEngine.Internal.$MethodUtility.InvokeMethod(Int64 instance, Int64* args, IntPtr method) 
(Filename: <Unknown> Line: 0)

先程の DLL が64bit 版なのが原因な気がしますが、詳しいことはよくわかりません。
何かご存じの方がいれば是非教えていただきたいです。

終わりに

自分の手から少し離れた位置にAvatarの手を表示すると魔法の手を操っているような 不思議な感覚が味わえました。
今のところ手を表示することしかできていませんが、ものを触れるようにすればもっと面白くなりそうです。