0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity + ARFoundation + Lightship】ロケーションベースARを作ろう!(コンパスバー)

Posted at

はじめに

こんにちは。
今回は前回に引き続き、「ロケーションベースARを作ろう!〜コンパスバー編〜」ということで、ARに方角とオブジェクトの方向を表示するコンパスの作り方を記事にしていこうと思います。

環境

  • MacBook Pro(2024/macOS Sequoia 15.3)
  • Unity(2022.3.53f1)
    • ARFoundation(5.1.5)
    • Niantic Lightship AR Plugin(1.4.2)
  • iPhone 15(iOS 18.1.1)

実装する機能

  • 東西南北を表示するコンパス
  • 生成されたオブジェクト(以下、Objective)の存在する方向を表示するアイコン

実装

こちらからコンパスバーUIの素材をダウンロードすることができます。

仕様

  • Canvas内にUIの形で表示
  • North, South, East, Westそれぞれを別々のRaw ImageUIとする
  • コンパスの長さを720unitとし、Input.compass.trueHeadingで得られる値を計算して、UIをx軸方向に動かす
  • Objectiveの方向は方位角ではなくUnity世界でのMain Cameraのz軸方向のベクトルとMain CameraからObjectiveへのベクトルの投影ベクトルで計算する

一部前回の記事のObjectSpawner.csに変更を加える形で紹介するところがあります。

まず、CompassManagerという名前でスクリプトを新規作成します。
とりあえずコードを全部貼ります。

