概要
メッシュを張ってカメラでそのメッシュを見たときに、カメラの視線とメッシュとの交点の座標を数学的に計算し表示させるプログラムをUnityで作ります。
結果
さっきの続き
— みこし (@Mikoshi_prog) October 16, 2019
メッシュとカメラの視線が交わるときだけ、交点を表す紫の球体を出現させるようにしました
点の内外判定を使って実装しました💪#Unity pic.twitter.com/oa2YpnLkVy
こちらのページで、上の動画のように実際にカメラを動かして体験できます。
方法
メッシュ
まずメッシュをスクリプトを使って張ります。メッシュの張り方はこちらのサイトを参考にさせていただきました。
本記事では、メッシュの頂点に球体を配置し、Unityのシーンからメッシュの頂点の位置を自由に動かせるという方針で実装していくので、参考にさせていただいたスクリプトに多少新たなコードを追加しています。
まず空のゲームオブジェクトに"Mesh_front"という名前を付け(後で"Mesh_back"も作ります)、MeshRenderer、MeshFilter、白いマテリアル、さらに次のスクリプトをアタッチします。
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としてあります。ここまでやると、下の図になります。
次に、Mesh_frontに球を渡します。とりあえず三角形のメッシュを作っていくので、球を3つ用意します。3つの球の名前をPoint0, Point1, Point2とし、Scaleを(0.1, 0.1, 0.1)としてあります。Mesh_frontのMakeMesh.csにあるSpheresのSizeを"3"に設定し、Point0, 1, 2を順番に入れます。ここまでやると、下の図になります。
さて、このままだとメッシュは片面にしか張られないので、もう片方の面にも張ります。やり方は簡単です。Mesh_frontを複製し、複製後のものを"Mesh_back"という名前にします。SphereのSizeを"3"にし、球をPoint2, 1, 0の順番に入れます。先ほどとは入れる順番が逆です。
カメラ
次に、Main Cameraにアタッチするスクリプトについて説明します。
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メソッド
このメソッドの実装はこちらのサイトの1:外積と法線ベクトルを用いる方法を参考にさせていただきました。
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メソッド
このメソッドの実装はこちらのサイトを参考にさせていただきました。ここで、カメラの座標を表すベクトルを
(p_x, p_y, p_z)
カメラの視線の方向ベクトルを
(d_x, d_y, d_z)
とします。このときパラメータtを用いて(91)式のように書くとすると
x=d_xt+p_x\\
y=d_yt+p_y\\
z=d_zt+p_z
となります。このx, y, zの値をとる座標がカメラの視線と平面との交点になっていればよいので、平面の方程式を
ax+by+cz+d=0
としたとき
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でなければ
t=-\frac{ap_x+bp_y+cp_z+d}{ad_x+bd_y+cd_z}
となります。あとはこのtの値をx, y, zに代入すればOKです。
CalculateOuterProductメソッド
このメソッドはVector3.Crossメソッドを用いて、vec1, vec2, vec3の3点を通る平面の法線ベクトルを求めています。
WhetherIntersectionIsInsidePolygonメソッド
このメソッドはこちらのサイトから、ほぼ完全に引用させていただきました。交点がメッシュの内側にあるのか外側にあるのかを判定します。
メインカメラのInspectorは下の図のようになっています。TPS Cameraというスクリプトは、カメラの位置や回転をXboxコントローラーで制御するためのスクリプトです。本記事では説明を割愛します。また、Find Coordinate Of Intersectionに入っているIntersectionとCoordinateというオブジェクトは後ほど作ります。
交点
次に、カメラとメッシュとの交点を作ります。本記事では交点が視覚的に分かりやすいように、交点の位置に球を配置することにします。球は紫色にし、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;
}
}
IntersectionController.csの中では、球の位置を交点の位置に合わせ続けるように実装してあります。Main cameraにFindCoordinateController.csをアタッチしてあるので、Unityでcameraの欄にはMain Cameraを入れておきます。
次に、画面上に交点の座標を表示するためのテキストを作ります。
上の図のようにテキストを配置し、CoordinateというテキストをMain CameraにアタッチしたFindCoordinateController.csに入れておきます。
x,y,z軸
最後に、メッシュが空中に浮いていると頂点の前後関係などが視覚的に分かりづらいと思ったので、x,x,z軸をオブジェクトとして作ることにしました。円柱を細く長く引き伸ばし、軸ごとの色を付けて置いておきます。
以上です。長い記事を読んでくださりありがとうございました!