2019年3月28日

Unity 垂線の足の求め方


任意の点から下ろした垂線とベクトルの交点の座標を取得する方法をメモメモ…



※このページの内容の動作確認にはUnity2018.2を使用しています。
たとえばベクトルABがあったとして、そこに点Pから下ろした垂線との交点を求めたいときの方法です。

Hの座標を求めたい!

答えを先に書いてしまうと、実は Unity には Vector3.Project という便利な関数が用意されているので、これを使えば一発で計算できます。

// 点Pから直線ABに下ろした垂線の足の座標を返す
Vector3 PerpendicularFootPoint(Vector3 a, Vector3 b, Vector3 p)
{
 return a + Vector3.Project(p - a, b - a);
}

意外と簡単でした。

Vector3.Project とは

Vector3.Project の宣言は次のようになります。(公式スクリプトマニュアル)
public static Vector3 Project(Vector3 vector, Vector3 onNormal);
この Vector3.Project 関数を使えば正射影ベクトルを計算することができます。正射影ベクトルとは何ぞや?という場合はこちらのページ(正射影ベクトルの公式の証明と使い方 | 高校数学の美しい物語)が分かりやすいので参考にしてください。ざっくり説明すると以下のような図になります。


Vector3.Project の2つ目の引数 onNormal を地面だと考えてください。そこに地面の真上から光を当てたとき、1つ目の引数 vector が地面に落とす影が、この関数の戻り値のベクトルになります。要は「vector の影を onNormal に投射したもの」ですね。垂直方向から光を当てているので、当然 Projection の先っちょの座標は vector の先端から下ろした垂線の足になるというわけです。

ワールド空間での座標を求める場合は、始点の座標を足し合わせるのを忘れないようにしましょう。

Vector3.Dot を使って書く

上の説明を見て「あれ?もしかしてこれって内積の性質と同じなのでは?」と思われる方もいるでしょうが、実際そのとおりです。Vector3.Project 関数は、内積(Vector3.Dot)を使っても普通に書けます。

// 点Pから直線ABに下ろした垂線の足の座標を返す
Vector3 PerpendicularFootPoint(Vector3 a, Vector3 b, Vector3 p)
{
 Vector3 ab = (b - a).normalized;
 float k = Vector3.Dot(p - a, ab);
 return a + k * ab;
}

ベクトルをいったん正規化して(つまり長さを1にして)、それを内積の結果でk倍しているだけです。結局のところ、Vector3.Project はこの計算を自動で行ってくれるのでシンプルに書けて便利ということだと思います。(もしかしたら内部で気の利いた処理をやってくれているのかもしれませんが…)


その他の注意点

あといくつか注意したい点…というか補足情報があります。

パフォーマンスの問題


上でも書いたとおり、Vector3.Project は正規化を自動で行ってくれます。正規化するということは平方根の計算が入ることになり、パフォーマンス的に問題が出る可能性があります。もし繰り返しの処理で負荷が気になる場合は、Vector3.Project を使用せず自前で実装しましょう。

// normalizedAB はあらかじめ正規化しておく
Vector3 PerpendicularFootPoint(Vector3 a, Vector3 normalizedAB, Vector3 p)
{
 return a + Vector3.Dot(p - a, normalizedAB) * normalizedAB;
}

点Bの座標を指定するかわりにあらかじめ正規化したベクトルABを渡すことで、関数内でいちいち正規化の処理がかけられるのを避けています。


2Dの場合


Vector3 には Project 関数が用意されていますが、残念ながら Vector2 にはありません。一旦 Vector3 で計算してから Vector2 に変換する…という方法でもいいのですが、こちらも内積を使ってちょちょいと書いてしまいましょう。

// 点Pから直線ABに下ろした垂線の足の座標を返す
Vector2 PerpendicularFootPoint(Vector2 a, Vector2 b, Vector2 p)
{
 Vector2 ab = (b - a).normalized;
 return a + Vector2.Dot(p - a, ab) * ab;
}

なお、もしも交点座標ではなく、単に垂直方向のベクトルを取得したい場合は Vector2.Perpendicular という関数が用意されている(バージョン2018.1から追加)らしいので、そちらを利用するともっとシンプルに書けますね。


線分の内部にある最も近い点


上で紹介した方法は点PからベクトルABへの垂線の足を求める方法ですが、それは言い換えると点Pから最も近いベクトルAB上にある点を求めるのと同じことですよね。このように「最短距離にある点」を取得したい場合、直線AB上の点ではなく、線分AB上の点(点Aと点Bの間のどこかにある点)に限定したいこともあると思います。一応その計算方法も見つけたのでついでに載せておきます。

// 点Pから最も近い線分AB上にある点を返す
Vector3 NearestPointOnLineSegment(Vector3 a, Vector3 b, Vector3 p)
{
 Vector3 ab = b - a;
 float length = ab.magnitude;
 ab.Normalize();

 float k = Vector3.Dot(p - a, ab);
 k = Mathf.Clamp(k, 0, length);
 return a + k * ab;
}

参考: How do i find the closest point on a line? - Unity Forum

ベクトルをk倍するときに、もとのベクトルの長さの範囲から出ないように調整しています。距離を算出する処理が2回も出てきているので(magnitudeとNormalize)、状況に応じて最適化するようにしましょう。

2点の延長線上にある点
2点の範囲内にある点


Vector3.ProjectOnPlane


あるベクトルを別のベクトルに投影するには Vector3.Project を使えばいいのですが、ベクトルを平面に投影したい場合は Vector3.ProjectOnPlane を使います。基本的な考え方は Project 関数と同じで「地面に対して垂直にベクトルの影を落とす」というイメージです。ただし Vector3.Project とは違い、引数には平面の法線ベクトル(つまり平面に対して垂直方向のベクトル)を指定する必要があるので、そこだけ注意してください。

以上、垂線や Vector3.Project に関連したメモでした。


0 件のコメント:

コメントを投稿