【Unity】平面の計算を楽できるPlane構造体の使い方

平面との距離や、レーザーが平面に当たる位置などを計算したいんだけど、数式を立てるのが難しくて困っているの…

Unityが提供しているPlane構造体を使えば、この辺の扱いが楽になるわ。

Unityには、平面に関する計算を楽に扱えるようにしたPlane構造体が存在します。

このPlane構造体を用いると、次のような計算を楽に行うことが可能です。

Plane構造体を使うメリット
  • 点と平面との最短距離を求める
  • 投影点を求める
  • レイキャストと平面との交点を求める
  • 点が面の表と裏どちらにあるかを判定する
  • 3点を通る平面を求める

これらの計算をPlane構造体のメソッドで行うことにより、数式を立てる必要がなくなるほか、ソースコードの見通しもよくなります。 [1]

本記事では、Plane構造体の使い方と使用例を解説していきます。なお、プリミティブオブジェクトのPlaneではありませんのでご注意ください。

動作環境
  • Unity2021.1.22f1

Plane構造体が表す平面

Plane構造体は、裏表のある無限に続く平面を表します。

構造体内部では、法線ベクトル原点からの距離の2つのパラメータを保持しています。

法線ベクトルは、矢印の向きが表面、その逆は裏面となります。
原点からの距離には符号があり、平面の表側に原点があるときは正、裏側に原点があるときは負となります。

平面の定義方法は3種類あり、それぞれオーバーロードされたコンストラクタとして初期化できます。

Plane構造体の初期化

次の3つのコンストラクタにより平面を定義して初期化します。

public Plane(Vector3 inNormal, Vector3 inPoint);
public Plane(Vector3 inNormal, float d);
public Plane(Vector3 a, Vector3 b, Vector3 c);

1つ目は、法線ベクトルinNormalと平面を通る点inPointで初期化するコンストラクタです。
2つ目は、法線ベクトルinNormalと原点からの距離dで初期化するコンストラクタです。
3つ目は、3点a、b、cを通る平面として初期化するコンストラクタです。3点は表面から見て時計回りとなります。

引数に指定する法線ベクトルinNormalは長さ1で正規化されている必要がある点にご注意ください。

なお、初期化したPlaneインスタンスの値を後から変更することも可能です。

Plane plane;

・・・(中略)・・・

// 後から法線ベクトルと平面を通る点で再定義
plane.SetNormalAndPosition(Vector3.up, Vector3.zero);

// 後から法線ベクトルと原点からの距離で再定義
plane.normal = Vector3.up;
plane.distance = 0;

// 後から3点を通る平面で再定義
plane.Set3Points(
    new Vector3(1, 2, 3),
    new Vector3(4, 5, 6),
    new Vector3(7, 8, 9)
);

初期化して平面の定義ができたら、メソッドやプロパティを通じて計算処理ができるようになります。

次はこれらの使い方について解説していきます。

点と平面の距離を求める

Plane.GetDistanceToPoint()メソッドにより求めることができます。

メソッドの形式は次の通りです。

public float GetDistanceToPoint(Vector3 point);

引数には、平面との距離を測定したい点を指定します。

実際の使い方は次の通りです。

PlaneDistanceExample.cs
using UnityEngine;

public class PlaneDistanceExample : MonoBehaviour
{
    // 平面オブジェクト
    [SerializeField] private Transform _planeObject;
    // 点オブジェクト
    [SerializeField] private Transform _pointObject;

    private void OnGUI()
    {
        // 平面を定義
        var plane = new Plane(_planeObject.up, _planeObject.position);
        // 点を取得
        var point = _pointObject.position;

        // 平面と点との距離を求める
        var distance = plane.GetDistanceToPoint(point);

        // 計算した距離を出力
        GUI.Box(new Rect(20, 20, 150, 23), $"distance = {distance:F1}");
    }
}

平面との距離は、点が表側にある時は正、裏側にある時は負となります。
すなわち、得られる距離の符号によって点の表裏判定ができます。

なお、表裏判定をするだけの場合は、次のメソッドを使うと簡単です。

public bool GetSide(Vector3 point);

