エフアンダーバー

個人開発の記録

【Unity】 Quaternionの使い方

Quaternion(クォータニオン、四元数)はUnityでオブジェクトを回転させる際に必要となる数です。 Quaternionの理論自体は非常に数学的で難しいのですが、Unity上で使うだけであればいくつかの特徴を覚えるだけで比較的容易に利用できます。

この記事では、Quaternionの使い方や注意点などをまとめます。 Quaternionの理論について知りたい場合には過去記事や他所の記事を参照してください。

www.f-sp.com

また、UnityのQuaternion APIについて詳しく知りたい場合には前回記事を参照してください。

www.f-sp.com

Quaternionとは

Quaternionは数学的には複素数の拡張に当たります。 しかし、ゲームプログラミングにおいてそんなことを意識することは基本的にありません。 多くのプログラマにとって、Quaternionは回転を表す魔法の数で、その使い方だけを覚えるのが普通です。 本記事でも、イメージを掴むことと使い方の理解を目的として説明していきます。

ということで、改めてQuaternionとは何かというと、回転の向きと大きさを表す数くらいに考えるのがいいと思います。 Unityやその他3DCGのエディタを使っていると各座標軸周りの三軸回転をイメージしがちですが、Quaternionの基本は任意軸回転です。 好きな方向に一つの軸をとり、その周りの回転を考えます。

UnityでQuaternionというと、TransformrotationプロパティやlocalRotationプロパティが有名だと思います。 これらはモデル等の現在の姿勢(どんな体勢でどこを向いているか)を表すものです。 Unityにおいてモデルは初期状態でY軸方向を頭上に、Z軸方向を向いていると決まっているため、その初期姿勢からどれくらい回転したかという情報はそのまま現在の姿勢を表します。 つまり、Quaternionは初期姿勢さえ決まれば、回転という操作によって姿勢を表現することもできるのです。

予備知識:右手系と左手系

Quaternionによる回転の話に入る前にちょっとだけ数学の話をします。 右手系と左手系についてです。 難しい話ではありませんが若干長いので、既に知っているという方は飛ばしてください。

三次元の座標系には右手系と左手系という二つの座標系のとり方が存在します。 簡単に言うと、X軸とY軸に対して、Z軸をどの向きに取るかという話です。 X軸とY軸に垂直となるようにZ軸をとるとすると、Z軸の候補には正反対の二つの向きが考えられます*1。 そのうちの片方を採用すると右手系に、もう片方を採用すると左手系になります。

右手系と左手系を見分けるには自分の手を使う方法が有名です。 まずどちらの手でもいいので拳を握ります。 その状態から親指を立て、人差し指を伸ばしてピストルの形を作ります。 さらに中指を親指と人差し指の向きに対して垂直な方向に立てればポーズが完成です。

この形のまま親指をX軸、人差し指をY軸の方向にそれぞれ向けたとき、中指がZ軸の方向を向いていれば、 使った方の手の座標系になります(右手なら右手系、左手なら左手系)。 中指が逆方向を向いていた場合には使っていない方の手の座標系です。

数学では一般に右手系を使いますが、Unityは左手系を使います。 Unityを含む3DCGの分野で左手系を使うことがあるのは、 カメラの視界に対して、右方向にX軸、上方向にY軸、奥方向にZ軸をとるのが直感的だからです。 その代わり、数学の知識をそのまま適用すると、方向が逆だったりします(外積の向きなど)。

f:id:fspace:20170830152909p:plain
左手座標系かどうかのチェック

で、何故こんな話をしたかというと、軸に対する回転の方向が右手系と左手系で逆になるからです。 軸の向きを向いたとき、右手系では時計回りを、左手系では反時計回りを正方向とすると決まっています。

これもまともに覚えると間違えやすいので手を使った覚え方があります。 右手系なら右手を、左手系なら左手を使います。 拳を握った状態から親指を立てて、その親指を軸の向きに向けます。 このとき、親指に対してその他の指が向いている方向が回転方向です。

Unityで回転操作を考えるときは左手をくるくる回して回転方向を確かめながらやるといいです。 傍から見ると変な人ですが、くまのぬいぐるみに話しかけるよりはだいぶハードルは低いはず *2

f:id:fspace:20170830153129p:plain
左手座標系における回転方向

Quaternionによる回転手順

