MRが楽しい

MRやVRについて学習したことを書き残す

HoloLens2で認識した空間マッピングをDevicePortalからダウンロードする

本日は HoloLens2 の小ネタ枠です。
HoloLens2で認識した空間マッピングをDevicePortalからダウンロードする手順を記事にします。
f:id:bluebirdofoz:20210116230931j:plain

DevicePortalからの空間マッピングのダウンロード手順

HoloLens2 の DevicePortal を開きます。
[Views]を開き、[3D View]をクリックします。
f:id:bluebirdofoz:20210116230945j:plain

[3D View]画面が開きます。
[Spatial mapping]の[Update]ボタンをクリックすると、現在 HoloLens2 が認識している空間マッピングを表示できます。
f:id:bluebirdofoz:20210116231012j:plain

表示された空間マッピングはマウス操作で様々な角度で確認できます。
また、[Save]ボタンの下には空間マッピングの三角ポリゴン数と分割ボリューム数が表示されます。
f:id:bluebirdofoz:20210116231024j:plain

[Save]ボタンをクリックすると、空間マッピングを obj 形式のデータで取得できます。
f:id:bluebirdofoz:20210116231034j:plain

取得した3Dデータは[3Dビューア]で開くことで確認できます。
f:id:bluebirdofoz:20210116231047j:plain

3Dデータを Unity に取り込むことで以下の仮想メッシュとして利用することもできます。
bluebirdofoz.hatenablog.com

空間マッピングのクリア

空間マッピングを一から取り直す場合は以下の手順で空間マッピングをクリアします。
[設定]パネルを開き、[システム]項目を選択します。
f:id:bluebirdofoz:20210116231108j:plain

[ホログラム]タブを開き、[すべてのホログラムを削除]をクリックすると空間マッピングがクリアされます。
f:id:bluebirdofoz:20210116231119j:plain

引数指定のUnityEventを介してMRTKのSliderから値を取得する

本日は MRTK の技術調査枠です。
引数指定の UnityEvent を介して MRTK の Slider から値を取得する手順を記事にします。

前提条件

前回記事の続きです。
bluebirdofoz.hatenablog.com

引数指定のUnityEvent

UnityEvent クラスを継承した新規クラスを作成すると、UnityEvent で動的に引数を渡すことができます。
docs.unity3d.com

以下の記事でも利用例を記述しています。
bluebirdofoz.hatenablog.com

UnityEventを仲介する

MRTK のスライダーイベントはイベントデータとして SliderEventData を渡すため、float の値を利用する関数を直接呼び出すことはできません。
UnityEvent を利用して UnityEvent に変換する関数を作成することで、Slider のイベント発生時に float の値を利用する関数を呼び出すことができます。

スクリプト例として以下のスクリプトを作成しました。
・SettingSliderEvent.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// SliderEventDataアクセスのため
using Microsoft.MixedReality.Toolkit.UI;

// UnityEvent を利用するため Events を追加
using UnityEngine.Events;

// 引数に float を受け取る UnityEvent<T0> の継承クラスを作成する
// Inspector ビューに表示させるため、Serializable を設定する
[System.Serializable]
public class ExtFloatSliderEvent : UnityEvent<float>
{
}

public class SettingSliderEvent : MonoBehaviour
{
    // 作成した継承クラスで UnityEvent を登録する
    /// <summary>
    /// スライダー値変更時実行処理
    /// </summary>
    [SerializeField, Tooltip("スライダー値変更時実行処理")]
    private ExtFloatSliderEvent SetExtFloatSliderEvent;

    public void OnSliderUpdated(SliderEventData eventData)
    {
        Debug.Log("SliderValue:" + eventData.NewValue.ToString());
        SetExtFloatSliderEvent.Invoke(eventData.NewValue);
    }
}

