【Unity】【C#】床面(Yaw)/YZ平面(Pitch)に射影された角度を、符号(方向)付きで求める [3D] 
2017/07/11 Tue [edit]
今回は前回の「2Dの外積」を利用して、3DでXZ平面(床面)またはYZ平面に射影された正負符号付き角度を求めてみよう。例えばこれは相対的にキャラの左右に向く角度(Yaw)と、上下に向く角度(Pitch)を求めるのに使える。
気をつけて欲しいのは今回用いる「2Dの外積」というのは本来の「外積」の意味ではないということだ。外積の定義は以下に参考URLを載せておくが、この 3Dの外積の1つの軸を無視して(0を代入したものと考えても良い)「XZ平面」または「YZ平面」に適用したものとなる。その使い方は前回詳しく記したので割愛するが、要するに「2Dの外積」は平面上で方向を表すのに便利なので、その性質を利用しようということだ。数学的にきちんと定義されているものではないので注意しよう。
(参考)外積の定義
また、Unity では Vector3.Angle() という便利な関数があるが、これも利用する。ただし、この関数が返す角度は大きさだけであって、その方向(正負符号)は含まれてない。あるオブジェクトから対象オブジェクトの角度を調べたとき、その方向(右か左/上か下か等)も知りたいことは多いだろう。それを計算するために「なんちゃって外積」(2D外積)を利用しようというわけだ(笑)。
今回は 2Dのときと違って、3D では少なくとも「XZ平面」「YZ平面」「XY平面」3つの平面を基準と考えてみる必要がある。しかし「XY平面」というのは 2Dで考えた平面と同じものなので、解説は省略することにする。Unity で 3D モードを使用する場合、「XZ平面」は床の平面となり利用頻度が高いので、これを中心に考えよう。「YZ平面」「XY平面」も同じように考えるものとする。
今回も正負の方向や2D外積の定義などかなり詳細に記しているので、既に知っている、またはとりあえず今すぐ使いたいなどの場合は「■XZ平面で符号付き角度を求める [3D]」まで読み飛ばした方が良いだろう。
■XZ平面(床面/Yaw)での2D外積の定義
■XZ平面(床面/Yaw)での2D外積と符号の対応
■XZ平面(床面/Yaw)で符号付き角度を求める [3D]
■YZ平面(Pitch)で符号付き角度を求める [3D]
■XY平面(Roll)で符号付き角度を求める [3D]
(※) Unity 5.6.2p2 / Windows10(x64) で確認
■3Dでの回転方向と符号の対応
まずはじめに、Unity 上での回転方向の符号を調べておこう。数学的なものを用いるときに気をつけなければならないのは、Unity は左手系座標で数学は右手系座標なので、符号や見た目方向が異なる点だ。ちなみに「右手系」「左手系」というのは親指を X軸、人差し指を Y軸、中指を Z軸に見立てて、座標軸の正方向に合わせればわかる。腕をひっくり返しても良い。詳しくは以下の参考URLでも見て欲しいが、とりあえずここでは理屈は抜きにして、実際に試すことで正負を調べてみよう。
(参考)3D 座標系

回転なしの状態。Z軸の奥の方向が正、手前方向が負となっている。

Rotation X を 30度回転した状態。前方が床に向かう方が正、空へ向かう方が負となる。

Rotation Y を 30度回転した状態。右を向く方が正、左を向く方が負となる。

Rotation Z を 30度回転した状態。左へ体を傾ける方が正、右へ傾ける方が負となる。
2Dモードを使っているときと同じ動作になる。
試してみると、Rotation の X を正方向に 30 度回転すると、床方向に傾き、Rotation の Y を正方向に 30 度回転すると、右方向を向き、Rotation の Z を正方向に 30 度回転すると、体を左に傾ける感じになる。ただしこれはあくまでカメラがZ軸に沿って、キャラ(▲)の後ろから写している場合であって、例えば向かい側から写していたら、逆向きに見えるので気をつけよう。つまり、常に何かを基準に考えることが必要である。まとめると以下のようになる。これら正負符号の方向を基準として、次にそれぞれの平面での2D外積を考えてみよう。
■XZ平面(床面/Yaw)での2D外積の定義
冒頭に書いたように、今回使う2D外積は本来の3Dのものとは異なる。その本来の外積の成分表示は参考資料を見て欲しいが、ここでは 2Dのように使うので、以下の式を使う。なお、Vector3 を利用したいので、成分[=座標]:(a1, a3), (b1, b3) → (ax, az), (bx, bz) に置き換えてある。式を見ただけではどういう計算をしているのかイマイチわからないので、図のように成分の外側と内側を掛け合わせて差を取るということを機会的に覚えてしまう方が良いだろう。理論より実践で使えることの方がよほど重要なことである(笑)。また、XZ平面(床面)で考えるにあたって、Y軸を無視して消去したものであることに注意して欲しい。y 成分に0を代入したもの、または XZ平面に射影したベクトルと考えても良い。
(参考)外積の成分表示

