HoloLensアプリを作ってみよう(QRコード編)

最終更新日

はじめに

 建設現場において、建築物の完成物や施工過程のイメージの共有は重要です。2Dの図面から3DCGのCADへ移行し、そしてBIMへと技術が進んでいます。現在はBIMの発展と共にVR, MR, ARといった技術が建築・建設に応用されようとしています。

前回はMR機器のHoloLensを使って3DCGのモデルを表示させました。

このアプリで表示される建物モデルの形状はわかるのですが、意図した位置に配置することができません。適切な場所に建物を表示するには、位置の基準となる目印のようなものが必要です。本記事ではQRコードをHoloLensで認識させて、その位置に建物のモデルを表示させます。

今回は、以下の機器とソフトウェアを使います。

・HoloLens2 (リンク)

・Unity 2021.3(LTS) & VisualStudio 2019 (リンク)

シーンの作成とMRTKのインストール

まず、前回の記事の「Unityでアプリの調整」(リンク)の手前まで進めて下さい。

QRコードの作成

プログラムでQRコードを出力することもできるのですが、今回はWebサービスを利用します。そこに「Farnsworth House」と入力して作成されたQRコードが以下になります。このQRコードをHololens2で認識してモデルを表示する基準点とします。

QRコードを認識できるプラグインのインストール

以下の手順に従います。

① NuGetForUnity(リンク)からNugetForUnity.○○.unitypackageをダウンロード

② Unityのメニュー「Assets」/「Import Package」/「Custom Package…」を選び、NugetForUnity.○○.unitypackageをインポート

③ メニューに「NuGet」が加わるので、そこから「Manage NuGet Packages」を選択

④ NuGetのダイアログで検索ボックスで探して「Mixed Microsoft.MixedReality.QR」をインストールする

QRコードを認識してモデルを配置する機能の作成

まず、QRコードの左上になるGameObject(Pivot_Buildingと命名)を作成し、X座標に-90度回転させ、建物オブジェクトの親にします。QRコードは親子関係は以下の画像のようになります。

そしてQRコードを読み込んで、指定のオブジェクトを表示するC#コードを作成します。こちらの記事(リンク)を参考に作成しました。リンク先のコードはそのまま使うことができないので、改良したものが以下のコードになります。

using System;
using System.Linq;
using System.Collections.Generic;

using UnityEngine;
using UnityEngine.XR.WSA;

using Microsoft.MixedReality.QR;
using Microsoft.MixedReality.OpenXR;

#if WINDOWS_UWP
using Windows.Perception.Spatial;
using Windows.Perception.Spatial.Preview;
#endif

public class HelloQR : MonoBehaviour
{
    //public GameObject pivot_qr;
    public GameObject pivot_building;

    public bool IsTrackerRunning { get; private set; }
    public bool IsSupported { get; private set; }

    public event EventHandler<bool> QRCodesTrackingStateChanged;
    public event EventHandler<QRCodeEventArgs<QRCode>> QRCodeAdded;
    public event EventHandler<QRCodeEventArgs<QRCode>> QRCodeUpdated;
    public event EventHandler<QRCodeEventArgs<QRCode>> QRCodeRemoved;

    private QRCodeWatcher qrTracker;

    private bool enumerationComplete = false;
    private bool capabilityInitialized = false;

    private QRCodeWatcherAccessStatus accessStatus;
    private System.Threading.Tasks.Task<QRCodeWatcherAccessStatus> capabilityTask;

#if WINDOWS_UWP
    private SpatialCoordinateSystem rootSpatialCoordinateSystem;
    private Queue<QRCodeInformation> spatialCoordinateSystems = new Queue<QRCodeInformation>();
#endif

    async void Start()
    {
        IsSupported = QRCodeWatcher.IsSupported();
        capabilityTask = QRCodeWatcher.RequestAccessAsync();
        accessStatus = await capabilityTask;
        capabilityInitialized = true;

        if (!IsSupported)
        {
            // Do something to notify the user it isn't supported.
        }

        // Get the root spatial coordinate system.
#if WINDOWS_UWP
        rootSpatialCoordinateSystem = PerceptionInterop.GetSceneCoordinateSystem(UnityEngine.Pose.identity) as SpatialCoordinateSystem;
#endif
    }