合わせて受け取った float 値をデバッグログに表示するパブリック関数を持つスクリプトを作成しました。
・DebugLogValue

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DebugLogValue : MonoBehaviour
{
    /// <summary>
    /// 受け取ったfloat値をデバッグログに表示する
    /// </summary>
    /// <param name="a_value"></param>
    public void ShowFloatValue(float a_value)
    {
        Debug.Log("ShowFloatValue : " + a_value.ToString());
    }
}

2つのスクリプトを適当なオブジェクトにアタッチします。
f:id:bluebirdofoz:20210115192736j:plain

前回記事と同様に、PinchSlider のイベントには SliderEentData を引数とする SettingSliderEvent.cs の関数を指定します。
f:id:bluebirdofoz:20210115192745j:plain

次に呼び出される SettingSliderEvent.cs のイベントに float 値を受け取る関数を指定します。
f:id:bluebirdofoz:20210115192755j:plain

シーンを再生して動作を確認します。
Slider の変化値を float 値を受け取る関数に UnityEvent を介して渡すことができました。
f:id:bluebirdofoz:20210115192803j:plain

MRTKのSliderから発生イベントと値を取得する

本日は MRTK の技術調査枠です。
MRTKのSliderから発生イベントと値を取得する手順を記事にします。

MRTKのSlider

MRTK には Slider という操作UXのテンプレートが含まれています。
摘みを操作することで連続的な値を直感的に設定することができます。
microsoft.github.io

Sliderの利用方法

Slider は[Mixed Reality Toolkit Foundation/SDK/Features/UX/Prefabs/Sliders]配下の PinchSlider.prefab をシーンに配置することで利用できます。
f:id:bluebirdofoz:20210114221115j:plain

またはメニューから[Mixed Reality Toolkit -> Toolbox]で[MRTK Toolbox]画面を開き、Slider を選択することで利用できます。
f:id:bluebirdofoz:20210114221126j:plain

シーンを再生すると動作が確認できます。
摘みの部分をハンドレイ、または、グラブ操作で掴んで操作することができます。
f:id:bluebirdofoz:20210114221137j:plain

値の取得方法

Slider の変更イベントを取得するには以下のイベント関数を利用します。
・OnValueUpdated:スライダーの値が変更されると呼び出されます
・OnInteractionStarted:ユーザーがスライダーを掴んだ際に呼び出されます
・OnInteractionEnded:ユーザーがスライダーを離した際に呼び出されます
・OnHoverEntered:ユーザーの手が近距離または遠距離インタラクションを使用してスライダーに触れた際に呼び出されます
・OnHoverExited:ユーザーの手がスライダーから離れた際に呼び出されます。

コード例として以下のサンプルスクリプトを作成しました。
・SliderEventTest.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// SliderEventData を利用するため
using Microsoft.MixedReality.Toolkit.UI;

public class SliderEventTest : MonoBehaviour
{
    // OnSliderUpdated 設定用関数
    public void DebugOnSliderUpdated(SliderEventData eventData)
    {
        Debug.Log("OnValueUpdated Event:" + eventData.NewValue);
    }

    // OnInteractionStarted 設定用関数
    public void DebugOnInteractionStarted(SliderEventData eventData)
    {
        Debug.Log("OnInteractionStarted Event:" + eventData.NewValue);
    }

    // OnInteractionEnded 設定用関数
    public void DebugOnInteractionEnded(SliderEventData eventData)
    {
        Debug.Log("OnInteractionEnded Event:" + eventData.NewValue);
    }

    // OnHoverEntered 設定用関数
    public void DebugOnHoverEntered(SliderEventData eventData)
    {
        Debug.Log("OnHoverEntered Event:" + eventData.NewValue);
    }

    // OnHoverExited 設定用関数
    public void DebugOnHoverExited(SliderEventData eventData)
    {
        Debug.Log("OnHoverExited Event:" + eventData.NewValue);
    }
}

サンプルスクリプトを適当なオブジェクトにアタッチします。
f:id:bluebirdofoz:20210114221156j:plain

