[Three.js] ベクトルの正規化(Normalize)、なぜわざわざ長さを1にするのか?
ベクトルの長さを1にする「正規化」の本質を、ゲームの移動ロジックやBlenderの「Apply Scale」の事例から紐解きます。方向と大きさを分離する理由と、実무での活用チップをまとめました。
- #Three.js
コメント
入力したパスワードは秘密コメントの閲覧、修正、削除に使われます。
まだコメントがありません。
ベクトルの長さを1にする「正規化」の本質を、ゲームの移動ロジックやBlenderの「Apply Scale」の事例から紐解きます。方向と大きさを分離する理由と、実무での活用チップをまとめました。
入力したパスワードは秘密コメントの閲覧、修正、削除に使われます。
まだコメントがありません。
Three.jsを勉強していると、ベクトルの後に儀式のように付いている .normalize() をよく目にします。講義では「便利だから」という言葉だけで片付けられがちですが、むしろそれが気になって仕方がありませんでした。
なぜ、まともなベクトルをあえて長さ1にするのか? いつ必要で、いつは不要なのか?
考えてみると、これは初めて出会う概念ではありませんでした。私は普段 Blender をよく使いますが、スケールを調整した後には必ず Apply Scale をしろと言われてきました。理由はいつも「後で不具合が出るから」でしたが、なぜ 不具合が出るのかを理解しないまま、習慣のように押してきました。
Three.jsの正規化を深掘りしてみると、Blenderでのあの行動が、実は 同じ問題を別の方法で解決していた ことが見えてきました。
正規化とは、ベクトルの 方向は維持したまま、長さだけを1にする作業 です。
「どれだけ遠くに行くか」は捨て
「どの方向を指しているか」だけを残す
そのため、正規化されたベクトルは通常このように使われます。
これらには共通点があります。 👉 大きさより「方向」が重要な情報 だという点です。
まず、チェスや昔のポケモンのような格子ゲームを思い出してみました。これらの世界は、事実上 連続空間ではありません。
斜め移動は数学的に見れば √2 ≈ 1.41 ですが、ゲームのルール上は単なる 「一回の移動」 です。
📌 重要なのは正確な距離の値ではなくルール なので、「斜めの方が速くないか?」といった問題は最初から発生しません。
Three.jsで扱う空間は全く異なります。
(x, y, z)ここで正規化を行わないと、問題がすぐに露呈します。
入力 W + D → (1, 1)。このベクトルの長さは √2 ≈ 1.41 です。
つまり、同じフレームの間:
この差は単なる体感速度の問題ではありません。 実際の座標値がより多く移動してしまうのです。
自由移動のゲームでは、通常このような流れをたどります。
(1, 1) = 「斜めに行きたい」)。| 移動 | 入力ベクトル | 実際の長さ | 正規化後 |
|---|---|---|---|
| 右 | (1, 0) | 1.0 | 1.0 |
| 上 | (0, 1) | 1.0 | 1.0 |
| 斜め | (1, 1) | 1.41 | 1.0 |
👀 見た目には斜めに行きますが、 📐 座標値基準では常に同じ距離だけ移動 します。
ここで Blender が繋がります。Blenderの 法線ベクトル (Normal Vector) は、ライティング計算で「面の方向」を表します。そして、この法線は 常に長さ1であるという前提 で計算されます。
しかし、スケールを変更して Apply Scale をしないと:
照明計算は通常 内積 (Dot Product) を使います。
0 ~ 1(私たちが期待する明るさ)。この値は:
👉 Apply Scale は結局、 「これらのベクトルをもう一度長さ1基準に合わせろ」 という意味だったのです。
.normalize() は 元のベクトルを直接書き換えます。
もし位置や力の大きさを維持する必要があるなら、 必ずコピー(クローン)して使いましょう。
const direction =
すべての場所に付ける必要はない。
しかし、「方向」という言葉が浮かんだ瞬間、ほぼ間違いなく使用する。
OrbitControls などを使えば内部で処理されますが、自分で作る場合は:
mesh.position.x += 1; (単純な座標移動)。ここで正規化をしないと、 距離によって速度が変わってしまいます。
Ray は 方向ベクトル です。
raycaster.setFromCamera(mouse, camera);
内部では方向ベクトルが正規化されている前提で計算されます。
内蔵マテリアルなら自動で処理されますが、以下のような場合は介入が必要です。
vec3 lightDir = normalize(lightPos - vPos); が必須です。正規化は単なる数学的なトリックではありません。
.normalize() を使う理由が、これで明確になりました。
.normalize() → (0.7, 0.7)。const v = new THREE.Vector3(3, 4, 0);
v.normalize(); // v は (0.6, 0.8, 0) になる
const direction = target.clone().sub(mesh.position).normalize();
mesh.position.add(direction.multiplyScalar(speed));
mesh.scale.set(10, 1, 1);