やりたいこと
壁に家具を設置するときに家具がどんな方向を向けばいいか知りたいなど、でこぼこな壁から垂直なベクトル(法線)を導きだしたいとき、ありますよね。
上図の緑のビームみたいなベクトルを導けたら、このベクトルにあわせて家具の角度を調整すればいいや、ということになります。
仕組み
上図で、白いビームを「検出ビーム」と呼びます。この検出ビームを大量に発射して、一本一本のビームがターゲットの法線(ビームが当たった点の垂線)を検出します。(これが上図の青ビーム)
この一本一本の法線の平均ベクトルを導き出せたら、上図の緑ビームがでてきます。
すなわち、たまたまでこぼこの激しいところにいても、検出される角度があさっての方向を向くことなく、「巨視的に見てだいたいこっちが垂直だろう」という方向を検出できるんですね。
実装
読み解くのが面倒な場合は冒頭のgithubからどうなっているか見るのが一番早いかと思います。
まずは、スクリプトから。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//検出ビームがPlaneから発射される
//各検出ビームが各々の法線ベクトルを計算し、最終的に全法線ベクトルの平均値を計算
//可視化が不要な場合はDebug.DrawRayを削除されたし
public class GetNormals : MonoBehaviour
{
[Tooltip("短すぎると対象まで検出ビームがたどり着かない")]
[SerializeField] public float rayLength = 10f;
[Tooltip("法線を出すターゲットのオブジェクト")]
[SerializeField] public Collider target;
[Tooltip("DivisionN * DivisionN の検出ビームが射出される")]
[SerializeField] public int divisionN = 30;
[Tooltip("0番目に一角を、1番目と2番目は0番目に隣り合う角を登録すること")]
[SerializeField] private GameObject[] edgesObjects;
private Vector3[] edges;
public Vector3 direction;
//結果
public Vector3 detectedNormal;
public Vector3 detectedPoint;
private void Update()
{
//常に法線を検出したい場合は//を外されたし
//Vector3[] detectionInfo = Detect();
//Debug.DrawRay(detectionInfo[1], detectionInfo[0], Color.green);
}
private void UpdatePosition()
{
//角の位置
edges = new Vector3[3];
for (int cnt = 0; cnt < 3; cnt++)
{
edges[cnt] = edgesObjects[cnt].transform.position;
}
//検出ビームの方向
direction = transform.up.normalized;
}
/// <summary>
/// 検出ビームを発射しまくり、平均法線ベクトルを出すまで
/// </summary>
/// <returns>
/// [0]: 平均法線ベクトル
/// [1]: 検出ビームが当たった点の平均
/// </returns>
///
public Vector3[] Detect()
{
//変数初期化
UpdatePosition();
//検出ビーム発射間隔を表す
Vector3 xVecUnit = (edges[1] - edges[0]) / divisionN;
Vector3 yVecUnit = (edges[2] - edges[0]) / divisionN;
Vector3[] normals = new Vector3[(divisionN + 1) * (divisionN + 1)];
Vector3[] hittedPoints = new Vector3[(divisionN + 1) * (divisionN + 1)];
//各点から検出ビームを発射し、結果を記録
for (int cnt1 = 0; cnt1 <= divisionN; cnt1++)
{
for(int cnt2 = 0; cnt2 <= divisionN; cnt2++)
{
//発射
Vector3[] hitInfo = LanchRay(edges[0] + xVecUnit * cnt1 + yVecUnit * cnt2);
//結果を記録
normals[cnt1 * (divisionN + 1) + cnt2] = hitInfo[0];
hittedPoints[cnt1 * (divisionN + 1) + cnt2] = hitInfo[1];
}
}
//平均を計算
detectedNormal = GetAverageVector(normals, false);
detectedPoint = GetAverageVector(hittedPoints, false);
return new Vector3[] { detectedNormal, detectedPoint };
}
/// <summary>
/// 一つの検出ビームを発射
/// </summary>
/// <param name="startPoint"> 発射ポイント</param>
/// <returns>
/// [0]: 法線ベクトル
/// [1]: 当たった点
/// 当たらなかった場合は Vector3.zero を返す
/// </returns>
private Vector3[] LanchRay(Vector3 startPoint)
{
//Debug.DrawRay(startPoint, direction*rayLength);
Ray ray = new Ray(startPoint, direction);
RaycastHit hit;
if(target.Raycast(ray, out hit, rayLength))
{
//当たった!
//Debug.DrawRay(hit.point, hit.normal, Color.blue);
return new Vector3[] { hit.normal, hit.point };
}
//当たらなかった!
return new Vector3[] { Vector3.zero, Vector3.zero};
}
//平均ベクトルの計算
private Vector3 GetAverageVector(Vector3[] vectors, bool shouldIncludeZero = false)
{
Vector3 sumVector = Vector3.zero;
int zeros = 0;
foreach(Vector3 vector in vectors)
{
if (!shouldIncludeZero && vector == Vector3.zero)
{
//shouldn't include zero vector
zeros++;
}
else
{
sumVector += vector;
}
}
if (vectors.Length - zeros == 0)
{
return Vector3.zero;
}
else
{
return sumVector / (vectors.Length - zeros);
}
}
}
このスクリプトの、Detect()を呼ぶと1回検出します。
もし常に検出しつづけてほしい場合はUpdate()内コメントアウトを抜いてください。
次に、検出器のプレハブを作りましょう。
まずはPlaneを作り、これにGetNormals.csをアタッチ。もし衝突判定が欲しくない場合はColliderを抜いて、そもそも検出器を見せたくない場合は透明にしてください。
このPlaneに、Edge0~2の3つの空オブジェクトを下図のように配置してください。(要するに、1と2を0と隣り合うように)
GetNormalsコンポーネントに各Edge、法線を検出する対象となるオブジェクト(public変数なのでスクリプト経由で動的に設定することも可)をアタッチ。
これで完了です。