vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

ゴリラ本メモ その1 - 4章

はじめに

ゴリラ本でおなじみ(?) 実例で学ぶゲーム3D数学 に再挑戦してみたくなりました。 (たぶん表紙の絵はゴリラではなくマンドリルっぽいのですが)

この本買ったは良いものの わかるわかる -> おや? -> 全然わからないorz -> 挫折 となっていたのですが、
「もう死んだ羊の話ばっかり読むのはいやじゃぁ!」と思いましたので、何とか前の挫折ポイントよりもう少し読み進めたいと思っています。

で、よくわからないまま読み進めてしまうのを防ぐために、
紹介されている式や例をコードとして書き起こして、計算結果を確認してみることにしました。

ベクトルの計算は、 Unity では Vector2 や Vector3 を使うことができます。

また、 NuGet で System.Numerics.Vectors をインポートすることで、
Unity 以外の C# アプリケーションでもベクトル計算ができます。

今回は大部分で System.Numerics.Vectors を使ってみることにしました。

理由? 興味があったからですw

なお今回は 4 章 ベクトルの演算 の、4.8.3 ~ 4 章終わりまでを取り上げます。

凄く中途半端なところから始める理由は、思いついたのが 4 章途中からであったこと、
4 章頭の部分で特にコードに置き換えて面白そうだと感じたところが少なかったためです。

UnityとSystem.Numerics.Vectors

今回登場するメソッドやクラスは、Unity 、 System.Numerics.Vectors ともにほぼ同じです。
(中の処理は違いがあったりするかもしれませんが)

違いとして気づいたのは下記の2つくらいです。

  • Vector2 の 変数 x, y (Vector3 なら x, y, z) が、Unity は小文字、 System.Numerics.Vectors は大文字
  • ベクトルの大きさを取得するのは Unity なら magnitude 、 System.Numerics.Vectors なら Length()

ベクトルから距離を計算する

さて、それではまず、 ある点(点A)から別の点(点B)へのベクトル(ベクトルD)を求めてみます。

原点から点AまでのベクトルをベクトルA、点BまでのベクトルをベクトルBとして、
ベクトルBからベクトルAを引きことで算出できます。

ベクトルDの大きさは、ベクトルDの大きさ(斜辺)の二乗 = 底辺(X)の二乗+高さ(Y)の二乗であることから算出する方法1、Length() を使う方法2があります。

計算用クラス

using System;
using System.Numerics;

public class DistanceSample {
    // 点Aから点Bまでのベクトル(ベクトルD)を取得する 2D
    public Vector2 GetVector(Vector2 pointA, Vector2 pointB) {
        return pointB - pointA;
    }
    // 点Aから点Bまでのベクトル(ベクトルD)を取得する 3D
    public Vector3 GetVector(Vector3 pointA, Vector3 pointB) {
        return pointB - pointA;
    }
    // ベクトルDの大きさを取得する 方法1 2D
    public float GetDistance1(Vector2 point) {
        // Dの長さの二乗 = 底辺(X)の二乗+高さ(Y)の二乗
        var squareDistance = Math.Pow(point.X, 2) + Math.Pow(point.Y, 2);
        var rootDistance = Math.Sqrt(squareDistance);
        return (float) rootDistance;
    }
    // ベクトルDの大きさを取得する 方法2 2D
    public float GetDistance2(Vector2 point) {
        return point.Length();
    }
    // ベクトルDの大きさを取得する 方法1 3D
    public float GetDistance1(Vector3 point) {
        var squareDistance = Math.Pow((double)point.X, 2) + Math.Pow((double)point.Y, 2) + Math.Pow((double)point.Z, 2);
        var rootDistance = Math.Sqrt(squareDistance);
        return (float)rootDistance;
    }
    // ベクトルDの大きさを取得する 方法2 3D
    public float GetDistance2(Vector3 point) {
        return point.Length();
    }
}

呼び出し元

class Program {
    static void Main(string[] args) {
        var pointA = new Vector2(-1, 8);
        var pointB = new Vector2(5, -4);

        var distanceSample = new DistanceSample();

        Vector2 vectorD = distanceSample.GetVector(pointA, pointB);
        float distance1 = distanceSample.GetDistance1(vectorD);
        float distance2 = distanceSample.GetDistance2(vectorD);
    }
}

結果は ベクトルD: Vector2(6, -12) 距離: 13.41641 となりました。

詳しい理屈などは書籍参照ということでm(__)m

内積を計算する

さぁどんどんいきましょう。

次は内積です。

ゴリラ本によると「2つのベクトルがどれぐらい似ているか」を表す、
ということでしたが、それだけだと今一つ分かりませんでしたorz

内積が必要となる例としては、下記の「物体になされた仕事量」や、物体の表面の明るさ、
といったものがわかりやすいと感じました。

内積はベクトル A 、 B の各要素同士を掛けて、それを足し合わせることで算出できます(方法1)。

また、ベクトル A 、 B の大きさと、ベクトル A 、 B のなす角度 θ のコサインをそれぞれかけることでも算出できます。(方法2)

さらに、 Unity や System.Numerics.Vectors の場合は内積を計算してくれる、 Dot() というメソッドもあります。(方法3)

計算用クラス

using System.Numerics;

