【MRDesignLab】BoundingBoxとAppBarを使う

投稿者: | 2017-08-11

7月は夏コミに出すUnibook8を書いていたのでブログの更新ができていなかったのですが、原稿も無事完成しましたので通常営業に戻っています。

少し間が開いてしまいましたが、今回はMRDesignLabのBoundingBoxとAppBarについて書きたいと思います。なお、過去の記事で紹介したInteractionManagerやButtonについての記事を前提にしていますので、まだ読んだことない方は先に確認いただくことをおススメします。

MRDesignLabを使いジェスチャー入力を取り扱う

【MRDesignLab】ButtonHolographicを使おう

BoundingBoxは現在もアップデートが続いており、つい先日も追加機能が実装されました。今回は最新機能の説明も含めた使い方を中心に説明したいと思います。

なお、環境はUnity 2017.1.0f3とMRDesignLabの2017/8/9時点のmasterブランチを使用しています。

基本的な使い方

AppBarとBoundingBoxはImmersiveアプリ(Unityで作ったアプリ)上で3Dモデルを操作するためのコントローラです。プリインアプリであるHologramsでの操作UIをアセットにしたものと思えば良いと思います。

使い方は非常にシンプルです。コントローラ本体はカメラや入力処理系がまとまったHoloLens.prefabに全て入っているので、まずはそちらをシーンに配置します。続いて、3Dモデルの操作はAirTapジェスチャーで操作対象を選択し、マニピュレーションジェスチャーを使って移動などを行うので、Tapとマニピュレーションジェスチャーが反応するようInteractionManagerを設定します。

BoundingBoxの使い方

自分の好きな3DモデルをBoundingBoxで操作するには、操作したいGameObjectにBoundingBoxTargetスクリプトをアタッチします。

なお、BoundingBoxTargetは過去の記事で説明したCompoundButtonを必要とします(自動でアタッチされます)。必要とする理由は後述します。

また、BoundingBoxTargetではインスペクタ上で設定を変更することで、許可/禁止する操作を指定できます。

Dragは移動、ScaleUniformは拡大縮小、RotateX/Y/Zは各ローカル軸を中心とした回転で、チェックボックスによって許可/禁止を設定できます。ローカル軸ごとに拡大縮小するScaleX/Y/Zも設定としてはありますが、現時点では実装されていません。

これまでは以上の設定でBoundingBoxを使うことができたのですが、8月上旬のアップデートにて「Flatting Mode」が追加されました。これにより今までのコントローラは3Dのワイヤーフレーム状のCubeだけでしたが、HoloShellのウィンドウのAdjustのような2Dのコントローラも使えるようになりました。

このモード指定もBoundingBoxTargetのインスペクタから設定ができます。

これまでの通り3DのCubeをコントローラとして使いたい場合は、「Do Not Flatten」を指定します。Flatting Modeを使いたい場合は「Flatten X/Y/Z」のいずれかを指定します。そうすると選択したx-y-z軸についてのBoundingBoxのScaleが縮小され平面形状になります。たとえばHoloShell上のウィンドウのような操作をしたい場合は「Flatten Z」を指定すればよいです。

「Flatten Auto」ではBoundingBoxTargetがアタッチされたGameObjectの形状に応じて自動的に指定されます。
BoundingBoxでは操作対象を選択したときに、BoundingBox.csにてGameObjectが持つmeshをもとにしてBoundingBoxのBoundsを計算しています。「Flatten Auto」を選択しているときは、計算したBoundsの各軸Scale値のうちほかと比べて著しく小さい値があった場合は、その軸のScaleを縮小して平面化します。