●成分の外側と内側を掛け合わせて差を取る
●XZ平面(床面)で2D外積を求める関数
using UnityEngine;
/// <summary>
/// XZ 平面(床面)での2D外積を求める
///・a×b = a1b3 - a3b1
///・2D 的に計算する。y軸を無視したものと考える。
///・外積 = 0 のとき、両ベクトルは平行(0または180度)。
///・外積 > 0 のとき、transform.forward を基準にすると左側。
///・外積 < 0 のとき、transform.forward を基準にすると右側。
///※Y軸での回転とは正負が逆になるので注意。
/// </summary>
/// <param name="a">基準の方向ベクトル</param>
/// <param name="b">対象の方向ベクトル</param>
/// <returns>2Dの外積</returns>
public static float CrossXZ(Vector3 a, Vector3 b)
{
return a.x * b.z - a.z * b.x;
}
コードにすると上記のようになる。図を見れば説明はいらないだろう。その対応はこれから調べることにする。
■XZ平面(床面/Yaw)での2D外積と符号の対応
さて、XZ平面での2Dでの外積の計算も示したので、今度は具体的に座標を代入して計算してみよう。調べる座標をすべて1で構成すれば、暗算でも可能だろう。▲の進行方向を基準として、XZ平面(床面)で左右の2D外積を求めると、以下の図のようになる。Y軸を無視しているので、上空から見てる感じと思って良いだろう。同じ象限にいれば符号は変わらないので、これと前述の「3Dでの回転方向と符号の対応」(Y軸で回転) と照らし合わせれば、符号が逆であることがわかる。ちなみに平行なベクトル(0または180度)の2D外積は0になる。
・(0, 0, 1) が基準で、(1, 0, 1) の XZ平面での 2D外積は、(0, 1)×(1, 1) = 0 * 1 - 1 * 1 = 0 - 1 = -1
・(1, 0, 0) が基準で、(1, 0, 1) の XZ平面での 2D外積は、(1, 0)×(1, 1) = 1 * 1 - 0 * 1 = 1 - 0 = +1
・(1, 0, 0) が基準で、(1, 0, -1) の XZ平面での 2D外積は、(1, 0)×(1, -1) = 1 * (-1) - 0 * 1 = (-1) - 0 = -1
・(0, 0, -1) が基準で、(1, 0, -1) の XZ平面での 2D外積は、(0, -1)×(1, -1) = 0 * (-1) - (-1) * 1 = 0 + 1 = +1
・(0, 0, -1) が基準で、(-1, 0, -1) の XZ平面での 2D外積は、(0, -1)×(-1, -1) = 0 * (-1) - (-1) * (-1) = 0 - 1 = -1
・(-1, 0, 0) が基準で、(-1, 0, -1) の XZ平面での 2D外積は、(-1, 0)×(-1, -1) = (-1) * (-1) - 0 * (-1) = 1 + 0 = +1
・(-1, 0, 0) が基準で、(-1, 0, 1) の XZ平面での 2D外積は、(-1, 0)×(-1, 1) = (-1) * 1 - 0 * (-1) = (-1) + 0 = -1
■XZ平面(床面/Yaw)で符号付き角度を求める [3D]
以上で、XZ平面(床面)では回転方向と2D外積の符号が逆であることがわかった。つまり符号付きで角度を求めるには、
XZ平面(床面)に射影されたベクトルの符号付き角度=射影されたベクトルの角度の大きさ × 2D外積の符号×(-1)
で良いことがわかる。注意して欲しいのは、これは Unity の座標系でのことであって、基準方向(軸)によって符号が異なったりするので気をつけよう。また、冒頭に書いた「座標系」でも回転方向や符号は異なるので、少なくとも1度は調べておいた方が良い。ともかく、Unity での XZ平面(床面)での符号付き角度の計算は以下のようになる。
●XZ平面(床面)での符号付き角度を返す
using UnityEngine;
/// <summary>
/// XZ平面(床面)での方向ベクトル同士の角度を符号付きで返す(度)
///・XZ平面(床面)に射影された from から to への角度を返す。
///・transform.forward を基準とすると、右向き(時計回り)が正、左向き(反時計回り)が負となる。
/// </summary>
/// <param name="from">基準の方向ベクトル</param>
/// <param name="to">対象の方向ベクトル</param>
/// <returns>-180 <= t <= 180 [degree]</returns>
public static float AngleXZWithSign(this Vector3 from, Vector3 to)
{
Vector3 projFrom = from;
Vector3 projTo = to;
projFrom.y = projTo.y = 0; //y軸を無視する(XZ平面に射影する)
float angle = Vector3.Angle(projFrom, projTo);
float cross = CrossXZ(projFrom, projTo);
return (cross != 0) ? angle * -Mathf.Sign(cross) : angle; //2D外積の符号を反転する
}
関数内部で使っている「CrossXZ()」(XZ平面での2D外積)は前述のものを使う。解説も前述の図や計算式を見れば十分だろう。
なお、XZ平面にベクトルを射影する部分は、y 成分に0を代入することで取得しているが、「Vector3.ProjectOnPlane()」を使って取得しても良い。結果は同じになる。
Vector3 projFrom = Vector3.ProjectOnPlane(from, Vector3.up);
Vector3 projTo = Vector3.ProjectOnPlane(to, Vector3.up);
//projFrom.y = projTo.y = 0; //※この行は不要になる
これを使って、基準となる方向ベクトル(引数:from)に transform.forward を、対象への方向ベクトル(引数:to)に target.transform.position - transform.position を代入すれば、常に進行方向に対して対象が右方向(時計回り)にいるか(正の値)、左方向(反時計回り)にいるか(負の値)とその角度がわかる。もちろん、引数の基準となる方向ベクトルと対象の方向ベクトルを逆にすると、符号が逆になる。
■YZ平面(Pitch)で符号付き角度を求める [3D]
ここまで理解できたら、あとは駆け足で行こう。YZ平面(Pitch)での2D外積の定義とその符号、回転との対応を同じように調べてみよう。
●YZ平面(Pitch)での2D外積の定義

