Skip to main content

線型代数学

ベクトル

 ベクトルとは、一般的には「大きさと向きをもつ量」であり「矢印で表すことのできる量」である。

image.png

 2次元のベクトルは2つの成分(x, y)があり、3次元の場合は3の成分(x, y, z)がある。Unityでは Vector2Vector3 の構造体があり、それぞれのベクトルを作成ができる。

Vector2 vec2D = new Vector2(vx, vy);     // vxとvyはfloatである
Vector3 vec3D = new Vector3(vx, vy, vz); // vzもfloatである

 ゲームの中で、ベクトルをよく使うものである。例えば、3D空間で、キャラクターの速度が「左向き:秒速2メートル、前方:秒速10メートル」であるとしたら…

// 3D空間だと、x:左右、y:上下、z:前後
Vector3 velocity = new Vector3(-2, 0, 10);

 また、ゲームの場合は、向きと量はもちろんそうであるが、その他に「位置」もベクトルで表現する。

// プレーヤーはシーンの中心点から前方100メートルにいる
Vector3 playerPosition = new Vector3(0, 0, 100);

演算

 やはり、ベクトルは重要なので、ベクトルの演算を学ばないといけない。

加算

 2つのベクトルを足すことができる。その結果は各成分の足し算である:

2次元の場合

image.png

3次元の場合

image.png

Unityでは「+」の演算子で2つのベクトルを足すことができる

Vector3 v = new Vector3(vx, vy, vz); // vx, vyとvzは何かのfloat型の数値
Vector3 w = new Vector3(wx, wy, wz); // wx, wyとwzは何かのfloat型の数値
Vector3 vw = v + w;

 幾何学的に、足し算は2つの矢印を連続し、1つ目のベクトルの始点から2つ目のベクトルの終点までに新しいベクトルを作成するイメージ:

image.png

減算

 引き算は同じ形で行う:

image.png

Vector3 v = new Vector3(vx, vy, vz); // vx, vyとvzは何かのfloat型の数値
Vector3 w = new Vector3(wx, wy, wz); // wx, wyとwzは何かのfloat型の数値
Vector3 vw = v - w;

 幾何学的には、引くベクトルの向きを反転してから連続に繋がる

image.png

練習

加算と減算を使い、2つのオブジェクトを相対的に移動

  • シーンに2つの3Dゲームオブジェクトを追加し、距離的に放してください。
    • 1つ目の名前は「Control」
    • 2つ目の名前は「Follower」
  • スクリプト「VectorTest」を作成し…
    • Follower と Control の相対位置を求める(減算)
    • Control をシーン内で動かしたら、Follower が移動を真似する(加算)
スクリプト:VectorTest
// ベクトル演算の練習」
public class VectorTest : MonoBehaviour
{
    // 移動すべきのオブジェクト
    [SerializeField] private GameObject control;

    // controlを追跡するオブジェクト
    [SerializeField] private GameObject follower;

    // controlとfollowerの相対位置
    private Vector3 relativePosition;
    
    private void Start()
    {
        // 初期位置を取得
        var v = control.transform.position;
        var w = follower.transform.position;
        
        // 相対位置を計算(減算で)
        relativePosition = w - v;
    }

    private void Update()
    {
        // followerの位置が、controlの位置 + 相対位置
        follower.transform.position = 
            control.transform.position + relativePosition;
    }
}

実数との掛け算、割り算

 ベクトルを実数との掛け算ができる。各成分との掛け算になる:

image.png

Unityでは、普通の掛け算演算子「*」で計算できる:

Vector3 v = new Vector3(vx, vy, vz); // vx, vyとvzは何かのfloat型の数値
Vector3 nv = v * n;                  // nは何かの実数(float)である

練習

先ほど作った「VectorTest」を更新し、相対位置の「倍率(scale)」を Inspector で変えられるようにする。

スクリプト:VectorTest
// ベクトル演算の練習」
public class VectorTest : MonoBehaviour
{
    // 移動すべきのオブジェクト
    [SerializeField] private GameObject control;

    // controlを追跡するオブジェクト
    [SerializeField] private GameObject follower;

    // 相対位置の倍率
    [SerializeField] private float scale = 1;
    
    // controlとfollowerの相対位置
    private Vector3 relativePosition;
    
    private void Start()
    {
        // 初期位置を取得
        var v = control.transform.position;
        var w = follower.transform.position;
        
        // 相対位置を計算(減算で)
        relativePosition = w - v;
    }

