🌐

[Unity][C# Script] 回転を自在にあやつろう。

2021/02/17に公開

はじめに

その Slerp、使い方はあってますか?

はい、私は間違ってました(真顔

さて、GameObject(ゲームオブジェクト) の rotation はクォータニオン型なのですが、実際に使われるときはVector3型で行うことも少なくありません。
 そして、 Vector3型の使い方がベクトル(方向)・座標・角度の3つあるのですが、これをあまり区別して説明されてなかったり、操作する組み合わせもそのぶんあり混同しやすいという罠。
 そしてウェブのサンプルを動かしているとき、たまーに期待していない挙動をしめすものがあったので、首をひねりながら調べたてことをまとめてみました。さあこれでスッキリです。

Vector3型は、使いみちに応じてサンプルでは以下のように表記します。
・方向(移動量)は Direction。単位ベクトルの長さがUnityの距離単位になる。
・座標は Position。単位はUnityの距離単位(m / 物理演算時)
・角度(オイラー角・回転量)は Angle。X,Y,Zがそれぞれ角度。単位は°(度)
・クォータニオンは Rotation で表記します。
また、各サンプルの target はターゲットの GameObject を想定しています。

1.ゲームオブジェクトの向きの操作

1) ゲームオブジェクトの向きは、transform.rotation

ゲームオブジェクトが向いている方向は、GameObject.transform.rotation であり、クォータニオン(Quaternion)型です。

// trQ → Q
Quaternion myRotation = transform.rotation;

2) 他のゲームオブジェクトとrotationを一致させる

Quaternion 型なので直接代入して問題ありません。

// Q → trQ
transform.rotation = target.transform.rotation;
transform.rotation = Quaternion.identity; // ワールド座標の正面

他にも、Quaternion 型であれば直接代入できます。

3) ゲームオブジェクトの向きを、度数で指定する。

transform.rotation の向きを度数で指定します。
 このときの Vector3型は、角度として扱われています。

// a) 角度指定(v3Angle → trQ)
transform.rotation = Quaternion.Euler( 0f, 0f, 10f); // Z軸を10°に設定

// b) 角度指定(v3Angle → trQ)
transform.eulerAngles = new Vector3( 0f, 0f, 10f); // 同上

// c) 角度指定(親子関係の子の相対角度)(v3Angle → trQ)
transform.localRotation = Quaternion.Euler( 0f, 0f, 10f); // Z軸を10°に設定
transform.localEulerAngles = new Vector3( 0f, 0f, 10f); // 同上

4) ゲームオブジェクトを度数で回転する。

transform.rotation の向きを度数で回転します。
 このときの Vector3型は、角度として扱われています。

// 回転(v3Angle → trQ (加算))
transform.Rotate ( 0f, 0f, 10f );  // Z軸を10°回転

5) ゲームオブジェクトの向きをVector3型(角度)で取り出す。

ゲームオブジェクトの向き(transform.rotation)はクォータニオン型のため、スクリプト内で角度を制御する場合はオイラー角(Vector3型)のほうが扱いやすいことが多いです。Inspectorの数値とも一致しますし。

// trQ → v3Angle
Vevtor3 myAngle = transform.eulerAngles; // 結果はVector3(Angle)型

6) ゲームオブジェクトの向きをVector3型(方向)で取り出す。

ゲームオブジェクトの向きをベクトル量で取り出します。たとえばゲームオブジェクトを前に進めるときの移動量を求めるときや、ゲームオブジェクトの前方の座標を得たいときなどは、これを使います。

// trQ → v3Direction
Vector3 myDirection = transform.forword; // 結果はVector3(Direction)型

// transform.rotation * Vector3.up も同値となる。

射出するオブジェクトの位置をオフセットするような場合は、以下のようにするとよいでしょう。

// v3Direction Offset(前に1.0、上に0.1、左に0.1オフセット)
transform.position += (transform.forword *1.0f)
                    + (transform.up *0.1f)
		    + (transform.right * -0.1f);

7) ゲームオブジェクトの向きをfloat型の角度(度数法)で取り出す。