CompassManager.cs
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class CompassManager : MonoBehaviour
{
    public GameObject compass;

    // Compass UI
    [SerializeField]
    private GameObject CompassUI;
    [SerializeField]
    private RawImage NorthIcon;
    [SerializeField]
    private RawImage SouthIcon;
    [SerializeField]
    private RawImage EastIcon;
    [SerializeField]
    private RawImage WestIcon;
    
    [SerializeField]
    private GameObject ObjectIcon;

    [SerializeField]
    private TextMeshProUGUI angleText;

    public struct AddedIcon
    {
        public GameObject icon;
        public GameObject spawnedObject;
        public Coordinates coordinates;
        public RectTransform rectTransform;

        public AddedIcon(GameObject icon, GameObject spawnedObject, Coordinates coordinates){
            this.icon = icon;
            this.spawnedObject = spawnedObject;
            this.coordinates = coordinates;
            this.rectTransform = icon.GetComponent<RectTransform>();
        }
    }

    private List<AddedIcon> addedIcons = new List<AddedIcon>();

    Coordinates current_coordinates;

    double lastCompassUpdateTime = 0;
    Quaternion correction = Quaternion.identity;
    Quaternion targetCorrection = Quaternion.identity;

    LocationInfo lastData;
    double lastLocationUpdateTime = 0;



    void Start()
    {
        Input.gyro.enabled = true;
        Input.compass.enabled = true;
        Input.location.Start();
        
        // icon Initiation
        // 180-degree turn -> x-delta 720

        // x=0が中心(初期はN)
        NorthIcon.rectTransform.localPosition = new Vector3(0, 0, 0);
        // x=720は一番外(初期はS)
        SouthIcon.rectTransform.localPosition = new Vector3(720, 0, 0);
        // x=360は右端(初期はE)
        EastIcon.rectTransform.localPosition = new Vector3(360, 0, 0);
        // x=-360は左端(初期はW)
        WestIcon.rectTransform.localPosition = new Vector3(-360, 0, 0);


    }

    // Update is called once per frame
    void Update()
    {

        float phone_deg = Input.compass.trueHeading;
        float phone_degForUI = phone_deg;

        lastData = Input.location.lastData;

        if(lastData.timestamp > lastLocationUpdateTime){
            lastLocationUpdateTime = lastData.timestamp;

            current_coordinates = new Coordinates(lastData.latitude, lastData.longitude, lastData.altitude);

        }

        

        Debug.Log("|Unity|Front: " + phone_deg);

        if(phone_deg < 180){

            // x軸の値をy, phone_degをxとして
            // [Nアイコン]x = 0 のとき y = 0, x = 180 のとき y = 720となってほしい 
            NorthIcon.rectTransform.localPosition = new Vector3(-720 * (phone_deg / 180), 0, 0);
            // [Sアイコン]x = 0 のとき y = 720, x = 180 のとき y = 0となってほしい 
            SouthIcon.rectTransform.localPosition = new Vector3(720 * (1 - phone_deg / 180), 0, 0);
            // [Eアイコン]Nアイコン + 360
            EastIcon.rectTransform.localPosition = new Vector3(-720 * (phone_deg / 180) + 360, 0, 0);
            // [Wアイコン]Nアイコン - 360
            WestIcon.rectTransform.localPosition = new Vector3(-720 * (phone_deg / 180) - 360, 0, 0);


            foreach(AddedIcon i in addedIcons){
                var compassForward = compass.transform.TransformDirection(Vector3.forward);
                var targetDirection = i.spawnedObject.transform.position - compass.transform.position;

                var pCompassForward = Vector3.ProjectOnPlane(compassForward, Vector3.up);
                var pTargetDirection = Vector3.ProjectOnPlane(targetDirection, Vector3.up);

                float angle = Vector3.SignedAngle(pCompassForward, pTargetDirection, Vector3.up);


                i.rectTransform.localPosition = new Vector3(360 * (angle / 90), 0, 0);

                Debug.Log("|Unity|Icon direction1: " + angle);

            }

        }else if(phone_deg > 180){

            //180<phone_deg<360のときは動作の仕方が異なる(アイコンの移動方向が違う)

            phone_deg -= 180;
            
            NorthIcon.rectTransform.localPosition = new Vector3(720 * (1 - phone_deg / 180), 0, 0);
            SouthIcon.rectTransform.localPosition = new Vector3(-720 * (phone_deg / 180), 0, 0);
            EastIcon.rectTransform.localPosition = new Vector3(-720 * (phone_deg / 180) - 360, 0, 0);
            WestIcon.rectTransform.localPosition = new Vector3(-720 * (phone_deg / 180) + 360, 0, 0);

            foreach(AddedIcon i in addedIcons){
                
                var compassForward = compass.transform.TransformDirection(Vector3.forward);
                var targetDirection = i.spawnedObject.transform.position - compass.transform.position;

                var pCompassForward = Vector3.ProjectOnPlane(compassForward, Vector3.up);
                var pTargetDirection = Vector3.ProjectOnPlane(targetDirection, Vector3.up);

                float angle = Vector3.SignedAngle(pCompassForward, pTargetDirection, Vector3.up);



                Debug.Log("|Unity|Icon direction2: " + angle);

                i.rectTransform.localPosition = new Vector3(360 * (angle / 90), 0, 0);


            }


        }else if(phone_deg == 180){

            NorthIcon.rectTransform.localPosition = new Vector3(720, 0, 0);
            SouthIcon.rectTransform.localPosition = new Vector3(0, 0, 0);
            EastIcon.rectTransform.localPosition = new Vector3(-360, 0, 0);
            WestIcon.rectTransform.localPosition = new Vector3(+360, 0, 0);

            foreach(AddedIcon i in addedIcons){
                var compassForward = compass.transform.TransformDirection(Vector3.forward);
                var targetDirection = i.spawnedObject.transform.position - compass.transform.position;

                var pCompassForward = Vector3.ProjectOnPlane(compassForward, Vector3.up);
                var pTargetDirection = Vector3.ProjectOnPlane(targetDirection, Vector3.up);

                float angle = Vector3.SignedAngle(pCompassForward, pTargetDirection, Vector3.up);

                i.rectTransform.localPosition = new Vector3(360 * (angle / 90), 0, 0);

                Debug.Log("|Unity|Icon direction3: " + angle);

            }


        }
        
        angleText.text = ((int)phone_degForUI).ToString();
    }


    public void AddObjectIcon(GameObject spawnedObject, Coordinates coordinates)
    {
        GameObject iconObj = Instantiate(ObjectIcon, compass.transform);
        iconObj.transform.SetParent(CompassUI.transform, false);
        addedIcons.Add(new AddedIcon(iconObj, spawnedObject, coordinates));

    }

    public void ResetIcons()
    {
        if(addedIcons.Count != 0){
            foreach(AddedIcon i in addedIcons){
                Destroy(i.icon);
            }
        }
        
        addedIcons.Clear();
    }

    private double ToRadian(double degree)
    {
        return degree * Math.PI / 180;
    }



}

