[JavaScript] Three.jsで地球の上をキャラクターが走り続けるアニメーションを作る(球面座標+法線+lookAt)

はじめに

以前、Three.jsで地球と月のミニチュアを実装しましたが、地球の上をキャラクターが走りながら回転するアニメーションを実装してみたのでそのメモです。

動画(パソコン)

動画(VR)



モデル制作者は、ニコニコ立体の"schwarz"さん。

"author": "schwarz",
"License": "CC Attribution",
"url": "https://3d.nicovideo.jp/works/td1809",
"author": "schwarz",
"License": "CC Attribution",
"url": "https://3d.nicovideo.jp/works/td22298",

1. やりたかったこと

やりたかったことは、とても単純だった。

  • 地球の上をキャラクターが走り続ける
  • ただし平面ではなく、球体の表面

よくある「地面を移動するキャラ」ではなく、 球体そのものを地形として扱う、というのがポイント。

見た目としては、

  • キャラクターが地球の縁を越えても落ちない
  • 常に地表に立っている
  • 走り続けると、そのまま地球を一周する

という状態を目指した。

一見すると簡単そうに見えるが、 実際には「平面前提の移動ロジック」がほとんど使えなくなる。


2. 発想の切り替え:平面移動を捨てる

最初にやるべきことは、平面の考え方を捨てることだった。

通常のキャラ移動は、

  • X/Z 平面を移動
  • Y は重力やジャンプ専用

という前提で作られている。

しかし球体では、

  • 「地面の上方向」は場所ごとに変わる
  • 重力の向きも常に変わる
  • 「前に進む」の定義が一定ではない

そこで今回は、 キャラを直接移動させるのではなく、球面上の位置を数式で決める という方針を取った。


3. 球面座標で位置を決める

地球を球体と考えた場合、位置は次の2つの角度で表せる。

  • θ(theta):経度(Y軸まわり)
  • φ(phi):緯度(上から下)

これを使うと、半径 r の球面上の点は次の式で求められる。

const x = r * Math.sin(phi) * Math.cos(theta);
const y = r * Math.cos(phi);
const z = r * Math.sin(phi) * Math.sin(theta);

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

「走る」という動作も、 実際には theta を少しずつ増やしているだけ。

theta += speed;

それだけで、キャラクターは地球を一周する。


4. 地球の中心を基準に配置する

上で求めた (x, y, z) は、原点を中心とした座標なので、 実際の地球の位置を加算する。

const pos = new THREE.Vector3(
  earthCenter.x + x,
  earthCenter.y + y,
  earthCenter.z + z
);

これで、

  • 地球がどこに置かれていても
  • スケールを変えても

同じロジックがそのまま使える。


5. キャラクターを「立たせる」

位置だけ決めても、キャラクターは正しく立たない。

球体では「上方向」は常に変わるため、 地球の中心からキャラへのベクトルを「上」として使う。

const normal = new THREE.Vector3(x, y, z).normalize();

この normal が、その地点の地表法線になる。

three.js では、 モデルの (0, 1, 0)(上方向)をこの法線に合わせることで、 キャラクターを地面に垂直に立たせられる。

const quat = new THREE.Quaternion().setFromUnitVectors(
  new THREE.Vector3(0, 1, 0),
  normal
);

character.quaternion.copy(quat);

6. 進行方向を向かせる

立たせるだけだと、キャラはどこを向いているか分からない。

そこで、球面上の接線方向を進行方向として使う。

今回、赤道(phi = Math.PI / 2)を走らせているので、 進行方向は次のように求められる。

const forward = new THREE.Vector3(
  -Math.sin(theta),
  0,
  Math.cos(theta)
).normalize();

これを「見る先」として使い、 lookAt で向きを決定する。

const target = pos.clone().add(forward);

const m = new THREE.Matrix4().lookAt(pos, target, normal);
character.quaternion.setFromRotationMatrix(m);

7. モデル固有の向き補正

ここで一つ現実的な問題が出る。

多くの3Dモデルは、

  • 前方向が -Z ではない
  • モデルごとに向きがバラバラ

という状態になっている。

そのため、最後にモデル固有の補正を入れる。

character.rotateY(-Math.PI);

この1行は「数学的に正しい」というより、 モデルを見て決める現実的な調整。


8. 2体を同時に走らせる

2体同時に走らせるのも難しくない。

やることは一つだけ。

  • theta を 180 度(π)ずらす
walkState1.theta = 0;
walkState2.theta = Math.PI;

あとは同じロジックをそれぞれに適用するだけで、 地球の反対側を同時に走るキャラクターが完成する。


9. 実際にやってみて分かったこと

簡単だと思って始めたら、思った以上に大変だった

というのが率直な感想。

理由は、

  • 平面前提の思考が通用しない
  • モデルの向き問題が地味に厄介
  • 「どの処理が姿勢を決めているか」を整理しないと破綻する

three.js と実際のモデルで破綻なく動かす には、思った以上に考えることが多かった。