MRが楽しい

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

MRTKv2.xでレイの衝突先が空間メッシュか否か判定する

本日はMRTKv2.xの小ネタ枠です。
MRTKv2.xでレイの衝突先が空間メッシュか否か判定する方法を記事にします。

前提条件

本記事の動作確認を実施するため、以下の前回記事の環境を利用します。
bluebirdofoz.hatenablog.com

レイの衝突先が空間メッシュか否か判定する

Physics.Raycast関数を利用してレイが衝突したオブジェクトのコライダーからレイヤー番号を取得することができます。
docs.unity3d.com

if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out hit, Mathf.Infinity))
{
    Debug.Log("layer : {hit.collider.gameObject.layer}");
}

また空間メッシュのレイヤー番号はスクリプトから以下の方法で取得できます。
レイヤー番号がこれと一致するかチェックすることでレイが衝突したオブジェクトが空間メッシュか判定できます。
bluebirdofoz.hatenablog.com

// 空間認識のオブザーバを取得する
IMixedRealitySpatialAwarenessMeshObserver SpatialAwarenessMeshObserver =
    CoreServices.GetSpatialAwarenessSystemDataProvider<IMixedRealitySpatialAwarenessMeshObserver>();

// オブザーバからレイヤー番号を取得する
int spatialAwarenessLayer = SpatialAwarenessMeshObserver.MeshPhysicsLayer;

サンプルスクリプト

現在のカメラ視点からレイを飛ばし、衝突したオブジェクトが空間メッシュか否か、または衝突しなかったか判定するサンプルスクリプトを作成しました。
・SpatialMapTrackingPointCheck.cs

using Microsoft.MixedReality.Toolkit;
using Microsoft.MixedReality.Toolkit.SpatialAwareness;
using Microsoft.MixedReality.Toolkit.Utilities;
using UnityEngine;

public class SpatialMapTrackingPointCheck : MonoBehaviour
{
    [SerializeField] Renderer _renderer = default;
    
    private enum HitType
    {
        Nothing, // 何も当たらなかった
        SpatialMap, // 空間マップに当たった
        Other // その他のオブジェクトに当たった
    }
    
    void Update()
    {
        UpdateTransformFromCamera();
    }
    
    /// <summary>
    /// カメラから一定距離になるように位置を更新する
    /// </summary>
    private void UpdateTransformFromCamera()
    {
        // カメラからの距離上限(5m)
        const float modelSetDistance = 3.0f;
        
        // カメラ前方の位置を計算
        var camTransform = CameraCache.Main.transform;
        var camPosition = camTransform.position;
        var targetPosition =
            camPosition +
            camTransform.forward *
            modelSetDistance;
            
        // Spatial Awarenessのレイヤー(31: Spatial Awareness)に対してのみレイキャストの判定を行う
        // 空間認識のオブザーバを取得する
        IMixedRealitySpatialAwarenessMeshObserver spatialAwarenessMeshObserver =
            CoreServices.GetSpatialAwarenessSystemDataProvider<IMixedRealitySpatialAwarenessMeshObserver>();

        // オブザーバからレイヤー番号を取得する
        int meshPhysicsLayer = spatialAwarenessMeshObserver.MeshPhysicsLayer;
        
        // カメラからレイキャストを飛ばして途中に障害物があれば、その座標と種類を取得する
        HitType hitType = HitType.Nothing;
        if (Physics.Raycast(camPosition, camTransform.forward, out var hit, modelSetDistance))
        {
            targetPosition = hit.point;
            if (hit.collider.gameObject.layer == meshPhysicsLayer)
            {
                hitType = HitType.SpatialMap;
            }
            else
            {
                hitType = HitType.Other;
            }
        }
        
        // 計算した位置を反映する
        transform.position = targetPosition;

        // オブジェクトをカメラの方向に向ける
        transform.LookAt(camPosition);
        
        // 衝突したオブジェクトの種類によって色を変える
        _renderer.material.color = hitType switch
        {
            HitType.Nothing => Color.white,
            HitType.SpatialMap => Color.red,
            HitType.Other => Color.green
        };
    }
}

スクリプトをゲームオブジェクトにアタッチすると、カメラの視点の正面3m以内に障害物が存在すれば、その位置にオブジェクトが追従します。

シーンを再生してシミュレーション上で動作を確認します。
空間メッシュの方向を向くとポイントオブジェクトが赤色に、それ以外のオブジェクトだと緑色に、衝突判定が撮れなかった場合は白色になります。


MRTKv2.xで空間メッシュと視線の衝突位置を取得する

本日はMRTKv2.xの小ネタ枠です。
MRTKv2.xで空間メッシュと視線の衝突位置を取得する方法を記事にします。

前提条件

本記事の動作確認を実施するため、以下の前回記事の環境を利用します。
bluebirdofoz.hatenablog.com

