Unityでクォータニオン(Quaternion)を扱うための基礎知識その2

前の記事を見てない方はこちらを

www.hildsoft.com

以下は断りの無い限り回転角は度数法を使用し、単位は省略します。

Unityのクォータニオン

その1で、UnityでのGameObjectのクォータニオンを取る方法を説明し、加工するメソッドをざっと列挙しました。

大抵はこのメソッドを使ってやりたいことはできると思うのですが、もちろん例外も出てきます。

ただ単にメソッドを使うだけではなく、クォータニオンの使うための基本的な考え方ができるようになることを目指しています。

クォータニオンって何をするもの?

f:id:hildsoft:20170615213830p:plain

Unityではクォータニオンを回転処理に使っています。

クォータニオンは回転操作であり、回転の状態を示すものです。

回転の状態は、基準状態から回転させたものと考えることができるので、実質は回転操作とだけ考えれば大丈夫です。

その回転方法ですが、空間上のある軸(x,y,zの3次元ベクトル)の周りを、ある角度(θ)を回転させます。

上記の図はZ軸を回転軸として回転させています。

クォータニオンを構成するもの

クォータニオンは、4つの変数(x,y,z,w)を持っています。

注意をしないといけないものが、クォータニオンのx,y,zが必ずしも回転軸を表すわけではないということです。 (θ=0が例外で、この状態をUnityのQuaternionは、Quaternion.identityとして持っています)

コンピューターグラフィックスで用いるクォータニオンは、 { \displaystyle
\vec{v} = (v_x, v_y, v_z)
} を軸に、 { \displaystyle
\theta
} 回転したものを

{ \displaystyle
x = v_x \sin\left(\frac{\theta}{2}\right) \
y = v_y \sin\left(\frac{\theta}{2}\right) \
z = v_z \sin\left(\frac{\theta}{2}\right) \
w = \cos\left(\frac{\theta}{2}\right)
}

となっていますが、覚える必要はありません。

直接クォータニオンの中の値を操作することは無いでしょう。(inverseくらいはできますが、ちゃんとメソッドが用意されています)

オイラー角との関係

オイラー角での回転は、x,y,z軸にそれぞれ指定の度数を回して回転を表現します。

クォータニオンでの回転は、上で書いた通り、一つ軸を決めて指定の度数を回して回転を表現します。

Unityには、Quaternion.Euler(x,y,z)というメソッドがあります。

x軸方向の回転、y軸方向の回転、z軸方向の回転を意味します。(回転の実行順は少し違いますが・・・)

このメソッドを使うことで、その回転をクォータニオンに変換してくれます。

逆に、QuaternionのインスタンスはeulerAnglesフィールドを持っているので、これでオイラー角への変換が可能です。

回転方向

クォータニオンは軸を決めるので、その軸方向から見たら回転は平面で考えることができます。

f:id:hildsoft:20170615221347p:plain

図を見てもらえるとわかると思いますが、同じ回転をさせたい場合にθとθ-360させたものは同じ結果になります。 同様にθ+360でも結果は一致します。

eulerAnglesで返される値も、0以上、360未満となります。

クォータニオンとオイラー角の回転は1:1対応じゃない

Quaternion.Euler(0f, 0f, 45f).eulerAngles
Quaternion.Euler(0f, 0f, 405f).eulerAngles
どちらも
Vector3(0f, 0f, 45f)
が返ってきます。

しかし、内部的には
Quaternion.Euler(0f, 0f, 45f) = (0.0, 0.0, 0.4, 0.9)
Quaternion.Euler(0f, 0f, 405f) = (0.0, 0.0, -0.4, -0.9)
と異なっています。

計算すると同じ回転になるので、処理上は問題ないのですが、このようなことがあることを知っておかないとデバッグするときにハマる可能性があります。

クォータニオンのもう一つの弱点

クォータニオンで表せる回転角は-180~180なんですね。

Slerpで2つのクォータニオンの間を取ろうとすると、小さい方の角度が与えられます。

f:id:hildsoft:20170615223549p:plain

大きい方の角度を使いたい場合は内積(Dot)を使うことによって判別できます。

この辺はサンプルを見た方が分かりやすいと思いますので、このソースをコピーして空のGameObjectに追加して試してみてください。

2017/06/19 パラメータを変更したときに発生するバグがあったため若干ソースコードを修正しました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class QuaternionTest : MonoBehaviour {

    // GIZMOS球サイズ
    const float gizmosArmLength = 3f;

    // 回転角
    float rotate=0;

    // 回転速度
    float rotateSpeed = 45f;

    // 回転軸
    Vector3 axis = new Vector3(0f, 0f, 1f).normalized;
    // 初期位置
    Vector3 basePosition = new Vector3(1f, 0f, 0f).normalized * gizmosArmLength;

    // 位置
    Vector3 initialPosition;
    Vector3 targetPosition;
    Vector3 slerpPosition;

    // 内積
    Vector3 dotPosition = Vector3.zero;
    float dot=0;

    void Update () {

        // 初期回転位置
        var baseQuaternion = Quaternion.identity;
        initialPosition = baseQuaternion * basePosition;

        // 回転
        rotate += rotateSpeed * Time.deltaTime;

        // 目標位置
        var targetQuaternion = baseQuaternion * Quaternion.AngleAxis(rotate, axis);
        targetPosition = targetQuaternion * basePosition;

        // 内積
        dot = Quaternion.Dot(baseQuaternion, targetQuaternion); // Dotはパラメータの前後関係なし
        dotPosition = axis * dot * gizmosArmLength;

        // baseとtargetの中間角度回転
        Quaternion slerpQuaternion = Quaternion.Slerp(baseQuaternion, targetQuaternion, 0.5f); // Slerpはパラメータの前後関係なし

        // 180度を越えた角度を考慮するか
        if (dot < 0)
        {
            slerpQuaternion = Quaternion.AngleAxis(180f, axis) * slerpQuaternion;
        }

        // 中間位置
        slerpPosition = slerpQuaternion * basePosition;

    }

    void OnDrawGizmos()
    {
        // ローカル座標に移動
        Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);

        // 初期位置
        Gizmos.color = Color.magenta;
        Gizmos.DrawSphere(initialPosition, 0.3f);
        Gizmos.DrawLine(Vector3.zero, initialPosition);


        // ターゲット
        Gizmos.color = Color.yellow;
        Gizmos.DrawLine(Vector3.zero, targetPosition);
        Gizmos.DrawSphere(targetPosition, 0.3f);

        // 中間
        Gizmos.color = Color.cyan;
        Gizmos.DrawLine(Vector3.zero, slerpPosition);
        Gizmos.DrawSphere(slerpPosition, 0.3f);

        // 回転球面
        Gizmos.color = Color.gray;
        Gizmos.DrawWireSphere(Vector3.zero, gizmosArmLength);

        // 内積
        if (dot < 0) Gizmos.color = Color.black;
        else Gizmos.color = Color.white;
        Gizmos.DrawLine(Vector3.zero, dotPosition);
        Gizmos.DrawSphere(dotPosition, 0.15f);

        // ローカル軸
        Gizmos.color = Color.red;
        Gizmos.DrawLine(Vector3.zero, Vector3.right * 2);
        Gizmos.color = Color.blue;
        Gizmos.DrawLine(Vector3.zero, Vector3.forward * 2);
        Gizmos.color = Color.green;
        Gizmos.DrawLine(Vector3.zero, Vector3.up * 2);

    }

}