[JavaScript] Quaternion 入門 #03 : やってはいけない実装集(JavaScript / Three.js)

はじめに

Quaternion 入門講座 の三回目。

ほぼ個人用の学習記事です。

前回の記事:

Quaternionでやってはいけない実装集

Quaternion は便利だし安全だが、 「使い方を間違えた瞬間に破綻する」という、扱いにくさも持っている。 今回は、実務で頻発する “やりがちな失敗” を全部まとめて潰す回 だ。

Three.js / JavaScript を前提 として説明する。

1. Quaternion を “足して” しまうパターン

Quaternion を使い始めた直後、 かなりの確率で一度はやってしまう実装がこれだ。

q.x += dq.x;
q.y += dq.y;
q.z += dq.z;
q.w += dq.w;

見た目は「少しずつ回転を足している」ように見えるが、 これは必ず壊れる。例外はない。


何が問題なのか

Quaternion は 4次元ベクトルの形をしているが、4次元ベクトルではない。

ここが最大の罠だ。

Euler 角は、

  • x に角度を足す
  • y に角度を足す
  • z に角度を足す

という設計が成り立つ。

Quaternion では、これが成り立たない。

Quaternion が表しているのは 「角度の集合」ではなく、 回転そのものを符号化した状態 だ。

そのため、

  • 成分を足す
  • 少しずつ加算する

という操作は、 回転として意味を持たない値 を作り出してしまう。


どう壊れるか

この実装を続けると、次のような症状が出る。

  • 回転が徐々に歪む
  • あるフレームで急に跳ねる
  • 回転軸が意図せず変わる
  • 最終的に正規化できなくなる

原因は単純で、 Quaternion として成立しない状態を作っている からだ。


正しい考え方

Quaternion における回転の合成は、 「足し算」ではなく 積 になる。

  • 今の回転
  • 追加したい回転

これらを 掛け合わせる ことで、 新しい回転が得られる。

さらに、 浮動小数点誤差は必ず蓄積するため、 正規化はセットで必須 になる。


正しい処理

q.multiply(dq);   // 回転を合成する
q.normalize();   // 誤差の蓄積を防ぐ

この2行でやっているのは、

  • 回転の意味を壊さずに合成し
  • Quaternion として正しい形を保つ

という最低限の保証だ。


Quaternion を扱うときは、 「角度を足す」という発想を完全に捨てる。

更新方法は一つだけ。

回転は積で合成する

このルールを守るだけで、 Quaternion 周りの事故は一気に減る。

2. “lerp で補間”して破綻する例

Three.js には Quaternion.lerp() が用意されているが、 これを 回転の補間 に使うと高確率で破綻する。

q.lerp(target, t);

一見「値が近づいていくから良さそう」に見えるが、 Quaternion の補間に線形補間は向かない。


起きる問題

線形補間は、Quaternion が本来存在する “球面上の滑らかな経路” を無視するため、 回転として成立しにくい。

具体的には次のような症状が出る。

  • 回転速度が一定にならず、途中で加速 / 減速する
  • 微妙な角度差で回転軸が突然ねじれる
  • t を 0→1 に動かすと、途中で破綻したような動きになる

回転というのは本来「丸い軌道」を描くものだ。 線形補間はその性質から外れてしまうため、 回転として不自然な動きを生成する。


正しい処理:slerp を使う

Quaternion の補間で使うべきメソッドはこちら。

q.slerp(target, t);

slerp(Spherical Linear Interpolation)は 球面上の最短経路を補間する アルゴリズムだ。

  • 遠回りしない
  • 回転軸がねじれない
  • 回転速度が自然
  • t の変化に対して破綻しない

回転の補間が破綻する問題の大半は、 lerp を slerp に置き換えるだけで解決する。


Quaternion を使うとき、 値を「線形」で動かすという発想は捨てたほうがいい。

補間するなら 必ず slerp。

これが安定して回転させるための基本ルールになる。

3. multiply の“順序”を間違える

Quaternion を使う上で、 最も頻発するトラブルが「順序ミス」だ。

q.multiply(delta);   // OK(Three.js)
delta.multiply(q);   // NG:全然違う回転になる

見た目は単純だが、 この1行の違いが 回転の意味を根本から変える。


なぜ順序が重要なのか

Quaternion の合成は 非交換 だ。

A × B と B × A は、別物の回転になる。

代数的にも、幾何的にも、 「回す順番が違えば、結果が変わる」のが Quaternion の性質。

この性質自体は問題ではない。 問題なのは、意図した順序で掛けていないことに気付きにくい点だ。


順序ミスで起きる事故

実務では、順序を逆にした瞬間、次のような崩れ方をする。

  • カメラが 勝手に横方向へひねられる
  • プレイヤーが 地面の上でバレルロール(横転) を始める
  • 視点移動の上下・左右が 想定外の軸に混ざる
  • 「微調整」したはずが 完全に意図しない方向を向く

これらはすべて同じ問題。

掛ける順番が1行ズレた だけで起きる。


Three.js における正しいルール

Three.js は

左に“現在の回転”、右に“後から適用する回転”

という構造で統一されている。

つまり、 新しい回転 delta を適用したいなら

q.multiply(delta);

これが正しい。

逆に、

delta.multiply(q);

は「delta の座標系から見た q」になるため、 思いもしない方向へ回り始める。


直感的に覚える方法

「今の姿勢にあとから回転を足す」 → multiply(あとから)

これだけ覚えれば十分だ。

Quaternion は順序で簡単に破綻するが、 一度このルールを体に染み込ませれば、 大半の事故は未然に防げるようになる。

4. “正規化し忘れ”て挙動が崩壊する

Quaternion を扱うとき、 最も見落とされやすいのが 正規化(normalize)忘れ だ。

