1. fullmated

    No comment

    fullmated
Changes in body
Source | HTML | Preview
@@ -1,129 +1,123 @@
この記事は[Unity #3 Advent Calendar 2018 20日目](https://qiita.com/advent-calendar/2018/unity3)の記事です。
こんにちは。sunamoと申します。
今回はジンバルロックに焦点を当てた記事を書きました。
Unityを使っていると回転の話で度々`Quaternion`が出現します。インスペクタを`Debug`モードにすると`rotation`が4変数になりますがそれが`Quaternion`の生の値です。`Quaternion`は、オイラー角表現で発生していたジンバルロックを回避できるというメリットがあります。しかしジンバルロックとは**具体的に**どのような現象なのでしょうか?
今更rotationの話をするのは二番煎じ感はありますが自分の中で把握できていなかったので、それについて調べたものをできる限り分かりやすく記したいと思います。
+ GameObjectの回転が意図通りにならない方
+ ジンバルロックを話では聞いたことあるが、具体的にどんな現象なのか分からない方
+ どのような条件でジンバルロックが発生するか知りたい方
上記向けになればいいなと思います。すでに理解していらっしゃる方は、間違いなどあればご指摘いただけると嬉しいです。
# 簡単に実験 (30秒!)
私が思う最も簡単にジンバルロックを発生させる方法はこの通りです。
1. 適当なGameObject(Cubeなど)を作成
2. インスペクタのTransformコンポーネントにある`rotation`の`x`を`90`に設定
3. 同じく`rotation`の`y,z`を動かす
`y`を変化させても`z`を変化させても同じ軸周りの回転になっていることが分かります。これがジンバルロックの簡単な再現となります。以下のgifアニメーションにその光景を示します。
gifアニメでは分かりやすさのために、Cubeの代わりにFeyeというモデルを動かしております。また、空間に固定された座標軸を細い線、Feyeに固定された座標軸を太い線で示しています。
![ezgif-1-71538e82118e.gif](https://qiita-image-store.s3.amazonaws.com/0/37881/3ce43b16-4650-9af1-6b02-853fa75f68ec.gif)
# もう一つの簡単な実験
次に先ほど作成したGameObjectに以下のコードをアタッチします。`Experiment1()`,`Experiment2()`,`Experiment3()`という3つのメソッドがあり、それぞれ $(x,y,z)=(90,90,0),(90,0,-90),(90,0,90)$度のオイラー角が表す回転を表しています。このメソッドを呼び出すと一体どうなるでしょうか。`Quaternion`を利用しているからジンバルロックは発生しないと思うかもしれません。それぞれの回転がどのような回転なのか想像してから実行してみるといいかもしれません。
```cs
using System.Collections;
using System.Collections.Generic;
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);
}
}
```
結果は次のgifアニメのようになります。
![インスペクタからQE.gif](https://qiita-image-store.s3.amazonaws.com/0/37881/396d04b1-17dd-a00e-d9ba-e719d5da9252.gif)
`Experiment1()`と`Experiment2()`のどちらを実行しても向きは同じで、`Experiment3()`ではそれらとは反対側に向きました。数値が違うのに向きが同じとは驚くべき結果であり、予想と反していた方も少なくないのではと思います。`y`,`z`を変えたときに同じ軸周りの回転をしていたのでジンバルロックが発生していました。`Quaternion`を使っていても関係ありませんでしたね。そもそもこの`Quaternion.Euler`関数はオイラー角表現を`Quaternion`表現に直しただけなので実質的にはオイラー角の特徴が残ると言えそうです。
# 意図していたであろう回転
先ほどは予想だにしない挙動を見せましたが、`Experiment1()`だったら次のように考えた人もいたのではないでしょうか。
*`Experiment1()`は* $(x,y,z)=(90,90,0)$ *が表現する回転なのだから、`x`を`90`度回すとFeyeが前方向に倒れるような回転をして、その次に`y`を`90`度回すのだからFeyeの中心軸回りに`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);
```
# なぜ意図した回転と違ったのか
以下の引用は`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軸`で回転すると思っていたことが間違いでした。このようにオイラー角は回す順序が大事で、回す順序が違うとたとえ数値が同じでも違った方位になります。
このようにオイラー角の定義は人間の直感に反するので`Quaternion.Euler(x,y,z)`を利用する前に、別の回転系の関数が使えないか検討することが望ましいかと思います。さらに例を挙げると`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)できるようです。
#まとめ
+ オイラー角はある方位の変化を一意に定めることができない
+ Unityではオイラー角による回転は、`z軸回転`->`x軸回転`->`y軸回転`の順に計算される
+ `rotation.x`が`-90,90`のときにジンバルロックが発生し、回転の自由度が落ちる
+ `Quaternion.Euler(x,y,z)`で複雑な回転を考える前に、まずは`transform.LookAt()`,`transform.Rotate()`,`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)
-
-# 雑記
-以下のリンクによるとUnityのrotationはyxz規約。z→x→yの順番で回転させている。
-http://naochang.me/?p=53
-
-https://en.wikipedia.org/wiki/Gimbal_lock