説明

Start関数内でジャイロの有効化、コンパスの有効化、ロケーションサービズの有効化を行います。
また仕様からそれぞれの方位の初期位置を決定します。180度回転でrectTransform.localPositionの値が720変わるようにします。

    void Start()
    {
        Input.gyro.enabled = true;
        Input.compass.enabled = true;
        Input.location.Start();
        
        // icon Initiation
        // 180-degree turn -> x-delta 720

        // x=0が中心(初期はN)
        NorthIcon.rectTransform.localPosition = new Vector3(0, 0, 0);
        // x=720は一番外(初期はS)
        SouthIcon.rectTransform.localPosition = new Vector3(720, 0, 0);
        // x=360は右端(初期はE)
        EastIcon.rectTransform.localPosition = new Vector3(360, 0, 0);
        // x=-360は左端(初期はW)
        WestIcon.rectTransform.localPosition = new Vector3(-360, 0, 0);

    }

後述しますが、AddObjectIcon関数を叩くことでObjectiveのアイコンを生成することにしています。また、その際に生成したアイコンの情報をAddedIcon構造体でListに保存します。アイコンに紐づくObjectiveのGameObjectのデータをもたせることで後で計算できるようにします。

    public void AddObjectIcon(GameObject spawnedObject, Coordinates coordinates)
    {
        GameObject iconObj = Instantiate(ObjectIcon, compass.transform);
        iconObj.transform.SetParent(CompassUI.transform, false);
        addedIcons.Add(new AddedIcon(iconObj, spawnedObject, coordinates));

    }

Update関数内でコンパスのアイコンの位置を随時更新します。
方角アイコンは一定の規則に従って更新します。
Objectiveアイコンは

  1. コンパスオブジェクト(後述しますが、端末の向いている方向を取得するためのGameObjectです)のz軸方面のベクトルを取得
  2. 端末からObjectiveに向けたベクトルを取得
  3. それぞれ平面に投影する
  4. 投影したベクトルの差をVector3.SignedAngleを用いて取得する
  5. アイコンのrectTransformを変更する

といった流れで処理します。
Vector3.SignedAngleは-180~180の値を返すので、90度でUIの一番端(x方向±360)になるように式を作りました。

