しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

HoloLens で Colider を利用せずに Unity の Button の注視検出をしてみたい

※ これは 2016/05/31 Unity HoloLens Technical Preview ver.5.4.0beta14(Windows版) 時点の情報です

最新版では動作が異なる可能性がありますのでご注意ください

前回 は Button へのタップ操作を検出できるようになりました

ただ、Button への注視に物理衝突判定用の Colider を利用しているところが普通の uGUI の使い方と異なっており冗長なので、Colider を利用しない方法を試してみます

まずはコード修正

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.VR.WSA.Input;

public class VRUI : MonoBehaviour
{
    /// <summary>
    /// ボタンの属する Canvas
    /// </summary>
    [SerializeField]
    private Canvas layoutRoot;

    /// <summary>
    /// ボタンのラベルテキスト
    /// </summary>
    [SerializeField]
    private UnityEngine.UI.Text buttonLabel;

    /// <summary>
    /// ターゲッテイング用カーソル
    /// </summary>
    [SerializeField]
    private GameObject cursor;

    /// <summary>
    /// ジェスチャ検知
    /// </summary>
    private GestureRecognizer gesture;

    /// <summary>
    /// 視線オーバーフラグ
    /// </summary>
    private bool isOver = false;

    /// <summary>
    /// ホールドジェスチャフラグ
    /// </summary>
    private bool isHold = false;

    /// <summary>
    /// タップカウント
    /// </summary>
    private int count = 0;

    /// <summary>
    /// 初期処理
    /// </summary>
    public void Awake()
    {
        this.gesture = new GestureRecognizer();
        this.gesture.SetRecognizableGestures(GestureSettings.Hold | GestureSettings.Tap);
        this.gesture.HoldStartedEvent += this.OnButtonHold;
        this.gesture.HoldCompletedEvent += this.OnButtonHoldEnd;
        this.gesture.HoldCanceledEvent += this.OnButtonHoldEnd;
        this.gesture.TappedEvent += this.OnButtonClick;
        this.gesture.StartCapturingGestures();
    }

    /// <summary>
    /// 破棄処理
    /// </summary>
    public void OnDestroy()
    {
        if (this.gesture != null && this.gesture.IsCapturingGestures())
        {
            this.gesture.StopCapturingGestures();
            this.gesture.HoldStartedEvent -= this.OnButtonHold;
            this.gesture.HoldCompletedEvent -= this.OnButtonHoldEnd;
            this.gesture.HoldCanceledEvent -= this.OnButtonHoldEnd;
            this.gesture.TappedEvent -= this.OnButtonClick;
        }
    }

    /// <summary>
    /// 定期的な処理
    /// </summary>
    public void FixedUpdate()
    {
        RaycastHit hit;
        if (Raycast(this.layoutRoot, new Ray(Camera.main.transform.position,  Camera.main.transform.forward), out hit))
        {
            this.cursor.transform.position = hit.point;
            this.cursor.transform.rotation = Quaternion.Euler(hit.normal);
            this.cursor.SetActive(true);

            this.isOver = true;
        }
        else
        {
            this.isOver = false;
            this.cursor.SetActive(false);
        }
    }

    /// <summary>
    /// グラフィックをもとに衝突判定する
    /// </summary>
    /// <param name="canvas">親の Canvas</param>
    /// <param name="ray">視線の位置と方向</param>
    /// <param name="hit">衝突結果</param>
    /// <returns>衝突したグラフィックがあった場合 true、それ以外は false</returns>
    public bool Raycast(Canvas canvas, Ray ray, out RaycastHit hit)
    {
        hit = new RaycastHit();

        var results = false;
        var depth = -1;

        // なぜか GetEnumelator が実装されてないので for で反復
        var graphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
        for(var i = 0; i < graphics.Count; i++)
        {
            var graphic = graphics[i];
            if (graphic.depth == -1 || graphic.gameObject == this.cursor)
            {
                continue;
            }

            Vector3 position;
            if (RayIntersectsRectTransform(graphic.rectTransform, ray, out position))
            {
                var screenPosition = Camera.main.WorldToScreenPoint(position);
                if (graphic.Raycast(screenPosition, Camera.main)
                    && depth < graphic.depth)
                {
                    hit.point = position;
                    results = true;
                }
            }

        }

        return results;
    }

    /// <summary>
    /// UI と Ray の交点を求める
    /// </summary>
    /// <param name="rectTransform">UI の RectTransform</param>
    /// <param name="ray">位置と方向</param>
    /// <param name="position">交点のワールド座標</param>
    /// <returns>交点があった場合 true、それ以外は false</returns>
    public static bool RayIntersectsRectTransform(RectTransform rectTransform, Ray ray, out Vector3 position)
    {
        position = Vector3.zero;

        var corners = new Vector3[4];
        rectTransform.GetWorldCorners(corners);
        var dummy = new Plane(corners[0], corners[1], corners[2]);

        float enter;
        if (!dummy.Raycast(ray, out enter))
        {
            // UI平面を通過しない場合
            return false;
        }

        // UI平面との交点
        var intersection = ray.GetPoint(enter);

        // UI矩形内に視線が収まっていればその交点ワールド座標を返す
        var bottom = corners[3] - corners[0];
        var left = corners[1] - corners[0];
        var bottomDot = Vector3.Dot(intersection - corners[0], bottom);
        var leftDot = Vector3.Dot(intersection - corners[0], left);
        if (bottomDot < bottom.sqrMagnitude && leftDot < left.sqrMagnitude
            && bottomDot >= 0 && leftDot >= 0)
        {
            position = corners[0] + leftDot * left / left.sqrMagnitude + bottomDot * bottom / bottom.sqrMagnitude;
            return true;
        }

        return false;
    }

    ~ 中略 ~
}

Oculus Rift 用の VR UI サンプル を参考に、これまでは Physics.Raycast を利用していたところを別途メソッドを増やして衝突判定するようにしました

Physics.Raycast は本来 3D オブジェクト用なので、Colider を利用しますが、Button のような UI は単純な判定範囲となるので普通に座標計算するようになっています

コードを修正したら Unity Editor 上で少々変更

f:id:matatabi_ux:20160719194504p:plain

Button の Box Colider のチェックを外して無効化してしまって

f:id:matatabi_ux:20160719194540p:plain

Button の親の Canvas を VRUI の Layout Root にドラッグドロップで設定します

さてこれでビルド実行

f:id:matatabi_ux:20160719194756g:plain

注視を検出できてはいますが・・・前と同じように範囲が二倍になっているような

これは Unity Editor 側の設定がまずそうな予感?!