●YZ平面(Pitch)で2D外積を求める関数
using UnityEngine;
/// <summary>
/// YZ 平面(Pitch)での2D外積を求める
///・a×b = a2b3 - a3b2
///・2D 的に計算する。x軸を無視したものと考える。
///・外積 = 0 のとき、両ベクトルは平行(0または180度)。
///・外積 > 0 のとき、transform.forward を基準にすると下側。
///・外積 < 0 のとき、transform.forward を基準にすると上側。
/// </summary>
/// <param name="a">基準の方向ベクトル</param>
/// <param name="b">対象の方向ベクトル</param>
/// <returns>2Dの外積</returns>
public static float CrossYZ(Vector3 a, Vector3 b)
{
return a.y * b.z - a.z * b.y;
}
●YZ平面(Pitch)での2D外積と符号の対応
・(0, 0, 1) が基準で、(0, -1, 1) の YZ平面での 2D外積は、(0, 1)×(-1, 1) = 0 * 1 - 1 * (-1) = 0 + 1 = +1
・(0, -1, 0) が基準で、(0, -1, 1) の YZ平面での 2D外積は、(-1, 0)×(-1, 1) = (-1) * 1 - 0 * (-1) = (-1) + 0 = -1
・(0, -1, 0) が基準で、(0, -1, -1) の YZ平面での 2D外積は、(-1, 0)×(-1, -1) = (-1) * (-1) - 0 * (-1) = 1 + 0 = +1
・(0, 0, -1) が基準で、(0, -1, -1) の YZ平面での 2D外積は、(0, -1)×(-1, -1) = 0 * (-1) - (-1) * (-1) = 0 - 1 = -1
・(0, 0, -1) が基準で、(0, 1, -1) の YZ平面での 2D外積は、(0, -1)×(1, -1) = 0 * (-1) - (-1) * 1 = 0 + 1 = +1
・(0, 1, 0) が基準で、(0, 1, -1) の YZ平面での 2D外積は、(1, 0)×(1, -1) = 1 * (-1) - 0 * 1 = (-1) - 0 = -1
・(0, 1, 0) が基準で、(0, 1, 1) の YZ平面での 2D外積は、(1, 0)×(1, 1) = 1 * 1 - 0 * 1 = 1 - 0 = +1
前述の「3Dでの回転方向と符号の対応」(X軸で回転) と照らし合わせてみると、符号が同じであることがわかる。つまり以下のようになる。
YZ平面(Pitch)に射影されたベクトルの符号付き角度=射影されたベクトルの角度の大きさ × 2D外積の符号
●YZ平面(Pitch)での符号付き角度を返す
using UnityEngine;
/// <summary>
/// YZ平面(Pitch)での方向ベクトル同士の角度を符号付きで返す(度)
///・YZ平面(Pitch)に射影された from から to への角度を返す。
///・transform.forward を基準とすると、右向き(時計回り)が正、左向き(反時計回り)が負となる。
///・transform.forward を基準とすると、下方向が正、上方向が負となる。
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <returns>-180 <= t <= 180 [degree]</returns>
public static float AngleYZWithSign(this Vector3 from, Vector3 to)
{
Vector3 projFrom = from;
Vector3 projTo = to;
projFrom.x = projTo.x = 0; //x軸を無視する(YZ平面に射影する)
float angle = Vector3.Angle(projFrom, projTo);
float cross = CrossYZ(projFrom, projTo);
return (cross != 0) ? angle * Mathf.Sign(cross) : angle;
}
関数内部で使っている「CrossYZ()」(YZ平面での2D外積)は前述のものを使う。解説も前述の図や計算式を見れば十分だろう。
なお、YZ平面にベクトルを射影する部分は、x 成分に0を代入することで取得しているが、「Vector3.ProjectOnPlane()」を使って取得しても良い。結果は同じになる。
Vector3 projFrom = Vector3.ProjectOnPlane(from, Vector3.right);
Vector3 projTo = Vector3.ProjectOnPlane(to, Vector3.right);
//projFrom.x = projTo.x = 0; //※この行は不要になる
これを使って、基準となる方向ベクトル(引数:from)に transform.forward を、対象への方向ベクトル(引数:to)に target.transform.position - transform.position を代入すれば、常に進行方向に対して対象が下方向にいるか(正の値)、上方向にいるか(負の値)とその角度がわかる。もちろん、引数の基準となる方向ベクトルと対象の方向ベクトルを逆にすると、符号が逆になる。
■XY平面(Roll)で符号付き角度を求める [3D]
最後に XY平面(Roll)での符号付き角度を求めてみよう。といっても XY平面というのは Unity の 2Dモードそのものなので、詳しい解説は 2Dの記事を見て欲しい。ここでは 2Dのときの引数を Vector2 → Vector3 に書き換えただけのものを掲載しておく。実は Unity は Vector2 ←→ Vector3 の相互変換を自動でやってくれるので必要はないのだが、インテリセンス(入力補完)は型で関数の一覧を出すので、あった方が何かと便利だろう。内容的には今までのものと同じなので、難しくはないハズだ。
●XY平面(Roll)で2D外積を求める関数と符号付き角度を返す関数
using UnityEngine;
//※Cross2D() と同じ内容で、引数を Vector3 にしたもの
/// <summary>
/// XY 平面(Roll)での2D外積を求める
///・a×b = a1b2 - a2b1
///・2D 的に計算する。z軸を無視したものと考える。
///・外積 = 0 のとき、両ベクトルは平行(0または180度)。
///・外積 > 0 のとき、transform.up を基準にすると左側。
///・外積 < 0 のとき、transform.up を基準にすると右側。
/// </summary>
/// <param name="a">基準の方向ベクトル</param>
/// <param name="b">対象の方向ベクトル</param>
/// <returns>2Dの外積</returns>
public static float CrossXY(Vector3 a, Vector3 b)
{
return a.x * b.y - a.y * b.x;
}
//※AngleWithSign() とほぼ同じ内容で、引数を Vector3 にしたもの
/// <summary>
/// XY平面(Roll)での方向ベクトル同士の角度を符号付きで返す(度)
///・XY平面に射影された from から to への角度を返す。
///・transform.up を基準とすると、左側(反時計回り)に位置するとき正、右側(時計回り)に位置するとき負となる。
/// </summary>
/// <param name="from">基準の方向ベクトル</param>
/// <param name="to">対象の方向ベクトル</param>
/// <returns>-180 <= t <= 180 [degree]</returns>
public static float AngleXYWithSign(this Vector3 from, Vector3 to)
{
Vector3 projFrom = from;
Vector3 projTo = to;
projFrom.z = projTo.z = 0; //z軸を無視する(XY平面に射影する)
float angle = Vector3.Angle(projFrom, projTo);
float cross = CrossXY(projFrom, projTo);
return (cross != 0) ? angle * Mathf.Sign(cross) : angle;
}
「Cross2D()」と「AngleWithSign()」の詳細は前回の記事を見て欲しい。
ちなみに 3Dでの XY平面というのは Unity の場合 Roll であって、例えるなら首をかしげて見る回転のことである。なので transform.forward を基準にするといつでも直角になってしまうので、transform.up を基準として考えた方が良い。つまり頭のてっぺん方向を基準として左に首をかしげた場合、正方向であり、右に首をかしげた場合、負の方向となる。前述の「3Dでの回転方向と符号の対応」(Z軸で回転) と照らし合わせておこう。
以上で、XZ平面(Yaw)、YZ平面(Pitch)、XY平面(Roll)の3平面に射影した角度を符号付きで求めたが、射影しないで直接の符号付き角度を求めたいなら、どの方向を符号の基準にするかの軸を引数にし、これら3平面のどれかの2D外積と Vector3.Angle() を掛け合わせば良いと思う。とは言え、大抵は床面を基準とした方向を知りたいことが多く、2つのベクトルの中間的な方向なら Vector3.Lerp() のような関数で求められるので、あまりまとめる必要はないかも知れない。
これら関数を上手く利用すれば、NavMeshAgent なしでも敵がプレイヤーを追跡してくるなんてものも簡単にできる。例えば以下のサンプルはプレイヤーがキャラ(猫耳娘)に一定距離近づくと、プレイヤーとの角度によって近づいてくるように作ってある。その角度は「AngleXZWithSign()」を使って求めている。またプレイヤー追跡は簡単な線形補間だ。経路検索してないのでとても軽い。ここでは300体のキャラを同時に動かしている。
|
ちなみに Yaw(ヨー)、Pitch(ピッチ)、Roll(ロール)は回転する方向のことであって、それぞれ Y, X, Z軸で回転するという意味ではない。なので例えば、右手系座標のときは Z, Y, X軸がそれぞれ Yaw, Pitch, Roll になったりもする。簡単な覚え方は、首を左右に振る回転をYaw(ヨー)、首を縦に振る回転をPitch(ピッチ)、首をかしげる回転をRoll(ロール)と覚えれば楽だ。これなら 座標系が異なるときでも回転方向がわかりやすいので、間違えることはないだろう(符号は座標系によるので注意)。
なお、さんざん「2D外積」という用語を使っているが、正式な名称でも定義でもないため、数学に詳しい人に言ったら怒られるかも(笑)。私が便宜上使っているだけの言葉なので、他人に説明するときは何らか公共性のある言葉に置き換えた方が良いかも知れない(投げっぱなし)。
(関連記事)
【Unity】【C#】符号(方向)付き角度を、2Dの外積を使って求める [2D]
【Unity】【C#】n度ごとの角度を返す(一定角度ごとの切り捨て、切り上げ、四捨五入)

- 関連記事
トラックバック
トラックバックURL
→http://fantom1x.blog130.fc2.com/tb.php/254-aa68f714
この記事にトラックバックする(FC2ブログユーザー)
| h o m e |