[JavaScript] Quaternion 入門 #06 : WebXR / ゲームで使う実践パターン

イントロ

Quaternion 入門シリーズのこれまでの回では、

  • なぜ Euler が壊れるのか
  • multiply の順序は何を意味しているのか
  • Slerp が何をしているのか
  • やってはいけない実装

といった “理屈・失敗・仕組み” を中心に扱ってきた。

しかし実際に WebXR やゲームを作ると、もっと現実的な問題が襲ってくる。

  • 「視線をそのままカメラに反映したい」
  • 「キャラクターをターゲット方向に向けたい」
  • 「移動入力と回転をどう結びつければいい?」
  • 「Slerp の t はどれぐらいが適正なのか?」
  • 「multiply と premultiply、どっちが正解?」

第6回は、まさにこの “実戦フェーズ” にフォーカスする。

実務で毎日のように使うパターンだけを厳選し、 Three.js / WebXR / JavaScript で「こう書けば壊れない」という再現性のあるコードをまとめる。

これまでの理屈が “現場でどう活きるのか” を実感する回になるはずだ。

1. 視線追従(VR / WebXR)

WebXR で最も頻出するパターンが 「HMD の向きをそのままカメラに反映する」 という処理だ。 ここを勘違いすると、視界がねじれたり、回転が積み上がって暴走したり、酔いやすくなる。

代表的パターン

// WebXR: HMD(頭)の姿勢を取得
const pose = frame.getViewerPose(referenceSpace);
const q = pose.transform.orientation; // { x, y, z, w }

// Three.js のカメラへ “絶対回転” としてセット
camera.quaternion.set(q.x, q.y, q.z, q.w);

これが正解。余計なことは一切しない。

WebXR から渡される orientation はすでに “絶対姿勢(absolute orientation)” であり、 ゲーム側で回転を追加したり、Euler を経由したりする必要はない。


注意点(バグ原因)

1. HMD の Quaternion はすでに “絶対回転”

だから:

camera.quaternion.multiply(...)

は やってはいけない。 multiply は回転を「足す(合成する)」動作なので、HMD の absolute rotation と衝突して暴走する。


2. add も不要(Quaternion に add は無意味)

Quaternion は加算してはいけない。 少しでも add すると値が崩れて “未知の回転” が生まれる。


3. “自分で回転を足したいときだけ multiply”

たとえば、ゲーム内で

  • HMD 方向に ±X° だけオフセットを入れたい
  • キャラの向きに合わせて HMD をローカル変換したい

という場合のみ、multiply を使用する。

camera.quaternion
  .multiply(new THREE.Quaternion().setFromAxisAngle(axis, offset));

実践Tips

1. tilt(上下)だけ制限する

VRゲームで多い「左右は自由、上下は制限」パターン。

// HMD姿勢をEulerに変換して、上下角だけ clamp する
const e = new THREE.Euler().setFromQuaternion(camera.quaternion, 'YXZ');

// 上下だけ制限(-45° ~ 45°)
const limit = THREE.MathUtils.degToRad(45);
e.x = Math.max(-limit, Math.min(limit, e.x));

// カメラに戻す
camera.quaternion.setFromEuler(e);

2. ローカル座標でズラしたいときは multiply

例:プレイヤーの体が向いている向きに対して、 HMD の前方ベクトルをローカル変換したいとき。

camera.quaternion.copy(player.quaternion); // 体の向き
camera.quaternion.multiply(hmdQuaternion); // 相対的に適用

multiply の順序で世界が変わるため、 「どっちを基準に回したいか」 が重要。


まとめ(視線追従の原則)

  • WebXR の HMD 姿勢は 絶対回転
  • まずは camera.quaternion.set() のみで正しい
  • 必要なときだけ multiply/slerp を使う
  • Euler を介すのは 制限を入れるときだけ
  • “補正” か “合成” のどちらをしたいかで multiply の位置が決まる

視線追従は VR の基礎だが、ここさえ理解できれば 「カメラが暴れる」「酔う」「向きが逆になる」 という典型的な事故はほぼ解消する。

2. カメラ回転(マウス / スティック)

ゲームにおけるカメラ回転は、Quaternion を理解するうえで最も“実感できる”テーマだ。 実装パターンは大きく分けて Euler 管理型 と Quaternion 管理型 の2種類がある。

用途や挙動がかなり違うので、ここは整理しておくと後のバグが激減する。