ゲームオブジェクトの向きの、xyzどれかひとつの軸の角度のみを取り出します。平面上で向いている角度を得るときは、yの値を得ることになります。

// trQ → v3Angle
float myAngleY = transform.eulerAngles.y; // y軸の回転量
// 実質、オブジェクトの向いている角度になります。

8) ゲームオブジェクトを度数法(0~360°)でゆっくり回転させる。

単純に回転させる場合はこれが便利ですね。サンプルは、1秒ごとにY軸(xz平面)にそって120度回転します。

// 回転(v3Angle → trQ)
transform.Rotate( 0f, 120.0f * Time.deltaTime ,0f );

GameObjectを円周軌道にそって移動させたい場合は、回転させたい中心に空の GameObject をおいてそれを親GameObjectに設定し、親のtransform.rotationを上のスクリプトで回転させるとよいでしょう。このように、オブジェクトを回転させたり傾けたりする場合は、オブジェクトの角度を直接操作するよりは、回転させたり傾けたりするパラメータの数に応じた親オブジェクトを作り、それを操作したほうが単純になることが多いと思います。

targetをスクリプトから親にするとき。
transform.parent = target.transform;

8) Vector3型(座標)を指定して、そちらを向かせる。

target.transform.position は Vector3型。座標として扱われています。

// v3Position → trQ
transform.LookAt ( target.transform.position ); // target は GameObject型

9) ゆっくりとVector3型(座標)を向かせる。

ホーミングの仕組みは、これで簡単に作れそうですね。
 サンプルは1秒間に120度の速度で回転します。

// trQ と v3Position → trQ
transform.rotation = Quaternion.RotateTowards ( transform.rotation, 
 Quaternion.LookRotation( target.transform.position - transform.position ),
 120.0f * Time.deltaTime );

(少し解説)
・Vector3 direction = target.transform.position - transform.position;
 targetのVector3型(方向)を算出
・Quaternion rotation = Quaternion.LookRotation( direction );
 Vector3型(方向)から Quaternion型(向き)に変換。
・transform.rotation = Quaternion.RotateTowards ( transform.rotation, rotation, 120.0f * Time.deltaTime );
 今の向きからtargetの向きに、1秒間に120度の速度で回転

10) 回転座標をy軸の回転のみに限定(アジャスト)する。

Rotationのxとzを0を代入します。

// v3Angle → Q
transform.rotation = Quaternion.Euler(new Vector3( 0f, transform.eulerAngles.y, 0f ));

相手と上下にずれていると、オブジェクトも上下を向くので補正したりするのに使います。

11) オブジェクトを、面に垂直に立つよう傾かせる

対象の面の法線ベクトルを取得できれば、それにRotationをあわせることができます。

// raycastHitは別途得たRaycastHit型を想定
Vector3 direction = raycastHit.normal;  // 面の法線ベクトル
transform.rotation = Quaternion.FromToRotation(Vector3.up, direction);

2. Quaternion型の操作

Quaternion型をVector3型に変換したり、またその逆の操作は、その仕組上計算コストが高いと考えています。なるべくQuaternion型のまま扱うことができれば、そのような心配がなくなるので、Quaternion型がどう扱えるか少々調べてみました。

Quaternion型は内部で 4つの数値 (Unity では x、y、z、w)を持ちます。この数値はどうやら専門的な知識なしにそれを直接操作しても期待した結果は得られないようで、実際には、Quaternion型はそのメソッドを使い操作することがメインとなるようです。
 ちょっと調べてみましたが、パターンはそれほど多くなさそうですね。

1) Quaternion型を 単位回転(identity)で初期化する。

回転していないクォータニオンにて初期化します。つまり、worldの正面(哲学)を向きます。

// Q → Q
Quaternion rotation = Quaternion.identity;

2) クォータニオンを回転する。

クォータニオンをクォータニオンで回転させた結果は、クオータニオン同士を*(掛け算記号による演算)することで求められます。

// Q → Q
Quaternion Rotation30 = Quaternion.Euler(0f, 30f, 0f);
Quaternion Rotation60 = Quaternion.Euler(0f, 60f, 0f);
Quaternion Rotation90 = Rotation30 * Rotation60;