Quaternionによる回転は作成・変換・適用の大きく3つのステップに分けられます。 もちろん変換せずにそのまま使うこともあるため、必ずしも全てのステップを踏む必要はありません。

以降、それぞれの手順について解説していきます。

//---------------------------------------
// Y軸周りを秒速45度で回転させるサンプル
//---------------------------------------

// Y軸周りを回転するQuaternionを作成
Quaternion rotY = Quaternion.AngleAxis(45.0f * Time.deltaTime, Vector3.up);

// 現在の回転値と合成
Quaternion newRotation = rotY * transform.rotation;

// 新しい回転値を設定
transform.rotation = newRotation;

Quaternionの作成

Quaternionを使うにはまず使用したい回転を表すQuaternionを作成する必要があります。

Vector3等ではnew Vector3(...)のように書くのが普通ですが、Quaternionではnew Quaternion(...)と書くことは滅多にありません。 代わりに次の4つの関数を主に利用します。

  • AngleAxis
  • Euler
  • LookRotation
  • FromToRotation

これらの関数はどれも結果(戻り値)としてQuaternionを返します。 それをQuaternion型の変数に代入するのが最初のステップになります。

Quaternion rot = Quaternion.AngleAxis(30.0f, Vector3.forward);

また、identityプロパティにより無回転を表すQuaternionの取得もできます。

Quaternion rot = Quaternion.identity;

AngleAxis

AngleAxisはどの向きの軸でどれくらいの角度回転させるかを指定してQuaternionを作成します。 これはQuaternionの作成方法としては最も基本的な方法です。

Quaternion.AngleAxis(回転角度, 回転軸の向き)

AngleAxisの第一引数は回転角度です。 角度は度数法(度)で指定します。 弧度法(ラジアン)を使いたい場合にはMathf.Rad2Degを掛けて度数法へと変換します。

AngleAxisの第二引数は回転軸の向きです。 Quaternionによる回転では必ず回転軸は原点を通るため、指定した向きで原点を通る直線が回転軸になります。

f:id:fspace:20170830153302g:plain
AngleAxis: 指定した軸周りの回転

using UnityEngine;

//----------------------------------------------------
// 左右キーでY軸周りを秒速90度で回転させるサンプル
//----------------------------------------------------
public class AngleAxisSample : MonoBehaviour
{
    private void Update()
    {
        // 左右キー入力に応じて回転速度を決定
        float speed = 0.0f;
        if (Input.GetKey(KeyCode.LeftArrow)) { speed += -90.0f; }
        if (Input.GetKey(KeyCode.RightArrow)) { speed += 90.0f; }

        // Y軸(Vector3.up)周りを1フレーム分の角度だけ回転させるQuaternionを作成
        Quaternion rot = Quaternion.AngleAxis(speed * Time.deltaTime, Vector3.up);

        // 元の回転値と合成して上書き
        transform.localRotation = rot * transform.localRotation;
    }
}

Euler

EulerはエディタのようにX軸、Y軸、Z軸それぞれの周りをどれくらいの角度回転させるかを指定してQuaternionを作成します。

Quaternion.Euler(X軸周りの回転角度, Y軸周りの回転角度, Z軸周りの回転角度)

指定順はX軸、Y軸、Z軸ですが、回転順はZ軸、X軸、Y軸の順番です。 この回転順だと、X軸が上下方向の回転、Y軸が水平方向の回転、Z軸が側転のような回転に対応します *3。 角度はAngleAxisと同様に度数法で指定します。

余談ですが、回転を各座標軸における回転角度で表す方法を考案者の名前から「オイラー角 (Euler angles)」と呼びます。 余りに多くの業績を残したためにオイラーと名の付く数学用語は山ほどありますがそのうちのひとつです。

f:id:fspace:20170830153500g:plain
Euler: Z軸、X軸、Y軸の順番で回転

using UnityEngine;

//----------------------------------------------------
// 二軸入力でモデルの向きを変えるサンプル
//----------------------------------------------------
public class EulerSample : MonoBehaviour
{
    private void Update()
    {
        // 水平方向と垂直方向の軸入力を取得
        float inputH = Input.GetAxis("Horizontal");
        float inputV = Input.GetAxis("Vertical");

        // 軸入力に応じて回転角度を計算
        float angleH = inputH * 45.0f;
        float angleV = inputV * -30.0f;

        // 上下方向(X軸)と水平方向(Y軸)の回転角度を指定して回転を作成して適用
        transform.localRotation = Quaternion.Euler(angleV, angleH, 0.0f);
    }
}