    private void Update()
    {
        // 倍率を適用する
        Vector3 scalePos = relativePosition * scale;
        
        // followerの位置が、controlの位置 + 相対位置
        follower.transform.position = 
            control.transform.position + scalePos;
    }
}

ベクトルの長さ

場合により、ベクトルの長さを求めたいときもある。これには「ピタゴラスの定理」(三平方の定理)を利用する。ピタゴラスの定理を思い出しましょう。直角三角形があれば…

image.png

c 辺の長さは:

image.png

つまり…

image.png

これはベクトルの成分とよく似ているので:

image.png

これができる!

image.png

ここで || v || はベクトルの「ノルム」といわれ、ベクトルの長さを表している。Unityでは、Vectorの便利な「magnitude」がある:

Vector2 p = new Vector2(3, 4);
float len = p.magnitude; // len は p の長さ(5)である

単位ベクトル

 ベクトルの長さを「1」にすると、そのベクトルは単位円の中に入り、各成分がsinとcosを一致させることができる。その他にも、ベクトルの長さを調整する前に、まず長さを1に戻さないといけないが必要である。その場合は単位ベクトルを求めないといけない。

image.png

 単位ベクトルを求めるのは、ベクトルをノルムで割るだけ:

image.png

Unityでは「normalized」というプロパティーで単位ベクトルを求めることができる。

// 任意の長さのベクトル v があるとして:
Vector3 v = new Vector3(30, 12, -15);

// uはvと同じ向き(方向)であるが、長さは「1」である(単位ベクトル)
Vector3 u = v.normalized;

ベクトル同士の”掛け算”(内積と外積)

 ベクトル同士の掛け算はできないが、2つの特別な関数がある。内積(dot product)と外積(cross product)。それぞれは限られた使い方があるが、必要な時にとても便利である。

内積

 内積の定義は以下の通りになる:

image.png

image.png

 ある2つのベクトル「v」と「w」の内積は、それぞれのベクトルのノルム(長さ)とその2つのベクトルの間の角度のコサインの掛け算である。答えは実数である。

 これは何に便利かというと、2つのベクトルの間の角度を求めたいなら、内積を使えば良いでしょう。まず、vとwを単位ベクトルにすると、長さは1になるので、式は単純になる:

image.png

そして、角度を求めるのは:

image.png

Unityで内積の計算を実装されている:

Vector3 v = new Vector3(vx, vy, vz);
Vector3 w = new Vector3(wx, wy, wz);
float dot = Vector3.Dot(v, w); // 内積

// 角度を求めたいなら
float radians = Mathf.Acos(dot);

// そして
float angle = radians * Mathf.Rad2Deg;
練習

警備員の視野を利用するゲーム

  • 警備員は「視野」があり、前方±角度しか見えない
  • 泥棒が視野の中に入ってれば→発見!
警備員のスクリプト
// 警備員の前に泥棒がいるかいないか確認する
public class GuardMan : MonoBehaviour
{
    // 泥棒への向きが欲しいので、泥棒が必要
    [SerializeField]
    private GameObject thief;
    
    // 視野(度)
    [SerializeField]
    private float viewAngle;

    // 色を変えるため
    private MeshRenderer rend;
    
    private void Start()
    {
	    rend = GetComponent<MeshRenderer>();
    }

    private void Update()
    {
        // 警備員の前向きのベクトル
        Vector3 v = transform.forward; // 青い矢印のこと

        // 泥棒への向き
        Vector3 w = thief.transform.position - transform.position;

        // 単位ベクトルにする
        w.Normalize();

        // 内積で角度のコサインを求める
        float dot = Vector3.Dot(v, w);
        
        // 内積からラジアン角度を求める
        float radian = Mathf.Acos(dot);
        
        // 度に変換
        float angle = radian * Mathf.Rad2Deg;
        
        // 警備員と泥棒の角度が視野よりも小さければ、発見!
        if (angle < viewAngle)
            rend.material.color = Color.red;
        
        // 見えない
        else
            rend.material.color = Color.green;
    }
}

 

外積

2つのベクトル外積の式は以下の通りである:

image.png

そして、Unityでも計算を実装されている:

