はじめに
「Quaternion 入門 #02」の今回は、オイラー角について。
前回の記事は以下。
[JavaScript] Quaternion 入門 #01:理解しなくていい回転の扱い方
JavaScript/Three.jsでQuaternionを使う入門記事。オイラー角→Quaternion変換、回転の合成(multiply)、補間(slerp)だけに集中し、ジンバルロックや補間の破綻を避ける実践手順をまとめる。
https://humanxai.info/posts/javascript-quaternion-01-intro/ほぼ自分用の学習記事ですが、全6回を予定してます。
1. オイラー角は「3回回す命令」だから順序が命
オイラー角は (x, y, z) の3つの数字を持っているから、
まるで「3つの角度」みたいに見える。
でも実体は、
X軸で回す → Y軸で回す → Z軸で回す
という 3回連続で回転させる命令セット。
そして、この「どの順番で回すか」がヤバい。
同じ (x, y, z) でも、
- XYZ
- ZYX
- YXZ
…順番が違うだけで、まったく別の姿勢になる。
ここが オイラー角の最大の地雷。
Three.jsの最小デモ(順序だけで姿勢が変わる)
import * as THREE from "three";
const e1 = new THREE.Euler(0.3, 0.7, 1.0, "XYZ");
const e2 = new THREE.Euler(0.3, 0.7, 1.0, "ZYX");
const v = new THREE.Vector3(1, 0, 0);
const v1 = v.clone().applyEuler(e1);
const v2 = v.clone().applyEuler(e2);
console.log("XYZ:", v1.toArray());
console.log("ZYX:", v2.toArray());
ここで大事なのは、
- “XYZ” が正しい
- “ZYX” が正しい
そういう話ではまったくない。
言いたいのはただひとつ。
“同じ値でも、順番が違えば姿勢が変わる”
それがオイラー角の本質。
2. 「回転順序」って結局なに
ここが一番読者がつまずく場所。
「角度が3つあるのに、なんで順番なんてあるの?」 「なんで“Zから回す”とか言い出すの?」
この疑問は完全に正しい。 ただ、答えは激しくシンプルだ。
3D回転は「一回回した後の軸」が変わる
たとえば、最初は
- X軸
- Y軸
- Z軸
がきれいに直交して存在する。
でも、X軸で一回回すと、Y軸とZ軸の方向は変わる。
X回転(1回目)
↓
Y軸とZ軸の向きがもう最初とは違う
2回目・3回目の回転は、もはや“元の軸”では回ってない
読者がよく誤解するのが、
pitch → yaw → roll この3つの回転は、それぞれ独立している
という認識。
実際は違う。
1回目の回転で軸がひしゃげるので、 2回目・3回目の回転は 「ねじれた新しい軸」で回される。
つまり、
- 順序が違う
- 回す軸が違う
- 結果が違う
という、ごく当たり前の話に落ちる。
角度は「状態」ではなく「命令」
オイラー角 (x, y, z) を「姿勢そのもの」だと思うと必ず破綻する。
実際は、
Xで回せ → 次はYで回せ → 最後にZで回せ (順序も込みの命令セット)
にすぎない。
だから、同じ (0.3, 0.7, 1.0) でも、
順序が違うだけで全然違う角度になる。
3. ジンバルロックは“事故”じゃなくて「自由度が潰れる現象」
ジンバルロックという言葉は、 どうしても「不具合」「バグ」「想定外の事故」みたいに聞こえる。
でも実際に起きていることは、驚くほど単純。
- もともと 3軸(X, Y, Z)で姿勢を表現していた
- ある姿勢になると 2つの軸が同じ向きを向く
- 結果として 実質2軸しか残らなくなる
- つまり 自由度が1つ消える
これだけ。
起きるべくして起きる現象であって、 偶然でもミスでもない。
なぜ「自由度が潰れる」のか
オイラー角は「回転を順番に適用する仕組み」だった。
その結果、
- 特定の角度(典型的には ±90度付近)で
- 回転軸が重なり
- 「別の回転」のはずだった操作が、同じ方向の回転になる
こうなると、
- yaw を回しても
- roll を回しても
見た目がほぼ同じ回転になる。
これは「入力が効いていない」のではなく、 効く自由度が物理的に存在しなくなっている。
Three.jsで「自由度が潰れる」雰囲気を見るデモ
FPSカメラなどでよく使われる "YXZ" 順序を例にする。
import * as THREE from "three";
const e = new THREE.Euler(0, 0, 0, "YXZ"); // FPSでよく使う順序
const obj = new THREE.Object3D();
function setAngles(yaw, pitch, roll) {
e.set(pitch, yaw, roll); // order = YXZ の意味で適用される
obj.rotation.copy(e);
}
setAngles(0.0, Math.PI / 2, 0.0); // pitch = 90deg
// ここから yaw と roll をいじると挙動が「同じ軸を回している感」になる
setAngles(0.5, Math.PI / 2, 0.0);
console.log(obj.rotation.toArray());
コード自体は壊れていない。 Three.js も正しく動いている。
壊れているのは「3軸で制御できる」という前提。
図で説明した方が早い理由
この現象は、コードより図の方が圧倒的に伝わる。
- 3つの輪っか(ジンバル)
- 90度回した瞬間
- 2つの輪っかが重なる
「90度で輪っかが重なる」 このイメージだけで十分。
数式も理屈も要らない。
ここまで来ると、読者の頭にはこう残る。
- ジンバルロックは事故じゃない
- オイラー角の構造上、避けられない
- 「角度を持っている」設計自体が危うい
次の流れとしては自然にこう繋がる。
「じゃあ、角度を足して運用するのは何がまずいのか?」
4. 「角度を足す設計」の限界:角度はベクトルじゃない
ここが オイラー角を実務で使うときの最大の落とし穴。
よくある実装はこうだ。
yaw += inputX;
pitch += inputY;
roll += something;
見た目は完全に正しい。 でも実際には、ここで全てが壊れ始める。
そもそも「回転は足し算で合成できる量ではない」
角度を (x, y, z) という数字で持っているから、
つい「3次元ベクトルみたいに足せる」と錯覚する。
しかし、回転はベクトルではない。
回転は本質的に 作用(オペレーション) であって、 「位置」や「量」ではない。
だから、
- 足し算で合成できない
- 順序に依存する
- 状態として安定しない
という性質が必ず出てくる。
足した結果の意味が「順序(order)」に依存する
Euler角の (pitch, yaw, roll) は、
- どの順番で回すか
- 途中で軸がどうねじれるか
を含めて初めて“意味”が決まる。
同じ数値でも、
- XYZ
- YXZ
- ZYX
これだけで全く違う姿勢になる。
つまり、足し算の結果は “値” ではなく “命令セット” になっている。
足すたびに「次に回る軸」が変わっていく
1ステップごとに軸がズレるため、
yaw += 0.1;
この一行の意味が、 毎フレーム違う“軸”の回転として適用される可能性がある。
見た目は滑らかでも、内部では地獄。
長い時間で見れば、 本来の向きとは別の方向にどんどん流れていく。
姿勢によっては自由度が潰れ、入力が別の回転に化ける
第3回のジンバルロックで触れたように、 自由度が1つ消える瞬間がある。
その状態で角度を足すと:
- yaw のつもりが roll になる
- roll のつもりが yaw になる
- pitch入力が「効いてない」ように見える
つまり、入力が違う意味に変換される。
これはバグじゃない。 Euler角の仕様。
結論:Euler角は「UI用」には便利だが「内部状態」には向かない
ここが一番重要なメッセージ。
Eulerは「UI表示」や「デバッグ表示」としては便利。 でも、内部の状態として保持すると破綻しやすい。
- いつ破綻するか予測が難しい
- 視点やキャラ制御で特に壊れやすい
- 入力を蓄積すると破綻が加速する
だから、実務では
- Eulerで入力を受け取る
- 内部ではEulerを使わない
という設計が多い。
(Quaternionの話は次回で良い。ここではまだ出さない。)
5. ここまでの結論(次回への導線)
ここまでで、オイラー角が「なんとなく不安定」ではなく 構造的に壊れる理由がはっきりした。
まとめると、こういう話だ。
- Euler が壊れるのは仕様であって事故ではない
- 回転順序(order)が “結果の意味” を決めてしまう
- ある姿勢で自由度が潰れるのは避けられない構造
- 角度を足す設計は、短期は動いても長期で必ず破綻する
つまり、 「Euler角で姿勢を蓄積する」こと自体が地雷。
ここまで読んだ人は必ずこう思うはずだ。
「じゃあ、実際どんな実装がダメなんだ?」
次回はそこに答える。
[JavaScript] Quaternion 入門 #03 “やってはいけない実装集”
- 足して壊れる
- 順序を間違えて壊れる
- lerp して壊れる
- 正規化しないで壊れる
実際に破綻するコードだけを並べる回になる。
付録:記事の“読みやすさ”を上げる小技
この記事は、理解させるより 詰まらせない ことを優先している。 そのために、意図的にやっている工夫をいくつか補足しておく。
回転順序は「理屈」より先に「結果」を見せる
「回転順序とは何か」を言葉で説明し始めると、 多くの読者はその時点で置いていかれる。
だから最初に出すのは理論じゃない。
“同じ角度でも結果が変わるログ”。
- 値は同じ
- order だけ違う
- 出力が違う
この事実を先に見せるだけで、
「あ、これはそういう性質のものなんだ」
という理解が一気に進む。
ジンバルロックは「自由度が1個消える」だけで十分
ジンバルロックを説明するときに、
- 数式
- 回転行列
- 微分
は一切いらない。
言うべきことはこれだけ。
「3軸で表現していたのに、ある姿勢で2軸になる」
自由度が1個消える。 それ以上でもそれ以下でもない。
ここが伝われば、 「なぜ入力が効かなくなるのか」は自然に腑に落ちる。
💬 コメント