LookRotation

LookRotationはLookという名前の通り、見たい方向を向かせるためのQuaternionを作成します。

Quaternion.LookRotation(正面方向, 頭上方向<省略可>)

LookRotationの第一引数は正面方向です。 回転させる物体の正面を向けたい方向を指定します。

LookRotationの第二引数は頭上方向ですが、天地をひっくり返したいというような用途でもなければ省略してしまって問題ないと思います。 デフォルト値としてY軸方向、つまり空に向かう方向が使用されます。

LookRotationは回転対象が初期状態でY軸方向に頭上、Z軸方向に正面を向けているという仮定のもとで動作します。 この仮定が成り立たない状態では指定した通りに回転しません。 Quaternionが姿勢を表現できるといってもその本質は回転量なので、初期状態でどこを向いているかが決まっていなければ、 どんな回転によって目的の姿勢になれるのかがわからないのです。

f:id:fspace:20170830153721g:plain
LookRotation: 正面と頭上の向きに合わせる回転

using UnityEngine;

//----------------------------------------------------
// モデルに常にカメラの方を向かせるサンプル
//----------------------------------------------------
public class LookRotationSample : MonoBehaviour
{
    private void Update()
    {
        // メインカメラの取得
        Camera camera = Camera.main;
        if (camera) // カメラ有効時のみ
        {
            // カメラに向かう方向を計算
            Vector3 forward = camera.transform.position - transform.position;
            if (forward != Vector3.zero) // 零ベクトルでない
            {
                // カメラの向きを正面とする回転を作成して適用
                transform.rotation = Quaternion.LookRotation(forward);
            }
        }
    }
}

FromToRotation

FromToRotationはある方向から(from)ある方向へ(to)と回転させるQuaternionを作成します。

Quaternion.FromToRotation(開始方向, 終了方向)

FromToRotationの引数は開始方向と終了方向です。 結果のQuaternionで開始方向を回転させると終了方向を向きます。

注意すべきは開始方向を終了方向へと向ける回転というのは無数に存在し、 FromToRotationの結果はそのうちのひとつにすぎないということです。 つまり、想像していた回転とは違う回転が返ってくる可能性があります。 キャラクター等のモデルを回転させる場合には、大抵LookRotationを利用した方が想像通りに回転します。

f:id:fspace:20170830153948g:plain
FromToRotation: 開始方向を終了方向へ向ける最小の回転

using UnityEngine;

//----------------------------------------------------
// カメラの向きに応じてモデルを回転させるサンプル
//----------------------------------------------------
public class FromToRotationSample : MonoBehaviour
{
    // 前フレームのマウス位置
    private Vector2 prevPosition;

    private void Update()
    {
        // メインカメラの取得
        Camera camera = Camera.main;
        if (camera) // カメラ有効時のみ
        {
            if (Input.GetMouseButton(0)) // 左マウスボタン押下中
            {
                // 現フレームのマウス位置
                Vector2 currentPosition = Input.mousePosition;
                if (!Input.GetMouseButtonDown(0)) // 左マウスボタン押下直後でない
                {
                    // モデルのスクリーン上の位置を取得
                    Vector2 center = camera.WorldToScreenPoint(transform.position);

                    // 前フレームと現フレームのモデル周りのマウスの動きを計算
                    Vector2 from = prevPosition - center;
                    Vector2 to = currentPosition - center;
                    if (from != Vector2.zero && to != Vector2.zero) // 零ベクトルでない
                    {
                        // マウスの移動角度分の回転を取得(2DなのでZ軸周りの回転)
                        Quaternion mouseRot = Quaternion.FromToRotation(from, to);

                        // カメラ視点の向きに切り替えてから回転差分を適用して元に戻す回転を計算
                        Quaternion cameraRot = camera.transform.rotation;
                        Quaternion deltaRot = cameraRot * mouseRot * Quaternion.Inverse(cameraRot);

                        // 元の回転値にマウスによる回転を合成して上書き
                        transform.rotation = deltaRot * transform.rotation;
                    }
                }

                // 現フレームのマウス位置を次フレームのために保存
                this.prevPosition = currentPosition;
            }
        }
    }
}