空間メッシュとの当たり判定を取得する

Physics.Raycast関数を利用して特定レイヤーとレイの衝突位置を取得することができます。
docs.unity3d.com

if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out hit, Mathf.Infinity, layerMask))
{
    Debug.Log("Did Hit");
}

また空間メッシュのレイヤー番号はスクリプトから以下の方法で取得できます。
bluebirdofoz.hatenablog.com

// 空間認識のオブザーバを取得する
IMixedRealitySpatialAwarenessMeshObserver SpatialAwarenessMeshObserver =
    CoreServices.GetSpatialAwarenessSystemDataProvider<IMixedRealitySpatialAwarenessMeshObserver>();

// オブザーバからレイヤー番号を取得する
int spatialAwarenessLayer = SpatialAwarenessMeshObserver.MeshPhysicsLayer;

サンプルスクリプト

現在のカメラ視点からレイを飛ばし、空間メッシュに衝突した位置に移動するサンプルスクリプトを作成しました。
・SpatialMapTrackingPoint.cs

using Microsoft.MixedReality.Toolkit;
using Microsoft.MixedReality.Toolkit.SpatialAwareness;
using Microsoft.MixedReality.Toolkit.Utilities;
using UnityEngine;

public class SpatialMapTrackingPoint : MonoBehaviour
{
    void Update()
    {
        UpdateTransformFromCamera();
    }
    
    /// <summary>
    /// カメラから一定距離になるように位置を更新する
    /// </summary>
    private void UpdateTransformFromCamera()
    {
        // カメラからの距離上限(5m)
        const float modelSetDistance = 5.0f;
        
        // カメラ前方の位置を計算
        var camTransform = CameraCache.Main.transform;
        var camPosition = camTransform.position;
        var targetPosition =
            camPosition +
            camTransform.forward *
            modelSetDistance;
            
        // Spatial Awarenessのレイヤー(31: Spatial Awareness)に対してのみレイキャストの判定を行う
        // 空間認識のオブザーバを取得する
        IMixedRealitySpatialAwarenessMeshObserver spatialAwarenessMeshObserver =
            CoreServices.GetSpatialAwarenessSystemDataProvider<IMixedRealitySpatialAwarenessMeshObserver>();

        // オブザーバからレイヤー番号を取得する
        int meshPhysicsLayer = spatialAwarenessMeshObserver.MeshPhysicsLayer;
        int layerMask = 1 << meshPhysicsLayer;
        
        // カメラからレイキャストを飛ばして途中に障害物があれば、その手前に移動する
        if (Physics.Raycast(camPosition, camTransform.forward, out var hit, modelSetDistance, layerMask))
        {
            targetPosition = hit.point;
        }
        
        // 計算した位置を反映する
        transform.position = targetPosition;

        // オブジェクトをカメラの方向に向ける
        transform.LookAt(camPosition);
    }
}

スクリプトをゲームオブジェクトにアタッチすると、カメラの視点の正面5m以内に空間メッシュが存在すれば、その位置にオブジェクトが追従します。

シーンを再生してシミュレーション上で動作を確認します。
空間メッシュの方向を向くと、スクリプトをアタッチしたオブジェクトが追従します。

SurfaceMagnetismを利用する場合

Raycastを利用する以外にもMRTKのSurfaceMagnetismを利用して視線やハンドレイの衝突位置を追跡することができます。
SurfaceMagnetismを利用する方法については以下の記事を参照ください。
bluebirdofoz.hatenablog.com

UnityのMeshクラスで作成したメッシュをAssetsフォルダに出力する

本日は Unity の技術調査枠です。
UnityのMeshクラスで作成したメッシュをAssetsフォルダに出力する手順を記事にします。

Mesh

スクリプトからメッシュの作成や変更を行うUnityEngineのクラスです。
Meshクラスには頂点データや面データが含まれていてサーフェス形式の3Dモデルを形成します。
docs.unity3d.com

作成したメッシュはMeshFilterに引き渡して各種コンポーネントがアクセスできるようにします。
メッシュをレンダリング(描画)するにはMeshRendererコンポーネントを利用します。
docs.unity3d.com
docs.unity3d.com

作成したメッシュをAssetsフォルダに出力する場合はAssetDatabase.CreateAsset関数を使用します。
docs.unity3d.com
docs.unity3d.com

Color color = Color.red;
Material material = new Material(Shader.Find("Specular"));
AssetDatabase.CreateAsset(material, "Assets/Artifacts/material.mat");
material.SetColor("_Color", color);
AssetDatabase.SaveAssets();

サンプルスクリプト

以下の前回記事を参考にシーンの再生と共にCubeの形状を作成するスクリプトを作成しました。
bluebirdofoz.hatenablog.com

今回は更に、作成したCubeのメッシュをAssetsフォルダに出力するボタンをEditor拡張で追加しました。
・CubeMesh.cs

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