引数には表裏判定する点の座標を指定します。
戻り値は、点が表側ならtrue、裏側または平面上ならfalseとなります。

ある点から平面に最も近い位置を求める

Plane.ClosestPointOnPlane()メソッドで計算できます。

public Vector3 ClosestPointOnPlane(Vector3 point);

引数には点の座標を指定します。
戻り値は平面に最も近い位置座標です。

得られる結果は、点を面まで垂直に移動した点、言い換えると平面への投影点となります。

ClosestPointExample.cs
using UnityEngine;

public class ClosestPointExample : MonoBehaviour
{
    // 平面オブジェクト
    [SerializeField] private Transform _planeObject;
    // 点オブジェクト
    [SerializeField] private Transform _pointObject;
    // 投影点オブジェクト
    [SerializeField] private Transform _planePointObject;

    private void OnGUI()
    {
        // 平面を定義
        var plane = new Plane(_planeObject.up, _planeObject.position);
        // 点を取得
        var point = _pointObject.position;

        // 投影点を求める
        var planePoint = plane.ClosestPointOnPlane(point);

        // 投影点の位置にオブジェクトを移動
        _planePointObject.position = planePoint;
    }
}

レイキャストの交点を求める

ある点からある向きに向かう線(レイ)と平面との交点は、Plane.Raycast()メソッドから求めることができます。

public bool Raycast(Ray ray, out float enter);

第1引数にはレイの情報を格納した構造体、第2引数には距離を受け取る変数を指定します。

戻り値はレイが平面に当たったらtrue、そうでない場合はfalseとなります。レイが平面と平行な場合もfalseとなります。表裏どちらからレイが平面に当たってもtrueとなります。

RaycastExample.cs
using UnityEngine;

public class RaycastExample : MonoBehaviour
{
    // 平面オブジェクト
    [SerializeField] private Transform _planeObject;
    // レイオブジェクト
    [SerializeField] private Transform _rayObject;
    // レイがヒットした点を表示するオブジェクト
    [SerializeField] private Transform _hitObject;

    private void OnGUI()
    {
        // 平面を定義
        var plane = new Plane(_planeObject.up, _planeObject.position);
        // レイを定義
        var ray = new Ray(_rayObject.position, _rayObject.forward);

        // レイと平面との当たり判定
        // ヒットした場合はenterに平面までの距離が格納される
        var isHit = plane.Raycast(ray, out var enter);
        
        // ヒットした場合のみオブジェクトを表示
        _hitObject.gameObject.SetActive(isHit);
        
        if (isHit)
        {
            // ヒットした場合は平面の位置に点を移動
            _hitObject.position = ray.origin + ray.direction * enter;
        }
        
        // 計算結果を出力
        GUI.Box(new Rect(20, 20, 300, 23), $"isHit = {isHit}, enter = {enter:F1}");
    }
}

平面の裏表をひっくり返す

Plane.Flip()メソッドを用いると、平面の位置をそのままに、裏表だけひっくり返すことができます。

public void Flip();

引数や戻り値はありません。また、Plane.flippedプロパティから新しいインスタンスとして取得することも可能です。

FlipExample.cs
using UnityEngine;

public class FlipExample : MonoBehaviour
{
    // 平面オブジェクト
    [SerializeField] private Transform _planeObject;

    private void OnGUI()
    {
        if (GUI.Button(new Rect(20, 20, 60, 23), "Flip"))
        {
            // 平面を定義
            var plane = new Plane(_planeObject.up, _planeObject.position);

            // 平面を反転
            plane.Flip();

            // 表示に反映
            _planeObject.SetPositionAndRotation(
                -plane.distance * plane.normal, // 平面を通る点
                Quaternion.FromToRotation(Vector3.up, plane.normal) // 平面の法線ベクトル
            );
        }
    }
}

さいごに

Plane構造体の使い方について解説しました。

プログラム中で何か平面情報を扱いたい場合、Plane構造体を用いると便利です。数学的なベクトル計算をPlane構造体で内包してくれます。

コライダーを用いる必要が無く、内部的には内積や外積だけで計算処理が実装されているので計算が軽いというメリットもあります。 [2]

ぜひ活用方法を検討してみてください。

参考サイト