Unityのジンバルロックを30秒で体感する

この記事は[Unity #3 Advent Calendar 2018 20日目](https://qiita.com/advent-calendar/2018/unity3)の記事です。

こんにちは。
今回はジンバルロックに焦点を当てた記事を書きました。

[2019/6/16]加筆修正しました。

#はじめに

Unityを使っていると回転の話で度々`Quaternion`が出現します。下記画像の右上にあるメニューから`Debug`モードにすると`rotation`が4変数になりますがそれが`Quaternion`の生の値です。

|通常時のInspectorビュー|Debugモード時のInspectorビュー|
|---|---|
|<img width="278" alt="スクリーンショット 2018-12-22 17.25.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/37881/3b455370-6187-0ea2-a6db-6b611b5eb9a8.png">|<img width="279" alt="スクリーンショット 2018-12-22 17.25.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/37881/2f5d739e-cff4-47cd-4055-1b5b51a28ce3.png">|


`Quaternion`は、オイラー角表現で発生していたジンバルロックを回避できるというメリットがあります。しかしジンバルロックとは**具体的に**どのような現象なのでしょうか？

今更`rotation`の話をするのは二番煎じ感はありますが自分の中で把握できていなかったので、それについて調べたものをできる限り分かりやすく記したいと思います。

+ GameObjectの回転が意図通りにならない方
+ ジンバルロックを話では聞いたことあるが、具体的にどんな現象なのか分からない方
+ どのような条件でジンバルロックが発生するか知りたい方

上記向けになればいいなと思います。すでに理解していらっしゃる方は、間違いなどあればご指摘いただけると嬉しいです。

# 簡単に実験 (30秒!)

私が思う最も簡単にジンバルロックを発生させる方法はこの通りです。

1. 適当なGameObject(Cubeなど)を作成
2. インスペクタのTransformコンポーネントにある`rotation`の`x`を`90`に設定
3. 同じく`rotation`の`y,z`を動かす

`y`を変化させても`z`を変化させても**同じ軸周りの回転**になっていることが分かります。これがジンバルロックの簡単な再現となります。以下のgifアニメーションにその光景を示します。
gifアニメでは分かりやすさのために、Cubeの代わりにFayeというモデルを動かしております。また、空間に固定された座標軸を細い線、Fayeに固定された座標軸を太い線で示しています。

![ezgif-1-71538e82118e.gif](https://qiita-image-store.s3.amazonaws.com/0/37881/3ce43b16-4650-9af1-6b02-853fa75f68ec.gif)

# ジンバルロックとは？
`オイラー角表現`において、`2つの回転軸が同じ向き`になってしまったときに`回転の自由度が1つ落ちてしまう`ことです。

- `オイラー角表現`: Unityの`transform.rotation`と言い換えておけばいいと思います。
- `2つの回転軸が同じ向き`: 先ほどの実験結果で見た「`y`を変化させても`z`を変化させても**同じ軸周りの回転**」となった現象における、y軸とz軸のこと
- `自由度が1つ落ちてしまう`: 3軸ではなく2軸の回転表現に成り下がることです。回転に必要な3つの数値 $(x,y,z)$ のうち１つの変数が意味を失っているとも言えそうです。

もう一つ分かりやすい動画を当記事でも貼り付けさせていただきます。

https://www.youtube.com/watch?v=zc8b2Jo7mno

0:59〜1:14が分かりやすいです。赤いリングが`90`度回転したとき、青いリングと緑色のリングの回転軸は等しくなっています。これがジンバルロックです。

# もう一つの簡単な実験

先ほどの現象をさらに考察するために、先ほど作成したGameObjectに以下のコードをアタッチします。

```cs

using UnityEngine;

public class QuatByEulerAngle : MonoBehaviour {

	[ContextMenu("Experiment")]
	public void Experiment()
	{
		transform.localRotation = Quaternion.Euler(90, 90, 0);
	}

	[ContextMenu("Experiment2")]
	public void Experiment2()
	{
		transform.localRotation = Quaternion.Euler(90, 0, -90);
	}

	[ContextMenu("Experiment3")]
	public void Experiment3()
	{
		transform.localRotation = Quaternion.Euler(90, 0, 90);
	}
}
```

`Experiment()`,`Experiment2()`,`Experiment3()`という3つのメソッドがあり、それぞれ $(x,y,z)=(90,90,0),(90,0,-90),(90,0,90)$度のオイラー角が表す回転を表しています。このメソッドを呼び出すと一体どうなるでしょうか。それぞれ異なる数値なので3つとも異なる位置に回転するに違いないと思うかもしれません。また、回転でなにかと便利な`Quaternion`を利用しているからジンバルロックは発生しないと思うかもしれません。
それぞれの回転がどのような回転なのか想像してから実行してみるといいかもしれませんね。

結果は次のgifアニメのようになります。

![インスペクタからQE.gif](https://qiita-image-store.s3.amazonaws.com/0/37881/396d04b1-17dd-a00e-d9ba-e719d5da9252.gif)

`Experiment()`と`Experiment2()`のどちらを実行しても向きは同じで、`Experiment3()`ではそれらとは反対側に向きました。数値が違うのに向きが同じとは驚くべき結果であり、予想と反した方々も少なくないのではと思います。
`y`,`z`を変えたときに同じ軸周りの回転をしており、回転の自由度が1つ落ちていたので、ジンバルロックが発生していました。`Quaternion`を使っていても関係ありませんでしたね。

# 意図していたであろう回転
先ほどは予想だにしない挙動を見せましたが、`Experiment()`だったら次のように考えた人もいたのではないでしょうか。

*`Experiment()`は* $(x,y,z)=(90,90,0)$ *が表現する回転なのだから、`x`を`90`度回すとFayeが前方向に倒れるような回転をして、その次に`y`を`90`度回すのだからFayeの中心軸周りに`90`度回るだろう。*

しかし結論的には`x,y,z`の順番では回転してくれません。もしそのように回転させたい場合は次のようにする必要があります。

```cs
transform.localRotation *= Quaternion.Euler(90, 0, 0);
transform.localRotation *= Quaternion.Euler(0, 90, 0);
```

こうすると先ほどの意図通りになります。

![ezgif-1-99da0bd69489.gif](https://qiita-image-store.s3.amazonaws.com/0/37881/3db7fb70-7e7d-287c-a31b-7fc8536a47df.gif)

`Experiment2()`と`Experiment3()`も同様に次のようにすると意図通りの回転になります。

```cs
transform.localRotation *= Quaternion.Euler(90, 0, 0);
transform.localRotation *= Quaternion.Euler(0, 0, -90);
```

```cs
transform.localRotation *= Quaternion.Euler(90, 0, 0);
transform.localRotation *= Quaternion.Euler(0, 0, 90);
```

# なぜ意図した回転と違ったのか
先ほどの数値`(x,y,z)`が`x軸`->`y軸`->`z軸`の順番で回転すると思っていたことが間違いだからです。以下の引用は`Quaternion.Euler(x,y,z)`のリファレンスですが、オイラー角`(x,y,z)`が入力されたとき、`z軸回転`→`x軸回転`→`y軸回転`の順番で回転の計算をしていると書かれています。

>Returns a rotation that rotates z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis (in that order).
>https://docs.unity3d.com/jp/460/ScriptReference/Quaternion.Euler.html

このようにオイラー角は回す順序が大事で、回す順序が違うとたとえ数値が同じでも違った方位になります。
本当は`x軸`->`y軸`->`z軸`で回したいのにいちいち`z軸`→`x軸`→`y軸`で回転したい場合の角度を考え直すことはかなり大変ですね。このように、回転順序に`z軸`→`x軸`→`y軸`という縛りが効いているオイラー角の定義は人間の直感に反することがあるので、もし複雑な回転を考えている場合は`Quaternion.Euler(x,y,z)`を利用する前に、`transform.LookAt()`,`Quaternion.AngleAxis()`などといった別の回転系の関数が使えないか検討することが望ましいかと思います。

さらに例を挙げると`Quaternion.Euler(90,45,45)`は`Quaternion.Euler(90,0,0)`と等しいですし、`Quaternion.Euler(80,45,45)`はどこに45度らしさが出ているのかちょっと分かりづらいです。（以下のスクショを見てください）

<img width="1014" alt="スクリーンショット 2018-12-21 00.17.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/37881/230dbf4b-729d-d090-c63a-a2ca1868451d.png">


ただ一つ言えることは、`Quaternion.Euler(90,0,0)`,`Quaternion.Euler(0,45,0)`,`Quaternion.Euler(0,0,30)`などのように、**どれか一つの成分のみが非ゼロである場合は想像通りに回る**ということです。つまりGameObjectに固定された座標系（今までの図の太線の軸）の周りで回転してくれます。


またジンバルロックは、2番目に回す軸周りの角度が`90`度をとるときに発生することが[数学的に証明](https://en.wikipedia.org/wiki/Gimbal_lock#Loss_of_a_degree_of_freedom_with_Euler_angles)できます。

#まとめ

+ 何かしらの物体の向きをAからBに変化させるとき、オイラー角だと数値を一意に定めることができない
+ Unityではオイラー角による回転は、`z軸回転`->`x軸回転`->`y軸回転`の順に計算される
+ ジンバルロックとは2番目に回す角度が`-90,90`度のときに、1番目と3番目に回す軸が同じになってしまう現象。すなわち回転の自由度が1つ落ちる
+ Unityでは`rotation.x`が`-90,90`度のときにジンバルロックが発生し、回転の自由度が落ちる
+ ワールド座標系のある1軸周り、またはローカル座標系のある1軸周りといった回転をしたい場合は`Quaternion.Euler(x,y,z)`が楽
+ 任意の軸周りの回転を考える場合は`transform.LookAt()`,`Quaternion.AngleAxis()`などが使えないか検討するのが吉


#参考
+ 実例で学ぶゲーム3D数学
+ [Unity - スクリプティング API: Quaternion.Euler](https://docs.unity3d.com/jp/460/ScriptReference/Quaternion.Euler.html)
+ [UnityでRotation（Quaternion）をうまく使いたい](http://spi8823.hatenablog.com/entry/2015/05/31/025903)