【Unity2022】スプラインに最も近い点を取得する

こじゃらこじゃら

次のようにスプラインで作られたコースを走る車の位置を計算するにはどうすればいいの?

このはこのは

このような位置を取得するAPIが用意されているわ。SplineUtility.GetNearestPointメソッドを使えばいいの。

あるオブジェクトに最も近いスプライン上の位置を取得する方法の解説記事です。

これは、SplineUtility.GetNearestPointメソッドにより取得できます。

Spline spline;
Vector3 pos;

・・・(中略)・・・

// スプライン上の直近の位置を求める
float distance = SplineUtility.GetNearestPoint(
    spline,               // スプライン
    pos,                  // 元の位置
    out var nearestPoint, // 直近の位置が得られる
    out var t             // 直近の位置における比率が得られる
);

直近のスプライン位置を求める方法には、次の2種類があります。

方法
  • 指定された位置に最も近いスプライン位置を取得する
  • 指定された直線に最も近いスプライン位置を取得する

また、得られる直近の位置は数学的に厳密な位置ではなく、近似された位置であることに注意する必要があります。近似される位置は、計算コストを払う代わりに精度を高めることも可能です。

本記事では、このようなスプライン上の直近の位置を取得する方法を解説していきます。

動作環境
  • Unity 2022.2.11f1
  • Splines 2.1.0

スポンサーリンク

前提条件

Unity 2022.1以降でUnityプロジェクトが開かれていて、Splinesパッケージがインストールされているものとします。

ここまでの手順がわからない方は、以下記事を参考にSplinesパッケージまでのインストールを済ませてください。

指定位置に最も近いスプライン位置を求める

次のSplineUtility.GetNearestPointメソッドを用いて取得できます。

public static float GetNearestPoint<T>(
    T spline,
    float3 point,
    out float3 nearest,
    out float t,
    int resolution = 4,
    int iterations = 2
) where T : ISpline;

第1引数splineには、スプライン曲線のオブジェクトを指定します。これは、曲線を表すSpline型や、曲線の経路を表すSplinePath型など、ISplineインタフェースを実装している型であれば何れでも可能です。

参考:Class SplinePath | Splines | 2.2.0

第2引数pointには、計算対象となる座標を指定します。第1引数splineの座標空間と一致させる必要があります。 [1]

第3引数nearestには、スプライン上の直近の位置を受け取る変数を指定します。

第4引数tには、直近の位置における割合を0~1の間に正規化された値として受け取る変数を指定します。経路の始点が0終点が1となります。

第5引数resolutionは、直近の位置を計算する際のセグメント分割数に関係する値です。GetNearestPointメソッドによって計算される位置は、以下のように近似された直線上になります。 [2]

この値が大きくなるほど分割数が増えて精度の高い結果が得られますが、計算量も比例して増えます。

第6引数iterationsには、計算の反復回数を指定します。反復回数を大きくすることで、下図のように繰り返し絞り込みの計算を行い結果の精度も上がります。こちらも反復回数が大きくなると計算量が増えます。

戻り値は、第3引数nearestで受け取る位置と第2引数pointとの距離です。

参考:Class SplineUtility | Splines | 2.2.0

サンプルスクリプト

以下、与えられたスプラインとオブジェクト位置から、スプライン上の直近位置を求めるサンプルスクリプトです。

NearestPointExample.cs
using UnityEngine;
using UnityEngine.Splines;

public class NearestPointExample : MonoBehaviour
{
    // スプライン
    [SerializeField] private SplineContainer _spline;

    // 入力位置のゲームオブジェクト
    [SerializeField] private Transform _inputPoint;

    // 出力位置(直近位置)を反映するゲームオブジェクト
    [SerializeField] private Transform _outputPoint;

    // 解像度
    // 内部的にPickResolutionMin~PickResolutionMaxの範囲に丸められる
    [SerializeField] 
    [Range(SplineUtility.PickResolutionMin, SplineUtility.PickResolutionMax)]
    private int _resolution = 4;

    // 計算回数
    // 内部的に10回以下に丸められる
    [SerializeField]
    [Range(1, 10)]
    private int _iterations = 2;

    private void Update()
    {
        // Nullチェック
        if (_spline == null || _inputPoint == null || _outputPoint == null)
            return;

        // ワールド空間におけるスプラインを取得
        // スプラインはローカル空間なので、ローカル→ワールド変換行列を掛ける
        // Updateを抜けるタイミングでDisposeされる
        using var spline = new NativeSpline(_spline.Spline, _spline.transform.localToWorldMatrix);

        // スプラインにおける直近位置を求める
        var distance = SplineUtility.GetNearestPoint(
            spline,
            _inputPoint.position,
            out var nearest,
            out var t,
            _resolution,
            _iterations
        );

        // 結果を反映
        _outputPoint.position = nearest;
    }
}

上記スクリプトをNearestPointExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトに保存し、各種パラメータを設定します。

Splineにはスプラインオブジェクト、Input Pointには計算元の位置とするゲームオブジェクト、Output Pointには計算結果(直近の位置)を反映するゲームオブジェクトをそれぞれ指定します。

例では以下のようなスプラインとキューブを指定するものとします。

実行結果

入力位置(緑キューブ)からスプラインへの直近位置(赤キューブ)が反映されるようになりました。

スクリプトの説明

サンプルスクリプトでは、ワールド空間の位置として計算を行うため、スプライン自体もワールド空間のものとして扱う必要があります。

しかし、スプラインオブジェクト(SplineContainerコンポーネント)が保持するスプラインはローカル空間となっています。

そのため、以下のようにローカル空間からワールド空間に変換したスプラインオブジェクトを一時的に作成しています。