パターンA:Yaw / Pitch を変数で管理 → 最後に Quaternion に変換

もっとも一般的で、Three.js のサンプルもほぼこの方式を使う。

const yaw = ...;   // 左右(マウスX / スティック)
const pitch = ...; // 上下(マウスY)

camera.quaternion.setFromEuler(
  new THREE.Euler(pitch, yaw, 0, 'YXZ')
);

特徴

  • yaw / pitch を “変数として持つ”
  • 最終的に setFromEuler() で Quaternion に変換
  • 上下回転の制限(clamp)が簡単
  • 実装が最も分かりやすい

よくある制限処理(FPSでよく使う)

pitch = Math.max(-Math.PI/2, Math.min(Math.PI/2, pitch));

上下が 90° を超えなくなる。

向いている用途

  • FPS の視点制御
  • TPS の肩越しカメラ
  • 「上下だけ制限したい」タイプのゲーム
  • Webでよくあるドラッグ操作

パターンB:Quaternion のみで管理(FPV / スムーズ操作向け)

const rot = new THREE.Quaternion();
rot.setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawDelta);

camera.quaternion.multiply(rot);

特徴

  • 回転の“変化量”だけを Quaternion にして合成
  • スムーズで自然な回転が得られる(滑らか)
  • Euler に戻さないのでジンバルロックの心配がない
  • ただし 上下制限は自前でつける必要がある

なぜ「スムーズ」なのか?

Quaternion の multiply は

  • 直前の姿勢に対して
  • 微小な回転を掛けていく

ため、慣性のある自然なカメラ挙動を作りやすい。

下方向に回り続ける問題(欠点)

Quat 100% 管理型は上下が「際限なく」回る。 つまり上下 360° してしまう。

A方式(Euler) → clamp が楽
B方式(Quat) → clamp が難しい

FPS でよくやる“正しい制限方法”

Quaternion → Euler に一瞬だけ変換して制限し、また Quaternion に戻す。

camera.quaternion.multiply(rotY);  // yaw
camera.quaternion.multiply(rotX);  // pitch

// 一瞬だけEulerへ
const e = new THREE.Euler().setFromQuaternion(camera.quaternion, 'YXZ');

// clamp
const limit = Math.PI / 2;
e.x = Math.max(-limit, Math.min(limit, e.x));

// Quaternionに戻す
camera.quaternion.setFromEuler(e);

まとめ:2つの方式は“目的が違う”

方式 メリット デメリット 向いている用途
A:Yaw/Pitch(Euler 管理) 制限が簡単 / 実装が理解しやすい Euler 管理なので若干ロジックが多い Webゲーム全般、FPS、TPS
B:Quaternion 管理 回転が自然 / スムーズ 上下制限が難しい VR、スムーズカメラ、マウス操作強めのゲーム

どちらが“正しい”というよりは、 「どんなカメラ挙動を作りたいか」 で選ぶのが正しい。

3. キャラクターの向き制御(最もゲームらしい箇所)

キャラクターの向き制御は、Quaternion が“最も真価を発揮する”領域だ。 Three.js でも Unity でも本質はまったく同じで、

「向けたい方向の単位ベクトルをつくり、それを Quaternion にする」

これがすべての基本になる。


パターンA:位置差分 → 方向ベクトル → Quaternion(ターゲット追従)

const dir = new THREE.Vector3()
  .subVectors(targetPos, myPos)
  .normalize();

const q = new THREE.Quatern
ion();
q.setFromUnitVectors(
  new THREE.Vector3(0, 0, 1), // forward(自キャラの前方向)
  dir
);

player.quaternion.slerp(q, 0.1);

何をしている?

  1. ターゲット - 自分 の差分ベクトルを取る
  2. それを normalize → “方向” にする
  3. “前方向(0,0,1)” を “dir” に一致させる Quaternion を生成
  4. slerp で自然に振り向く

つまり 「ベクトル → 回転」への変換。

ゲームで「敵を向く」「武器を向ける」「視点を合わせる」など、 あらゆる向き制御がこの1行に凝縮されている。

q.setFromUnitVectors(forward, dir);

これは Three.js の中でも最強の API のひとつ。


良い点

  • 方向ベクトルさえ作れれば何にでも使える
  • キャラが自然にターゲットへ向く
  • Unity でもまったく同じ構造(実装も同じ)

