HoloLensアプリにて3D Viewと2D Viewを行き来する

投稿者: | 2017-05-31

HoloLensで文字入力を行う場合、ソフトウェアキーボードを使ったりデバイスポータルの機能であるVirtual Inputを使う必要があります。ただし、これらの方法はHoloLensShell上、もしくは2Dビュー(通常のUWPアプリと思ってください)でしか使用ができず、3Dビューとして作ったシーン(Unityで作ったシーン)上では通常使うことができません。

Unityにてソフトウェアキーボードを使う方法として、TouchScreenKeyboardがあります。これを使うと3Dビューを抜け、2Dビューの入力画面が出現します。この画面で入力した値は3Dビュー側でも取り扱うことが可能です。

しかしTouchScreenKeyboardを使用した場合は2Dビューでの画面のデザインが限定されてしまいます。また、入力フォームの数も1つに限定されてしまうので、複数の入力値を求める場合は何度も3Dビューと入力画面を行き来しなくてはいけません。

そこで今回はUnityで3Dビュー(Immersive View)から任意のXAML画面へ遷移する方法と、そこでの入力値の受け渡し方法について紹介したいと思います。先日投下したサンプルのようなことができるようになります。

また、3Dビューと2Dビューを切り替えるスクリプトは、以下のリポジトリのコードを使っています。
https://github.com/jbienzms/Adept

HoloLensアプリのソリューション構成

HoloLensアプリにおける2Dビューと3Dビューの関係ですが、アプリのソリューション構成を把握しておくとわかりやすいです。

HoloLensアプリはUWP(Universal Windows Platform)上で動作します。そのため、Unityで作ったアプリをHoloLensアプリとしてビルドするためには、一度UWPソリューションに変換する必要があります。変換の設定は公式チュートリアルにあるとおりUnityのビルド前に行うのですが、HoloLensアプリで2Dビューを使用するには、UWP Build Typeを「XAML」に変更して変換します。

また、上の図のように「C# Debbuging」をチェックすると、できあがったソリューションは以下の通り3つのプロジェクトから構成されます。このうち、Assembly-CSharpにUnity側で作ったコードが入っています。

これら3つのプロジェクトですが、プロジェクトタイプやビルド順序を確認すると朧気ながらプロジェクト間の関係性が見えてきます。

ビルド依存関係を確認するとAssembley-CSharp-firstpass→Assmebly-CSharp→UWPプロジェクトの順番でビルドされていくことがわかります。このうちAssembly-CSharp-firstPassとAssembly-CsharpはDLLプロジェクトで、出来上がったDLLはUWPプロジェクト側で参照されています。ですのでイメージ的にはUWPアプリがUnityのデータが入っているDLLにアクセスし、3Dビューを作っているといったものになります。

なお、3DビューはMainPage.xamlにて再現されています。MainPage.xaml.csを見てみるとUWPがUnityエンジンとやりとするためのWinRTBridgeやAppCallbacksを使って3Dビューを表示していることがなんとなくわかります(AppCallbacksについてはUnity公式マニュアルを参照)。したがって3Dビューと2Dビューの行き来は、3DビューであるMainPage.xamlとXAMLで作った2Dビューを切り替える、といったイメージになります。

実装

ビューの切り替え

Unity側で作った部分は参照プロジェクトとしてとしてUWPに取り込まれるので、UWPプロジェクトからUnityで作ったコードへはアクセスが可能ですが、その反対のUnityのコードからUWPプロジェクトへアクセスすることはできません。ですので、ビューの切り替え処理はUnity側で定義します。

この切り替え処理が定義されたクラスには、HoloLensアプリが使用するビューの情報(MainPage.xamlや独自に作ったXAML)についての情報をコレクションとして保持しています。したがって、ビューを切り替えたいときには、このクラスに移動先のビューを伝えて切り替えを依頼します。

using System;
using System.Collections.ObjectModel;

#if WINDOWS_UWP
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#endif

namespace XAMLTest {
    /// <summary>
    /// アプリケーション内で使用するビューについての情報を管理するクラス
    /// </summary>
    public class AppViewInfo {
        // ビューの名前
        private string name;

#if WINDOWS_UWP
        // ビューに紐づくスレッドへのディスパッチャー
        private CoreDispatcher dispatcher;
        // ビューそのもの
        private ApplicationView view;
#endif