3) 2つの角度の差を返す

Quaternion型同士の差からQuaternion型の差を得るには、逆クォータニオンを掛けるとよいらしいです。すこし不思議。

// Q → Q
// 必要な回転量を得る(Inverse したクォータニオンを乗算)
// targetRotation は Quaternion型
Quaternion requiredRotation = targetRotation
                             * Quaternion.Inverse(transform.rotation); 

4) Quaternion型に Vector3型(角度)を代入する。

Vector(オイラー角) と Quaternion の相互変換を行う方法です。なお、Quaternionは0~360°に相当する数値しか保持しないようです。

// a) v3Angle → Q
Quaternion rotation = Quaternion.Euler(new Vector3(0f, 30f, 0f));

// b) v3Angle → Q
Quaternion rotation = Quaternion.identity;
rotation.eulerAngles = new Vector3(0f, 30f, 0f);

5) Quaternion型を Vector3型(角度)に変換。

Quaternion と Vector3(オイラー角) の相互変換を行う方法です。2)とは逆方向の型に変換します。取り出される範囲はそれぞれ0~360°になります。

// Q → v3Angle
Vector3 angle = Quaternion.eulerAngles;

6) Quaternion型を Vector3型(方向)に変換。

GameObjectであれば、transform.forwardで向きをベクトル量に変換できます。Quatanion型でこれと同等の操作をしたいときは、Vector3.upを*(乗算記号による演算)します。

// Q(rotationQ) → v3Direction
Vector3 direction = rotationQ * Vector3.up; // 記述順番は厳守のこと

7) Vector3型(方向)を Quaternion型に変換。

// v3Direction → Q(rotationQ)
Vector3 direction = new Vector3( 3f, 2f, 0f );
transform.rotation = Quaternion.FromToRotation(Vector3.up, direction);

8) Quaternion型をfloat(度数法 / 0~360°)で取り出す。

Quaternion から、xyzのうち1つの軸について角度を取り出します。

float angleY = Quaternion.eulerAngles.y; // y軸の回転量

-180° ~ 180° にする場合は、以下で変換するとよいでしょう。
angleY = angleY <= 180f ? angleY : (360f - angleY );

9) Vector3型(座標)から回転させる量(Quaternion型)を得る。

元となる位置とtargetの位置からtargetの方向を回転量(Quaternion型)として得ます。これをtransform.rotatonに代入することも可能ですが、その場合はおそらく通常はtansform.LookAtを使ったほうがスッキリすることが多いはずです。

// a v3Position → Q
transform.rotation =  Quaternion.FromToRotation(transform.position, 
                                                target.transform.position );

10) Vector3型(方向)から回転量を得る。

Quaternion.LookRotationは方向(移動量)から回転量を得ます。下のサンプルは第2パラメータ(Vector3.up)が省略されています。

// v3Direction → Q
transform.rotation = Quaternion.LookRotation( target.transform.position
                                              - transform.position );

public static Quaternion LookRotation (Vector3 forward, Vector3 upwards= Vector3.up);
・forward 向かせたい方向
・upwards 上方向を定義するベクトル。 Vector3.up は Vector3(0f, 1f, 0f) と同義。
transform.LookAt とは、場合に応じた使い分けができそうですね。

11) 回転量(Quaternion型)から回転量(Quaternion型)へ制限つき回転を行う。

transfom のホーミングの仕組みで利用したQuaternion.RotateTowardsです。

// Q → Q
// toRotation = 目標のRotation
// step = 回転量制限(サンプルは120°/秒)
transform.rotation = Quaternion.RotateTowards( ransform.rotation,
                                          toRotation, 120.0f * Time.deltaTime );

12) 2つの角度の差を角度をfloat(度数法 / 0~180°)で返す。

GameObjectが視界内に入っているか、あるいは顔の向く角度以外は無視するなどの判定に使えそうです。
 クォータニオンの差分で出力されるわけでないので、得た値を利用して物体を回転させる事は出来ないようです。

// targetRotation は Quaternion型
float angle = Quaternion.Angle(transform.rotation, targetRotation);  

