2016年12月5日月曜日

【HoloLens開発】ユニティちゃんとHoloLensで戯れる - シェアリング編1 -

今回は2台のHoloLensを使い、「シェアリング(空間共有)」を利用して片方のHoloLensで操作しているユニティちゃんをもう片方のHoloLensで表示させたいと思います。

連載目次
Part 7: 【HoloLens開発】ユニティちゃんとHoloLensで戯れる - シェアリング編1 - ←本記事
Part 8: 【HoloLens開発】ユニティちゃんとHoloLensで戯れる - シェアリング編2 - (予定)

【空間共有】
空間共有をすることにより、ある端末Aで見ているものを端末Bで見ることができるようになります。これを使えば、部屋のコーディネイトや初音ミクなどのバーチャルアイドルを複数の人と見ることができたりもします。

実装にはサーバーが必要となります。サーバーの役割はHoloLensの空間データを保持し、HoloLensのアクセスポイントとなることです。サーバーで稼働させるサービス内でセッションを作成し、そこにアクセスすることでHoloLens同士でデータのやり取りができるようになります。

今回の実装の構成としてはこんな感じです。

  ① サーバーのシェアリングサービスに接続する(1台目)。
  ② アンカー情報をアップロードする。
  ③ サーバーのシェアリングサービスに接続する(2台目)。
  ④ アンカー情報をダウンロードし、反映させる。
  ⑤ 1台目のHoloLensと接続したゲームパッドを使いユニティちゃんを操作し、
      オブジェクト情報をサーバーに送信する。受信した側でオブジェクト情報を反映させる。
  ⑥ 2台のHoloLensで同じ状態のユニティちゃんを見ることができる。

また今回実装したものを動かしてみた様子はこちらの動画です。

【共有サーバー】
音声認識編でダウンロードしたHoloToolkit-Unityの中にSharingService.exeというファイルがあり、これをサーバーアプリとして使います。このファイルは [HOLOTOOLKIT_ROOT] > External > HolotoolKit > Sharing > Server にあります。このアプリをコマンドプロンプトから「-local」オプションをつけて実行するとサービスが実行されます。ちなみにUnityのHoloToolkitツールバーの中にLaunch Sharing Serviceがありますが、アセットとしてエクスポートした際に一緒に持ってこれていません。[HOLOTOOLKIT_ROOT] > External をコピーしプロジェクト直下に置いておくとここからでも起動できるようになります。
実行するとIPアドレスが表示されるのでHoloLensからこのIPアドレスにアクセスすることで空間共有できるようになります。

アプリから接続すると接続された旨のログが出るので、これを見て接続されたかどうかを確認します。たまに繋がらないことがあるのでその際はサービスとアプリの再起動を試してみてください。それでも繋がらない場合はアプリに設定したIPアドレスの確認とPC側のファイアウォール等の確認設定をしてみてください。

【空間共有の実装】
以下の手順で空間共有を実装していきます。実装は公式のサンプル「Holograms_240」を参考にしています。

・前準備
まずは必要なものの追加を行います。
HoloToolkit > Sharing > Prefab > SharingをHierarchyに追加します。

追加したらSharingを選択し、Add ComponentからCustom Messagesスクリプトを追加します。
このスクリプトはHoloLens同士のデータのやり取りに用いられます。

Hierarchyを右クリックしてCreate Emptyを選択し、空のゲームオブジェクトを生成します。これをHologramCollectionとします。unitychanプレハブをこの中に入れます。

HologramCollectionを選択し、Add ComponentからImport Export Anchor Managerスクリプトを追加します。このスクリプトにはHologramCollection以下にSpatial Anchorを付与し、アンカー情報をサーバーにエクスポートまたはサーバーからインポートする役割があります。

これで追加すべきものは完了したので設定値の変更とコードの追記をしていきます。

・サーバーの指定
Sharingプレハブを選択し、Sharing StageのServer AddressにサーバーのIPアドレスを入力します。IPアドレスが複数ある場合はHoloLensと同じネットワーク内のIPアドレスを指定します。

・メッセージの送信
CustomMessagesに追記をしていきます。TestMessageIDに送信するメッセージの種類を追加し、それぞれメッセージの送信処理を追加します。