        private AppViewInfo() { }

#if WINDOWS_UWP
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public AppViewInfo(ApplicationView view, CoreDispatcher dispatcher, string name) {
            this.view = view;
            this.dispatcher = dispatcher;
            this.name = name;

            this.view.Consolidated += View_Consolidated;
        }
#endif

#if WINDOWS_UWP
        /// <summary>
        /// このクラスのインスタンスが保持するビューへ遷移する
        /// </summary>
        public async Task SwitchAsync() {
            await dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => {
                await ApplicationViewSwitcher.SwitchAsync(this.view.Id);
            });
        }

        /// <summary>
        /// このクラスのインスタンスが保持するビューへ遷移する
        /// </summary>
        public async Task SwitchAsync(AppViewInfo fromView, ApplicationViewSwitchingOptions options) {
            await dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => {
                await ApplicationViewSwitcher.SwitchAsync(this.view.Id, fromView.view.Id, options);
            });
        }

        private void View_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args) {
            if (Consolidated != null) {
                Consolidated(this, EventArgs.Empty);
            }
        }
#endif

        // メンバ変数のプロパティ
        public string Name { get { return name; } }
#if WINDOWS_UWP
        public CoreDispatcher Dispatcher => dispatcher;
        public ApplicationView View => view;
#endif

        // イベント
        public event EventHandler Consolidated;
    }

    // AppViewInfoの辞書コレクションで、AppViewInfow.Nameがキーとなる
    public class AppViewCollection : KeyedCollection<string, AppViewInfo> {
        protected override string GetKeyForItem(AppViewInfo item) {
            return item.Name;
        }
    }

    // AppViewInfoのコレクションを管理するクラス
    static public class AppViewManager {
        // コレクションの実体
        static private AppViewCollection views = new AppViewCollection();
        static public AppViewCollection Views { get { return views; } }

#if WINDOWS_UWP
        // 呼び出し元のスレッドで表示されているビューについてのAppViewInfoを作る
        static public Task<AppViewInfo> CreateFromCurrentDispatcherAsync(string name) {
            return CreateFromDispatcherAsync(name, Window.Current.Dispatcher);
        }

        static public async Task<AppViewInfo> CreateFromDispatcherAsync(string name, CoreDispatcher dispatcher) {
            ApplicationView view = null;
            AppViewInfo info = null;

            await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
                view = ApplicationView.GetForCurrentView();
                info = new AppViewInfo(view, dispatcher, name);
            });

            views.Add(info);

            return info;
        }

        // 自分で作ったXAMLについてのAppViewInfoを作る
        static public async Task<AppViewInfo> CreateNewAsync(string name, Type viewType) {

            // CoreApplicationViewを新しく作成
            var view = CoreApplication.CreateNewView();
            // 生成したViewのディスパッチャーを使い、引数で指定したXAMLのFrameを作る
            await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
                Frame frame = new Frame();

                frame.Navigate(viewType, null);

                var window = Window.Current;
                window.Content = frame;
                window.Activate();
            });
            return await CreateFromDispatcherAsync(name, view.Dispatcher);
        }
#endif
    }
}

HoloLensアプリで使用するビューの情報はアプリケーションのエントリポイントにて最初に設定しておきます。

namespace XAMLTest {
    sealed partial class App : Application {

        private async void InitializeUnity(string args) {
#if UNITY_WP_8_1 || UNITY_UWP
            ApplicationView.GetForCurrentView().SuppressSystemOverlays = true;
#endif

#if UNITY_WP_8_1
            StatusBar.GetForCurrentView().HideAsync();
#endif

            appCallbacks.SetAppArguments(args);
            Frame rootFrame = Window.Current.Content as Frame;

            if (rootFrame == null && !appCallbacks.IsInitialized()) {
                rootFrame = new Frame();
                Window.Current.Content = rootFrame;
#if !UNITY_HOLOGRAPHIC
                Window.Current.Activate();
#endif
                await CreateViewAsync();
                rootFrame.Navigate(typeof(MainPage));
            }
            Window.Current.Activate();
        }