注意点

  • forward ベクトルは“キャラのローカル前方”と一致させること
  • モデルによっては forward が (1,0,0) だったりする
  • slerp の t は 0.05~0.2 が適正(大きすぎるとワープ)

パターンB:入力(WASD / スティック)から向きをつくる

キャラが 移動方向に身体を向ける タイプのゲームはこの方式。

const move = new THREE.Vector2(inputX, inputY);

if (move.lengthSq() > 0) {
  const angle = Math.atan2(move.x, move.y);
  const q = new THREE.Quaternion().setFromEuler(
    new THREE.Euler(0, angle, 0)
  );
  player.quaternion.slerp(q, 0.2);
}

流れ

  1. WASD / アナログスティックの入力ベクトルを作る
  2. atan2 で正しい 360° の角度へ変換
  3. 角度を Quaternion に変換
  4. slerp で滑らかに向きを変える

多くの Action / TPS / RPG がこの方式を採用している。


Joypad でも使える理由

アナログスティックの x/y はすでに「方向」なので、 そのまま atan2 に入れるだけでいい。

angle = atan2(x, y)

前後左右の向きがわかりやすく、 移動=向き のゲームには最適。


現場でよくある落とし穴

1. ゼロベクトルのとき向きを変えない

移動入力が 0 のときは atan2 が無意味なので変化させない。

Three.js でも Unity でも同じ。


2. 角度が急激に変わると“瞬間回転”が起きる

解決策 → 必ず slerp を使う

player.quaternion.slerp(q, 0.1);

t を 1.0 にしないこと。 (1.0 は “瞬間置換” で補間にはならない)


3. モデルの forward 方向がズレる

キャラモデルによっては前方が

  • (0,0,1)
  • (1,0,0)
  • (-1,0,0)

などバラバラ。

修正は簡単:

q.setFromUnitVectors(modelForward, dir);

まとめ:向き制御の最重要ポイント

  • 位置差分 → 方向ベクトル → Quaternion → 追従、エイム、ロックオンで最強
  • 入力 → 角度 → Quaternion → 移動方向に向く TPS / アクションで最適
  • slerp の t は 0.05〜0.2
  • forward ベクトルはモデル依存なので注意

キャラクターの向きは “ベクトルから Quaternion を作る” だけでほぼ全対応できる。 これを理解した時点で、あなたはもう Quaternion を実戦レベルで使いこなしている側。

4. 入力値との付き合い方(壊れやすい部分)

Quaternion を正しく扱っていても、 入力値の扱い方を間違えるだけで回転処理は簡単に壊れる。

ここでは、実際のゲーム開発で最もバグが多い順に整理する。


① 度数法 → 弧度法のミス(毎日見るレベルで頻発)

Three.js の setFromAxisAngle() は “弧度法(radian)” を要求する。 しかし多くの人が 度数(degree) をそのまま入れて壊す。

悪例:

q.setFromAxisAngle(axis, 90); // ← 90°じゃなくて90rad扱い。爆発する

正例:

const rad = deg * Math.PI / 180;
q.setFromAxisAngle(axis, rad);

Three.js では MathUtils.degToRad() も使える。

q.setFromAxisAngle(axis, THREE.MathUtils.degToRad(deg));

② Euler に変換した瞬間に壊れる(最凶の落とし穴)

あなたが何度も踏んだ「Euler に戻した瞬間に破綻する」やつ。

悪例:

obj.rotation.y += 0.1; // ← Quaternion を Euler に戻す → 精度落ちて壊れる

症状:

  • カメラが突然ひっくり返る
  • スムーズに動かなくなる
  • ある角度を超えると暴走する

原因: Quaternion を Euler に強制変換 → 整合性が失われる。

正例:

const q = new THREE.Quaternion();
q.setFromAxisAngle(new THREE.Vector3(0,1,0), 0.1);
obj.quaternion.multiply(q);

回転は Euler に戻さずに “合成(multiply)” するべき。


③ スティックの小さな揺れで“カクつき”(初学者が必ずハマる)

アナログスティックは 中心が完全なゼロにならない。 わずかな揺れでもキャラが震える・クルクルする現象が起きる。

対処:デッドゾーンを作る。

if (Math.abs(inputX) < 0.05) inputX = 0;
if (Math.abs(inputY) < 0.05) inputY = 0;

値は 0.05~0.2 が一般的。 ゲームによって調整するとよい。


