「内積・外積って結局何に使うの?」3Dゲームの視界判定でわかるベクトルの真価

コラム

高校数学でベクトルを学ぶと、多くの人がこんな疑問にぶつかります。

内積とか外積って、テストの計算はできるけど、いったい何に使うか全然イメージが湧かない……。ただのパズルみたいに感じます。 すごくよくわかります。でも実はこれ、3Dゲームの世界を作る上で絶対に欠かせない最強の道具なんです。

たとえば、3Dゲームで「敵がプレイヤーを発見するシステム」はどう作られているのでしょうか? 「プレイヤーの座標」と「敵の座標」から複雑な角度の計算をしている……と思いきや、実は難しい角度の計算をしなくても、「内積と外積」だけで一発で判定できるのです。

この記事では、「テストのための数学」から「モノを作るための数学」へと視点を切り替え、ベクトルの真価を解き明かします。

1. 内積とは

内積を理解するには、「教科書の公式」と「プログラミング用の公式」の2つを繋ぎ合わせる必要があります。まずは直感的なイメージから見ていきましょう。

高校数学で習う内積の定義は、次のようなものでした。

AB=ABcosθ\vec{A} \cdot \vec{B} = |\vec{A}||\vec{B}|\cos\theta

もし、2つのベクトルの長さ(A|\vec{A}|B|\vec{B}|)がどちらも「1」だったらどうなるでしょうか?(※長さを1に揃えることを正規化と呼びます) 長さが1同士なら、公式は驚くほどシンプルになります。

AB=cosθ\vec{A} \cdot \vec{B} = \cos\theta

つまり、長さを1に揃えたベクトルの内積は、純粋に **「間の角度のコサイン(cosθ\cos\theta)」**になるのです。 これが何を意味するのか、具体的な角度で見てみましょう。内積の値は、2つのベクトルの 「方向のシンクロ率」 を表すパラメーターに化けます。

  • 00^\circ(完全に同じ向き): 内積は 1.01.0(シンクロ率100%)
  • 6060^\circ(少し斜め前): 内積は 0.50.5
  • 9090^\circ(真横・直角): 内積は 0.00.0(シンクロ率0%)
  • 120120^\circ(少し斜め後ろ): 内積は 0.5-0.5
  • 180180^\circ(完全に真逆): 内積は 1.0-1.0

このように、「何度か?」という具体的な角度を出さなくても、1.01.0-1.0 \sim 1.0 の数値を見るだけで「だいたいどのくらい同じ方向を向いているか」が一目で分かるわけです。

なるほど、向きが同じかどうかが直感的に分かるんですね! ……でも、それって普通に「角度(度数)」を直接計算して判定しちゃダメなんですか?わざわざ内積の数値で判定する理由がわかりません。 そこがゲーム開発における最大のポイントです!実は、コンピュータにとって「角度を直接求める計算」はめちゃくちゃ重い(処理に時間がかかる)んです。

ベクトルから直接「角度(θ\theta)」を求めようとすると、逆三角関数(arccos\arccos / アークコサイン)という非常に複雑な計算を行わなければなりません。1秒間に何万回、何十万回と視界判定や衝突判定を行う3Dゲームにおいて、そんな重い計算を多用していたらゲームの動作が重くなってしまいます。

そこで登場するのが、内積の 「もう一つの公式(成分表示)」 です。

A=(Ax,Ay,Az)\vec{A}= (A_x , A_y , A_z) ,  B=(Bx,By,Bz)\vec{B} = (B_x , B_y , B_z) とするとき、

AB=AxBx+AyBy+AzBz\vec{A} \cdot \vec{B} = A_x B_x + A_y B_y + A_z B_z

となります。

なんと、各軸(X, Y, Z)の成分同士を 「ただ掛けて足すだけ」 で内積が求まってしまいます!ここには sin\sincos\cosarccos\arccos も一切登場しません。

これが、内積がゲーム開発で愛される本当の理由です。 内積とは、 「処理の重い角度計算を一切せずに、超高速な『掛け算と足し算』だけで、2つのベクトルがどれくらい同じ方向を向いているかを知るための天才的手法」 なのです。

2. 内積を使った視界判定の計算