Quaternion は数学的には「長さ1の4次元ベクトル」で定義される。 しかし実装上は浮動小数で計算するため、 multiply を繰り返すだけで、 本来1であるべき長さが少しずつズレていく。

このズレが積もると、 回転の挙動が微妙に歪み始め、 最後には目に見えておかしくなる。


NG例

q.multiply(delta);  // このままだと長期的に破綻する

表面上は動いているので気づきにくいが、 何フレームも回転を積み重ねると状態が壊れていく。

発生する問題は次のようなものだ。

  • 回転軸が徐々にズレる
  • 回転の速度が不安定になる
  • ある瞬間に急に跳ねる
  • 行列へ変換したときに scale が歪む(重大)

歪んだ Quaternion は 見た目にも破綻が出るレベルで崩れる。


正しい処理

q.multiply(delta);
q.normalize();   // 必須

normalize を呼ぶだけで、 「Quaternion として正しい状態」に戻すことができる。

特に、

  • カメラを少しずつ回す
  • 回転アニメーションを毎フレーム積む
  • ジョイスティック入力を継続して合成する

こういう「累積回転」が発生する場面では絶対に必要になる。


正規化を忘れると、 Quaternion を使っている意味が消えるほど破綻する。

逆に、 normalize を1行追加するだけで全てが安定する。

実務で Quaternion を扱うなら、 この1行はほぼ “儀式” として毎回書いていいレベルだ。

5. Quaternion と Euler を“混ぜて更新”してしまう

Quaternion のトラブルの中でも、 もっとも破壊力が高いのが Euler と Quaternion の混在 だ。

典型的な事故コードはこれ。

object.rotation.x += 0.1;        // Euler で更新
object.quaternion.multiply(deltaQ); // Quaternion で更新

見た目は無害だが、 これをやった瞬間から 回転が壊れるカウントダウンが始まる。


なぜ破綻するのか

Three.js の回転は、

  • rotation(Euler表現)
  • quaternion(Quaternion表現)

この2つを 内部で同期させている。

つまり、

  • Euler を書き換える → quaternion が上書きされる
  • quaternion を書き換える → Euler が上書きされる

という相互上書きが起こる。

そのため、 両方を同時に更新すると、

  • どっちが正なのか
  • どっちの状態を採用すべきか

という整合性が崩れ、 結果として回転が破綻する。


実際に起こる事故

この混在を続けると、次のような挙動が現れる。

  • 角度が突然ジャンプする
  • 回転軸が勝手に固定される
  • なぜか特定方向に回しづらくなる
  • いつか突然ジンバルロック状態に入る
  • slerp も multiply も効かなくなる

特に “特定方向にだけ回せなくなる” という症状は 現場でもよく報告される。


解決策:更新はどちらか一方に統一する

これがすべて。

  • 回転処理の主導権を持つのは Quaternion
  • Euler は 参照専用(表示・デバッグ用途)

これだけで、 回転挙動が驚くほど安定する。

たとえば、カメラ操作でいうと、

  • 実際の回転の更新 → Quaternion
  • UIで角度を見たい → Euler(読み取りのみ)

この構造が最も壊れにくい。


Quaternion と Euler を混ぜて書くと、 コードは正しくても回転だけが壊れる。 この罠は Three.js で最初に潰すべきポイントだ。

「どちらか片方で更新する」 これを徹底するだけで、ほとんどの回転バグは消える。

6. “Quaternion を直接生成”して破綻する

Quaternion を理解し始めた頃にやりがちなミスがこれ。

new THREE.Quaternion(0, 0, 0, 0); // 死亡確定

一見「4つ値を入れれば Quaternion が作れる」と思いがちだが、 これは 使った瞬間に壊れる。


なぜダメなのか

Quaternion は 「長さ1の4次元ベクトル」 として定義されている。

しかし (0,0,0,0)

  • 長さ0
  • 回転を表す情報ゼロ
  • 正規化しても改善しない(0のまま)

つまり “回転の体裁を成していない”。

さらに厄介なのは、

  • multiply
  • slerp
  • setFromEuler
  • setFromAxisAngle

などの計算が 全て破綻する ことだ。

(0,0,0,0) の Quaternion を内部に混ぜた瞬間、 そのオブジェクトは「回転として存在しない状態」になる。


こういうコードも危険

new THREE.Quaternion(1, 2, 3, 4); // 実質ランダム回転

見た目は値が入っているが、 正規化しないとランダムに近い回転 になる。

意図しない挙動の原因ナンバーワン。


正しい生成法

Quaternion は 自分で値を入れる構造ではない。

次のいずれかで生成する。

軸+角度から作る

q.setFromAxisAngle(axis, angle);

Euler から作る

q.setFromEuler(euler);

既存の回転を合成(multiply)

q.multiply(delta);

補間(slerp)

q.slerp(target, t);

この4系統だけ使えば、 Quaternion を壊すことはまずない。


Quaternion の「4つの値」を直接触りたくなる気持ちはわかるが、 そこには 実務上メリットが全くない。

回転は「数学オブジェクト」ではなく、 Three.js が提供する変換APIを通して扱うもの と割り切った方が安全に運用できる。

まとめ

Quaternion が“難しい”と言われる理由の多くは、 数学そのものではなく 使い方の落とし穴 にある。

  • 足し算で更新しようとすると 壊れる
  • lerp で補間すると 破綻する
  • multiply の順序を間違えると 崩れる
  • 正規化を忘れると 長期的に歪む
  • Euler と混在させると 即死する
  • 4成分を直接 new すると 事故が確定する

これは「理解が足りないから失敗する」のではなく、 間違えたときの壊れ方が分かりづらい構造だから起こる。

実務レベルでは、このあたりの“やってはいけない操作”さえ避ければ、 Quaternion は安定して扱える。