PinchSlider の各イベントに作成した関数への参照を設定します。
f:id:bluebirdofoz:20210114221209j:plain

シーンを再生して Slider を操作すると各種イベントが発生し、その時点の値が取得できる様子がログで確認できます。
f:id:bluebirdofoz:20210114221219j:plain

Unityで複数のゲームオブジェクトのスケールをメッシュ形状に合わせて調整する

本日は Unity の小ネタ枠です。
Unityで複数のゲームオブジェクトのスケールをメッシュ形状に合わせて調整する方法を記事にします。
f:id:bluebirdofoz:20210113180232j:plain

前提条件

前回記事の応用になります。
bluebirdofoz.hatenablog.com

サンプルシーン

以下のような様々な大きさや形状の3Dモデルを子オブジェクトに持つオブジェクトを作成しました。
f:id:bluebirdofoz:20210113180244j:plain

コード例

複数のゲームオブジェクトのメッシュ形状から Bounds を計算して指定のスケールサイズに収めるサンプルコードを作成しました。
以下の処理を行います。
1.アタッチしたゲームオブジェクトの子オブジェクトを全てチェックする。
2.MeshFilter が設定されているオブジェクトからメッシュ形状の Bounds を取得する
3.取得したメッシュ形状の Bounds 全てを含むワールド座標系の Bounds を取得する
4.ワールド座標の Bounds の最大長の辺が指定のスケールに収まるようにローカルスケールを調整する
5.アタッチしたゲームオブジェクトのローカル座標系の Bounds を設定する
6.シーンで結果が確認できるように Bounds サイズの BoxCollider を設定する
・ControlChildObjBounds.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ControlChildObjBounds : MonoBehaviour
{
    /// <summary>
    /// 子オブジェクトの統合Bounds
    /// </summary>
    public Bounds childObjBounds;
    
    void Start()
    {
        // オブジェクトのローカルスケールをオブジェクトが 1m 長に収まるよう調整する
        ChangeWorldBoundsSize(1.0f);

        // Boundsの大きさと形状が見た目に分かるようコライダーを追加する
        BoxCollider collider = this.gameObject.AddComponent<BoxCollider>();
        // 計算されたバウンドボックスに合わせてコライダーの大きさと位置を変更する
        collider.center = childObjBounds.center;
        collider.size = childObjBounds.size;
    }

    /// <summary>
    /// ワールド座標での全体のバウンドサイズを元にローカルスケールを調整する
    /// </summary>
    public void ChangeWorldBoundsSize(float size)
    {
        // ワールド座標のバウンドサイズを計算する
        Bounds objBounds = CalcChildObjWorldBounds(this.gameObject, new Bounds());

        // バウンドの最大長の辺の長さを取得する
        float maxlength = Mathf.Max(objBounds.size.x, objBounds.size.y, objBounds.size.z);

        // スケール調整の係数を取得する
        float coefficient = size / maxlength;

        // ローカルスケールを変更する
        this.transform.localScale = this.transform.localScale * coefficient;

        // ローカル座標でのバウンドサイズを計算する
        childObjBounds = CalcLocalObjBounds(this.gameObject);
    }

    /// <summary>
    /// 現在オブジェクトのローカル座標でのバウンド計算
    /// </summary>
    private Bounds CalcLocalObjBounds(GameObject obj)
    {
        // 指定オブジェクトのワールドバウンドを計算する
        Bounds totalBounds = CalcChildObjWorldBounds(obj, new Bounds());

        // ローカルオブジェクトの相対座標に合わせてバウンドを再計算する
        // オブジェクトのワールド座標とサイズを取得する
        Vector3 ObjWorldPosition = this.transform.position;
        Vector3 ObjWorldScale = this.transform.lossyScale;

        // バウンドのローカル座標とサイズを取得する
        Vector3 totalBoundsLocalCenter = new Vector3(
            (totalBounds.center.x - ObjWorldPosition.x) / ObjWorldScale.x,
            (totalBounds.center.y - ObjWorldPosition.y) / ObjWorldScale.y,
            (totalBounds.center.z - ObjWorldPosition.z) / ObjWorldScale.z);
        Vector3 meshBoundsLocalSize = new Vector3(
            totalBounds.size.x / ObjWorldScale.x,
            totalBounds.size.y / ObjWorldScale.y,
            totalBounds.size.z / ObjWorldScale.z);

        Bounds localBounds = new Bounds(totalBoundsLocalCenter, meshBoundsLocalSize);

        return localBounds;
    }

    /// <summary>
    /// 子オブジェクトのワールド座標でのバウンド計算(再帰処理)
    /// </summary>
    private Bounds CalcChildObjWorldBounds(GameObject obj, Bounds bounds)
    {
        // 指定オブジェクトの全ての子オブジェクトをチェックする
        foreach (Transform child in obj.transform)
        {
            if(!child.gameObject.activeSelf)
            {
                // 無効なゲームオブジェクトは無視する
                continue;
            }

            // メッシュフィルターの存在確認
            MeshFilter filter = child.gameObject.GetComponent<MeshFilter>();

            if (filter != null)
            {
                // オブジェクトのワールド座標とサイズを取得する
                Vector3 ObjWorldPosition = child.position;
                Vector3 ObjWorldScale = child.lossyScale;

                // フィルターのメッシュ情報からバウンドボックスを取得する
                Bounds meshBounds = filter.mesh.bounds;

                // バウンドのワールド座標とサイズを取得する
                Vector3 meshBoundsWorldCenter = meshBounds.center + ObjWorldPosition;
                Vector3 meshBoundsWorldSize = Vector3.Scale(meshBounds.size, ObjWorldScale);

                // バウンドの最小座標と最大座標を取得する
                Vector3 meshBoundsWorldMin = meshBoundsWorldCenter - (meshBoundsWorldSize / 2);
                Vector3 meshBoundsWorldMax = meshBoundsWorldCenter + (meshBoundsWorldSize / 2);

                // 取得した最小座標と最大座標を含むように拡大/縮小を行う
                if (bounds.size == Vector3.zero)
                {
                    // 元バウンドのサイズがゼロの場合はバウンドを作り直す
                    bounds = new Bounds(meshBoundsWorldCenter, Vector3.zero);
                }
                bounds.Encapsulate(meshBoundsWorldMin);
                bounds.Encapsulate(meshBoundsWorldMax);
            }

            // 再帰処理
            bounds = CalcChildObjWorldBounds(child.gameObject, bounds);
        }
        return bounds;
    }
}