第1章の知識を使って、実際にゲームの視界判定を作ってみましょう。 やりたいことは「敵キャラクターの視界(例えば前方 9090^\circ )に、プレイヤーが入っているかを判定する」ことです。

用意するのは、次の2つのベクトルです。

  1. 敵の正面ベクトル(敵が今向いている方向)
  2. 敵からプレイヤーへ向かうベクトル(プレイヤーの位置 - 敵の位置)

内積で視界をスパッと切り取る

無事に2つのベクトルの長さを「1」に揃えられたら、あとは「掛け算と足し算(成分の計算)」で内積を出します。

さて、敵の視界が前方 9090^\circ(中心から左右に 4545^\circ ずつ)だったとしましょう。 cos450.707\cos 45^\circ \approx 0.707 です。

第1章で、「長さを1に揃えた内積の値は cosθ\cos\theta になる」と学びましたよね。 00^\circ(真正面)なら 1.01.04545^\circ(視界のギリギリ端っこ)なら 0.7070.707。 つまり、内積の計算結果が 0.7070.707 より大きければ、プレイヤーは視界に入っていると一瞬で判定できるのです!

(敵の向いている方向)(敵からプレイヤーへの方向)>0.707(\text{敵の向いている方向}) \cdot (\text{敵からプレイヤーへの方向}) > 0.707

この式を満たすときにプレイヤーは敵の視界に入ったことになるのです。

あっ!ということは、ゲームの裏側では「プレイヤーが敵から見て何度($^\circ$)の位置にいるか」は、一度も計算されていないってことですか!? その通りです!これが内積の魔法です。

重い角度計算(arccos\arccos)を完全にスルーして、「内積の値が 0.7070.707 より大きいか?」というシンプルな条件式だけで、見事に視界の扇形を切り取ることができました。

ただし、ここでゲーム開発特有の絶対に外せない落とし穴があります。 計算前に、絶対に2つのベクトルの長さを1(正規化 / Normalize)にしておくことです!

先ほど「長さが1なら内積は cosθ\cos\theta になる」と言いました。 正規化を忘れると、プレイヤーが遠くにいるだけで内積の値が巨大になってしまい、視界の計算がめちゃくちゃにバグります。「遠くにいる敵の背後を通ったのに見つかった」といった謎のバグは、この正規化忘れが原因であることが多いです。

3. シミュレーターで実験

理屈が分かったところで、実際にどう動くのかをブラウザ上で体験してみましょう。

中央の青いキャラクターが「敵」、緑(赤)が「プレイヤー」です。薄い青色の範囲が敵の視界範囲です。 プレイヤーをドラッグで動かすと、リアルタイムで「内積の値」が計算されて表示されます。内積と視界判定の関係性を試してみてください。

4. 外積の利用

内積で「見えているか(前方にいるか)」は分かりました。 次に必要なのは、 「見えている相手が右にいるのか、左にいるのか」 です。ここで使うのが外積です。

高校数学では外積を「平行四辺形の面積」として学びますが、ゲームでは次の性質が特に重要です。

  • 外積は、2つのベクトルに垂直なベクトルを作る
  • ベクトルを掛ける順番で、向き(符号)が変わる

この「向き」が左右判定に直結します。

左右判定の実装イメージ

  1. 敵の正面ベクトル敵→プレイヤーベクトル の外積を取る
  2. その結果を ワールド上方向ベクトル(Up / Z軸) と内積する
  3. 符号で左右を決める
  • > 0 なら右
  • < 0 なら左
  • = 0 ならほぼ真正面(または真後ろ)

つまり、内積が「前後判定」、外積(+Upとの内積)が「左右判定」を担当します。 この2つを組み合わせることで、敵AIは「視界内に入ったプレイヤーが右側にいるから右へ回避」などの自然な行動を作れます。

めんどくさいように思えますがやってることは四則演算だけなので、コンピューターの世界ではとても効率のいい、左右の判定方法なんです。

5. 実際の使用例(Unreal Engine 5 実装編)

ここからは実践として、「UEでどう書くか」を最小構成で見ます。 C++に詳しくない方は飛ばしていただいて構いません。

1. まずは必要なベクトルを取得する