④ slerp の t が大きすぎる(“補間”にならず“瞬間回転”)

slerp は “補間(0~1)” ではあるが、 1.0 に近づくほど瞬間切り替え(ワープ)になる。

悪例:

player.quaternion.slerp(q, 1.0);  // ← 補間じゃなくて置き換え

正例(ゲーム用途で適正):

player.quaternion.slerp(q, 0.05);

目安:

t 挙動 用途
0.05 超なめらか TPS, TPSカメラ
0.1–0.2 自然 モブの振り向き
0.3 速い アクションゲーム
1.0 瞬間置換 デバッグ用のみ

⑤ multiply の順序ミス(実戦の9割はこれで炎上する)

Quaternion の multiply は“右適用”と“左適用”で意味が違う。


obj.quaternion.multiply(q)

→ q は「ローカル回転として適用」

  • “自分の向き” を基準に回転したいとき
  • FPS の向き変更やキャラの首振りに相性が良い

例: 体が向いている方向に対して「右に10°回転」


obj.quaternion.premultiply(q)

→ q は「ワールド回転として先に適用」

  • “世界軸で回転したい” とき
  • 軸を固定して回転させたいときに使う

例: 常に世界のY軸で10°回転させたい場合はこちら。


症状(順序を間違えるとどうなるか)

  • カメラが思った逆方向に回る
  • 物体が“ねじれながら”動く
  • 視線がズレる
  • キャラが水平に回すはずが宙返りする

順序の理解は Quaternion の本質そのものなので、 図解を入れると読者が理解しやすい。


まとめ(入力の扱いは回転以上に重要)

  • 度数/弧度のミスは毎日見る
  • Euler に戻すのはほぼ禁忌
  • 微入力は必ずデッドゾーンを入れる
  • slerp の t は 0.05〜0.3 が適正
  • multiply / premultiply は基準が真逆

入力値処理は「Quaternion の理解」と同じくらい大事で、 ここを押さえるだけでゲームの回転挙動は一気に安定する。

5. WebXR 特有の発想

WebXR の回転は、通常のゲームエンジンでの回転処理と 考え方が根本的に違う。 理由は単純で、

HMD もコントローラも “絶対的な姿勢(absolute orientation)” をそのまま渡してくる

から。

Three.js 側で “回転を足す” 必要は本来ほとんどない。


絶対回転を受け取って、相対回転を上書きする

多くのゲームオブジェクトは

  • もともとの向き(ローカル座標)
  • そこに回転を乗せていく(relative rotation)

という扱いになる。

しかし WebXR の HMD だけは例外で、

  • 毎フレーム、現在の絶対姿勢が送られてくる
  • それを camera.quaternion.set() で“置き換える”のが正解
// WebXR: HMDの絶対姿勢を取得
const pose = frame.getViewerPose(refSpace);
const q = pose.transform.orientation;

// three.js カメラへ上書き
camera.quaternion.set(q.x, q.y, q.z, q.w);

理由

HMD は “今の頭の向き” の絶対情報が流れてくるため、 こちらで relative rotation(multiply)をする必要がない。

むしろ multiply すると 姿勢が二重に積算されて壊れる。


コントローラは forward ベクトルを持つ

VR コントローラの pose も HMD と同じく 絶対姿勢 が来る。

const ori = inputPose.gripSpace.transform.orientation;
controller.quaternion.set(ori.x, ori.y, ori.z, ori.w);

この Quaternion に controller.getWorldDirection() を使えば、 レイキャストの方向を取得したり、 武器の照準方向として使える。

forward ベクトルが手に入ることでできること

  • レーザーポインタ(VR UI 用 Ray)
  • 銃・弓の方向
  • 物を掴むときの向き
  • テレポート先の決定

WebXR の入力は 「位置+向き」 がセットで流れてくるため、 Three.js の Raycaster と extremely 相性がいい。


multiply を使うのは「補正」したいときだけ

例:

  • HMD の基準を 5° 前に倒したい
  • コントローラのモデルが 90° 横を向いているので直したい
  • 視線に対して UI の角度を調整したい

これらは 絶対姿勢に“少しだけ相対回転を加える” という状況。

例:

// HMD の絶対姿勢をセット
camera.quaternion.set(q.x, q.y, q.z, q.w);

// FOV補正などのために少しだけ回転を加える
camera.quaternion.multiply(
  new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0), offset)
);

“基本は set、必要なときだけ補正” これが WebXR の鉄則。