Vector3 v = new Vector3(vx, vy, vz);
Vector3 w = new Vector3(wx, wy, wz);
Vector3 cross  = Vector3.Cross(v, w); // 外積の結果は新しいベクトルである

 式よりも、外積の幾何学的な意味は重要。2つの3Dベクトル「v」と「w」は平面を定義すし、それで「v」と「w」の外積の結果は:

  • 新しい3Dベクトルである
  • このベクトル「v」と「w」で定義された平面を直交する
  • また、長さは

image.png

image.png

従って、2つのベクトルがあれば、そのベクトルで作られた「面」を直交するベクトルを求めることができる。面の「法線ベクトル」、または3D空間の傾きの方向を求めることができる!

行列

 数を縦と横に矩形状に配列したものである。横に並んだ一筋を行(row)、縦に並んだ一筋を列(column)と呼ぶ。例えば、下記のような行列:

image.png

は2つの行と3つの列によって構成されているため、(2,3)型または2×3型の行列と呼ばれる。Unity は行列のクラスがないため、自分で作ってみましょう。

練習

「Matrix」クラスを実装して:

  • サイズ(行数、列数)を設定できるようにする
  • 各要素を(行:r、 列 c)
    • float Get(int r, int c) で取得できるようにする。
    • void Set(int r, int c, float value)で代入できるようにする。
  • string ToString(); のメソッドを実装し、中身を文字列として返すようにする

最後に、TestMatrix スクリプトを作成し、操作を確認してください。

補足:C#の1次元の配列を2次元の行列へ

 行列は「行」と「列」2次元があるが、C#の配列は1次元しかない。2次元の行列を作成するため、以下の変換を行わないといけない:int i = r * cols + c;

ここで、

  • i = 1次元(配列)の指数
  • r = 行列の「行」
  • c = 行列の「列」
  • cols = 行列の列数

例えば、4×3の行列で「 列:3、行:2」の要素を求めたい。ならば…

image.png

  •  c = 2
  •  r = 1
  •  cols = 4
  •  i = r * cols + c = 1 * 4 + 2 = 6
Matrix クラス
// 行列クラス
public class Matrix 
{
	// 列の数
	private int rows;

	// 行の数
	private int cols;

	// 行列の数値(配列)
	private float[] data;

	// コンストラクタ(インスタンスする時に呼び出す)
	// rows: 行の数
	// cols: 列の数
	public Matrix(int rows, int cols)
	{
		this.cols = cols;
		this.rows = rows;

		data = new float[cols * rows];
	}

	// 数値を読む
	// r: 行
	// c: 列
	// 行列の数値を返す
	public float Get(int r, int c) 
	{
		// 配列の位置(指数)を計算
		int i = r * cols + c;;
		return data[i];
	}
	
	// 数値を代入
	// r: 行
	// c: 列
	// value:代入したい数値
	public void Set(int r, int c, float value)
	{
		// 配列の位置(指数)を計算
		int i = r * cols + c;

		// 代入
		data[i] = value;
	}

	// 文字列にする
	public override string ToString()
	{
		// 表示用の文字列
		string s = "";

		// 1行ずつ
		for (int j = 0; j < rows; j++)
		{
			// 1列ずつ
			for (int i = 0; i < cols; i++)
			{
				// 数値を取得
				float v = Get(j, i);
				
				// 数値を文字列にして、連結する
				s += $"{v:0.00}  ";
			}

			// 改行
			s += "\n";
		}

		// 返す
		return s;
	}
}
TestMatrix スクリプト
// 行列を確認するため
public class MatrixTest : MonoBehaviour
{
	private void Start()
	{
		PrintMatrix();
    }

    private void PrintMatrix()
	{
		Matrix m = new Matrix(2, 3);
		
		m.Set(0, 0, 1);
		m.Set(0, 1, 9);
		m.Set(0, 2, -13);
		
		m.Set(1, 0, 20);
		m.Set(1, 1, 5);
		m.Set(1, 2, -6);

        // ToString() を実装したので、自動的に文字列に変換される
		Debug.Log(m);
	}
}

足し算(加法)

 2つの行列「A」と「B」、「A」と「B」のサイズが「m × n」である場合、A+Bの結果行列「C」は、各要素の足し算である。

image.png

image.png

image.png


もっと簡単にすると、各要素 cij の値は:

image.png

ここで「i」は「行」と「j」は「列」とする

練習

「Matrix」クラスを拡張して:

  • Matrix Sum (Matrix b) を追加し、加法を実装する
  • 自分と渡された行列「b」を足し、新しい行列を返す
  • 行数、または列数が合わない場合は Debug.LogError でエラーを表示し、null を返す