視界判定で必要なのは、基本的に次の3つです。

  • 敵の正面方向 Forward
  • 敵からプレイヤーへの方向 ToPlayer
  • ワールド上方向 Up(通常 FVector::UpVector
const FVector EnemyPos = EnemyActor->GetActorLocation();
const FVector PlayerPos = PlayerActor->GetActorLocation();

const FVector Forward = EnemyActor->GetActorForwardVector();
const FVector ToPlayerRaw = PlayerPos - EnemyPos;
const FVector Up = FVector::UpVector;

2. ゼロ除算・NaN対策をして正規化する

ここが実装の重要ポイントです。 ToPlayerRaw がゼロ長(同座標など)のときに普通の正規化をすると壊れます。

const FVector ForwardN = Forward.GetSafeNormal();
const FVector ToPlayerN = ToPlayerRaw.GetSafeNormal();

if (ToPlayerN.IsNearlyZero())
{
    // 同じ位置などで方向が定義できないので、判定をスキップ
    return;
}

GetSafeNormal() を使うことで、ゼロ長時の危険を回避できます。

3. 内積を計算して視界内か判定する

視界角(半角)を ViewHalfAngleDeg とすると、

const float Dot = FVector::DotProduct(ForwardN, ToPlayerN);
const float Threshold = FMath::Cos(FMath::DegreesToRadians(ViewHalfAngleDeg));

const bool bInSight = (Dot >= Threshold);

これで「前方扇形に入っているか」を高速に判定できます。

4. 外積で左右を判定する(視界内のときだけ)

const FVector Cross = FVector::CrossProduct(ForwardN, ToPlayerN);
const float Side = FVector::DotProduct(Cross, Up);

// Side > 0: 右 / Side < 0: 左

必要なら FMath::IsNearlyZero(Side) で「ほぼ中央」を特別扱いします。

5. ifに組み込むとこうなる

if (bInSight)
{
    if (Side > KINDA_SMALL_NUMBER)
    {
        // 右
    }
    else if (Side < -KINDA_SMALL_NUMBER)
    {
        // 左
    }
    else
    {
        // ほぼ正面
    }
}
else
{
    // 視界外
}

まとめコード(最小版)

#include "Math/Vector.h"
#include "Math/UnrealMathUtility.h"

void CheckSightAndSide(const AActor* EnemyActor, const AActor* PlayerActor, float ViewHalfAngleDeg)
{
    if (!EnemyActor || !PlayerActor) return;

    const FVector EnemyPos = EnemyActor->GetActorLocation();
    const FVector PlayerPos = PlayerActor->GetActorLocation();

    const FVector ForwardN = EnemyActor->GetActorForwardVector().GetSafeNormal();
    const FVector ToPlayerN = (PlayerPos - EnemyPos).GetSafeNormal();
    if (ToPlayerN.IsNearlyZero()) return; // ゼロ長ガード

    const float Dot = FVector::DotProduct(ForwardN, ToPlayerN);
    const float Threshold = FMath::Cos(FMath::DegreesToRadians(ViewHalfAngleDeg));
    const bool bInSight = (Dot >= Threshold);

    if (bInSight)
    {
        const FVector Cross = FVector::CrossProduct(ForwardN, ToPlayerN);
        const float Side = FVector::DotProduct(Cross, FVector::UpVector);
        // Side > 0: 右 / Side < 0: 左 / ほぼ0: 正面付近
    }
}

6. まとめ

内積や外積は、公式だけ見ると難しく感じます。 でも今回見たように、意味はとてもはっきりしています。

  • 内積は「どれくらい同じ向きを向いているか」を数で表す
  • 外積は「どちら側にあるか(右か左か)」を見分ける

つまりベクトルは、図形の感覚をそのまま計算にできる道具です。 角度や位置関係の直感が、式に変わり、さらに判定に変わります。

数学を学ぶときに大切なのは、公式を暗記することだけではありません。 「この式は何を言っているのか」 を言葉で説明できるようになることです。 内積・外積は、その練習にとても向いています。

「これ、将来使うのかな?」と思う内容ほど、実は現実の仕組みの中で静かに活躍しています。 今回の視界判定のように、数学は問題集の中だけで終わらない力を持っています。

高橋 アイコン 高橋