動作確認

スクリプトを子オブジェクトに持つオブジェクトに追加します。
f:id:bluebirdofoz:20210113180258j:plain

シーンを再生すると、子オブジェクトが 1m 立方のサイズに収まるようにオブジェクトが縮小されます。
更に子オブジェクトのメッシュ形状を内包する BoxCollider がオブジェクトに追加されます。
f:id:bluebirdofoz:20210113180312j:plain
f:id:bluebirdofoz:20210113180323j:plain
f:id:bluebirdofoz:20210113180333j:plain

2021/07/22追記

MeshFilter ではなく MeshRenderer を利用すると、ワールド座標の Bounds を取得することができます。
MeshRenderer を用いた改良版のスクリプトは以下の記事を参照ください。
bluebirdofoz.hatenablog.com

Unityで複数のゲームオブジェクトのメッシュ形状に合わせてBoundsを設定する

本日は Unity の小ネタ枠です。
Unity で複数のゲームオブジェクトのメッシュ形状に合わせて Bounds を設定する方法を記事にします。
f:id:bluebirdofoz:20210112231910j:plain

Boundsの組合せ

複数の Bounds を組み合わせるには Bounds クラスの Encapsulate 関数を使います。
docs.unity3d.com

サンプルシーン

以下のような様々な大きさや形状の3Dモデルを子オブジェクトに持つオブジェクトを作成しました。
f:id:bluebirdofoz:20210112231920j:plain