    void Update()
    {
        // Wait until QR tracking is ready before we start looking for QR codes.
        if (qrTracker == null && capabilityInitialized && IsSupported && accessStatus == QRCodeWatcherAccessStatus.Allowed)
            SetupQRTracking();

#if WINDOWS_UWP
        lock (spatialCoordinateSystems)
        {
            while (spatialCoordinateSystems.Count > 0)
            {
                QRCodeInformation qrCodeInformation = spatialCoordinateSystems.Dequeue();

                // Do what you want with the QR code information. Examples below.

                // Get the text from the QR code.
                string qrCodeText = qrCodeInformation.Data;

                // Get the relative transform from the unity origin
                System.Numerics.Matrix4x4? relativePose = qrCodeInformation.QRSpatialCoordinateSystem.TryGetTransformTo(rootSpatialCoordinateSystem);
                if (relativePose != null)
                {
                    System.Numerics.Vector3 scale;
                    System.Numerics.Quaternion rotation1;
                    System.Numerics.Vector3 translation1;

                    System.Numerics.Matrix4x4 newMatrix = relativePose.Value;

                    // Platform coordinates are all right handed and unity uses left handed matrices. so we convert the matrix
                    // from rhs-rhs to lhs-lhs 
                    // Convert from right to left coordinate system
                    newMatrix.M13 = -newMatrix.M13;
                    newMatrix.M23 = -newMatrix.M23;
                    newMatrix.M43 = -newMatrix.M43;

                    newMatrix.M31 = -newMatrix.M31;
                    newMatrix.M32 = -newMatrix.M32;
                    newMatrix.M34 = -newMatrix.M34;

                    System.Numerics.Matrix4x4.Decompose(newMatrix, out scale, out rotation1, out translation1);
                    Vector3 translation = new Vector3(translation1.X, translation1.Y, translation1.Z);
                    Quaternion rotation = new Quaternion(rotation1.X, rotation1.Y, rotation1.Z, rotation1.W);

                    // Pose is the position and rotation of the QR code.
                    Pose pose = new Pose(translation, rotation);

                    pivot_building.SetActive(true);
                    pivot_building.transform.position = translation;
                    pivot_building.transform.rotation = rotation;
                }
                else
                    spatialCoordinateSystems.Enqueue(qrCodeInformation);// Re-queue it to process again.
            }
        }
#endif
    }

    #region QR tracking Setup
    private void SetupQRTracking()
    {
        try
        {
            qrTracker = new QRCodeWatcher();
            IsTrackerRunning = false;
            qrTracker.Added += QRCodeWatcher_Added;
            qrTracker.Updated += QRCodeWatcher_Updated;
            qrTracker.Removed += QRCodeWatcher_Removed;
            qrTracker.EnumerationCompleted += QRCodeWatcher_EnumerationCompleted;

            StartQRTracking();
        }
        catch (Exception ex)
        {
            Debug.Log($"QRCodesManager exception setting up the tracker {ex.ToString()}");
        }
    }

    public void StartQRTracking()
    {
        if (qrTracker != null && !IsTrackerRunning)
        {
            try
            {
                enumerationComplete = false;
                qrTracker.Start();
                IsTrackerRunning = true;
                QRCodesTrackingStateChanged?.Invoke(this, true);
            }
            catch (Exception ex)
            {
                Debug.Log($"QRCodesManager exception starting QRCodeWatcher {ex.ToString()}");
            }
        }
    }

    #endregion

    #region Events

    private void QRCodeWatcher_Removed(object sender, QRCodeRemovedEventArgs args)
    {
        try
        {
            QRCodeRemoved?.Invoke(this, QRCodeEventArgs.Create(args.Code));
        }
        catch (Exception ex)
        {
            Debug.Log($"QRCodeWatcher_Removed {ex.Message}.");
        }
    }

    private void QRCodeWatcher_Updated(object sender, QRCodeUpdatedEventArgs args)
    {
        try
        {
#if WINDOWS_UWP
            lock (spatialCoordinateSystems)
                spatialCoordinateSystems.Enqueue(new QRCodeInformation(args.Code.Data, SpatialGraphInteropPreview.CreateCoordinateSystemForNode(args.Code.SpatialGraphNodeId), args.Code.PhysicalSideLength));
#endif

            QRCodeUpdated?.Invoke(this, QRCodeEventArgs.Create(args.Code));
        }
        catch (Exception ex)
        {
            Debug.Log($"\nQRCodeWatcher_Updated {ex.Message}.");
        }
    }