public class InnerProductSample {
    // 内積を算出する 方法1 2D
    public float GetInnerProduct1(Vector2 pointA, Vector2 pointB) {
        return (pointA.X * pointB.X) + (pointA.Y * pointB.Y);
    }
    // 内積を算出する 方法1 3D
    public float GetInnerProduct1(Vector3 pointA, Vector3 pointB) {
        return (pointA.X * pointB.X) + (pointA.Y * pointB.Y) + (pointA.Z + pointB.Z);
    }
    // 内積を算出する 方法2 2D
    public float GetInnerProduct2(Vector2 pointA, Vector2 pointB, float cosTheta) {
        return pointA.Length() * pointB.Length() * cosTheta;
    }
    // 内積を算出する 方法2 3D
    public float GetInnerProduct2(Vector3 pointA, Vector3 pointB, float cosTheta) {
        return pointA.Length() * pointB.Length() * cosTheta;
    }
    // 内積を算出する 方法3 2D
    public float GetInnerProduct3(Vector2 pointA, Vector2 pointB) {
        return Vector2.Dot(pointA, pointB);
    }
    // 内積を算出する 方法3 3D
    public float GetInnerProduct3(Vector3 pointA, Vector3 pointB) {
        return Vector3.Dot(pointA, pointB);
    }
}

呼び出し元

class Program {
    static void Main(string[] args) {
        var pointA = new Vector2(3, 4);
        var pointB = new Vector2(8, 3);

        var innerProductSample = new InnerProductSample();

        var innerProduct1 = innerProductSample.GetInnerProduct1(pointA, pointB);
        // var innerProduct2 = ??
        var innerProduct3 = innerProductSample.GetInnerProduct3(pointA, pointB);
    }
}

方法2について

方法1、方法3については特に問題ないと思います。

ただ方法2で算出しようとすると、 Cosθ の値が必要になってきます。

θ の角度がわかっている場合、または点Aまたは点Bの、X または Y が 0 であれば問題なさそうですが。。。

なお、ベクトルAとベクトルBの大きさを掛けたもので内積を割ると、 Cosθ を算出できます。

var theta = Math.Acos(innerProduct1 / (pointA.Length() * pointB.Length()));
// radian -> 角度への変換.
var angle = theta * 180d / Math.PI;

すでに何をしたいかわからなくなってきてますが/(^o^)\

投影

θ の角度を求める方法として、投影を使う方法もあります。

点AからベクトルBに対して、垂直になるような線を引いたときにできる三角形の底辺となる ベクトル A|| を用います。

f:id:mslGt:20180201075600p:plain

詳しい計算は書籍を参照していただくとして(そればっかり)、結果としては下記のようになります。

var aLength = pointA.Length();
var bLength = pointB.Length();
var calc = (float) (innerProduct1 / Math.Pow(bLength, 2));
var vBottom = new Vector2 {
    X = pointB.X * calc,
    Y = pointB.Y * calc
};

var theta2 = Math.Acos((double)vBottom.Length() / (double)aLength);
var angle2 = theta2 * 180d / Math.PI;

var innerProduct2 = innerProductSample.GetInnerProduct2(pointA, pointB, (float)Math.Cos(theta2));

外積を計算する

次は外積です。

外積は 3D のみで計算でき(2Dでは計算不可)、ベクトルA、ベクトルBを通る平面に対して垂直のベクトルが得られます。

ということで、両方の Z を 0 にすると、X 方向が 0 のベクトルが得られるはず。

var pointA = new Vector3(3, 4, 0);
var pointB = new Vector3(8, 3, 0);

var outerProductSample = new OuterProductSample();

var outerProduct1 = outerProductSample.GetOuterProduct1(pointA, pointB);
var outerProduct2 = outerProductSample.GetOuterProduct2(pointA, pointB);

結果は (0, 0, -23) となりました。
考えは間違っていなさそうです。

また、外積の大きさはベクトルA・ベクトルBの大きさと、 Sinθ を掛けたものと等しいということです。

というわけで計算してみます。

var pointA = new Vector3(3, 4, 2);
var pointB = new Vector3(8, 3, 5);

var outerProductSample = new OuterProductSample();

var outerProduct1 = outerProductSample.GetOuterProduct1(pointA, pointB);
var outerProduct2 = outerProductSample.GetOuterProduct2(pointA, pointB);


var radian = Math.Acos(
    Vector3.Dot(pointA,pointB) / (pointA.Length() * pointB.Length()));

var length1 = outerProduct1.Length();

var length2 = pointA.Length() * pointB.Length() * Math.Sin(radian);

結果は length1 が 26.9443874 、 length2 が 26.94438715941152 でした。あれ?

う~ん、完全に正確な値が必要でないのなら小数点第二位くらいで丸めて使うのがよさそうです。

おわりに

やっぱりコードであったり、何かの形に落として実際に計算してみる、確かめてみる、というのは大事ですね。

本当は全部自分で計算した方が。。。と思わないでもないですが、心が折れそうなのでいったん後回しに。。。

Unity にしろ System.Numerics.Vectors にしろ、便利なメソッドが用意されているため、
通常はそれを使えばよいわけですが、どういう理屈でそのように計算されるのか、
ということが少しでもわかっていると、うまくいかない場合や応用が必要な時などに役立つのではないかと。

次は5章をとばして6章の行列を試してみる予定です。

5章はベクトルクラスを自作する?内容で、実際にそれを使うタイミングで見た方がわかりやすい気がするので。。。

話が別方向にずれて帰ってこなくなったら。。。お察しくださいm(__)m

参照

内積

外積