コード例

複数のゲームオブジェクトのメッシュ形状から Bounds を計算するサンプルコードを作成しました。
以下の処理を行います。
1.アタッチしたゲームオブジェクトの子オブジェクトを全てチェックする。
2.MeshFilter が設定されているオブジェクトからメッシュ形状の Bounds を取得する
3.取得したメッシュ形状の Bounds 全てを含むワールド座標系の Bounds を取得する
4.アタッチしたゲームオブジェクトのローカル座標系に Bounds を変換する
5.シーンで結果が確認できるように Bounds サイズの BoxCollider を設定する
・ChildObjBounds.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ChildObjBounds : MonoBehaviour
{
    /// <summary>
    /// 子オブジェクトの統合Bounds
    /// </summary>
    public Bounds childObjBounds;
    
    void Start()
    {
        // 
        childObjBounds = CalcLocalObjBounds(this.gameObject);

        // Boundsの大きさと形状が見た目に分かるようコライダーを追加する
        BoxCollider collider = this.gameObject.AddComponent<BoxCollider>();
        // 計算されたバウンドボックスに合わせてコライダーの大きさと位置を変更する
        collider.center = childObjBounds.center;
        collider.size = childObjBounds.size;
    }

    /// <summary>
    /// 現在オブジェクトのローカル座標でのバウンド計算
    /// </summary>
    private Bounds CalcLocalObjBounds(GameObject obj)
    {
        // 指定オブジェクトのワールドバウンドを計算する
        Bounds totalBounds = CalcChildObjWorldBounds(obj, new Bounds());

        // ローカルオブジェクトの相対座標に合わせてバウンドを再計算する
        // オブジェクトのワールド座標とサイズを取得する
        Vector3 ObjWorldPosition = this.transform.position;
        Vector3 ObjWorldScale = this.transform.lossyScale;

        // バウンドのローカル座標とサイズを取得する
        Vector3 totalBoundsLocalCenter = new Vector3(
            (totalBounds.center.x - ObjWorldPosition.x) / ObjWorldScale.x,
            (totalBounds.center.y - ObjWorldPosition.y) / ObjWorldScale.y,
            (totalBounds.center.z - ObjWorldPosition.z) / ObjWorldScale.z);
        Vector3 meshBoundsLocalSize = new Vector3(
            totalBounds.size.x / ObjWorldScale.x,
            totalBounds.size.y / ObjWorldScale.y,
            totalBounds.size.z / ObjWorldScale.z);

        Bounds localBounds = new Bounds(totalBoundsLocalCenter, meshBoundsLocalSize);

        return localBounds;
    }

    /// <summary>
    /// 子オブジェクトのワールド座標でのバウンド計算(再帰処理)
    /// </summary>
    private Bounds CalcChildObjWorldBounds(GameObject obj, Bounds bounds)
    {
        // 指定オブジェクトの全ての子オブジェクトをチェックする
        foreach (Transform child in obj.transform)
        {
            // メッシュフィルターの存在確認
            MeshFilter filter = child.gameObject.GetComponent<MeshFilter>();

            if (filter != null)
            {
                // オブジェクトのワールド座標とサイズを取得する
                Vector3 ObjWorldPosition = child.position;
                Vector3 ObjWorldScale = child.lossyScale;

                // フィルターのメッシュ情報からバウンドボックスを取得する
                Bounds meshBounds = filter.mesh.bounds;

                // バウンドのワールド座標とサイズを取得する
                Vector3 meshBoundsWorldCenter = meshBounds.center + ObjWorldPosition;
                Vector3 meshBoundsWorldSize = Vector3.Scale(meshBounds.size, ObjWorldScale);

                // バウンドの最小座標と最大座標を取得する
                Vector3 meshBoundsWorldMin = meshBoundsWorldCenter - (meshBoundsWorldSize / 2);
                Vector3 meshBoundsWorldMax = meshBoundsWorldCenter + (meshBoundsWorldSize / 2);

                // 取得した最小座標と最大座標を含むように拡大/縮小を行う
                if (bounds.size == Vector3.zero)
                {
                    // 元バウンドのサイズがゼロの場合はバウンドを作り直す
                    bounds = new Bounds(meshBoundsWorldCenter, Vector3.zero);
                }
                bounds.Encapsulate(meshBoundsWorldMin);
                bounds.Encapsulate(meshBoundsWorldMax);
            }

            // 再帰処理
            bounds = CalcChildObjWorldBounds(child.gameObject, bounds);
        }
        return bounds;
    }
}