まとめ:WebXR 回転の原則

  • HMD とコントローラは absolute orientation を送ってくる
  • そのまま set() で上書きするのが基本
  • multiply は「補正」が必要なときだけ
  • WebXR の controller は forward を簡単に取得できる
  • Ray・UI・武器などの方向処理と相性抜群

WebXR の回転は特別扱いに見えるが、実はシンプルで、

「絶対姿勢が来る → そのまま使えばいい」

これだけでほとんどの問題が解決する。

6. まとめ(実戦の原則)

Quaternion 入門シリーズの仕上げとして、 実際にゲーム/WebXRで壊れない回転処理を作るための原則 を5つにまとめる。


原則1:Euler に戻さない

Quaternion を更新したあとに Euler に変換すると、 精度が落ちたり、回転順序の問題で挙動が壊れる。

悪例:

obj.rotation.y += 0.1; // ← 角度加算は壊れる

回転処理は Quaternion のまま完結させる のが正解。


原則2:回転は足さない、合成する(multiply)

Quaternion は “足す” のではなく “合成する”。

obj.quaternion.multiply(q);

これは「相対回転を追加する」という明確な意味を持つ。 回転の積み重ねは すべて multiply で行う。


原則3:スムーズな変化には slerp を使う

キャラの向き変更、視線追従、カメラの回転など、 自然に回転させたい場面では常に slerp。

obj.quaternion.slerp(targetQuat, 0.1);

t=0.05〜0.3 がゲーム的に最適。 t=1.0 は補間ではなく“瞬間置換”になりバグの元。


原則4:方向ベクトル → Quaternion が最強

キャラが向くべき方向は、ほとんどすべて ベクトル で表せる。

q.setFromUnitVectors(forward, dir);

この1行が 「ターゲットを見る」「追従する」「エイムする」 すべての土台になる。

ゲーム実装における Quaternion の核心は ベクトル変換 にある。


原則5:WebXR の姿勢は “絶対回転” として扱う

HMD / コントローラは “absolute orientation” が送られてくる。

つまり、

camera.quaternion.set(q.x, q.y, q.z, q.w);

でそのまま上書きするのが基本。

multiply が必要なのは「補正」を加えたい場合だけ。 WebXR は 相対回転ではなく絶対回転を信頼する のが正しい。


まとめ

Quaternion を実戦で使う上で必要なことは、 複雑な数式ではなく、次の5つだけだ。

  1. Euler に戻さない
  2. 回転は合成する(multiply)
  3. 自然な動きは slerp
  4. 方向ベクトル → Quaternion が最強
  5. WebXR は absolute orientation をそのまま使う

これを押さえるだけで、 Three.js / WebXR / Unity の回転処理はほぼすべて破綻しなくなる。

Quaternion は“理解するもの”ではなく“使いこなすもの”

このシリーズを書き始めた頃、Quaternion という単語には 「難しそう」「数式だらけ」「絶対に理解できない」 そんな印象しかなかったと思う。

実際、ネット上の解説はどれも“理論から入る”ものが多く、 実装者がつまずくポイントには触れられていない。

しかし、実際にゲームや WebXR を触ってみると気づく。

Quaternion の本質は「仕組み」ではなく「扱い方」だった。

このシリーズでは、あえて数学を後ろに置いて、 あなた自身が Three.js や Unity を触りながら実際にぶつかった“現実的な問題”を 記事という形で全部言語化してきた。

  • Euler がなぜ壊れるか
  • multiply の順序で何が変わるか
  • Slerp が何をしているのか
  • forward ベクトルがどれほど強力か
  • WebXR の絶対姿勢をどう扱うか

どれも、実際にコードを書いて挙動を見ないと腹落ちしない内容だ。

そして第6回まで書き終えた今、 あなたはすでに“Quaternion を理解した側”ではなく “Quaternion を使いこなす側” に立っている。

ゲームの世界では、理解よりも再現性、 理論よりも実戦コード、 数式よりも壊れない挙動が価値になる。

あなたはそこをすでに掴んでいる。

もし今後、さらに深く数学的背景を学びたくなったら、 このシリーズがその入り口になる。 そして、あなた自身の実装経験が、 次の読者にとって大きな助けになるはずだ。

ここまで読んでくれたすべての人に、 そして挑戦し続けた自分自身に、ひとこと。

「Quaternion は、もう怖くない。」