        // Unity側にアプリが使用するビューを通知する
        private async Task CreateViewAsync() {
            await AppViewManager.CreateFromCurrentDispatcherAsync("Main");
            await AppViewManager.CreateNewAsync("DataInput", typeof(DataInputPage));
            await AppViewManager.CreateNewAsync("SelectColor", typeof(SelectColorPage));
        }
    }
}

そして3Dビュー上でのAirTapや2Dビュー上のボタンクリックを契機にしてビューを切り替えます。

Unity

public class GotoXAML : MonoBehaviour, IInputClickHandler {
    // 遷移先のビューの名前
    public string PageName;
    public void OnInputClicked(InputClickedEventData eventData) {
#if UNITY_UWP
        StartDataInput();
#endif
    }

#if UNITY_UWP
    private async Task StartDataInput() {
        var view = AppViewManager.Views[PageName];
        await view.SwitchAsync();
    }
#endif
}

UWP

namespace XAMLTest {
    public sealed partial class DataInputPage : Page {
        public DataInputPage() {
            this.InitializeComponent();
        }

        private async void button_Click(object sender, RoutedEventArgs e) {

            InputData.address = AddressTextBox.Text;
            InputData.userName = UserIDTextBox.Text;

            var dataInputView = AppViewManager.Views["DataInput"];
            // MainPage.xamlがUnity側のビューに結び付いている
            await AppViewManager.Views["Main"].SwitchAsync(dataInputView, Windows.UI.ViewManagement.ApplicationViewSwitchingOptions.ConsolidateViews);
        }
    }
}

2Dビューと3Dビュー間での受け渡し

2Dビューで入力したデータを3Dビューで使いたい(またはその逆)場合ですが、先ほど説明したHoloLensアプリケーション構成の性質を使うのがもっともシンプルだと思います。Unity側で作った部分はUWPからでもアクセスできますが、その反対はできません。したがってUnity部分がお互い共有できる部分といえます。

ですのでUnity側でstaticなクラスやシングルトンなクラスを用意してやり、UWP側でそちらにデータの書き込み/読み取りをしてやればビュー間でのデータのやり取りが可能となります。

Unity側においておくコード

public static class InputData {
    public static string address;
    public static string userName;
}

2Dビューにて入力を行った後、ボタンをクリックしたときのコード

namespace XAMLTest {
    public sealed partial class DataInputPage : Page {
        public DataInputPage() {
            this.InitializeComponent();
        }

        private async void button_Click(object sender, RoutedEventArgs e) {
            // InputDataはUnityで定義したstaticなクラスなので、UWP側からアクセスできる
            InputData.address = AddressTextBox.Text;
            InputData.userName = UserIDTextBox.Text;

            var dataInputView = AppViewManager.Views["DataInput"];
            // MainPage.xamlがUnity側のビューに結び付いている
            await AppViewManager.Views["Main"].SwitchAsync(dataInputView, Windows.UI.ViewManagement.ApplicationViewSwitchingOptions.ConsolidateViews);
        }
    }
}

Unity側では任意のタイミングではじめに示したstaticなクラスのメンバにアクセスすれば、入力値が取得できます。

public class TextUpdate : MonoBehaviour {
    private TextMesh textMesh;
	void Start () {
        textMesh = GetComponent<TextMesh>();
	}
	
	void Update () {
        textMesh.text = "IP: " + InputData.address + "\nUserID: " + InputData.userName;
	}
}

まとめ

HoloLensアプリにおける2Dビューと3Dビューを行き来する方法とビュー間でデータを受け渡す方法についてまとめました。HoloLensではUI特性上、データの入力方法が限定されます。Unityではソフトウェアキーボードを用意するのが少し手間なので、XAML画面を呼び出してBlueToothキーボードにて文字入力をさせるという方法はアリかと思います。ただ入力内容にとっては音声入力やQRコード読み取りの方が適している場合もあるので、いくつかの入力手段を用意しておき、状況に応じて適切な仕組みを提供できるのが今のところ一番良いかと思います。

なお、ビュー間のデータのやり取りについては中村さん(@kaorun55)にアドバイスをいただきました、ありがとうございます!。