概要
ARやVRなどの開発を行っているとGUIも3D空間に配置する必要があります。
しかしUnityのuGUIは2Dで扱うことを想定しており、通常のGraphicRaycastはスクリーンスペースの位置から判定を行うものになっています。
つまり、3D空間に置かれた(World Space
な)Canvasに対して、3D空間上にあるポインタ(例えば指やコントローラなど)からuGUIに対してRaycastしようとすると簡単には行きません。
そこで、ARやVRでも利用できるRaycastの仕組みを作らないとなりません。
以前、VRTKというフレームワークを参考に自分でEventSystemを拡張したのですが、その際に利用したWorld SpaceなCanvasへの3DオブジェクトからのRaycastをするために必要な部分について、(改めて必要になったので)メモとして残しておきたいと思います。
実際に動かしたやつはこんな感じです↓
3D空間のポインタからuGUIを操作するやつ。 #Unity pic.twitter.com/VWKd9wHF2q
— edom18@AR / MESON (@edo_m18) 2019年12月20日
処理フロー
まずはざっくり概観してみましょう。
- Canvasに配置されている
Graphic
すべてを判定対象にする Ray
の方向とGraphic
のforward
方向が同じかチェックするRay
の位置とGraphic
の位置の距離を計算するRay.GetPoint
を利用してワールドの位置を求める- 求めたワールド位置をカメラのスクリーンスペース位置に変換する
- 変換した位置を元に、
RectTransform
がそれを含んでいるかチェックする - 含んでいたら
Raycast
成功
コード
さて、全体の流れを説明したところでコードを見てみます。
コード自体はそこまで長くありません。
private void Raycast(Canvas canvas, Camera eventCamera, Ray ray) { if (!canvas.enabled) { return; } if (!canvas.gameObject.activeInHierarchy) { return; } IList<Graphic> graphics = GraphicRegistry.GetGraphicsForCanvas(canvas); for (int i = 0; i < graphics.Count; i++) { Graphic graphic = graphics[i]; if (graphic.depth == -1 || !graphic.raycastTarget) { continue; } Transform graphicTransform = graphic.transform; Vector3 graphicForward = graphicTransform.forward; float dir = Vector3.Dot(graphicForward, ray.direction); // Return immediately if direction is negative. if (dir <= 0) { continue; } float distance = Vector3.Dot(graphicForward, graphicTransform.position - ray.origin) / dir; Vector3 position = ray.GetPoint(distance); Vector2 pointerPosition = eventCamera.WorldToScreenPoint(position); // To continue if the graphic doesn't include the point. if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera)) { continue; } // To continue if graphic raycast has failed. if (!graphic.Raycast(pointerPosition, eventCamera)) { continue; } Debug.Log($"Raycast hit at {graphic.name}", graphic.gameObject); } }
すべてのGraphicを評価する
IList<Graphic> graphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
によって対象Canvas
にあるGraphic
オブジェクトのリストを得ることが出来ます。
ワールド空間の位置からスクリーンスペースの位置を求める
ワールド空間からスクリーンスペースの位置を求める方法です。
今回は特定オブジェクトがポインタの役割を果たして、そのオブジェクトからのRay
を使いたいわけなので、少しだけ計算が必要となります。
コード部分は以下。
Transform graphicTransform = graphic.transform; Vector3 graphicForward = graphicTransform.forward; float dir = Vector3.Dot(graphicForward, ray.direction); // Return immediately if direction is negative. if (dir <= 0) { continue; } float distance = Vector3.Dot(graphicForward, graphicTransform.position - ray.origin) / dir;
まず、対象Graphic
のforward
とray.direction
の内積を取りその値で分岐します。
これはuGUI
のオブジェクトのforward
方向はこちらを向いている面の反対側になります。
そのため、Ray
の方向と同じ場合を向いている場合(つまり内積が0より上の場合)だけ処理すればいいことになります。
なので内積結果が0以下だった場合は無視しているわけです。
そして同じ方向を向いていた場合はレイの位置とレイがぶつかった位置の距離を計算します。
コードにすると以下の部分。(分かりやすいように改行を入れています)
float distance = Vector3.Dot(graphicForward, graphicTransform.position - ray.origin);
distance /= dir;
「Graphic
のforward
方向」と「Graphic
オブジェクトの位置からレイの位置を引いたベクトル」の内積を計算し、dir
で割っています。
本来はVector3.Distance(from, to);
で求めてもいいのですが、距離計算は負荷が高めなのと、上記計算で内積と、すでに計算済みのdir
との除算のみで求まるためそちらを利用しています。(dir
を求める必要があるため一石二鳥、というわけです)
なぜこれで距離が求まるのかと言うと、差分ベクトルとの内積によってGraphic
のforward
方向、すなわちGraphic
が存在する平面との最短距離が求まります。
そしてそれを実際にGraphic
が存在する位置の長さとするためには三角関数を利用します。
つまり、半径 / cos(θ)が実際に求めたい距離です。
そして実はdir
の内積の結果はまさにこのcos(θ)
の値となっているため、それで割ることで距離が求まっていた、というわけです。
図にすると以下です。
RectTransformUtilityを利用して当たり判定
そして最後に、取得した全Graphic
オブジェクトに対して当たり判定をしてやればOKなわけです。
当たり判定自体は関数が用意されていてRectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera)
を使います。
引数には対象となるGraphic
のRectTransform
、スクリーンスペースのposition
、そして判定対象となるCamera
です。
対象オブジェクトにイベントを送る
今回は主にuGUIのオブジェクトをワールド空間から把握するためのもののメモなので、ちゃんとしたイベント周りの構築については以下の過去の記事を参考にしてみてください。
ただ、ざっくりでいいからイベントを送りたい、というケースもあるかと思います。
その場合は以下のように、Rayがヒットしたオブジェクトに対してイベントを送ってやればOKです。
// graphic object is detected by ray casting. ExecuteEvents.Execute(graphic.gameObject, new BaseEventData(eventSystem), ExecuteEvents.submitHandler); ````