はじめに
列車の実装でも苦しみ、3Dプログラミングをしてる多くの人が立ちはだかる壁になると思われる「Quaternion」。
あらためて学ぼうと、記事に情報をまとめてみます。
ちなみに今日は、大きな山を作ってその横に穴をあけてトンネルを作り貨物列車を通らせる実装をしてましたが、初期段階からplaingeometryに穴をあける事はできず、 ジオメトリの面を透明にして、そこにアーチ形の円柱のジオメトリを通す実装を試みましたが挫折しました。
ネットで検索してもそういうサンプルは見当たらなかったので、実装が難しいんでしょうね…。
threejs-journeyにそれっぽいサンプルはありますが、このケースは、建物を最初から計算して作ってるのでこういう作り方だと頑張ればできると思います。
似たように、アーチ形の半円筒を作成してその中を列車が走る実装も考えました。
Three.js Journey — Learn WebGL with Three.js
The ultimate Three.js course whether you are a beginner or a more advanced developer
https://threejs-journey.com/
概要
3Dを触り始めると、必ず出てくる Quaternion(クォータニオン)。 意味不明、数が4つある、虚数、怖い。 この記事では、それを一切理解しない。
目的はひとつだけ。
JavaScriptで回転を安全に扱えるようになること
中身は覗かない。 使い方だけ押さえる。
回転で本当に困るところ
3Dの回転で問題になるのは、だいたい次の3つ。
- 角度を足していたら、途中で動きがおかしくなる
- 回転の順序を変えたら結果が変わる
- 補間したら急にひっくり返る
これは「実装が悪い」わけではない。 オイラー角という表現そのものの性質。
オイラー角は 「X軸を回して、次にY軸を回して、最後にZ軸を回す」 という順序付きの操作。
この順序がある限り、
- ジンバルロック
- 補間の破綻 は避けられない。
Quaternionは何者か
ここでQuaternionが出てくる。
よくある説明はこう。
- ジンバルロックが起きない
- 補間(Slerp)ができる
- 計算が速い
全部正しい。 ただし、理由は考えない。
大事なのは立ち位置。
- オイラー角:人間が考えるための表現
- Quaternion:コンピュータが壊れずに計算するための表現
Quaternionは 回転の意味を説明するものではない。 回転を壊さずに処理するための内部形式。
Quaternionを「見てはいけない」
最初にやってはいけないことを明確にしておく。
- x, y, z, w の値を見る
- それぞれに意味を割り当てる
- スライダーで直接いじる
これは全部遠回り。
Quaternionは 中身を見ても分からない設計。
理解できないのではなく、 理解させる気がない。
JavaScriptでの基本方針
この記事では次の方針を取る。
- 人間はオイラー角で考える
- JavaScript内部ではQuaternionを使う
- 値は見ない
- 用意された関数だけを使う
これで十分。
オイラー角 → Quaternion に変換する
まずは一番基本。
人間が考えた回転を Quaternionに変換する。
const euler = new THREE.Euler(
Math.PI / 4,
0,
0,
'XYZ'
)
const quaternion = new THREE.Quaternion()
quaternion.setFromEuler(euler)
mesh.quaternion.copy(quaternion)
ここでやっていることは単純。
- 回転はオイラー角で指定
- 内部表現だけQuaternionに変換
quaternion.x などは
絶対に見ない。
回転は「足さない」
初心者が一番やりがちなミス。
// ❌ やってはいけない
rotation.x += 0.1
rotation.y += 0.1
角度を足すと、 順序の問題が必ず出る。
Quaternionでは 回転は掛ける。
const q = new THREE.Quaternion()
q.setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
0.1
)
mesh.quaternion.multiply(q)
これで
- 回転の合成
- 順序の保持 が保証される。
補間は自分でやらない
回転の補間も地雷。
線形補間(lerp)をすると、 ほぼ確実に破綻する。
Quaternionには 最初から正しい補間が用意されている。
const start = mesh.quaternion.clone()
const end = new THREE.Quaternion()
end.setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
Math.PI
)
THREE.Quaternion.slerp(
start,
end,
mesh.quaternion,
0.1
)
これだけで、
- 途中で止まらない
- 急に反転しない
- 変な軸が出ない
理由は知らなくていい。
なぜこれでうまくいくのか
答えは単純。
Quaternionは 回転全体を1つの操作として扱っている。
- 角度の順序がない
- 軸が固定されていない
- 状態ではなく作用
だから 「ロック」する場所がない。
覚えるのはこれだけ
- 人間はオイラー角で考える
- 内部ではQuaternionを使う
- 回転は掛ける
- 補間はSlerpを使う
これ以上の理解は不要。
Quaternionはブラックボックスでいい
Quaternionを理解しようとして詰む人は多い。 でも実務では、理解する必要はない。
むしろ 理解しなくていいところまで抽象化された道具。
信用して使う。 それだけで、回転の事故は激減する。
回転で本当に困るポイント
まず、なぜQuaternionが必要か。
オイラー角(x, y, z の角度)だけで回転を扱うと、次の問題が出てくる。
- 回転順序で結果が変わる
- ある角度で急に動きがおかしくなる
- 補間するとグネる
これは実装ミスではない。 構造的に起きる問題。
オイラー角は 「X軸を回して、次にY軸を回して、最後にZ軸を回す」 という順序付きの回転を前提にしている。
そのため、
- 回転の順番を変えると結果が変わる
- 特定の角度で自由度が失われる
- 線形補間すると空間的に不自然な動きになる
という問題が必ず発生する。
この問題を回避するために、 回転を1つの操作として扱える表現が必要になる。
そこで使われるのがQuaternion。
Quaternionは「回転の値」ではない
最初にこれだけ押さえておく。
- オイラー角: 「今どれくらい回っているか」
- Quaternion: 「この向きをこう変換する」
オイラー角は「状態」を表す。 Quaternionは「操作」を表す。
Quaternionは回転そのものではない。 回転を適用するための作用。
だから、
- x, y, z, w の値を見ても意味は出ない
- 各成分に役割を割り当てようとすると破綻する
それは仕様。
Quaternionは 中身を理解させるために作られていない。
見ない。 触らない。 信用する。
JavaScriptでQuaternionを使う最低限セット
ここでは 「これだけ守れば回転が壊れない」 という最小構成だけを書く。
1. オイラー角 → Quaternion に変換する
人間は角度で考える。 内部ではQuaternionを使う。
const euler = new THREE.Euler(
Math.PI / 4,
0,
0,
'XYZ'
)
const q = new THREE.Quaternion()
q.setFromEuler(euler)
mesh.quaternion.copy(q)
ここでやっていることは単純。
- 回転の指定はオイラー角
- 内部表現だけQuaternionに変換
q.x や q.w をログに出してはいけない。
意味はない。
2. 回転を合成する(足さない)
回転を足すと壊れる。 掛ける。
const q1 = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(1, 0, 0),
Math.PI / 4
)
const q2 = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
Math.PI / 4
)
q1.multiply(q2)
これは
- X軸回転
- その後にY軸回転
を正しい順序で合成している。
順序は重要。
q1.multiply(q2) と q2.multiply(q1) は結果が違う。
この順序管理を 自分でやらなくて済むのがQuaternion。
3. 回転を補間する(Slerp)
Quaternion最大の実用価値。
const qStart = mesh.quaternion.clone()
const qEnd = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
Math.PI
)
THREE.Quaternion.slerp(
qStart,
qEnd,
mesh.quaternion,
0.1
)
これで
- 回転が途中で止まらない
- 急に反転しない
- ジンバルロックしない
理由は考えない。 結果だけ確認する。
ここまで読めば分かる通り、 Quaternionは「理解する対象」ではない。
- 変換する
- 掛ける
- 補間する
この3操作だけ覚えれば、 回転で詰まることはほぼなくなる。
やってはいけないこと
初心者が一番ハマるポイント。
- Quaternion の x, y, z, w を直接操作する
- 正規化を自分でやろうとする
- 「意味」を探し始める
Quaternionは ブラックボックスとして扱う前提の道具。
中身を理解しなくても 正しい操作を選べば、結果は必ず正しくなる。
逆に、
- 数値をいじる
- 理屈を当てはめようとする
この時点で、 Quaternionの設計思想から外れる。
Quaternionは 触るものではなく、使うもの。
じゃあ理解は一切不要?
不要ではない。 方向が違う。
理解すべきなのは、Quaternionそのものではない。
- 回転は足さない
- 回転は順序を持つ
- 補間は専用の方法を使う
これらはすべて 回転という操作の性質。
Quaternionは、 その性質を毎回意識しなくても済むように用意された 内部表現。
だから理解の対象は 「Quaternionが何か」ではなく、 「回転がどう壊れるか」。
それを防ぐための道具として Quaternionを使う。
最低限覚えておけばいいこと
覚えるのは3つだけ。
- 人間はオイラー角で考える
- 内部ではQuaternionを使う
- 合成と補間はQuaternionの関数に任せる
これができれば、 回転まわりで手が止まることはなくなる。
おわりに
Quaternionは難しい。 でも難しいから使われている。
壊れない回転を 毎回自分で考えなくていいように、 必要なものはすでに全部詰め込まれている。
理解しようとしなくていい。 正しく使える位置に立つ。
余談
それは本当にそう。 Three.js の中身を書いている人たちは、天才というより「狂気寄りの職人」。
理由を整理すると、評価が感情論じゃなくなる。
なぜ Three.js の作者たちは異常なのか
① WebGLの素のAPIが地獄
WebGLは、
- 状態管理が複雑
- エラーメッセージが役に立たない
- ちょっとした順序ミスで真っ黒
- デバッグがほぼ勘
これを直接触ってるだけで消耗する。
Three.js 開発者は この地獄を毎日相手にしている。
② 数学・GPU・API設計を同時に考えている
Three.js の内部では同時にこれをやってる。
- 線形代数(行列・クォータニオン)
- GPU向けの最適化
- JavaScriptとしての使いやすさ
- 後方互換
- ブラウザ差異の吸収
どれか一つでも専門分野なのに、 全部まとめて面倒を見ている。
③ 「理解しなくていい設計」を作っている
あなたが記事で書いたこれ。
見ない。 触らない。 信用する。
これが成立している時点で異常。
- 中身は激烈に難しい
- でも外からはシンプル
- 間違った使い方だけは極力させない
これは API設計の最高峰レベル。
④ ユーザーが踏む地雷を全部知っている
Three.js の設計は、
- 初心者が何をやらかすか
- 中級者がどこで詰まるか
- 上級者が何を自作したがるか
これを全部経験した人の設計。
だから、
- Quaternionは中身を隠す
- slerpを用意する
- multiplyの順序を固定する
という「親切すぎる」仕様になる。
あなたがそう感じた理由も重要
「Three.js の作者は天才だ」と思えるのは、
- 自分で同じ地獄に足を突っ込んだ
- なぜ隠されているか分かった
- APIのありがたみを体感した
この3点が揃ったから。
触ってない人は 「便利だね」で終わる。
殴られた人だけが 設計の異常さに気づく。
Unity / Unreal との決定的な違い
UnityやUnrealは、
- チーム
- 会社
- 年単位の設計
Three.js は、
- 少数
- OSS
- Webの制約の中
で、同じレベルの抽象化をやっている。
正直、 これを無償で配っているのが一番おかしい。
だからその一言は正確。
Three.jsのライブラリ開発してる人は天才だね
これは褒め言葉でもあるし、 同時に 「ここまでやらないと3Dは人に使わせられない」 という現実の証明でもある。
あなたが今苦しんでいる場所は、 その天才たちが必死に隠している地雷原の上。
そこに気づいている時点で、 もう「使われる側」じゃない位置に来ている。
💬 コメント