    private void QRCodeWatcher_Added(object sender, QRCodeAddedEventArgs args)
    {
        try
        {
            if (enumerationComplete)
            {
#if WINDOWS_UWP
                lock (spatialCoordinateSystems)
                    spatialCoordinateSystems.Enqueue(new QRCodeInformation(args.Code.Data, SpatialGraphInteropPreview.CreateCoordinateSystemForNode(args.Code.SpatialGraphNodeId), args.Code.PhysicalSideLength));
#endif
            }

            QRCodeAdded?.Invoke(this, QRCodeEventArgs.Create(args.Code));
        }
        catch (Exception ex)
        {
            Debug.Log($"\nQRCodeWatcher_Added {ex.Message}.");
        }
    }

    private void QRCodeWatcher_EnumerationCompleted(object sender, object e) => enumerationComplete = true;

    #endregion
}

public static class QRCodeEventArgs
{
    public static QRCodeEventArgs<TData> Create<TData>(TData data) => new QRCodeEventArgs<TData>(data);
}

[Serializable]
public class QRCodeEventArgs<TData> : EventArgs
{
    public TData Data { get; private set; }

    public QRCodeEventArgs(TData data) => Data = data;
}

public class QRCodeInformation
{
    public string Data { get; set; }
    public float Length { get; set; }
#if WINDOWS_UWP
    public SpatialCoordinateSystem QRSpatialCoordinateSystem { get; set; }

    public QRCodeInformation(string data, SpatialCoordinateSystem qrSpatialCoordinateSystem, float length)
    {
        Data = data;
        Length = length;
        QRSpatialCoordinateSystem = qrSpatialCoordinateSystem;
    }
#endif

    public QRCodeInformation() { }
}

新しく作成したGameObject(Scriptと命名)に上のプログラムを適用して、そこの「Pivot_building」というパラメータに建物オブジェクトの親になっているGameObject(pivot_Building)を割り当てます。

それでは、3Dモデルを1/100の模型サイズに縮小して、前回の記事(リンク)を参考にアプリをビルドしてみましょう。

デモ動画1

QRコードの左上にGameObject(Pivot_Building)が配置され、その子ノードである建物オブジェクトが表示されていることがわかります。

構築順にモデルを表示する機能の作成

MRTKには様々なUIが付属されています。そのひとつであるボタンを使って、建物が構築される順に従って建築モデルを表示する機能を作ります。まず、前回と同様にMixedRealityFeatureTool.exeを実行させ「Mixed Reality Toolkit Tools」をUnityにインストールします。

するとメニューから「Mixed Reality」/「Toolkit」/「Toolbox」が追加されます。それをクリックすると、使えるUIの一覧が出てくるので、適当なボタンを選んでカメラの子ノードとして配置して、カメラから奥行45cmの画面下に設置します。HoloLensにおけるUIは位置によって使いやすさが変わってくるので、何回か調整して押しやすい場所に設置します。

QRコードによって建物の位置を定めた後に、ボタンを押すことで建物の部分を順々に表示するプログラム(記載省略)を作成します。どの部分が新しく表示されたか気づけるように赤いマテリアルを適用して、次の部分を表示する時はマテリアルを戻す機能を作成します。

それでは、前回の記事(リンク)を参考にHoloLensアプリをビルドしてみましょう。

デモ動画2

中央下に見えるボタンを押すと、建設順に表示されていくのがわかります。

(余談ですが直前にドクターヘリがここに離着陸していました。暑かったです)

終わりに

今回の記事は以上になります。QRコードの位置に合わせて建物のモデルを配置して、ボタンによって構築順に建物が表示されるアプリを作成しました。このようにHololensやiPadを用いたAR, MRの技術は応用範囲が広いので様々な用途に使えそうです。Microsoft HoloLensのYoutubeチャンネル(リンク)などをご覧になり、どのような応用方法が提案がされているのかチェックしてみてください。

それでは、ここまでお読みいただきありがとうございました。

jyun-ichi

シンテグレートのプログラミング担当。入社2年目。業務はPythonやUnity(C#)の開発。運動と車と読書の田舎暮らし。

シェアする