動作確認

スクリプトを子オブジェクトに持つオブジェクトに追加します。
f:id:bluebirdofoz:20210112231936j:plain

シーンを再生すると、子オブジェクトのメッシュ形状を内包する BoxCollider がオブジェクトに追加されます。
f:id:bluebirdofoz:20210112231950j:plain
f:id:bluebirdofoz:20210112232004j:plain
f:id:bluebirdofoz:20210112232014j:plain

アプリパッケージでhololensにアプリをインストールする(HoloLens2版)

本日は HoloLens2 の小ネタ枠です。
以下の記事の HoloLens2 版の DevicePortal での手順をまとめます。
bluebirdofoz.hatenablog.com

DevicePortalからのインストール手順

HoloLens2 の DevicePortal を開きます。
[Views]を開き、[Apps]をクリックします。
f:id:bluebirdofoz:20210111225301j:plain

[Apps]画面を開いたら[ファイルを選択]ボタンをクリックします。
f:id:bluebirdofoz:20210111225313j:plain

[Deploy or Install Application]ダイアログが開きます。
インストールする appx ファイルを選択して[開く]ボタンをクリックします。
f:id:bluebirdofoz:20210111225323j:plain

framework など追加でインストールする依存パッケージがある場合は[Allow me to select framework package]のチェックを入れます。
[Next]または[Install](追加パッケージがない場合)をクリックします。
f:id:bluebirdofoz:20210111225334j:plain

追加パッケージをインストールする場合は次のページで再び[ファイルを選択]ボタンをクリックし、インストールする appx ファイルを選択して[開く]ボタンをクリックします。
f:id:bluebirdofoz:20210111225347j:plain

この状態で[Install]をクリックするとインストールが開始されます。
f:id:bluebirdofoz:20210111225400j:plain

インストールが完了すると、以下の通り「Successfully」のメッセージが表示されます。
f:id:bluebirdofoz:20210111225410j:plain

Unityでオブジェクトの配置座標を一定間隔で移動する

本日は Unity の小ネタ枠です。
Unityでオブジェクトの配置座標を一定間隔で移動する方法を記事にします。
f:id:bluebirdofoz:20210110232419j:plain

ゲームオブジェクトの移動

Unityのシーン上のオブジェクトの配置座標を移動させるには以下の2つの方法があります。
・座標軸をマウスドラッグする
f:id:bluebirdofoz:20210110232342j:plain

・TransformのPositionを編集する
f:id:bluebirdofoz:20210110232400j:plain

一定間隔での移動

一定間隔で移動させるには Ctrl キーを押したままドラッグします。
f:id:bluebirdofoz:20210110232435j:plain

デフォルト設定では 0.25 ずつ移動します。
f:id:bluebirdofoz:20210110232447j:plain

移動距離の設定

移動量を変更する場合はメニューから[Edit -> Grid and Snap Settings..]を選択します。
f:id:bluebirdofoz:20210110232457j:plain

[Grid and Snap]ダイアログが開きます。
[Increment Snap]のパネルで移動量の調整ができます。
f:id:bluebirdofoz:20210110232507j:plain

[Move]項目で移動量を調整できます。
[1]を設定すると、Ctrl キーを押したままドラッグで 1 ずつ移動させることができます。
f:id:bluebirdofoz:20210110232516j:plain