Quaternionで回転する(ソースコードつき)
HMDは軸がフリーの回転をする
数学を好んでいようとそうでなかろうとHMDを使ってVRをやるには3次元空間が必要である。
そして3次元空間の計算には座標移動と回転がもれなく必要である。
特にVRではどの回転軸も固定されない。
よくある三人称視点のゲームを考えてみるとY軸(プレイヤーからみて上方向)以外の方向で回転することはあまりない。
なぜQuaternionか?
Unreal EngineにはFRotatorというオイラー角でジンバルロックのことをあまり考えずに回転ができる便利な構造体が用意されている。
ただUnityにはそういうものはないのでオイラー角で回転させようとすると色々考える必要がある。*1
UnityであろうとUnreal Engineであろうと内部ではQuaternionを使って回転している。 表示したときに直感的にわかりやすいのはオイラー角であるが、オイラー角だけで回転させようとすると諸々の考慮が必要である。
例えば角度を出したときに値がどういう範囲(0°~360°か?や-180°~180°)で返されるか?であったり、あるオブジェクトを回転させるときに2つの軸で90°回転させた場合はジンバルロックについての考慮が必要である。
しかし、Quaternionを使えばこのような考慮をせずとも回転させることができる。 きみもQuaternionを使って雑に回転させよう!
※Quaternionについての数学的な細かい解説はしません。
※数学を専門的に学んだわけじゃないので数学的に間違ってたりしたらごめんね。
Quaternionって何?
話をかんたんにするために平面で考えてみよう。
例えば 画像のy軸と原点と点Bを通る直線がなす角Qと 画像のx軸と原点と点Aを通る直線がなす角Rとの差分を求めたいときを考える。 x軸y軸は直交する。
オイラー角の場合
オイラー角の場合はY軸とX軸は直行するので
∠D = 90° - (90°-∠Q) - (90°-∠R)
= ∠Q + ∠R - 90°
となる。
Quaternionの場合
さて、Quaternionで求めてみる。
Quaternionの値には回転の角度ではなくどちら回りに回転させるかの向きも表している。(重要)
向きがあるってどういうことだ?
∠Dの角度を出したいときはQ と R の掛け算になる。なぜ掛け算になるかは数学の話になるので割愛する。 Q, R, DはQuaternionで∠Q, ∠R, ∠Dの角度と向きを表すとする。
ただし Quaternionには向きが存在するために掛け算には順番が存在する (掛け算の交換法則が成り立たない)。 つまりQ * R と R * Qの値が異なる。
Q * RはDだが
R * QはD'になる。
掛け算をする操作が矢印の方向をたどってみることに対応する。
最終的にどちらを向いているかを考えるとよい。
あるいは引き算で引かれる数と引く数を逆にすると絶対値は同じだが符号が逆になってしまうことに近いかもしれない。
実際にUnityで計算してみる - Quatenionで他のオブジェクトに回転を複製してみる
一番左のCubeが回転の複製元である。複製元には回転軸とは別の軸で回転を入れておく。
Z軸を中心に回転させる。
ほかの複製先のCube(RotateObject)は補間の係数を変えて複製元(SourceObject)から回転させている。 複製先にもそれぞれ回転軸とは別の軸で回転が入っている。
複製元を回転させるソースコード
クリックすると展開されます
public class RotationTest_Source : MonoBehaviour { [SerializeField] protected GameObject RotateObject; [SerializeField] protected float RotateDegreeX = 0.0f; [SerializeField] protected float RotateDegreeY = 0.0f; [SerializeField] protected float RotateDegreeZ = 0.0f; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { if(RotateObject==null) { return; } Vector3 rotateEular = new Vector3 ( RotateDegreeX ,RotateDegreeY , RotateDegreeZ ); RotateObject.transform.Rotate(rotateEular,Space.Self); } }
回転を複製するソースコード
//Rotate source [SerializeField] public GameObject SourceObject; [SerializeField] public GameObject RotateObject; [SerializeField] protected float RotationRate = 1.0f; Quaternion PrevFrameSourceRotation = Quaternion.identity; // Start is called before the first frame update void Start() { PrevFrameSourceRotation = SourceObject.transform.rotation; } // Update is called once per frame void Update() { if(SourceObject == null || RotateObject == null) { return; } Quaternion currentSrcRotation = SourceObject.transform.rotation; Quaternion deltaSrcRotation = Quaternion.Inverse(PrevFrameSourceRotation) * currentSrcRotation ; Quaternion addRotation = Quaternion.Lerp(Quaternion.identity, deltaSrcRotation ,RotationRate); RotateObject.transform.rotation *= addRotation; PrevFrameSourceRotation = currentSrcRotation; }
1つ前のフレームの回転と現在の回転の差分をとっているところが
Quaternion deltaSrcRotation = Quaternion.Inverse(PrevFrameSourceRotation) * currentSrcRotation ;
にあたる。 Rの向きが上記の図でQの逆を向いていたがこれにはQuaternion.Inverse(PrevFrameSourceRotation)が対応する。 この差分を割合でUpdateで現在の回転に加えている。
RotateObject.transform.rotation *= addRotation;
で現在の回転に掛けることが回転を加えることを表す。
かける順番が正しいケース
Quaternion deltaSrcRotation = Quaternion.Inverse(PrevFrameSourceRotation) * currentSrcRotation ;
複製元(SourceObject)と同じように回転する。
かける順番が正しくないケース
Quaternion deltaSrcRotation = currentSrcRotation * Quaternion.Inverse(PrevFrameSourceRotation) ;
最初と最後はピッタリ合う。ただし途中の軌跡が異なる。 複製元と複製先に入っている回転と、回転なしの状態(つまりX軸, Y軸, Z軸の向き)の中間地点あたりの方向を軸にした回転となっている。
*1:LookAt関数みたいな用途がハッキリしているものはある。