LoginSignup
34
39

More than 5 years have passed since last update.

[Unity]3D空間上に表示する距離・角度の影響を受けないUI

Last updated at Posted at 2017-07-09

やりたいこと

ゲームで3D空間上に表示するUIで、下記条件を満たすUIを作りたかったので、試行錯誤しました。

仕様

  1. 特定の3Dオブジェクトに関するUIを、そのオブジェクトより必ず前面に表示されるようにしたい
  2. カメラとの距離・角度に関係なく常に同じサイズ・角度でUIを表示したい

つまりどういうことかというと、

cursorview_demo3.gif

こんな感じの、特定の3Dオブジェクトに追従するUIに使うのに必要でした。

問題点

仕様2を満たすだけなら、Camera.WorldToScreenPointを使えばすぐですが・・・

当然ですがCanvasに普通にUIを配置すると、こんな感じに表示類が必ず前面に出てしまうため、カメラワークやオブジェクトの配置によっては、

▼敵への照準なのにプレイヤーより前に出てしまう
cursor_bad.png

▼壁の向こうにいる敵が事前に分かってしまう
hp_gauge_bad.gif

というような問題が出てしまいます。
※逆にこういうのが問題にならないゲームなら普通にWorldToScreenPointを使えばOKです。
↓みんな大好きテラシュールブログに詳しく使い方載ってます
http://tsubakit1.hateblo.jp/entry/2016/03/01/020510

結果的にどうなるか

この記事で紹介する方法で、「指定したCollider群、またはRenderer群より必ず前面にオブジェクトが表示される」ようにできます。

前述の例で言うと、

▼プレイヤーが前にいるときはきちんと奥に表示
cursor_good.png

▼壁の向こう側の敵の情報は隠れる
hp_gauge_good.gif

こんな感じです。

「理屈はどうでもいいから早く使わせてくれ!」って人はこちら
https://github.com/fluncle/WorldUI
※Demoシーンを見れば大体の使い方がわかると思います

対応の方針

・UIオブジェクトは、1つのまとまり単位でWorldSpaceのCanvasを作る
  → HPゲージ1つにCanvas1つ、照準1つにCanvas1つ、という具合
・設定するCollider/Rendererの表面に、かつカメラに対して水平にUIをオフセットする

位置関係としては、下図のような感じになるようにします。

image.jpg

3D空間にCanvasオブジェクト配置

WorldSpaceのCanvasを用意。

canvas.jpg

常にカメラと並行にする

よくあるビルボード表示。
オブジェクトが必ずカメラと同じ角度になるようにします。

Billboard.cs
using UnityEngine;

/// <summary>
/// 常にカメラの方を向くオブジェクト回転をカメラに固定
/// </summary>
public class Billboard : MonoBehaviour {
    void LateUpdate() {
        // 回転をカメラと同期させる
        transform.rotation = Camera.main.transform.rotation;
    }
}

カメラとの距離を変えてもスクリーン上は等倍にする

距離が離れればその分大きくなり、近づけば小さくすることで、
スクリーン上の見た目の大きさを変えないようにするコンポーネントです。

FixedScreenScaleObject.cs
/// <summary>
/// カメラからの距離を取得
/// </summary>
private float GetDistance() {
    return (transform.position - Camera.main.transform.position).magnitude;
}

private void LateUpdate() {
    transform.localScale = Vector3.one * _baseScale * GetDistance();
}

_baseScaleは基準スケールで、サンプルスクリプトではStartメソッド時のスケールを使って、カメラからの距離が1のときのスケール値を算出するようにしていますが、キャンバスに表示したときと同じような大きさに見えるように「画角によって一意にベーススケールが決まる」ようなロジックを作る必要がありそうです。
また、毎フレームmagnitude(距離)を取るのは負荷が高いので、sqrMgnitudeをそれっぽく使ったりなどの工夫が必要。
それぞれ、要改修。

カメラと並行に表面位置までオフセットする

