はじめに
以前、Three.jsで地球と月のミニチュアを実装しましたが、地球の上をキャラクターが走りながら回転するアニメーションを実装してみたのでそのメモです。
[JavaScript] Three.jsで地球と月のミニチュアを実装
地球と月の模型をThree.jsで作成し、リアルな動きとエフェクトを加える方法をステップバイステップで学びます。
https://humanxai.info/posts/javascript-creating-miniature-earth-and-moon/動画(パソコン)
動画(VR)
モデル制作者は、ニコニコ立体の"schwarz"さん。
"author": "schwarz",
"License": "CC Attribution",
"url": "https://3d.nicovideo.jp/works/td1809",
「博麗霊夢」 / schwarz さんの作品 - ニコニ立体
3D投稿サービス「ニコニ立体」
https://3d.nicovideo.jp/works/td1809"author": "schwarz",
"License": "CC Attribution",
"url": "https://3d.nicovideo.jp/works/td22298",
「ただの魔法使い」 / schwarz さんの作品 - ニコニ立体
3D投稿サービス「ニコニ立体」
https://3d.nicovideo.jp/works/td222981. やりたかったこと
やりたかったことは、とても単純だった。
- 地球の上をキャラクターが走り続ける
- ただし平面ではなく、球体の表面
よくある「地面を移動するキャラ」ではなく、 球体そのものを地形として扱う、というのがポイント。
見た目としては、
- キャラクターが地球の縁を越えても落ちない
- 常に地表に立っている
- 走り続けると、そのまま地球を一周する
という状態を目指した。
一見すると簡単そうに見えるが、 実際には「平面前提の移動ロジック」がほとんど使えなくなる。
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 と実際のモデルで破綻なく動かす には、思った以上に考えることが多かった。
💬 コメント