[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class CubeMesh : MonoBehaviour
{
    [SerializeField] private Material _material;
    
    void Start()
    {
        // メッシュを作成
        Mesh mesh = new Mesh();

        // 頂点リストを作成
        List<Vector3> vertices = new List<Vector3>
        {
            new Vector3(1.0f, 1.0f, 1.0f),
            new Vector3(-1.0f, 1.0f, 1.0f),
            new Vector3(-1.0f, -1.0f, 1.0f),
            new Vector3(1.0f, -1.0f, 1.0f),
            new Vector3(1.0f, -1.0f, -1.0f),
            new Vector3(-1.0f, -1.0f, -1.0f),
            new Vector3(-1.0f, 1.0f, -1.0f),
            new Vector3(1.0f, 1.0f, -1.0f),
        };

        // 面を構成するインデックスリストを作成
        List<int> triangles = new List<int> {
            0, 1, 2,  // 奥面
            0, 2, 3,  // 奥面
            4, 5, 6,  // 前面
            4, 6, 7,  // 前面
            0, 4, 7,  // 右面
            0, 3, 4,  // 右面
            6, 2, 1,  // 左面
            6, 5, 2,  // 左面
            6, 1, 0,  // 上面
            7, 6, 0,  // 上面
            4, 3, 2,  // 下面
            5, 4, 2,  // 下面
        };

        // メッシュに頂点を登録する
        mesh.SetVertices(vertices);

        // メッシュに面を構成するインデックスリストを登録する
        mesh.SetTriangles(triangles, 0);

        // 作成したメッシュをメッシュフィルターに設定する
        MeshFilter meshFilter = GetComponent<MeshFilter>();
        meshFilter.mesh = mesh;
        
        // メッシュにマテリアルを設定する
        MeshRenderer meshRenderer = GetComponent<MeshRenderer>();
        meshRenderer.material = _material;
    }
    
#if UNITY_EDITOR
    // 作成したCubeのメッシュをAssetsフォルダに出力するためのエディタ拡張
    [CustomEditor(typeof(CubeMesh))]
    public class CubeMeshEditor : UnityEditor.Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            var cubeMesh = target as CubeMesh;
            // 生成したMeshをAssetフォルダにエクスポートする
            if (GUILayout.Button("Export Mesh"))
            {
                if (!Application.isPlaying) return;
                if (cubeMesh is null) return;
                var mesh = cubeMesh.GetComponent<MeshFilter>().sharedMesh;
                AssetDatabase.CreateAsset(mesh, "Assets/Sandbox/MeshTest/ExportMesh/CubeMesh.asset");
                AssetDatabase.SaveAssets();
            }
        }
    }
#endif
}

シーンを再生すると、メッシュが構築されてCubeが生成されます。

この状態でサンプルスクリプトのInspectorに表示されている[Export Mesh]ボタンを押下すると以下のフォルダにAssetsファイルが出力されます。

Assets/Sandbox/MeshTest/ExportMesh/CubeMesh.asset

出力されたCubeMesh.assetを確認すると、Cubeの形状のメッシュデータが確認できます。

MRTKv2.xでシミュレータ内にテスト用の空間メッシュを表示する その2(用意した3Dモデルのテスト空間を表示する)

本日はMRTKv2.xの小ネタ枠です。
MRTKv2.xでシミュレータ内にテスト用の空間メッシュを表示する方法を記事にします。
今回は用意した3Dモデルのテスト空間を表示する方法についてです。

前回記事

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

3Dモデルをインポートする

シミュレータではHoloLens2でスキャンした3Dモデルを利用できます。
以下の記事を参考にHoloLens2がスキャンした空間メッシュをobjファイルとしてダウンロードします。
learn.microsoft.com

ダウンロードしたobjファイルをUnityのAssetフォルダにドラッグしてインポートします。

用意した3Dモデルのテスト空間を表示する

MixedRealityToolkitオブジェクトのInspectorビューを開き、プロファイルの[SpatialAwareness]タブを開きます。
前回記事で追加したSpatialObjectMeshObserverのパネルを開きます。

デフォルトのプロファイルは編集できないので[Clone]を実行し、任意のプロファイル名を設定してプロファイルをコピーします。

これでプロファイルが編集可能になります。

[Spatial Mesh Object]の項目に取り込んだ3Dモデルをドラッグして設定します。
これで設定は完了です。

シーンを再生すると取り込んだ3Dモデルがテスト空間として利用されます。

利用可能な3Dモデルについて

利用可能な3Dモデルには制約があるようでHoloLensから取得した以外の3Dモデルはそのままだと正常に表示されませんでした。
エディター内で作成したモデルはサイズ設定や階層構造が一部おかしくなってしまいました。


MRTKv2.xでシミュレータ内にテスト用の空間メッシュを表示する