/// BoundingBox.cs RefereshBounds()より抜粋
// BoundsのScaleでもっとも大きい値を取得する
float maxAxisThickness = Mathf.Max(Mathf.Max(targetBoundsLocalScale.x, targetBoundsLocalScale.y), targetBoundsLocalScale.z);
// 判定後のFlatting Modeのプレースホルダ
FlattenModeEnum newFlattenedAxis = FlattenModeEnum.DoNotFlatten;
// BoundingBoxTargetにて指定されたFlatting Modeによって分岐
switch (FlattenPreference)
{
    // 操作対象がFlatting Modeを使わない場合は何もしない
    case FlattenModeEnum.DoNotFlatten:
        // Do nothing
        break;
    // 自動モードが選択された場合
    case FlattenModeEnum.FlattenAuto:
        // 操作対象のBoundsの各軸のScaleと、Scaleの最大値との割合を計算し、
        // それが閾値を下回っていればその軸のBoundのScaleを小さくする = 平面化する
        // どの軸も閾値を下回らなければ、Flatting Modeは使わない。
        if (Mathf.Abs(targetBoundsLocalScale.z / maxAxisThickness) < FlattenAxisThreshold) {
            newFlattenedAxis = FlattenModeEnum.FlattenZ;
            targetBoundsLocalScale.z = FlattenedAxisThickness * maxAxisThickness;
        }
        else if (Mathf.Abs(targetBoundsLocalScale.y / maxAxisThickness) < FlattenAxisThreshold) {
            newFlattenedAxis = FlattenModeEnum.FlattenY;
            targetBoundsLocalScale.y = FlattenedAxisThickness * maxAxisThickness;
        }
        else if (Mathf.Abs(targetBoundsLocalScale.x / maxAxisThickness) < FlattenAxisThreshold) {
            newFlattenedAxis = FlattenModeEnum.FlattenX;
            targetBoundsLocalScale.x = FlattenedAxisThickness * maxAxisThickness;
        }
        break;
    
    // 操作対象にてFlatting Modeが指定されていたら、指定された軸のScaleを縮小する
    case FlattenModeEnum.FlattenX:
        newFlattenedAxis = FlattenModeEnum.FlattenX;
        targetBoundsLocalScale.x = FlattenedAxisThickness * maxAxisThickness;
        break;
    case FlattenModeEnum.FlattenY:
        newFlattenedAxis = FlattenModeEnum.FlattenY;
        targetBoundsLocalScale.y = FlattenedAxisThickness * maxAxisThickness;
        break;
    case FlattenModeEnum.FlattenZ:
        newFlattenedAxis = FlattenModeEnum.FlattenZ;
        targetBoundsLocalScale.z = FlattenedAxisThickness * maxAxisThickness;
        break;
}

平面化の判定の際の閾値と、平面化したときのBoundingBoxの厚みはBoundingBoxManipulateのインスペクタより設定することができます。

なお、一点注意事項があるのですが8/10時点ではバグがあり、Do Not Flattenを指定した場合にDragによる移動が動作しません。

【2017/8/16 追記】
現在は修正されているので、以下の対応は不要です。

暫定対処として、BoundingBoxManipulate.csの651行目付近のRefreshActiveHandles()を少し修正することにより、正常に動作させることができます。