Quaternionの変換

目的の回転が前述の単純な作成方法で得られない場合には、複数のQuaternionを合成・補間等することによって目的のQuaternionを得ます。

Quaternionの合成は2つのQuaternionをくっつけて1つのQuaternionにしてしまう操作です。 本来なら2つのQuaternionを順番に適用するところを、合成結果を利用することで1つのQuaternionで同じ操作ができてしまいます。 Quaternionの合成には*演算子を使用します。

Quaternionの補間は2つのQuaternionから中間的な回転を表すQuaternionを得る操作です。 例えば、ある向きを向かせたい場合に、現在の向きから目的の向きへと変化する途中の向きを表示することで滑らかに変化するように見せたいときなどに使います。 Quaternionの補間には主にSlerp関数を使用します。

合成・補間以外の変換操作としては逆向きの回転を取得するInverse関数があります。

*演算子(合成)

*演算子を二つのQuaternionに対して適用するとQuaternionの合成ができます。

(後に回転させるQuaternion) * (先に回転させるQuaternion)

Quaternionを合成すると、二段階の回転を同等な一段階の回転へと変換することができます。

*演算子の使用に際して注意すべきなのは、左右どちらにどちらのQuaternionを書くかです。 二段階の回転ではどちらの回転を先に適用するかで回転結果が変わるため、合成結果も二種類存在します。 Unityでは右側に書いたQuaternionの回転を先に適用することになっています。

合成と代入を同時に行う*=演算子もありますが、これはどちらが先に適用されるのか分かりづらくなるためおすすめしません。

f:id:fspace:20170830154143g:plain
合成により二段階の回転を一段階の回転へ

f:id:fspace:20170830154208g:plain
回転順が変わると結果も変わる

using UnityEngine;

//----------------------------------------------------
// マウスでモデルを回転させて鑑賞するサンプル
//----------------------------------------------------
public class Multiply1Sample : MonoBehaviour
{
    // 前フレームのマウス位置
    private Vector2 prevPosition;

    private void Update()
    {
        // メインカメラの取得
        Camera camera = Camera.main;
        if (camera) // カメラ有効時のみ
        {
            if (Input.GetMouseButton(0)) // 左マウスボタン押下中
            {
                // 現フレームのマウス位置
                Vector2 currentPosition = Input.mousePosition;
                if (!Input.GetMouseButtonDown(0)) // 左マウスボタン押下直後でない
                {
                    // マウスの上下左右の移動量から回転角度を決定
                    Vector2 delta = currentPosition - prevPosition;
                    float angleH = delta.x * -30.0f * Time.deltaTime;
                    float angleV = delta.y * 30.0f * Time.deltaTime;

                    // カメラの向きに応じて水平方向と上下方向の回転を作成して合成
                    Quaternion rotH = Quaternion.AngleAxis(angleH, camera.transform.up);
                    Quaternion rotV = Quaternion.AngleAxis(angleV, camera.transform.right);
                    Quaternion deltaRot = rotH * rotV;

                    // 元の回転値に追加の回転を合成して上書き
                    transform.localRotation = deltaRot * transform.localRotation;
                }

                // 現フレームのマウス位置を次フレームのために保存
                this.prevPosition = currentPosition;
            }
        }
    }
}

Slerp

Slerpは二つのQuaternionの補間をします。

Quaternion.Slerp(Quaternion1, Quaternion2, 影響度)

補間は二つのQuaternionの中間的なQuaternionを得る操作です。

Slerpは第一引数、第二引数で基となる二つのQuaternionを指定して、 第三引数でどちらをどの程度強く影響させるかを指定します。 影響度は0から1の間の数値で、0に近いほど先に指定したQuaternionに、1に近いほど後に指定したQuaternionに近い回転になります。

f:id:fspace:20170830154504g:plain
Slerp: 補間によるアニメーション

using UnityEngine;

//----------------------------------------------------
// マウスクリックで回転アニメーションさせるサンプル
//----------------------------------------------------
public class SlerpSample : MonoBehaviour
{
    // 現在の補間影響度
    private float Position;