本日はMRTKv2.xの小ネタ枠です。
MRTKv2.xでシミュレータ内にテスト用の空間メッシュを表示する方法を記事にします。

SpatialAwarenessの設定

MRTKv2.xでは空間メッシュはSpatialAwarenessの設定で管理されます。
本項目のオブザーバにSpatialObjectMeshObserverを追加することでUnityEditor上で動作する空間メッシュオブザーバを設定できます。
learn.microsoft.com

SpatialObjectMeshObserverの設定手順

MixedRealityToolkitオブジェクトのInspectorビューを開き、プロファイルの[SpatialAwareness]タブを開きます。

[+ Add Spatial Observer]をクリックして新規オブザーバを追加します。
これでオブザーバ種別が未設定のNew data providerがリストに追加されます。

追加したオブザーバをSpatialObjectMeshObserverに変更します。
パネルを開き、[Type]のプルダウンから[Microsoft.MixedReality.Toolkit.SpatialObjectMeshObserver -> SpatialObjectMeshObserver]を選択します。

これでUnityEditor上で動作する空間メッシュオブザーバを設定できました。

シーンを再生して空間メッシュが表示されれば成功です。

以下の記事で紹介した空間メッシュの有効無効の切り替えもUnityEditor上で確認できます。
bluebirdofoz.hatenablog.com

UnityでHierarchy階層の一番上のオブジェクトを参照する

本日はUnityの小ネタ枠です。
UnityでHierarchy階層の一番上のオブジェクトを参照する方法についてです。

Transform.root

Transform.rootはそのTransformの階層の一番上のTransformの参照を返します。
また、そのTransform自身が一番上のTransformである場合は自身のTransformを返します。

var rootTransform = transform.root

docs.unity3d.com

サンプルスクリプト

transform.rootで取得したTransformが自身か否かをチェックすることで自身が階層の一番上のオブジェクトか否かを判定する以下のサンプルスクリプトを作成しました。
・HierarchyRootTest.cs

using UnityEngine;

public class HierarchyRootTest : MonoBehaviour
{
    [ContextMenu("CheckHierarchyRoot")]
    public void CheckHierarchyRoot()
    {
        var hierarchyRoot = transform.root;
        
        // 取得したルートオブジェクトが自身のtransformと一致する場合は階層の一番上のオブジェクトと判定する
        if (hierarchyRoot == transform)
        {
            Debug.Log("This object is the top of the hierarchy.");
        }
        else
        {
            Debug.Log($"Hierarchy Root: {hierarchyRoot.name}");
        }
    }
}

階層の一番上のオブジェクトに本コンポーネントをアタッチしてCheckHierarchyRoot関数を実行すると、階層の一番上のオブジェクトと判定します。

それ以外のオブジェクトで実行した場合はその階層の一番上のオブジェクト名を表示します。

Unityでオブジェクトのグローバルスケールを参照・設定する

本日はUnityの小ネタ枠です。
Unityでオブジェクトのグローバルスケールを参照・設定する方法についてです。

Transform.lossyScale

グローバルスケールはワールド空間におけるオブジェクトの絶対的なスケール値です。
Transform.lossyScaleにアクセスすることでオブジェクトのグローバルスケールを取得できます。
docs.unity3d.com

var lossyScale = transform.lossyScale;

本変数は読み取り専用のため、グローバルスケールを設定したい場合はローカルスケールで逆算して設定を行う必要があります。

サンプルスクリプト

アタッチしたオブジェクトのグローバルスケールを取得し、設定する以下のサンプルスクリプトを作成しました。
・ScaleCheckTest.cs

using UnityEngine;

public class ScaleCheckTest : MonoBehaviour
{
    [SerializeField]
    private Vector3 setWorldScale = default;

    private void Start()
    {
        // 現在のワールドスケールを取得
        var lossyScale = transform.lossyScale;
        Debug.Log($"{lossyScale}");
    }
    
    [ContextMenu("SetWorldScale")]
    public void SetWorldScale()
    {
        // 現在のワールドスケールを取得
        var lossyScale = transform.lossyScale;
        Debug.Log($"{lossyScale}");
        
        // ワールドスケールを設定
        // lossyScaleは読み取り専用のため、localScaleを使って設定する
        transform.localScale = new Vector3(
            setWorldScale.x / lossyScale.x,
            setWorldScale.y / lossyScale.y,
            setWorldScale.z / lossyScale.z
            );
        
        // 再度ワールドスケールを取得
        lossyScale = transform.lossyScale;
        Debug.Log($"{lossyScale}");
    }
}

スクリプトをオブジェクトにアタッチしてシーンを再生すると、そのオブジェクトのグローバルスケールがログに出力されます。

setWorldScale変数を設定してコンテンツメニューから関数を呼び出すとローカルスケールで逆算して設定が行われます。