TestMessageIDにはユニティちゃんの位置、向き、アニメーションのためのパラメータを用意しておきます。

public enum TestMessageID : byte
{
    HeadTransform = MessageID.UserMessageIDStart,
    StageTransform,             // ユニティちゃんの位置、向き用
    Speed,                      // 移動方式Bタイプの移動モーション用
    Jump,                       // ジャンプ用
    Rest,                       // レスト用
    SpeedAndDirection,          // 移動方式Aタイプの移動モーション用
    Max
}

追加したIDそれぞれにメッセージ送信処理を追加します。ちなみにStageTransform関連はHolograms_240と同等です。

// ユニティちゃんの位置情報を送信
public void SendStageTransform(Vector3 position, Quaternion rotation)
{
    if (this.serverConnection != null && this.serverConnection.IsConnected())
    {
        NetworkOutMessage msg = CreateMessage((byte)TestMessageID.StageTransform);

        AppendTransform(msg, position, rotation);

        this.serverConnection.Broadcast(
            msg,
            MessagePriority.Immediate,
            MessageReliability.ReliableOrdered,
            MessageChannel.Avatar);
    }
}

// アニメーターのSpeed値を送信
public void SendSpeed(float speed)
{
    if (this.serverConnection != null && this.serverConnection.IsConnected())
    {
        // Create an outgoing network message to contain all the info we want to send
        NetworkOutMessage msg = CreateMessage((byte)TestMessageID.Speed);

        msg.Write(speed);

        this.serverConnection.Broadcast(
            msg,
            MessagePriority.Immediate,
            MessageReliability.ReliableOrdered,
            MessageChannel.Avatar);
    }
}

// アニメーターのJump値を送信(0より大きければ受信側でtrueとする)
public void SendJump(int jump)
{
    if (this.serverConnection != null && this.serverConnection.IsConnected())
    {
        NetworkOutMessage msg = CreateMessage((byte)TestMessageID.Jump);

        msg.Write(jump);

        this.serverConnection.Broadcast(
            msg,
            MessagePriority.Immediate,
            MessageReliability.ReliableOrdered,
            MessageChannel.Avatar);
    }
}

// アニメーターのRest値を送信(0より大きければ受信側でtrueとする)
public void SendRest(int rest)
{
    if (this.serverConnection != null && this.serverConnection.IsConnected())
    {
        NetworkOutMessage msg = CreateMessage((byte)TestMessageID.Rest);

        msg.Write(rest);

        this.serverConnection.Broadcast(
            msg,
            MessagePriority.Immediate,
            MessageReliability.ReliableOrdered,
            MessageChannel.Avatar);
    }
}

// アニメーターのSpeed値とDirection値を同時送信
public void SendSpeedAndDirection(float speed, float direction)
{
    if (this.serverConnection != null && this.serverConnection.IsConnected())
    {
        NetworkOutMessage msg = CreateMessage((byte)TestMessageID.SpeedAndDirection);

        msg.Write(speed);
        msg.Write(direction);

        this.serverConnection.Broadcast(
            msg,
            MessagePriority.Immediate,
            MessageReliability.ReliableOrdered,
            MessageChannel.Avatar);
    }
}

これを送信したいタイミングで実行します。
今回は前回作成したXboxController内で適宜実行しています。
例としてサーバーのセッションを掴んだタイミングと移動操作時を挙げておきます。

void Start()
{
    // 省略

    SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;
}

private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
{
    CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
}

セッションを掴んだ際のコールバックメソッドを用意しておき、OnStart内でコールバックを定義します。 これで実際にサーバーのセッションを掴んだ際にオブジェクトの位置情報を送信することができます。

float h = 0f;
float v = 0f;
#if WINDOWS_UWP
GamepadReading reading;
if (controller != null)
{
    reading = controller.GetCurrentReading();
    h = (float)reading.LeftThumbstickX;     // 左スティックのX方向(左右)をhで定義
    v = (float)reading.LeftThumbstickY;     // 左スティックのY方向(上下)をvで定義
#endif
    float speed = Mathf.Sqrt(v * v + h * h);        // vとhから左スティックの傾き具合を算出
    if (speed > 0.1)
    {
        if (Camera.main != null)
        {
            anim.SetFloat("Speed", speed);

            // 省略

            // アンカーが確立していれば送信
            if (ImportExportAnchorManager.Instance.AnchorEstablished)
            {
                CustomMessages.Instance.SendSpeed(speed);
                CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
            }
        }
    }
    else
    {
        anim.SetFloat("Speed", 0);
        // アンカーが確立していれば送信
        if (ImportExportAnchorManager.Instance.AnchorEstablished)
        {
            CustomMessages.Instance.SendSpeed(0);
            CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
        }
    }
#if WINDOWS_UWP
}
#endif