// Update is called once per frame
    void Update()
    {

        // 〜中略~

        if(phone_deg < 180){

            // x軸の値をy, phone_degをxとして
            // [Nアイコン]x = 0 のとき y = 0, x = 180 のとき y = 720となってほしい 
            NorthIcon.rectTransform.localPosition = new Vector3(-720 * (phone_deg / 180), 0, 0);
            // [Sアイコン]x = 0 のとき y = 720, x = 180 のとき y = 0となってほしい 
            SouthIcon.rectTransform.localPosition = new Vector3(720 * (1 - phone_deg / 180), 0, 0);
            // [Eアイコン]Nアイコン + 360
            EastIcon.rectTransform.localPosition = new Vector3(-720 * (phone_deg / 180) + 360, 0, 0);
            // [Wアイコン]Nアイコン - 360
            WestIcon.rectTransform.localPosition = new Vector3(-720 * (phone_deg / 180) - 360, 0, 0);


            foreach(AddedIcon i in addedIcons){
                var compassForward = compass.transform.TransformDirection(Vector3.forward);
                var targetDirection = i.spawnedObject.transform.position - compass.transform.position;

                var pCompassForward = Vector3.ProjectOnPlane(compassForward, Vector3.up);
                var pTargetDirection = Vector3.ProjectOnPlane(targetDirection, Vector3.up);

                float angle = Vector3.SignedAngle(pCompassForward, pTargetDirection, Vector3.up);


                i.rectTransform.localPosition = new Vector3(360 * (angle / 90), 0, 0);

                Debug.Log("|Unity|Icon direction1: " + angle);

            }

            // 以下省略

コンパスにアイコンを追加する処理をObjectSpawnerに追加します。

ObjectSpawner.cs

    //前略
public class ObjectSpawner : MonoBehaviour
{
    // For coordinates calculate constants
    const double EPSILON = 1e-8;
    const double SEMEMAJOR_AXIS_GRS80 = 6378137;
    const double FLATTENING_GRS80 = 298.257222101;

    // 生成するオブジェクト
    [SerializeField]
    private GameObject spawnObject;

    // 生成したオブジェクトを保持するリスト
    private List<GameObject> spawnedObjects = new List<GameObject>();
    private double target_latitude;
    private double target_longitude;
    private double target_altitude;
    private double current_altitude;


+   [SerializeField]
+   private CompassManager compassManager;

    
    // Start is called before the first frame update
    void Start()
    {
        // compass&Locationシステムの有効化
        Input.compass.enabled = true;
        Input.location.Start();

    }

    public void StartAR(){

        if (Input.location.isEnabledByUser){
            SpawnObjectData[] data = getSpawnDataCSV();
            foreach(SpawnObjectData i in data){
                target_latitude = i.latitude;
                target_longitude = i.longitude;
                target_altitude = i.altitude;

                LocationInfo lastData = Input.location.lastData;
                current_altitude = Input.location.lastData.altitude;

                Coordinates current_coordinates = new Coordinates(lastData.latitude, lastData.longitude, lastData.altitude);
                Coordinates target_coordinates = new Coordinates(target_latitude, target_longitude, target_altitude);


                Vector3 spawnPoint = CalculateSpawnPoint(current_coordinates, target_coordinates);
                GameObject spawnedObject = Instantiate(spawnObject, spawnPoint, Quaternion.identity);
                spawnedObjects.Add(spawnedObject);

+               compassManager.addWindmillIcon(spawnedObject, target_coordinates);
            }

+           // コンパスに追加
+           compassManager.objects = spawnedObjects;
            

        }

    }
    //以下省略

UnityEditorでのUI作成

ヒエラルキーでUI>Canvasをクリックし、Canvasを作成します。

CleanShot 2025-02-18 at 20.34.28@2x.png

Canvasのサイズの設定をします。その前にカメラのGameタブから画角を縦型16:9に変えましょう。

CleanShot 2025-02-18 at 21.01.24@2x.png

以下のように設定してください。
CleanShot 2025-02-18 at 20.35.52@2x.png

Canvasの子として空のGameObject、その子としてImage(以下、Mask)を作成し、ImageにMaskコンポーネントをアタッチしてください。このとき赤枠で囲んだチェックは外してください。またMaskの横幅は720に設定して、PosX, PosYは0にしてください。

CleanShot 2025-02-18 at 20.39.20@2x.png

Maskの子としてImageを作成し細長くしてください。横幅は720に設定してください。

CleanShot 2025-02-18 at 20.42.02@2x.png

Maskの子としてRaw Imageを作成して、以下のように設定してください。

CleanShot 2025-02-18 at 20.44.39@2x.png

他のS, E, Wも同様に設定し、PosXをそれぞれ720, 360, -360としてください。

CleanShot 2025-02-18 at 20.50.23@2x.png

Compassの子としてImage、その子としてTextを作成します。テキストは中央揃えにして図のように配置すると良いでしょう。
CleanShot 2025-02-18 at 21.35.36@2x.png

Compass_ObjIconのImageのPrefabを作成しておいてください。
CleanShot 2025-02-18 at 21.40.33@2x.png

UIから離れて、XR Origin>Camera Offset>Main Cameraの子として空のGameObjectを作成し、名前をCompassObjとします。
CleanShot 2025-02-18 at 21.31.49@2x.png

シーンに空のGameObjectを作成し、CompassManagerとして作成し、CompassManagerコンポーネントをアタッチしてください
CleanShot 2025-02-18 at 21.31.02@2x.png

以下のように設定してください。
CleanShot 2025-02-19 at 19.12.08@2x.png

実行結果

このここまで作ったら、ObjectSpawnerSpawnObject関数を叩いたときにアイコンも生成されます。
実行するとこんな感じです(若干UIが異なっています)。

Result.gif

おわりに

具体的に開発した内容を引っ張ってきたので汎用性に乏しい記事となってしまいました。
雑になってしまって申し訳ありませんが、ある程度の枠組みを流用して皆様のケースに合わせて使ってください。
また、この実装ならARでない普通のゲームにも導入することができると思います。参考にしてください。
では、また。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?