if( angle < 15f )
{
  // 視界内
}

13) ゆっくりと対象の方向を向ける(球面回転)

1秒かけて回転するサンプルです。

// float timeCount = 0f; 別の場所で定義
// Quaternion fromRotation = transform.rotation;
// Quaternion targetRotation = Quaternion.LookRotation( target.transform.position - transform.position );

transform.rotation = Quaternion.Slerp ( fromRotation, targetRotation, timeCount );
timeCount += Time.deltaTime;

※球面回転と非球面回転は以下と考えてください。
 球面回転  精度:高 処理速度:低
 非球面回転 精度:低 処理速度:高

14) ゆっくりと対象の方向を向ける(非球面回転)

1秒かけて回転するサンプルです。

// float timeCount = 0f; 別の場所で定義
// Quaternion fromRotation = transform.rotation;
// Quaternion targetRotation = Quaternion.LookRotation( target.transform.position - transform.position );

transform.rotation = Quaternion.Leap ( fromRotation, targetRotation, timeCount );
timeCount += Time.deltaTime;

15) ゆっくりと対象の方向を向ける(非等速・回転量指定)

下のサンプルは、1秒間にかけて目的の方向に回転しようとします。角度が離れている場合は回転量が多く、近づくにつれ補正の割合が小さくなります。
 よって、指定された方向をそのものを向くことはありません(哲学)

Quaternion targetRotation = Quaternion.LookRotation( target.transform.position
                                                       - transform.position );
transform.rotation = Quaternion.Lerp( transform.rotation, targetRotation,
                                                             Time.deltaTime );

サンプル等で回転にSlerpやLerpを使っているものを参考にする場合、意図した使いかたかどうかに留意してください。

16) ドリフトするホーミングミサイル

ホーミングミサイルで、ミサイルの向きに加速する表現です。
 自分がいま向いている方向から targetのある向きにむけて旋回し、その方向に加速しますが、旋回速度によっては進行方向とミサイルの向きがアンマッチになり、ドリフトしているように見えます。
 下のサンプルでは、1秒間に120°の割合で等速回転します。

// Vector3 moveSpeed = new Vector3(0f,0f,0f); 別の場所で定義
// float accSpeed = 3.0f; 別の場所で定義
// float maxSpeed = 3.0f; 別の場所で定義
Quaternion targetRotation = Quaternion.LookRotation( target.transform.position
                                                        - transform.position );
transform.rotation = Quaternion.RotateTowards( transform.rotation,
                                         targetRotation, 120f*Time.deltaTime );
moveSpeed += transform.rotation * Vector3.forward * accSpeed * Time.deltaTime;
if( moveSpeed.magnitude >= maxSpeed )
{
   moveSpeed = speed.normalized * maxSpeed;
}

17) 起き上がり小法師

起き上がり小法師のように、GameObjectをコルーチンでプルプルと振動させます。
コルーチンを呼び出すとき、振動を与える角度を度数(float)で与えます。
振動の中心はtransform.positionになります。

// StartCoroutine(PuruDo(30)); メインルーチンなどから呼び出す

IEnumerator PuruDo(float shockAngle)
{
    float purupuruTime = 2f; // 振動時間
    float amplitude = 20f; // 振幅
    float cycleAcc = 20f; // 振動速度

    while (purupuruTime > 0f)
    {
        // 振動量
        float tiltAngle = purupuruTime * amplitude * (Mathf.Sin(purupuruTime * cycleAcc));
        // 振動量を振動角度に割り振る
        var shoccRad = (transform.eulerAngles.y - shockAngle) * Mathf.Deg2Rad; ;
        var angleX = Mathf.Cos(shoccRad) * tiltAngle;
        var angleZ = Mathf.Sin(shoccRad) * tiltAngle;

        transform.eulerAngles = new Vector3(angleX, transform.eulerAngles.y, angleZ);
        purupuruTime -= Time.deltaTime;
        yield return null;
    }
}

3. Vector3型のベクトル(方向)、座標、角度の操作

1) ベクトル(方向)からベクトル(角度)への変換

いったんQuaternionを経由してみます。