TestMatrix を更新し、以下の2つの行列を足してください。

image.png

image.png

image.png

Sum メソッド
// 行列の加算
public Matrix Sum(Matrix b)
{
	// 同じサイズかを確認する
	if (cols != b.cols || rows != b.rows)
	{
		Debug.LogError("行列のサイズが合わない");
		return null;
	}

	// 結果の行列
	Matrix result = new Matrix(rows, cols);
	
	// 1行ずつ
	for (int r = 0; r < rows; r++)
	{
		// 1列ずつ
		for (int c = 0; c < cols; c++)
		{
			float va = Get(r, c);      // 自分の数値を求める
			float vb = b.Get(r, c);    // 相手の数値を求める

			result.Set(r, c, va + vb); // 足して、代入
		}
	}

	// 返すだけ
	return result;
}
TestSum メソッド
void TestSum()
{
	// 3行2列の行列を作成
	Matrix a = new Matrix(3, 2);

	// 数値を代入
	a.Set(0, 0, 4);
	a.Set(0, 1, 6);
	
	a.Set(1, 0, -1);
	a.Set(1, 1, 0);
	
	a.Set(2, 0, 8);
	a.Set(2, 1, -3);

	Debug.Log(a);

	Matrix b = new Matrix(3, 2);

	b.Set(0, 0, 5);
	b.Set(0, 1, 1);
	
	b.Set(1, 0, -2);
	b.Set(1, 1, 2);
	
	b.Set(2, 0, 2);
	b.Set(2, 1, 3);

	Debug.Log(b);

	// 行列の加算
	Matrix c = a.Sum(b);
	Debug.Log(c);
}

掛け算(乗法)

 2つの行列「A」と「B」をかけることができる。ただし、「A」の列数は「B」の行数を等しくする条件を満たさないといけない。つまり、「A」は「l × m」であれば、「B」は「m × n」を確かめないといけない。この場合は、結果行列「C」のサイズは「l × n」になる。

この場合は、各要素 cij の掛け算の式は以下にある:

image.png

つまり…

image.png

行列「C」の「i, j」要素は、行列「A」の「i行目」× 行列「B」の「j列目」を全て足す結果。例えば:

image.pngimage.png

 注意:行列の積は可換でない(行列による変換の話のときに、ここを注意しましょう)

image.png

練習

「Matrix」クラスを拡張して:

  • Matrix Multiply (Matrix b) を追加し、乗法を実装する
  • 自分と渡された行列「b」を足し、新しい行列を返す
  • 行数、または列数が合わない場合は Debug.LogError でエラーを表示し、null を返す
  • 上記のAとB行列で確認。結果は:

image.png

Multiply メソッド
// 行列の掛け算
public Matrix Multiply(Matrix b)
{
	// 掛け算ができることを確認
	if (cols != b.rows)
	{
		Debug.LogError("行と列の数が一致していない");
		return null;
	}

	// 返す行列(自分の行数 x 相手の列数)
	Matrix result = new Matrix(rows, b.cols);

	// 1行ずつ(結果行列)
	for (int i = 0; i < rows; i++)
	{
		// 1列ずつ(結果行列)
		for (int j = 0; j < b.cols; j++)
		{
			// 1数値ずつ
			float sum = 0;
			for (int k = 0; k < cols; k++)
			{
				sum += Get(i, k) * b.Get(k, j);
			}
			result.Set(i, j, sum);
		}
	}

	return result;
}
TestMultiply メソッド
void TestMultiply()
{
	// 2行3列の行列を作成
	Matrix a = new Matrix(4, 2);

	// 数値を代入
	a.Set(0, 0, 1);
	a.Set(0, 1, 0);
	
	a.Set(1, 0, 3);
	a.Set(1, 1, -1);
	
	a.Set(2, 0, 2);
	a.Set(2, 1, 2);
	
	a.Set(3, 0, 5);
	a.Set(3, 1, 1);
	
	Debug.Log(a);

	Matrix b = new Matrix(2, 3);

	b.Set(0, 0, 4);
	b.Set(0, 1, 1);
	b.Set(0, 2, 2);
	
	b.Set(1, 0, 5);
	b.Set(1, 1, 0);
	b.Set(1, 2, 1);
	
	Debug.Log(b);

	// 行列の掛け算
	Matrix c = a.Multiply(b);
	Debug.Log(c);
}