かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

ARFoundation と UIWidgets を使う

ARFoundation を使うと Unity で iOS/Android 両対応の AR アプリが簡単に作れるということで注目が高そうな機能です。

docs.unity3d.com

AR の世界をタップしたとき何か当たったかどうかを判定するためには ARRaycastManagerRaycast すれば OK です。こんな感じです。タップした場所に適当な大きさのキューブと綺麗に削除する機能を用意してます。

public class ARTapHandler : MonoBehaviour
{
    public ARRaycastManager raycastManager;
    private readonly List<GameObject> _cubes = new List<GameObject>();

    // Update is called once per frame
    void Update()
    {
        if (Input.touchCount == 1)
        {
            var touch = Input.GetTouch(0);
            if (touch.phase != TouchPhase.Began)
            {
                return;
            }

            var hits = new List<ARRaycastHit>();
            if (!raycastManager.Raycast(touch.position, hits))
            {
                return;
            }

            var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
            cube.transform.position = hits[0].pose.position;
            cube.transform.localScale = new Vector3(0.05f, 0.05f, 0.05f);
            _cubes.Add(cube);
        }
    }

    public void ClearCubes()
    {
        foreach (var cube in _cubes)
        {
            GameObject.Destroy(cube);
        }

        _cubes.Clear();
    }
}

UIWidgets と使うと…

この画面に UIWidgets を適用してみると、まぁ当たり前ですが UI 部品をタップしても上記ロジックでは AR の世界に対する処理も走ってしまいます。 ぱっと思いつくのは、Raycast をする前にタップした場所が UI の置いてある場所なのか、そうじゃないのかを判定しないといけません。 ということで前回の UIWidgets で透明なところは後ろ側にイベントを飛ばすという処理に書いてあった、タップした場所が透明かどうかを判定するロジックを組み込んで、これを実現してみようと思います。

blog.okazuki.jp

ということで、上記コードに処理を追加してやりました。

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.UIWidgets.engine;
using UnityEngine;
using UnityEngine.XR.ARFoundation;

namespace UnityAppTest.Ar
{
    public class ARTapHandler : MonoBehaviour
    {
        public UIWidgetsPanel uiWidgetsPanel;
        public ARRaycastManager raycastManager;
        private readonly List<GameObject> _cubes = new List<GameObject>();

        // Update is called once per frame
        void Update()
        {
            if (Input.touchCount == 1)
            {
                var touch = Input.GetTouch(0);
                if (touch.phase != TouchPhase.Began)
                {
                    return;
                }

                // ここから
                var rTex = uiWidgetsPanel.texture as RenderTexture;
                if (rTex != null)
                {
                    var tex = new Texture2D(rTex.width, rTex.height, TextureFormat.RGBA32, false);
                    RenderTexture.active = rTex;
                    tex.ReadPixels(new Rect(0, 0, rTex.width, rTex.height), 0, 0);
                    tex.Apply();
                    Vector2 local;
                    RectTransformUtility.ScreenPointToLocalPointInRectangle(uiWidgetsPanel.rectTransform, touch.position, null, out local);

                    var rect = uiWidgetsPanel.rectTransform.rect;

                    local.x += uiWidgetsPanel.rectTransform.pivot.x * rect.width;
                    local.y += uiWidgetsPanel.rectTransform.pivot.y * rect.height;
                    var uvRect = uiWidgetsPanel.uvRect;
                    var u = local.x / rect.width * uvRect.width + uvRect.x;
                    var v = local.y / rect.height * uvRect.height + uvRect.y;
                    Debug.Log("alpha = " + tex.GetPixelBilinear(u, v).a);
                    if (tex.GetPixelBilinear(u, v).a != 0.0f)
                    {
                        Debug.Log("UI world!!");
                        return;
                    }
                }
                // ここまで

                Debug.Log("AR world!!");
                var hits = new List<ARRaycastHit>();
                if (!raycastManager.Raycast(touch.position, hits))
                {
                    return;
                }

                var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
                cube.transform.position = hits[0].pose.position;
                cube.transform.localScale = new Vector3(0.05f, 0.05f, 0.05f);
                _cubes.Add(cube);
            }
        }

        public void ClearCubes()
        {
            foreach (var cube in _cubes)
            {
                GameObject.Destroy(cube);
            }

            _cubes.Clear();
        }
    }
}

コメントで、ここから、ここまでとなってる部分を何かしら共通部品のメソッドに括りだせば捗りそうですね。 とりあえず先日作った透明で Unity の Skybox をうつしてたやつを ARFoundation に移植して上記スクリプトを試してみました。背景がオレンジの部分が ARFoundation で認識された平面になります。透明部分をタップするとちゃんと Cube が作られて、透明じゃない UIWidgets の UI 部分をタップすると Cube を作る処理がされないことがわかります。

因みに画面左下のバツボタンには上記の ClearCubes を割り当ててます。

f:id:okazuki:20191220171555g:plain