// v3Direction → Q(rotationQ) → v3Angle
Vector3 direction = new Vector3( 3f, 2f, 0f );
Quaternion rotation = Quaternion.FromToRotation(Vector3.up, direction);
Vector3 angle = Quaternion.EulerAngles;

2) ベクトル(角度)からベクトル(方向)への変換

おなじく、いったんQuaternionを経由してみます。

Vector3 angle = new Vector3( 30f, 90f, 0f );
Quaternion rotation = Quaternion.Euler( angle );
Vector3 direction = rotation * Vector3.up;

3) 2つの位置間の角度を得る

方向制御するために、2つの位置間の角度を得るのはよく使う操作ですが、unityでは実際には操作対象はtransformである場合が多いでしょうから、これを使う機会はあまりないかもしれません。
ターゲットをロックオンしているときなど、向いている方向を固定したまま移動方向に応じてアニメーションを変更する場合は、-180~180°(あるいは0~360°)の値をとる角度を求める必要があります。

// v3Position と v3Position → floatAngle
// a) Vector3.SignedAngle
Vector3 targetDir = target.transform.position - transform.position;
float angle = Vector3.SignedAngle(targetDir, transform.forward,  Vector3.up);
// -180~180°となる模様

// b) Atan2をつかう(サンプルはy軸。高低差は考慮しない)
float angleY = Mathf.Atan2( target.transform.position.x - transform.position.x , target.transform.position.z - transform.position.z ) * Mathf.Rad2Deg;
// -180~180°となる模様

// c) Quaternion.FromToRotationを使う(サンプルはy軸。高低差は考慮しない)
Quaternion rotation = Quaternion.FromToRotation(fromDirection,targetDirection);
float angleY = rotation.eulerAngles.y;
// 0~360°となる模様

// d) Vector3.Angle(サンプルは角度)
Vector3 targetDir = target.transform.position - transform.position;
float angle = Vector3.Angle(targetDir, transform.forward );
// 0~180°となる模様。第3~4象限がないため、移動等には使えなさそう。
// GameObjectが視界内に入っているかなどの判定に使えそう。

4) ベクトルの正規化

ベクトルの大きさが単位ベクトルになるよう調整します。

// v3Direction → v3Direction
Vector3 direction = target.transform.position - transform.position;
Vector3 normalizedDirection = direction.normalized;

5) ベクトルの長さ(読み取り専用)

ベクトルの長さは、2点間の距離を求めるのに使われます。便利です。

Vector3 direction = target.transform.position - transform.position;
float distance = direction.magnitude;

ピュタゴラスの定理
Mathf.Sqrt( Mathf.Pow( target.transform.position.x - transform.position.x ,2) + Mathf.Pow( target.transform.position.z - transform.position.z ,2) );

6) ベクトルの長さの2乗(読み取り専用)

ベクトルの長さは、2点間の距離を求めるのに使われます。便利ですがそれゆえに多用されます。ちょっとでも処理を軽くしたい場合はこちらを使うことも検討します。

Vector3 direction = target.transform.position - transform.position;
float sqrDistance = direction.sqrMagnitude;

ベクトルの長さはできるだけ2乗同士のまま扱ったほうが処理が速いことが期待できる。平方根の演算をはさまないため。

7) ベクトル(角度)を回転させる

// Y軸に90°、Z軸に-90°回転
Vector3 angle = transform.eulerAngles;
angle.y +=  90;
angle.z -=  90;

8) ベクトル(方向)を回転させる

// Y軸に90°、Z軸に-90°回転
Vector3 direction = transform.forward;
direction =  Quaternion.Euler( 0f, 90f, -90f ) * direction;

9) じわじわと回転

自分がいま向いている方向から targetのある向きにむけて旋回します。結果をQuaternionではなくVector3で得る必要がある場合は、こちらを使うことになります。

// v3Position → v3Direction
Vector3 targetDir = target.transform.position - transform.position;
Vector3 newDirection = Vector3.RotateTowards( transform.forward, targetDirection, 120.0f * time.deltaTime );

10) 直線上にある 2 つのベクトル間をゆっくりと補間する