    private void Update()
    {
        // 補間基の二つの回転
        Quaternion rotA = Quaternion.AngleAxis(-45.0f, Vector3.up);
        Quaternion rotB = Quaternion.AngleAxis(45.0f, Vector3.up);

        if (Input.GetMouseButton(0)) // 左マウスボタン押下中
        {
            // Bの影響が大きくなるように変化
            this.Position = Mathf.Clamp01(Position + Time.deltaTime);
        }
        else
        {
            // Aの影響が大きくなるように変化
            this.Position = Mathf.Clamp01(Position - Time.deltaTime);
        }

        // 補間結果の回転を設定
        transform.localRotation = Quaternion.Slerp(rotA, rotB, Position);
    }
}

Quaternionの適用

目的の回転が得られたら最後にそのQuaternionを適用します。

回転というとモデルが回るのをイメージしてしまいますが、Quaternionが実際に回転させる対象は点です。 モデルを構成する頂点すべてが同じように回転することでモデルが回っているように見えるのです。 Quaternionの適用には*演算子を使用します。

とはいえ実際には*演算子によって点を回転させるよりも、 Transformコンポーネントのrotationプロパティのように、 Quaternionの設定だけして適用はUnityに任せてしまうことが多いです。

*演算子(適用)

*演算子をQuaternionとVector3に対して適用すると回転の適用ができます。

(回転を表すQuaternion) * (回転させたい点)

Quaternionによる回転を適用すると、回転結果の点(の座標)が返ってきます。

適用時の*演算子の注意点として、Quaternionは必ず左側に書く必要があります。 また、*=演算子は利用できません。

using UnityEngine;

//----------------------------------------------------
// カメラの向きに応じて水平に移動させるサンプル
//----------------------------------------------------
public class Multiply2Sample : MonoBehaviour
{
    private void Update()
    {
        // メインカメラの取得
        Camera camera = Camera.main;
        if (camera) // カメラ有効時のみ
        {
            // 水平方向と垂直方向の軸入力を取得
            float inputH = Input.GetAxis("Horizontal");
            float inputV = Input.GetAxis("Vertical");

            // カメラの向きに合わせて移動方向を回転
            Vector3 viewDir = new Vector3(inputH, 0.0f, inputV);
            Vector3 worldDir = camera.transform.rotation * viewDir;

            // 水平移動になるように移動方向を修正
            Vector3 finalDir = Vector3.Normalize(new Vector3(worldDir.x, 0.0f, worldDir.z));

            // 軸入力の大きさに従ってフレームあたりの移動距離を計算
            float magnitude = Mathf.Sqrt(inputH * inputH + inputV * inputV);
            float speed = Mathf.Min(magnitude, 1.0f) * 3.0f;
            float distance = speed * Time.deltaTime;

            // 計算した方向と距離に従って平行移動
            transform.position += finalDir * distance;
        }
    }
}

おまけ

作ったものの説明に使いづらくてボツった画像。

f:id:fspace:20170830154740p:plain
顔の横に当てればなんとなくそれっぽいかなのポーズ

f:id:fspace:20170830154801p:plain
どことなく宇宙兄弟を思い出すポーズ

ユニティちゃんライセンス

ユニティちゃんライセンス

この作品はユニティちゃんライセンス条項の元に提供されています

おわりに

いろいろと作っていたら随分と投稿が遅くなってしまった・・・。

「初心者用…初心者用…」と思いながら書いてはみたもののやはり難しいですね、主に書くべきものと書くべきでないものの選別が。 ある程度理解している相手を想定するなら、自分に必要な情報だけを拾ってくれると信じて、思いついたことをすべて書いてしまうのですが、 下地がない相手にそれをしてしまうととっつきにくい文章になりがちです。 ただ、どうしても書かなくていい情報の判断に迷ってしまうんですよね・・・いずれは必要になる情報なので。

サンプルコードは前回のAPIの記事が仕様確認のためのシンプルなコードだったので、今回は実用的なコードにしました。 個人的にはシンプルな方が読みやすくて好みなのですが、サンプルコードを弄って使い方を覚えるという人もいるみたいなので。 この記事のコードが読みづらいという方は前回記事を参照してください。



執筆時のUnityのバージョン:2017.1.0p4

*1:簡単のため直交座標系を仮定しています

*2:元ネタがわからない人はググってみてください

*3:ただし、Unityに対応したモデルに限る