今回のキモの部分です。
この図の、「表面までオフセット」の処理を受け持ちます。

↓こちらの記事を参考にさせてもらいました。
[Unity] 任意の無限遠の平面とベクトルとの交点を求める
http://qiita.com/edo_m18/items/c8808f318f5abfa8af1e

FrontBoundsObject.cs
private void LateUpdate () {
    if (_renderers == null && _colliders == null) {
        // オブジェクト表面として、レンダラーかコライダーが設定されていなければ何もしない
        return;
    }

    // 毎フレーム位置の補正をする前に、本来の位置(_baseLocalPosition)にリセットする
    transform.localPosition = _baseLocalPosition;

    UpdateClosestPoint();

    // 表面の形状やUI次第ではUIの一部がオブジェクトにめり込むことがあるため
    // 算出した表面から更に_marginDistance分オフセットする
    transform.position -= _cameraT.forward * _marginDistance;
}

まず、基本のUpdateの流れはこちらの通り。
表面の座標は"UpdateClosestPoint"メソッドで行っている。
その前に、_baseLocalPositionで毎回本来表示すべき位置に移動させている。
UIの表示位置を変えたい場合は、"SetPosition"か、"SetLocalPosition"メソッドを使うことに注意。
 

FrontBoundsObject.cs
private void UpdateClosestPoint() {
    Vector3 mostClosestPoint;
    // 指定されているレンダラーorコライダーで、カメラに最も近い表面座標を取得
    if (_renderers != null && _renderers.Length > 0) {
        mostClosestPoint = GetClosestPoint(_renderers, _cameraT.position);
    } else {
        mostClosestPoint = GetClosestPoint(_colliders, _cameraT.position);
    }

    // UIからカメラへの方向
    Quaternion quaternion = Quaternion.LookRotation(transform.position - _cameraT.position);
    // UIの位置からまっすぐ進んで、カメラに最も近い表面座標の平面上にぶつかる位置を取得する
    Vector3 intersectPoint = CalcIntersectPoint(
        normal: quaternion * Vector3.back,
        planePosition: mostClosestPoint,
        startPosition: transform.position,
        startForward: quaternion * Vector3.forward
    );
    transform.position = intersectPoint;
}

UpdateClosestPointメソッド。
1. GetClosestPointを使って、カメラに最も近い表面座標を取得
2. CalcIntersectPointでカメラに最も近い表面座標の平面上座標を取得
3. UIのトランスフォームに座標を設定
という流れです。CalcIntersectPointの中身については、前述の
http://qiita.com/edo_m18/items/c8808f318f5abfa8af1e
を参考にしました。詳細はこちらをご覧ください!

GetClosestPointで表面座標を取る際、レンダラーやコライダーのBoundsを使っているため、
実際には厳密な表面座標が取れていません。
要改修。

まとめ

・常にカメラと並行にする(Billboard.cs)
・カメラとの距離を変えてもスクリーン上は等倍にする(FixedScreenScaleObject.cs)
・カメラと並行に表面位置までオフセットする(FrontBoundsObject.cs)

この3つのコンポーネントで、「3D空間上に表示する距離・角度の影響を受けないUI」を実現しました。
実際には、ステンシルバッファやカメラを分けて描画順で解決できるように、ゲーム仕様で解決するほうが軽いかもしれないです。
が、いちいちカメラを分けたりシェーダーを書いたりするのが面倒くさい(例えばモック開発やハッカソンなど)では役に立つ・・・といいなと思ってます。

▼FixedScreenScaleObjectの課題
・画角によって一意にベーススケールが決まるロジックが必要
・magnitudeを使わない、負荷の低い方法で距離やスケールを計算するべき

▼ FrontBoundsObjectの課題
・凹凸が激しいモデルや大きなUIを扱うとモデルにめり込みやすい
・Boundsで表面を計算しているため、余計なオフセットが出やすい

このあたり、「こうすれば解決するぞ!」というのがあればぜひご教授ください!

34
39
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
34
39