ゲームプログラミングのための数学 - ベクトル
はじめに
プログラマの視点で言えば,ベクトルは
構造体になります.
配列にすることもあります.
一方で,単なる数値をスカラーと呼びます.
それぞれのメンバ変数を成分(component)と言います.
成分の数が
つまり,3D(3-dimension)のベクトルは成分が3つのベクトル,ということになります.
ゲームプログラミングで扱うベクトルは,成分が2つ,3つ,4つの場合が一般的です.
ここでは,3次元ベクトルを取り上げて,C++のコードを交えつつ説明してみます.
成分が3つなので,データ構造は次のようになります.
struct Vector3
{
float x;
float y;
float z;
};
一般的にはメンバ変数はprivateにすべきなのです.
しかし,ベクトルの場合は数学的に定義が決まっていて,隠ぺいする意味がありません.
いちいちアクセッサを経由する方が面倒です.
変更するとすれば,floatをdoubleにするくらいでしょうか.
全てpublicで良いので,classにもせず,structで実装します.
コンストラクタ
まずは,デフォルトコンストラクタを実装します.
struct Vector3
{
Vector3() noexcept = default;
};
分かりやすくするために,こんな感じで既に出た実装は省略して,
メンバ関数であればVecotr3の中に,それ以外はVector3を書かずにコードを書きます.
ここでは,デフォルトコンストラクタは何もしないことにします.
また,何もしないので例外も出ないということでnoexceptも指定しておきます.
デフォルトコンストラクタの挙動は,各成分を0で初期化するか,
何もしないかで判断が分かれるところです.
0で初期化し忘れていて,メモリ上に偶然あった値を参照してしまい,
結果としてバグになってしまう,ということもよくあります.
しかし,大量のVector3のデータを扱う場合に,
どうせ読み込んだデータで上書きするのに0初期化が入るとすると,
結構な量の無駄な処理をすることになります.
そのため,ここでは何もしないことを選びました.
例えば,有名なUnreal Engine 4にあるFVectorも,
デフォルトコンストラクタでは何もしないことを選択しています.
次に,各成分の値を渡して,それぞれの成分を初期化するコンストラクタを実装します.
struct Vector3
{
constexpr Vector3(float vx, float vy, float vz) noexcept
: x(vx)
, y(vy)
, z(vz)
{}
};
今のところ,constexprにすることで圧倒的なメリットがある,というわけではないのですが,
できるなら今のうちから対応しておいた方が良いだろう,という理由でconstexprにしています.
単純にfloatの値を代入するだけなので例外も起きないはずなので,noexceptも指定します.
なお,コード例のようにメンバ変数の初期化の際に","を先頭に持っていく書き方をすると,
変数名の開始が縦に揃うので,見やすくなる気がします.
最後に,各成分を同じ値で初期化するコンストラクタを用意します.
例えば,0で初期化したい場合に3回も0を書くのは面倒です.
そういった場合に重宝します.
struct Vector3
{
explicit constexpr Vector3(float v) noexcept
: x(v)
, y(v)
, z(v)
{}
};
explicitを忘れてはいけません.
そうしないと,意図せずfloatの値がVector3に変換されるかもしれません.
逆ベクトル
ベクトル
これを実装すると,次のようになります.
inline constexpr const Vector3 operator-(const Vector3 & v) noexcept
{
return Vector3(-v.x, -v.y, -v.z);
}
加算
ベクトル
成分別の和を成分として持つベクトルになります.
つまり,
これを実装すると,次のようになります.
struct Vector3
{
Vector3 & operator+=(const Vector3 & v) noexcept
{
x += v.x;
y += v.y;
z += v.z;
return *this;
}
};
inline constexpr const Vector3 operator+(const Vector3 & a, const Vector3 & b) noexcept
{
return Vector3(a.x + b.x, a.y + b.y, a.z + b.z);
}
加算代入は,通常の変数への加算代入と同じように変数への参照が返るようにするため,
メンバ変数として実装します.
加算は,組み込み型による加算と同じように,変更できない一時オブジェクトを返すだけなので,
通常の関数として実装します.
constexprやconst,noexceptを忘れないようにします.
ベクトルの加算は,平行移動を表すのに用いられます.
減算
減算の実装については,加算の演算子を減算の演算子に置き換えるだけなので,
説明は省きます.
struct Vector3
{
Vector3 & operator-=(const Vector3 & v) noexcept
{
x -= v.x;
y -= v.y;
z -= v.z;
return *this;
}
};
inline constexpr const Vector3 operator-(const Vector3 & a, const Vector3 & b) noexcept
{
return Vector3(a.x + b.x, a.y + b.y, a.z + b.z);
}
ベクトルの減算は,逆ベクトルの加算と考えられます.つまり,
また,始点が同じと仮定した場合,右側のベクトル
左側のベクトル
これは,カメラを考える場合に必要になります.
乗算
ベクトルの乗算には色々な種類がありますが,ここではスカラーとの積を考えます.
ベクトル
スカラーの積はそれぞれの成分にスカラーを掛けたものになります.
通常は,演算子を書かずに
これを実装すると,次のようになります.
struct Vector3
{
Vector3 & operator*=(const float s) noexcept
{
x *= s;
y *= s;
z *= s;
return *this;
}
};
inline constexpr const Vector3 operator*(const Vector3 & v, const float s) noexcept
{
return Vector3(v.x * s, v.y * s, v.z * s);
}
inline constexpr const Vector3 operator*(const float s, const Vector3 & v) noexcept
{
return Vector3(s * v.x, s * v.y, s * v.z);
}
紙の上では,スカラーは右にも左にも掛けられます.
同じことをプログラム上で実現しようとすると,仮引数の順序を考慮して,
2種類用意する必要があります.
また,成分別に積ができると便利なことが多々あるので,成分別の積も実装します.
このような成分別の処理をcomponent-wiseと言います.
struct Vector3
{
Vector3 & operator*=(const Vector3 & v) noexcept
{
x *= v.x;
y *= v.y;
z *= v.z;
return *this;
}
};
inline constexpr const Vector3 operator*(const Vector3 & a, const Vector3 & b) noexcept
{
return Vector3(a.x * b.x, a.y * b.y, a.z * b.z);
}
ベクトルに対するスカラーの乗算は,例えば拡大縮小などを表すのに用いられます.
また,この後に搭乗する除算などにも利用されます.
除算
除算は,乗算を利用して次のように実装します.
struct Vector3
{
Vector3 & operator/=(const float s)
{
return *this *= (1.0f / s);
}
};
inline constexpr const Vector3 operator/(const Vector3 & v, const float s)
{
return v * (1.0f / s);
}
一般的に,除算は乗算よりも実行に時間がかかる処理なので,逆数に変換した上で,
3回除算する代わりに,1回の除算と3回の乗算にするわけです.
乗算と同様に,成分別の除算も実装します.
struct Vector3
{
Vector3 & operator/=(const Vector3 & v)
{
x /= v.x;
y /= v.y;
z /= v.z;
return *this;
}
};
inline constexpr Vector3 operator/(const Vector3 & a, const Vector3 & b)
{
return Vector3(a.x / b.x, a.y / b.y, a.z / b.z);
}
ゼロ除算
除算をする場合,0で割ってしまうことを考慮する必要があります.
仕様上は,0で除算(/)や剰余(%)を求めることは未定義動作となっています.
場合によっては,NaN(Not a Number)という特殊な値になります.
このNaNを計算に混ぜてしまうと,すべてNaNになってしまうので,
非常にやっかいです.
ただ,ゲームプログラミングではベクトルの除算も非常に多く行われるため,
常にチェックしていると処理に時間がかかりすぎて画面の更新が遅れ,
表示がカクカクしてしまいます.
そのため,デバッグ時のみだったり,特定のマクロが定義されている場合のみ
除数のチェックをするようにしたりします.
Unreal Engine 4の場合,0での除算はしてしまった上で,NaNになっていないかどうかを
コンストラクタでチェックするようにしています.
DirectXMathでは特にチェックはしていないようです.
ドット積(dot product)
あるベクトル
またはドット積と言います.
ドット積という理由は,
式で書くと,次のようになります.
struct Vector3
{
constexpr float dot(const Vector3 & v) const noexcept
{
return x * v.x + y * v.y + z * v.z;
}
};
このドット積は,次の長さを求めるのに利用したり,他にも色々なところで利用されます.
長さ
あるベクトル
ベクトルの長さは,それぞれの成分の2乗の和の平方根になります.
つまり,ベクトル
次のような式になります.
これは,ドット積を使って次のような式で表せます.
そのため,ベクトルの長さを返すメンバ関数の実装は次のようになります.
#include <cmath>
struct Vector3
{
float length() const
{
return std::sqrt(dot(*this));
}
};
std::sqrtはconstexprでないため,必然的にlength関数もconstexprではなくなります.
ところで,平方根の計算はまぁまぁ重い処理のため,避けられるなら避けたいところです.
あるベクトル
これを利用すると,どちらのベクトルの方が長いか,といったことを知りたいだけの場合,
平方根を取らない状態の値で十分ということになります.
そこで,長さの二乗を返す関数lengthSquaredを用意して,次のような実装にすることもあります.
struct Vector3
{
constexpr float lengthSquared() const noexcept
{
return dot(*this);
}
float length() const
{
return std::sqrt(lengthSquared());
}
};
ドット積と長さと角度
あるベクトル
このことから,内積の符号は2つのベクトルが成す角度で決まることが分かります.
掛け算だけの処理なので,手軽に前後の判定ができることになります.
先ほどの式を
更に,
これを実際のコードに落とし込むと,次のようになります.
float theta = acos(u.dot(v) / (u.length() * v.length()));
acos(
単位ベクトル(unit vector)
長さが1のベクトルを単位ベクトルと言います.
また,あるベクトル
正規化(normalize)と言います.
単位ベクトルを返すメンバ関数の名前をunitとすると,次のような実装になります.
struct Vector3
{
inline const Vector3 unit() const;
};
inline const Vector3 Vector3::unit() const
{
return *this / length();
}
一般的には,単位ベクトルを返す関数はnormalizeという名前になります.
しかし,normalizedという名前が使われることもあります.
また,変数を書き換えて単位ベクトルに変えてしまうような場合もあります.
この分かりにくさを避けるために,最近はunitという関数名を使うようにしています.
なお,他のメンバ関数と違い,構造体の外に定義を書いているのは,除算を利用するために,
実装を除算の後に回す必要があるからです.
また,constexprでないlength関数で割る関係上,unit関数もconstexprにはできません.
なお,長さ0のベクトルの単位ベクトルを求めてしまうと,0で除算することになってしまうので
注意しましょう.
クロス積(cross product)
3次元ベクトルの場合,あるベクトル
両方に直交するベクトル
struct Vector3
{
constexpr const Vector3 cross(const Vector3 & v) const noexcept
{
return Vector3(
y * v.z - z * v.y,
z * v.x - x * v.z,
x * v.y - y * v.x
);
}
};
なお,このベクトルの向きは2つのベクトルに直交していますが,大きさについては次の式が成り立ちます.
数学的に正しいかどうかは検討していないのですが,私は次のような覚え方をしています.
まず,
すると,次の式が成り立ちます.
つまり,
ある2つのベクトルのクロス積で次の順番のベクトルが求まるわけです.
なお,順番を遡る場合,求まるベクトルは
さて,ここで
念のために
このようにパターンさえ覚えてしまえば,細かいことは分からなくても,
検索しなくてもクロス積を実装できるようになります.
なお,クロス積は日本語では外積と言われたりもするんですが,外積を英語にすると,
outer productやexterior productとなります.
別の単語がある,ということは別の概念があるということで,勘違いの元でもあるので,
ここでは外積という言葉は使わず,クロス積という言葉を使っています.
プロジェクション(projection)
数学的にはもう少し厳密な意味があるのですが,ここではプロジェクション(projection)とは,
あるベクトル
プロジェクションベクトル
そして,方向は
このことから,プロジェクションベクトル
この式の右辺に,
1を掛けているだけなので,結果は変わりません.
これを実装すると,次のようになります.
struct Vector3
{
inline constexpr const Vector3 projection(const Vector3 & v) const;
};
inline constexpr const Vector3 projection(const Vector3 & v) const
{
return (dot(v) / v.dot(v)) * v;
}
乗算を利用するために,実装は乗算の宣言の後に回します.
ちなみに,プロジェクションは日本語だと射影とか投影と表現されています.
これもカメラを考える場合に役立ちます.
ここでも,除算があるので0で割らないように注意しましょう.
リジェクション(rejection)
リジェクション
プロジェクション
この式を変形すると,次の式になります.
よって,プロジェクションを利用することでリジェクションは次のように簡単に実装できます.
struct Vector3
{
inline constexpr const Vector3 rejection(const Vector3 & v) const;
};
inline constexpr const Vector3 rejection(const Vector3 & v) const
{
return *this - projection(v);
}
減算を利用しているので,実装は減算の後ろに回します.
中で利用しているプロジェクションに除算が含まれるので,
リジェクションも長さ0のベクトルを渡さないように注意しましょう.
なお,このリジェクションに対応する日本語が良く分からないので,ご存じの方がいれば,
教えていただけると幸いです.
軸に沿った分解
実装とは関係無いのですが,理解しておくとこの後の説明が分かりやすくなります.
これは,それぞれ3次元のx軸,y軸,z軸を表しています.
あるベクトル
さて,ここで
仕組み上,
そして,
先ほどみたような感じですね.もう少し変形してみましょう.
すると,次のような式になります.
つまり,ベクトル
さて,プロジェクションの解説で登場した
この式から,単位ベクトルとのドット積は,その単位ベクトルを軸としたときの成分を求める計算になる,ということが分かります.
法線(normal)
こちらも実装とは関係無いのですが,よく出てくる用語なので説明しておきます.
法線とは,平面に対して垂直な長さ1のベクトルです.
一般的には,
反射(reflection)
物理的な反射を考える場合,摩擦や反発係数などを考える必要があるのですが,
ここでは理想的な反射をする場合を考えます.
次の図のように,ある面に入ってくる光の方向を表すベクトルを
反射ベクトル
ここで,
struct Vector3
{
inline constexpr const Vector3 reflection(const Vector3 & n) const noexcept;
};
inline constexpr const Vector3 reflection(const Vector3 & n) const noexcept
{
return *this + (-2.0f * dot(n)) * n;
}
屈折(refraction)
距離(distance)
あるベクトル
よって,実装は次のようになります.
struct Vector3
{
inline float distance(const Vector3 & v) const;
};
inline float Vector3::distance(const Vector3 & v) const
{
return (v - *this).length();
}
減算を利用したいので,実装を後に回すためにdistance関数は宣言だけして,
定義は減算の後ろに回しています.
なお,Unreal Engine 4だと次のように一時的なVector3の構築が起きないようになってます.
inline float square(const float v) noexcept
{
return v * v;
}
struct Vector3
{
float distance(const Vector3 & v) const
{
return std::sqrt(square(v.x - x) + square(v.y - y) + square(v.z - z));
}
};
実際のところ,どちらの実装の方が効率が良いのかは計測してみないと分からないです.
最近のコンパイラは賢いので,もしかしたら良い感じにしてくれるかもしれません.
今後の課題
- SIMD(Single Instruction Multiple Data)を使った高速化
- 単体テストの追加
その他の話題
2次元ベクトルは,昔ながらの2Dゲームなどで使えそうだと気付くかもしれませんが,
4次元ベクトルは何に使うのでしょうか?
実は,4次元ベクトルは行列と組み合わせて使うと, 拡大縮小や回転と同時に
平行移動も表現できるのです.
追加の実装
UnityやUnreal Engineでは,文字列に変換する関数も提供されていたりします.
この辺は,ベクトルの実装そのものとは直接関係無いのでここでは省きますが,
シリアライズなども考慮して実装してみるのも面白いかもしれません.
参考文献
- 実例で学ぶゲーム3D数学
- Mathematics for 3D Game Programming and Computer Graphics Third Edition
- Foundations of Game Engine Development Volume 1: Mathematics
- DirectXMath
- glm
- Unreal Engine 4(FVector)
- Unity(UnityEngine.Vector3)
ソースコード全文
#ifndef VECTOR3_H_INCLUDED
#define VECTOR3_H_INCLUDED
#include <cmath>
struct Vector3
{
float x;
float y;
float z;
Vector3() noexcept = default;
constexpr Vector3(float vx, float vy, float vz) noexcept
: x(vx)
, y(vy)
, z(vz)
{}
explicit constexpr Vector3(float v) noexcept
: x(v)
, y(v)
, z(v)
{}
Vector3 & operator+=(const Vector3 & v) noexcept
{
x += v.x;
y += v.y;
z += v.z;
return *this;
}
Vector3 & operator-=(const Vector3 & v) noexcept
{
x -= v.x;
y -= v.y;
z -= v.z;
return *this;
}
Vector3 & operator*=(const float s) noexcept
{
x *= s;
y *= s;
z *= s;
return *this;
}
Vector3 & operator*=(const Vector3 & v) noexcept
{
x *= v.x;
y *= v.y;
z *= v.z;
return *this;
}
Vector3 & operator/=(const float s)
{
return *this *= (1.0f / s);
}
Vector3 & operator/=(const Vector3 & v)
{
x /= v.x;
y /= v.y;
z /= v.z;
return *this;
}
constexpr float dot(const Vector3 & v) const noexcept
{
return x * v.x + y * v.y + z * v.z;
}
float length() const
{
return std::sqrt(dot(*this));
}
inline const Vector3 unit() const;
constexpr const Vector3 cross(const Vector3 & v) const noexcept
{
return Vector3(
y * v.z - z * v.y,
z * v.x - x * v.z,
x * v.y - y * v.x
);
}
inline constexpr const Vector3 projection(const Vector3 & v) const;
inline constexpr const Vector3 rejection(const Vector3 & v) const;
inline constexpr const Vector3 reflection(const Vector3 & n) const noexcept;
inline float distance(const Vector3 & v) const;
};
inline constexpr const Vector3 operator-(const Vector3 & v) noexcept;
inline constexpr const Vector3 operator+(const Vector3 & a, const Vector3 & b) noexcept;
inline constexpr const Vector3 operator-(const Vector3 & a, const Vector3 & b) noexcept;
inline constexpr const Vector3 operator*(const Vector3 & v, const float s) noexcept;
inline constexpr const Vector3 operator*(const float s, const Vector3 & v) noexcept;
inline constexpr const Vector3 operator*(const Vector3 & a, const Vector3 & b) noexcept;
inline constexpr const Vector3 operator/(const Vector3 & v, const float s);
inline constexpr const Vector3 operator/(const Vector3 & a, const Vector3 & b);
//==============================================================================
// Inline functions
//==============================================================================
inline const Vector3 Vector3::unit() const
{
return *this / length();
}
inline constexpr const Vector3 Vector3::projection(const Vector3 & v) const
{
return (dot(v) / v.dot(v)) * v;
}
constexpr const Vector3 Vector3::rejection(const Vector3 & v) const
{
return *this - projection(v);
}
inline constexpr const Vector3 Vector3::reflection(const Vector3 & n) const noexcept
{
return *this + (-2.0f * dot(n)) * n;
}
inline float Vector3::distance(const Vector3 & v) const
{
return (v - *this).length();
}
inline constexpr const Vector3 operator-(const Vector3 & v) noexcept
{
return Vector3(-v.x, -v.y, -v.z);
}
inline constexpr const Vector3 operator+(const Vector3 & a, const Vector3 & b) noexcept
{
return Vector3(a.x + b.x, a.y + b.y, a.z + b.z);
}
inline constexpr const Vector3 operator-(const Vector3 & a, const Vector3 & b) noexcept
{
return Vector3(a.x - b.x, a.y - b.y, a.z - b.z);
}
inline constexpr const Vector3 operator*(const Vector3 & v, const float s) noexcept
{
return Vector3(v.x * s, v.y * s, v.z * s);
}
inline constexpr const Vector3 operator*(const float s, const Vector3 & v) noexcept
{
return Vector3(s * v.x, s * v.y, s * v.z);
}
inline constexpr const Vector3 operator*(const Vector3 & a, const Vector3 & b) noexcept
{
return Vector3(a.x * b.x, a.y * b.z, a.z * b.z);
}
inline constexpr const Vector3 operator/(const Vector3 & v, const float s)
{
return v * (1.0f / s);
}
inline constexpr const Vector3 operator/(const Vector3 & a, const Vector3 & b)
{
return Vector3(a.x / b.x, a.y / b.y, a.z / b.z);
}
#endif // VECTOR3_H_INCLUDED
Discussion