ゲームパッドの処理はFixedUpdate内で行われますが、メッセージ送信をFixedUpdate毎に実施すると複数のHoloLensで実行すると自分の位置情報と受信した位置情報が常に書き換わってしまうためオブジェクトの表示挙動がおかしくなってしまいます。その対策として今回はゲームパッドを接続しているHoloLensが1台のため、ゲームパッドを接続している場合のみメッセージを送信することとしました。
他に可能な対策としては、最初にサービスに接続した端末のみ送信するといった方法が挙げられます。

・メッセージ受信
各メッセージIDに対してメッセージ受信用のコールバックを用意することで、受信処理を行うことができます。

void Start () {
    // 省略

    // コールバックの設定
    CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.StageTransform] = this.OnStageTransfrom;
    CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.Speed] = this.OnSpeed;
    CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.Jump] = this.OnJump;
    CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.Rest] = this.OnRest;
    CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.SpeedAndDirection] = this.OnSpeedAndDirection;
}

void OnStageTransfrom(NetworkInMessage msg)
{
    msg.ReadInt64();

    // オブジェクトを位置情報を受信したものに更新する
    transform.localPosition = CustomMessages.Instance.ReadVector3(msg);
    transform.localRotation = CustomMessages.Instance.ReadQuaternion(msg);

}

void OnSpeed(NetworkInMessage msg)
{
    // アニメーターのSpeed値を受信したものに更新する
    msg.ReadInt64();
    float speed = msg.ReadFloat();

    anim.SetFloat("Speed", speed);
}

void OnJump(NetworkInMessage msg)
{
    // アニメーターのJump値を受信したものに更新する
    msg.ReadInt64();
    bool jump = msg.ReadInt32() > 0;

    anim.SetBool("Jump", jump);
}

void OnRest(NetworkInMessage msg)
{
    // アニメーターのRest値を受信したものに更新する
    msg.ReadInt64();
    bool rest = msg.ReadInt32() > 0;

    anim.SetBool("Rest", rest);
}

void OnSpeedAndDirection(NetworkInMessage msg)
{
    // アニメーターのSpeed値とDirection値を受信したものに更新する
    msg.ReadInt64();
    float speed = msg.ReadFloat();
    float direction = msg.ReadFloat();

    anim.SetFloat("Speed", speed);
    anim.SetFloat("Direction", direction);
}

OnStart内で各TestMessageIDに対してコールバックを定義しています。なお、コールバックメソッドの引数であるNetworkInMessageの最初のlong値(ReadInt64で取得する値)には送信側のユーザーIDが入っているので、long値を使う際には注意が必要です。また、ユーザーIDを利用した場合分けなども可能です。

・Player Settings
Player SettingsのPublishing Settings配下にあるCapabilitiesに以下の3項目を追加します。
    InternetClient
    InternetClientServer
    PrivateNetworkClientServer

これでシェアリングが可能となります。

動画ではユニティちゃんを動かしていない端末から撮影しています。このようにデモ撮影するだけでも空間共有の利用が有効です。
ユニティちゃんがカクカクした動きになっていますが、HoloLens同士で空間認識に微妙なズレがあり、地面の位置が若干上下しているためと思われます。
対策としては操作している端末でのみ重力判定し、残りの端末では重力を使わずに表示する、などが考えられます。

今回は1つのオブジェクトを複数のHoloLensで見る方法について紹介いたしました。
次回は複数のHoloLensで作成したオブジェクトを同じ空間で見る実装について掲載する予定です。
12月中旬ごろに公開を予定していますのでご期待ください。
140 180 HoloLens , sharing , Unity , シェアリング , 空間共有

記載されている会社名、および商品名等は、各社の商標または登録商標です。

0 コメント:

コメントを投稿

Related Posts Plugin for WordPress, Blogger...