// ワールド空間におけるスプラインを取得
// スプラインはローカル空間なので、ローカル→ワールド変換行列を掛ける
// Updateを抜けるタイミングでDisposeされる
using var spline = new NativeSpline(_spline.Spline, _spline.transform.localToWorldMatrix);

NativeSplineは主に座標空間を変換したスプラインを扱うのに使用される構造体で、例ではローカル空間からワールド空間に変換したスプラインを一時的に作成しています。

コンストラクタの第2引数SplineContainer自身のローカル→ワールド変換行列localToWorldMatrixを指定することで、空間の変換を実現しています。

内部的にはNativeArray構造体を使用してテンポラリバッファを確保しているようです。 [3]

参考:Struct NativeSpline | Splines | 2.2.0

そして、先のワールド空間におけるスプライン入力位置より、スプライン上の直近位置を計算します。

// スプラインにおける直近位置を求める
var distance = SplineUtility.GetNearestPoint(
    spline,
    _inputPoint.position,
    out var nearest,
    out var t,
    _resolution,
    _iterations
);

例では直近位置のnearestしか使っていませんが、ほかにもスプライン位置tや距離distanceも同時に得られます。

直線に最も近いスプライン位置を求める

次のように指定した直線に最も近いスプライン上の位置を取得することも可能です。

直線はRay構造体として引数に渡します。

Transform inputRay;

・・・(中略)・・・

// スプラインにおける直近位置を求める
var distance = SplineUtility.GetNearestPoint(
    spline,
    new Ray(inputRay.position, inputRay.forward),
    out var nearest,
    out var t
);

このメソッドは以下のようにオーバーロードされています。

public static float GetNearestPoint<T>(
    T spline,
    Ray ray,
    out float3 nearest,
    out float t,
    int resolution = 4,
    int iterations = 2
) where T : ISpline;

参考:Class SplineUtility | Splines | 2.0.0

一つ目のメソッドとの違いは、第2引数がRay型の構造体に変わったところです。

Rayは向きの2つの情報を持つ構造体です。レイキャストの判定に用いられたりしますが、ここでは指定した点を通る直線として扱われます。 [4]

サンプルスクリプト

以下、指定された直線に最も近いスプライン位置を求めるサンプルスクリプトです。

NearestPointFromRayExample.cs
using UnityEngine;
using UnityEngine.Splines;

public class NearestPointFromRayExample : MonoBehaviour
{
    // スプライン
    [SerializeField] private SplineContainer _spline;

    // 入力Rayのゲームオブジェクト
    [SerializeField] private Transform _inputRay;

    // 出力位置(直近位置)を反映するゲームオブジェクト
    [SerializeField] private Transform _outputPoint;

    // 解像度
    // 内部的にPickResolutionMin~PickResolutionMaxの範囲に丸められる
    [SerializeField] 
    [Range(SplineUtility.PickResolutionMin, SplineUtility.PickResolutionMax)]
    private int _resolution = 4;

    // 計算回数
    // 内部的に10回以下に丸められる
    [SerializeField]
    [Range(1, 10)]
    private int _iterations = 2;

    private void Update()
    {
        // Nullチェック
        if (_spline == null || _inputRay == null || _outputPoint == null)
            return;

        // ワールド空間におけるスプラインを取得
        // スプラインはローカル空間なので、ローカル→ワールド変換行列を掛ける
        // Updateを抜けるタイミングでDisposeされる
        using var spline = new NativeSpline(_spline.Spline, _spline.transform.localToWorldMatrix);

        // スプラインにおける直近位置を求める
        var distance = SplineUtility.GetNearestPoint(
            spline,
            new Ray(_inputRay.position, _inputRay.forward),
            out var nearest,
            out var t,
            _resolution,
            _iterations
        );

        // 結果を反映
        _outputPoint.position = nearest;
        
        // 距離をログ出力
        Debug.Log($"distance : {distance}");
    }
}

確認のため、距離をログ出力するようにしました。入力となる直線はz軸方向としました。

上記スクリプトをNearestPointFromRayExample.csという名前で保存し、適当なゲームオブジェクトにアタッチし、1つ目の例同様にインスペクターより各種パラメータを設定します。

画像の例では、Rayの前方向を赤、後ろ方向を青と色付けして分かるようにしました。

実行結果

直線との距離が最短となるような位置(赤キューブ)が反映されていることが確認できます。

Rayの前方向だけでなく、後ろ方向も直近位置を求める対象として扱われます。

ログには直線と直近位置との距離が出力されています。この距離は前後関係のない純粋な距離です。 [5]

スクリプトの説明

以下部分でRay構造体として直線を指定しています。

// スプラインにおける直近位置を求める
var distance = SplineUtility.GetNearestPoint(
    spline,
    new Ray(_inputRay.position, _inputRay.forward),
    out var nearest,
    out var t,
    _resolution,
    _iterations
);

向きはz軸正方向とするため、forwardプロパティを指定しています。

メソッドの戻り値として直線と直近の位置nearestとの距離が返されます。これをログ出力したいため、以下のように戻り値をそのまま使っています。

// 距離をログ出力
Debug.Log($"distance : {distance}");

さいごに

スプラインとの直近位置は、SplineUtility.GetNearestPointメソッドから求められます。

ただし、実際に計算される位置は厳密な位置ではなく、スプライン上のいくつかの点を結んだ直線上の位置として近似されます。

位置の近似精度は、セグメントの分割数と計算回数によって高めることが可能ですが、計算コストもかかるため状況に合わせて調整して使うと良いでしょう。

関連記事

参考サイト

スポンサーリンク