カメラの視線とメッシュとの交点の座標を数学的に求める

# 概要
メッシュを張ってカメラでそのメッシュを見たときに、カメラの視線とメッシュとの交点の座標を数学的に計算し表示させるプログラムをUnityで作ります。

# 結果
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">さっきの続き<br>メッシュとカメラの視線が交わるときだけ、交点を表す紫の球体を出現させるようにしました<br>点の内外判定を使って実装しました💪<a href="https://twitter.com/hashtag/Unity?src=hash&amp;ref_src=twsrc%5Etfw">#Unity</a> <a href="https://t.co/oa2YpnLkVy">pic.twitter.com/oa2YpnLkVy</a></p>&mdash; みこし (@Mikoshi_prog) <a href="https://twitter.com/Mikoshi_prog/status/1184488843740712961?ref_src=twsrc%5Etfw">October 16, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

[こちら](https://quietmikoshi.github.io/find_intersection_of_plane_and_straight_line/)のページで、上の動画のように実際にカメラを動かして体験できます。

# 方法
## メッシュ
まずメッシュをスクリプトを使って張ります。メッシュの張り方は[<b>こちら</b>](https://matcha-choco010.net/2018/08/25/unity%E3%81%A7mesh%E3%82%92%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%8B%E3%82%89%E4%BD%9C%E3%82%8B/)のサイトを参考にさせていただきました。

本記事では、メッシュの頂点に球体を配置し、Unityのシーンからメッシュの頂点の位置を自由に動かせるという方針で実装していくので、参考にさせていただいたスクリプトに多少新たなコードを追加しています。

まず空のゲームオブジェクトに"<b>Mesh_front</b>"という名前を付け(後で"<b>Mesh_back</b>"も作ります)、<b>MeshRenderer</b>、<b>MeshFilter</b>、白いマテリアル、さらに次のスクリプトをアタッチします。

```C#:MeshMaker.cs
using System.Collections.Generic;
using UnityEngine;

public class MeshMaker : MonoBehaviour {
    public GameObject[] sphere;
    private Vector3[] point; 
    private Mesh mesh;
    private MeshFilter meshFilter;

    private void Start() {
        point = new Vector3[sphere.Length];
        for (int i = 0; i < sphere.Length; i++) point[i] = sphere[i].gameObject.transform.position;

        mesh = new Mesh();
        List<Vector3> verticles = new List<Vector3>();
        for (int i = 0; i < point.Length; i++) verticles.Add(point[i]);
        mesh.SetVertices(verticles);

        List<int> triangles = new List<int>();
        for (int i = 0; i < point.Length; i++) triangles.Add(i);
        mesh.SetTriangles(triangles, 0);

        meshFilter = GetComponent<MeshFilter>();
        meshFilter.mesh = mesh;
    }

    private void Update() {
    }
}

```

メッシュの頂点を表す球をsphere、球の頂点の座標をpointとしてあります。ここまでやると、下の図になります。
<img width="960" alt="mesh_front.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498199/0485b3b3-858b-88ce-6beb-21746bdee501.png">
次に、Mesh_frontに球を渡します。とりあえず三角形のメッシュを作っていくので、球を3つ用意します。3つの球の名前をPoint0, Point1, Point2とし、Scaleを(0.1, 0.1, 0.1)としてあります。Mesh_frontのMakeMesh.csにあるSpheresのSizeを"<b>3</b>"に設定し、Point0, 1, 2を<b>順番に</b>入れます。ここまでやると、下の図になります。
<img width="960" alt="three spheres.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498199/380d5341-782c-4e3d-1b40-6e62e97bd12e.png">
さて、このままだと<b>メッシュは片面にしか張られない</b>ので、もう片方の面にも張ります。やり方は簡単です。Mesh_frontを複製し、複製後のものを"<b>Mesh_back</b>"という名前にします。SphereのSizeを"<b>3</b>"にし、球をPoint2, 1, 0の順番に入れます。<b>先ほどとは入れる順番が逆です。</b>
<img width="959" alt="mesh_back.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498199/de87f510-1516-b120-1a98-78a20df33bbe.png">
## カメラ
次に、Main Cameraにアタッチするスクリプトについて説明します。

```FindCoordinateController.cs
using UnityEngine;
using UnityEngine.UI;

public class FindCoordinateOfIntersection : MonoBehaviour
{
    public GameObject[] spheres; //メッシュの頂点の球
    public GameObject intersection_sphere; //交点の球
    public Text coordinate; //画面上に出す座標のテキスト
    private Vector3[] point; //メッシュの頂点の座標
    private Vector3 normal, intersection; //normal: メッシュの法線ベクトル、intersection: 交点の座標
    private MeshMaker meshMaker;
    private float[] abcd; //メッシュが貼られている平面の方程式の係数
    private float parameter; //交点の座標を求めるときに使うパラメータ
    private bool intersectionIsInsidePolygon; //交点がメッシュの中に存在しているかどうか

    private void Start() {
        point = new Vector3[spheres.Length];
        for (int i = 0; i < spheres.Length; i++) point[i] = spheres[i].gameObject.transform.position;
        normal = CalculateOuterProduct(point[0], point[1], point[2]); //3点point[0]~[2]を通る平面の法線ベクトルを求める
    }

    private void Update() {
        abcd = CalculateEquationOfPlane(point[0], point[1], point[2]);

        //gameObject.transform.rotation * Vector3.forward, gameObject.transform.positionはカメラの視線の方向ベクトル
        intersection = CalculateCoordinateOfIntersection(abcd, gameObject.transform.rotation * Vector3.forward, gameObject.transform.position);

        intersectionIsInsidePolygon = WhetherIntersectionIsInsidePolygon(point, intersection, normal);

        //交点がメッシュの内部にあるときだけ交点をアクティブにする
        intersection_sphere.SetActive(intersectionIsInsidePolygon);

        //交点がメッシュの内部にあり、かつ交点がカメラの前側にあるときに、テキストに交点の座標を表示する
        if (intersectionIsInsidePolygon && parameter > 0f) coordinate.text = "(" + intersection.x.ToString("F2") + ", " + intersection.y.ToString("F2") + ", " + intersection.z.ToString("F2") + ")";
        else coordinate.text = "※交点が存在しません※";
    }

    //変数intersectionの値を読み取る、書き込むプロパティ
    public Vector3 Intersection {
        get { return intersection; }
        private set { intersection = value;  }
    }

    //このメソッドは、vec1,vec2,vec3の3点を通る平面の方程式ax+by+cz+d=0のa,b,c,dを配列で返す
    private float[] CalculateEquationOfPlane(Vector3 vec1, Vector3 vec2, Vector3 vec3) {
        float[] ans = new float[]{
            normal.x,
            normal.y,
            normal.z,
            -normal.x * vec1.x - normal.y * vec1.y - normal.z * vec1.z
        };
        return ans;
    }

    //このメソッドでは、カメラの視線とメッシュとの交点の座標が求められる
    private Vector3 CalculateCoordinateOfIntersection(float[] plane, Vector3 angle, Vector3 position) {
        parameter = -(plane[0] * position.x + plane[1] * position.y + plane[2] * position.z + plane[3]) / (plane[0] * angle.x + plane[1] * angle.y + plane[2] * angle.z);
        float x = angle.x * parameter + position.x;
        float y = angle.y * parameter + position.y;
        float z = angle.z * parameter + position.z;
        return new Vector3(x, y, z);
    }

    //このメソッドでは、vec1,vec2,vec3の3点を通る平面の法線ベクトルが求められる
    private Vector3 CalculateOuterProduct(Vector3 vec1, Vector3 vec2, Vector3 vec3) {
        Vector3 tmp1 = vec1 - vec2;
        Vector3 tmp2 = vec1 - vec3;
        return Vector3.Cross(tmp1, tmp2); //Vector3.Crossは外積を求めるメソッド
    }

    //このメソッドは引用させていただきました
    private bool WhetherIntersectionIsInsidePolygon(Vector3[] vertices, Vector3 intersection, Vector3 normal) {
        float angle_sum = 0f;
        for (int i = 0; i < vertices.Length; i++) {
            Vector3 tmp1 = vertices[i] - intersection;
            Vector3 tmp2 = vertices[(i + 1) % vertices.Length] - intersection;
            float angle = Vector3.Angle(tmp1, tmp2);
            Vector3 cross = Vector3.Cross(tmp1, tmp2);
            if (Vector3.Dot(cross, normal) < 0) angle *= -1;
            angle_sum += angle;
        }
        angle_sum /= 360f;
        return Mathf.Abs(angle_sum) >= 0.1f;
    }
}
```

各メソッドの説明をしていきます。

#### CalculateEquationOfPlaneメソッド
このメソッドの実装は[<b>こちら</b>](https://mathtrain.jp/heimen)のサイトの<b>1：外積と法線ベクトルを用いる方法</b>を参考にさせていただきました。

```math
p(x-x_0)+q(y-y_0)+r(z-z_0)=0\\
⇔px+qy+rz-px_0-qy_0-rz_0=0
```
という変形のもとに実装しました。法線ベクトルの値は、すでにStartメソッドの中で計算済みです。
#### CalculateCoordinateOfIntersectionメソッド
このメソッドの実装は[<b>こちら</b>](http://tau.doshisha.ac.jp/lectures/2009.intro-seminar/html.dir/node29.html)のサイトを参考にさせていただきました。ここで、カメラの座標を表すベクトルを

```math
(p_x, p_y, p_z)
```
カメラの視線の方向ベクトルを

```math
(d_x, d_y, d_z)
```
とします。このときパラメータ***t***を用いて(91)式のように書くとすると

```math
x=d_xt+p_x\\
y=d_yt+p_y\\
z=d_zt+p_z
```
となります。<b>この*x*, *y*, *z*の値をとる座標がカメラの視線と平面との交点になっていればよい</b>ので、平面の方程式を

```math
ax+by+cz+d=0
```
としたとき

```math
a(d_xt+p_x)+b(d_yt+p_y)+c(d_zt+p_z)+d=0\\
⇔(ad_x+bd_y+cd_z)t=-(ap_x+bp_y+cp_z+d)\\
```
よって左辺が0でなければ

```math
t=-\frac{ap_x+bp_y+cp_z+d}{ad_x+bd_y+cd_z}
```
となります。あとはこの*t*の値を*x*, *y*, *z*に代入すればOKです。
#### CalculateOuterProductメソッド
このメソッドは<b>Vector3.Cross</b>メソッドを用いて、vec1, vec2, vec3の3点を通る平面の法線ベクトルを求めています。
#### WhetherIntersectionIsInsidePolygonメソッド
このメソッドは[<b>こちら</b>](http://edom18.hateblo.jp/entry/2018/11/28/200032)のサイトから、ほぼ完全に引用させていただきました。交点がメッシュの内側にあるのか外側にあるのかを判定します。

メインカメラのInspectorは下の図のようになっています。TPS Cameraというスクリプトは、カメラの位置や回転をXboxコントローラーで制御するためのスクリプトです。本記事では説明を割愛します。また、Find Coordinate Of Intersectionに入っている<b>Intersection</b>と<b>Coordinate</b>というオブジェクトは後ほど作ります。
<img width="960" alt="main camera.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498199/1fe0098f-8960-dad3-ea4c-43e02cb47b16.png">
## 交点
次に、カメラとメッシュとの交点を作ります。本記事では交点が視覚的に分かりやすいように、交点の位置に球を配置することにします。球は紫色にし、<b>IntersectionController.cs</b>というスクリプトをつけます。

```C#:IntersectionController.cs
using UnityEngine;

public class IntersectionController : MonoBehaviour
{
    private FindCoordinateOfIntersection f;
    public GameObject camera;
    
    private void Start() {
        f = camera.GetComponent<FindCoordinateOfIntersection>();
    }

    private void Update() {
        gameObject.transform.position = f.Intersection;
    }
}
```

<b>IntersectionController.cs</b>の中では、球の位置を交点の位置に合わせ続けるように実装してあります。Main cameraにFindCoordinateController.csをアタッチしてあるので、Unityでcameraの欄にはMain Cameraを入れておきます。
<img width="959" alt="intersection.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498199/a6622e52-59a4-7526-dc1f-c11b4f85b107.png">
次に、画面上に交点の座標を表示するためのテキストを作ります。
<img width="959" alt="text.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498199/32f40a9f-2499-1d9a-86c6-96c1b66170ae.png">
上の図のようにテキストを配置し、<b>Coordinate</b>というテキストをMain CameraにアタッチしたFindCoordinateController.csに入れておきます。
## x,y,z軸
最後に、メッシュが空中に浮いていると頂点の前後関係などが視覚的に分かりづらいと思ったので、x,x,z軸をオブジェクトとして作ることにしました。円柱を細く長く引き伸ばし、軸ごとの色を付けて置いておきます。
<img width="960" alt="axis.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498199/6342729e-0fd0-5052-5058-f274eab14c41.png">

以上です。長い記事を読んでくださりありがとうございました！