時間値をもとに、直線上のどこに位置するかを求める必要があれときは、これを使うのが便利そうです。

// v3Position
// float timeCount = 0f; 別の場所で定義
transform.position = Vector3.Lerp( startMarker.position, endMarker.position, timeCount );
timeCount += Time.deltaTime;

11) 方向(移動量)を、回転量を指定して回転

方向(移動量)を回転する場合、三角関数を使用して回転することもできますが、回転量をQuaternionで表現できる場合は、クォータニオンにVector3を*(掛け算)するとすっきりする場合があります。
「Quaternion * Vector3」と、演算の順番は決まっていることに注意してください。

// v3Direction と Q → v3Direction
Vector3 targetDir = target.transform.position - transform.position;
Quaternion Rotation30 = Quaternion.Euler(new Vector3(0f, 30f, 0f));
Vector3 newDirection = Rotation30 * targetDir; // 必ずQuaternion * Vector3

4.(解説)クォータ二オンとオイラー角について

Unityユーザーズマニュアル「Unity の回転と向き」より

3D アプリケーションの回転は、たいていクォータ二オンかオイラー角のどちらかによって表されています。
それぞれ、利点と欠点があります。Unity では内部でクォータ二オンを採用していますが、編集しやすいように、インスペクターでは同等のオイラー角で表しています。

オイラー角とクォータ二オンの違い

・オイラー角(Euler angles)
オイラー角は、3つの角度の値を X、Y、Z 軸に順に当てはめて回転を表す簡易な方法です。
オイラー角をあるオブジェクトに応用する場合は、オブジェクトを各軸に沿って与えられた角度で回転させます。

利点 - オイラー角は、3つの角度から構成される直感的で「人間が理解できる」フォーマットで表現されています。
利点 - オイラー角は、180度以上の回転を経て、1つの向きから別の向きへの回転を表現できます。
制限 - オイラー角はジンバルロック (Gimbal lock) として知られている制限があります。

ジンバルロックについて
対象をそれぞれの軸に沿って順に回転させるとき、第一、第二の回転によって、結果的に 3つ目の軸にそろってしまうことがあります。
つまり、第三の回転値を 3つ目の軸に反映できないために、「角度の自由」が失われてしまいます。

・クォータニオン(Quaternion)

クォータニオンは、オブジェクトの向きや回転を表すのに使用されます。
この表現法では、内部で 4つの数字 (Unity では x、y、z、w と呼びます) で構成されています。
ただし、この数字は、角度や軸を表現しているわけでなく、通常、直接アクセスすることはありません。
クォータ二オン (四元数)について特別に興味がない限り、クォータニオンが 3D 空間の回転を表しているのだということを知っているだけで十分で、
通常、x、y、z プロパティーを操作したり変更することはありません。

ベクトルが位置と方向 (方向は原点から測ります) を表現するのと同様に、クォータ二オンは向きと回転を表現できます。
回転は、回転「基準」か「単位」を基準に測られます。回転を、ある向きからもう一方の向きへの方向として計測するため、クォータ二オンでは、180度より大きな回転を表現することができません。

利点 - クォータ二オンの回転は、ジンバルロックの影響を受けません。
制限 - 単体のクォータ二オンでは、180度を超す回転を表すことができません。
制限 - クォータ二オンの数的表現は、直感的に理解できません。
Unity では、ゲームオブジェクトのすべての回転は、内部ではクォータ二オンで保存します。なぜなら、利点のほうが、制限を上回るためです。

しかし、Transform Inspector では、回転は、理解しやすく編集しやすいオイラー角で表示されます。
ゲームオブジェクトを回転させるために Inspector に新しい値を入力すると、目に見えない内部で、新しいクォータニオン回転値に変換されます。

ex.関係ありそうな記事

https://zenn.dev/k1togami/articles/f2ea13c663cabf
https://note.com/k1togami/n/n42389783cc98
https://note.com/k1togami/n/nadc12cef7700

2021.2.17 noteより転記
2021.2.19 一部修正。言葉遣いの統一など
2022.9.6、2022.10.9 2022.11.24 transform回転にまつわる記述・挙動ついての誤記を修正。

Discussion