private void RefreshActiveHandles()
{
    foreach (GameObject handleGo in Interactibles)
    {
        BoundingBoxHandle handle = handleGo.GetComponent<BoundingBoxHandle>();
        OperationEnum handleOperation = GetBoundingBoxOperationFromHandleType(handle.HandleType, handle.HandleTypeFlattened);
        bool operationPermitted = (handleOperation & permittedOperations) != 0;
        bool flattenedTypePermitted =
            (FlattenedAxis == FlattenModeEnum.DoNotFlatten && handle.HandleTypeFlattened == BoundingBoxHandle.HandleTypeFlattenedEnum.None) ||
            (FlattenedAxis != FlattenModeEnum.DoNotFlatten && handle.HandleTypeFlattened != BoundingBoxHandle.HandleTypeFlattenedEnum.None) ||
            (handleOperation == OperationEnum.Drag);    // ここを追加

AppBarの使い方

AppBarはAdjustやRemoveといった操作ができるボタン(Default Button)をあらかじめ備えているので、使用するのに特別な設定は必要ありません。
Default Buttonのうち、使用可能なボタンを限定したい場合はAppBarのインスペクタより設定することが可能です。

Show/HideはAppBarを短縮表示モードに切り替える/戻すボタン、Adjust/Doneはオブジェクトの操作を開始/終了するためのボタン、Removeはオブジェクトを削除するボタンです。

また、AppBarはボタンをエディタから追加することが可能です。

AppBarで使用されるボタンはButton Prefabで指定したものから生成して使われます。ボタンのアイコンプロファイルも設定することができますが、デフォルトではフォントベースであるDefaultButtonIconProfileが使用されます。
新たに追加するボタンはCustom Buttons以下で設定できますが、その設定項目は以下の通りです。

Button Name
追加したボタンのGameObject名
Label Text
ボタンに表示されるテキスト
Icon
ボタンのアイコン
Interaction Receiver
ボタンをタップしたときのアクションを定義したInteraction Receiver

以前はInteractionReceiverを指定しても、追加したボタンをタップしたときに何も反応しなかったのですが、現在は修正されておりますので上記設定だけで正しく動作します。
そして以下のようなInteractionReceiverを継承したスクリプトを用意すれば、Custom Buttonを押したときに何かしらのアクションを起こすことができます。

using HUX.Receivers;
using UnityEngine;
using HUX.Interaction;

public class SampleReceiver : InteractionReceiver {
    protected override void OnTapped(GameObject obj, InteractionManager.InteractionEventArgs eventArgs)
    {
        if(obj.name == "Custom01")
        {
            Debug.Log("CustomButton \"Custom01\" was Tapped");
        }
    }
}

小ネタ

BoundingBoxTargetを動的に追加する

BoundingBoxでの操作対象を動的に追加したい場合、AddComponentメソッドを使ってBoundingBoxTargetをアタッチすればよいと思われるかもしれませんが、実はそれでは実機で意図した通りに動作しません。動作させるにはBoundingBoxTargetのStartメソッドを次のように変更する必要があります。

private void Start()
{
    // 以下、4行を追記
    TagOnSelected = new FilterTag();
    TagOnSelected.Tag = "hidden";
    TagOnDeselected = new FilterTag();
    TagOnDeselected.Tag = FilterTag.DefaultTag;

    Button button = GetComponent<Button>();
    button.FilterTag = TagOnDeselected;
}

FilterTagとはMRDesignLabにおけるRaycastMaskのような機能を持ったオブジェクトです。InteractibleObjectというクラスがFilterTagをメンバとしてもっており、ButtonやCompoundButtonはこのクラスを継承しています。

using System;
using UnityEngine;

namespace HUX.Interaction
{
    /// <summary>
    /// Objects wanting to recieve messages can inherit from this class.
    /// They will only recieve a message from the Focus Manager or Interaction Manager
    /// if their custom Filter Tag is valid on the Focus Manager.
    /// </summary>
    public class InteractibleObject : MonoBehaviour
    {
#region public members
        public FilterTag FilterTag;
#endregi
    }
}


FilterTagはString型のTagとconst string型のDefaultTagの2つのメンバ変数をもつオブジェクトで、DefaultTagには「Default」と設定されています。


[System.Serializable]
public class FilterTag
{
    public const string DefaultTag = "Default";
    public string Tag = DefaultTag;
}

このFilterTagをもつオブジェクトはTagに「Default」と設定されているときだけGazeによるフォーカスの対象となります。したがって、例えばTagに「hidden」と設定されている場合は、いくらオブジェクトをタップしても何も反応はしません。(Gazeによるフォーカスの仕組みは過去の記事を参照ください)

さて、サンプルシーンでのBoundingBoxTargetではTag On Selectedに「hidden」が、Tag On Deselectedには「Default」が指定されています。

また、Startメソッドを見てみると、起動時はアタッチされているButtonのFilterTagにTagOnDeselectedをセットしています。

/// BoundingBoxTarget.cs(再掲)

public FilterTag TagOnSelected;
public FilterTag TagOnDeselected;

private void Start()
{
    // 以下、4行を追記
    TagOnSelected = new FilterTag();
    TagOnSelected.Tag = "hidden";
    TagOnDeselected = new FilterTag();
    TagOnDeselected.Tag = FilterTag.DefaultTag;

    Button button = GetComponent<Button>();
    button.FilterTag = TagOnDeselected;
}

すなわち、アプリ起動時にGazeによるフォーカスの対象となるようDefaultなFilterTagがButtonに設定されますが、修正していないコードで動的にBoundingBoxTargetを追加したときは、設定したFilterTagのTagがnullなのでフォーカスの対象とならず正常に動作しません。ただし、エディタで動かす場合はエディタ拡張スクリプトの方でTagに値が設定され正常に動作するので注意が必要です。

なお、BoundingBoxの対象となるとButtonのFilterTagが入れ替えられます。

/// BoundingBoxManipulate.cs
// 操作対象となるGameObjectについてのプロパティ
public override GameObject Target
{
    get
    {
        return target;
    }

    set
    {
        if (target != value)
        {
            // BoundingBoxTarget.csに対してSendMessageする
            if (value != null)
            {
                value.SendMessage("OnTargetSelected", SendMessageOptions.DontRequireReceiver);
            }
            if (target != null)
            {
                target.SendMessage("OnTargetDeselected", SendMessageOptions.DontRequireReceiver);
            }
            target = value;
            // Reset active handle to drag
            SetHandleByOperation(OperationEnum.Drag);
            ManipulatingNow = false;
        }
/// BoundingBoxTarget.cs
public void OnTargetSelected()
{
    //Debug.Log("Selecting target" + name);
    GetComponent<Button>().FilterTag = TagOnSelected;
}

public void OnTargetDeselected ()
{
    //Debug.Log("Deselecting target " + name);
    GetComponent<Button>().FilterTag = TagOnDeselected;
}

BoundingBoxの操作対象として選択された状態となると、ButtonのFilterTagが「hidden」に設定されるので、BoundingBoxTargetのTappedメソッドがコールされなくなります。見た目上は何の変化もありませんが、不測の事態が発生したときに参考になる、かもしれません。

AppBarのボタンの見た目を変える

AppBarはただプレハブとして用意されているSquareボタンを並べているわけではなく、ベースとなるボタンのオブジェクトを「Background Bar」として別に用意しています。

この背景となるオブジェクトは、AppBarの状態に応じて大きさが変わるようになっています。

/// AppBar.cs
private void Update() {
   FollowBoundingBox(true);
   // AppBarは状態を持っており、各種ボタンを押すと状態が変化する
   switch (State) {
       // 通常モード
       case AppBarStateEnum.Default:
       default:
           targetBarSize = new Vector3 (numDefaultButtons, 1f, 1f);
           if (boundingBox != null)
               boundingBox.AcceptInput = false;
           break;
       // Hiddenモード、AppBarが最小化されている
       case AppBarStateEnum.Hidden:
           targetBarSize = new Vector3(numHiddenButtons, 1f, 1f);
           if (boundingBox != null)
               boundingBox.AcceptInput = false;
           break;
       // Manipulationモード、いわゆるAdjust状態
       case AppBarStateEnum.Manipulation:
           targetBarSize = new Vector3(numManipulationButtons, 1f, 1f);
           if (boundingBox != null)
               boundingBox.AcceptInput = true;
           break;
   }
   // AppBarの状態に応じて、ベースとなるボタンのShapeの大きさを変えている
   backgroundBar.transform.localScale = Vector3.Lerp(backgroundBar.transform.localScale, targetBarSize, 0.5f);
}

したがって、AppBarのインスペクタにてButton Prefabを入れ替えたとしてもAppBar自体の形状は変わりません。
AppBarの形状を変更したい場合は、まずは背景であるBackgroundBarが表示されないようRendererをオフにしてprefabを更新します。

続いて、AppBarのButton prefabを入れ替えます。ここでは丸形状のCircleButtonに変更しました。

最後に、AppBar内のボタンを表すクラスであるAppBarButtonのInitialize()を修正します。

/// AppBarButton.cs
public void Initialize(AppBar newParentToolBar, AppBar.ButtonTemplate newTemplate, ButtonIconProfile newCustomProfile)
{
    // TODO move this into the tag manager
    VisibleFilterTag = new FilterTag();
    VisibleFilterTag.Tag = "Default";
    HiddenFilterTag = new FilterTag();
    HiddenFilterTag.Tag = "Hidden";
    template = newTemplate;
    customIconProfile = newCustomProfile;
    parentToolBar = newParentToolBar;
    cButton = GetComponent<CompoundButton>();
    // cButton.MainRenderer.enabled = false;    // この行をコメントアウト
    cButton.MainRenderer.enabled = true;        // こちらを追加
    text = GetComponent<CompoundButtonText>();
    text.Text = template.Text;
    icon = GetComponent<CompoundButtonIcon>();}

AppBarではアプリ起動時にコード内で定義されているボタンの仕様を決めるテンプレートを用意してボタンを生成しています。デフォルトではBackgroudBarを使うために、ボタンのprefabが持っているベースのオブジェクトは描画されないのですが、ボタンの見た目を変えたいときは、上記修正を反映させることにより元々のボタンの形のまま描画されます。

まとめ

MRDesignLabのBoundingBoxとAppBarのバグフィックスを含めた使い方と小ネタについてまとめました。実はGazeを当てているBoundingBoxのハンドルの色を変える、カメラとBoundingBoxの距離に応じてハンドルの大きさを変えるといった改造ネタもあるのですが、記事を書いていたら膨大な量になってしまったので、次回以降に公開したいと思います。

なお、今回ブログを書くにあたって以下のソースを解析しました。また、先日からVALUをはじめておりまして、VALUERの皆様には解析したコメント付きソースコードを何らかの形で提供する予定です。興味があるかたはVALUERになっていただけると幸いです。

  • AppBar.cs
  • AppBarButton.cs
  • BoundingBox.cs
  • BoundingBoxGizmo.cs
  • BoundingBoxGizmoShell.cs
  • BoundingBoxHandle.cs
  • BoundingBoxManipulate.cs
  • BoundingBoxTarget.cs