[Unity] 3次元空間で定義した平面を自由に扱う方法
はじめまして。
はじめまして。地方の建設会社でSEをしているSeikyoといいます。
いつものニックネームが使われていたので好きな日本酒の名前にしました。
いつもQiitaにはお世話になっております。が、建設業関係者の記事があまりに少ない。
ITエンジニアの皆さん。ブルーオーシャンはここにありますよ。助けてください。
1. 問題設定:4点だけでは「上下左右」は決まらない
最初の気づきがこれだった。
3D空間に3点を置けば「平面」は定義できる。だが平面そのものには上下左右が無い。「この壁の右はどっち?」は、平面だけ見ても答えが出ない。
そこで問いを立てた。
Q.3次元空間において、4点を指定して1つの平面を定義する。このとき、平面を構成する点とは別の点(Robot)からその平面をみたとき、上下左右はどのように区別するか?
上下左右は、その平面を見ている主体から見て初めて決まる。カメラやキャラクターの「視線に対する右・上」と同じ考え方だ。
なので必要なのは、4点(+主体の位置)から次の3本のベクトルを作ること。
| ベクトル名 | 記号 | 意味 |
|---|---|---|
| 右 | 'r' | 平面で寝てるベクトルの右成分 |
| 上 | 'u' | 平面で寝てるベクトルの上成分 |
| 法線 | 'n' | 平面から垂直に飛び出しているベクトル |
2. 法線を求める
4点は完全な平面とは限らないので、対角線2本の外積で法線を出す。
// 4点の重心(順序に関係なく同じ)
Vector3 center = (points[0] + points[1] + points[2] + points[3]) * 0.25f;
// 対角線2本の外積 → 平面の法線
Vector3 diag1 = points[2] - points[0];
Vector3 diag2 = points[3] - points[1];
Vector3 n = Vector3.Cross(diag1, diag2).normalized;
外積 Vector3.Cross(a, b) は「a と b の両方に垂直なベクトル」を返す。2本の対角線に同時に垂直な向き=平面の正面(法線)になる。
法線の表裏を主体側に向ける
外積で出た法線は表か裏か分からない。主体がいる側を「正面」とするため、
主体位置座標 − 平面中心座標 と法線の内積がマイナスなら反転する。
if (robot != null && Vector3.Dot(n, robot.position - center) < 0f)
n = -n;
内積の符号は「2つのベクトルが同じ向きを向いているか(正)/逆か(負)」を表す。これで法線が必ず主体側を向く。
この方法の前提と限界(任意順に強くするには)
この計算が成り立つのは、4点が時計回り or 反時計回りで入っている(開始点はどこでもよい)場合に限る。points[0]-points[2]とpoints[1]-points[3]が必ず対角線になるからだ。
もしユーザーが Z字型・蝶々型(例:左上→右下→右上→左下)のような順でクリックすると、points[2]-points[0]が対角線ではなくなり、最悪2ベクトルが平行で外積がゼロ→法線が計算不能になるリスクがある。
任意順を許容したい場合、
(1) 入力4点から面積が最大になる三角形の3点を選んでその2辺から法線を出す
(2) 各辺について座標の和と差の積を全頂点ぶん足し合わせる手法を使う
といった順序に依存しない計算にすると堅牢になる。
本記事では現状の実装のまま進める。
3. 「上」をどう決めるか:壁・床・天井のハイブリッド
ここが一番悩んだところ。平面は法線まわりに自由に回せる(ロール不定)ので、「平面自身の唯一の上」は数学的に存在しない。外から基準を1つ持ち込む必要がある。
-
壁(法線が水平に近い) → 基準はワールドの絶対的な上
Vector3.up -
床・天井(法線が垂直に近い) →
Vector3.upは法線と平行になり、射影すると消えてしまう。なので主体の据え付け初期の正面を基準にする
ポイントは、床・天井で「主体の真上の軸」を使ってはいけないこと。それも法線と平行でなくなってしまう。なので正面の軸(forward)を使う。さらに、主体が動いても基準がブレないよう、起動時の向きを一度だけ記録して固定する。
private Vector3 _robotInitialForward;
void Start()
{
if (robot != null)
_robotInitialForward = robot.forward; // 起動時に一度だけ保存
}
// 法線が垂直に近い(|n・up| が1付近)なら床・天井
Vector3 upHint;
float verticality = Mathf.Abs(Vector3.Dot(n, Vector3.up));
if (verticality < 0.9f)
upHint = Vector3.up; // 壁
else
upHint = _robotInitialForward; // 床・天井
if (upHint.sqrMagnitude < 0.0001f) // 保険
upHint = Vector3.forward;
基準となる上ベクトルの平面要素を抽出して、面に寝た上ベクトルを作る
基準の上ベクトルは平面に対して必ず上方向を向くわけではない。法線方向の成分を引くことで、必ず面の上に寝たベクトルになる。
// upHint から法線方向の成分を抜く
Vector3 u = (upHint - Vector3.Dot(upHint, n) * n).normalized;
数式で書くとこれ。
$$\vec u = \frac{\vec u_{hint} - (\vec u_{hint}\cdot \vec n)\vec n}{|\vec u_{hint} - (\vec u_{hint}\cdot \vec n)\vec n|}$$
平面に寝た右ベクトルは法線と平面に寝た上ベクトルの外積で
Vector3 r = Vector3.Cross(n, u).normalized;
注意:左右が反転する
最初Vector3.Cross(u, n)にしていたら、右が逆を向いて四隅判定の左右が反転した。Unity は左手座標系なので、ここはVector3.Cross(n, u)が正解だった。順番を入れ替えると向きが反転する。
4. 四隅を順不同で仕分ける
r(右)と u(上)というものさしができたので、各点が中心から見て右にどれだけ・上にどれだけかを内積で測り、四隅を仕分ける。
- 上成分が小さい2点=下、大きい2点=上
- 下の2点で右成分が小さい方が左下、大きい方が右下(上も同様)
void SortCorners(Vector3 center, Vector3 r, Vector3 u)
{
// 上成分で並べる → 前2つが下、後ろ2つが上
points.Sort((a, b) =>
Vector3.Dot(a - center, u).CompareTo(Vector3.Dot(b - center, u)));
// 下の2点を左→右
if (Vector3.Dot(points[0] - center, r) > Vector3.Dot(points[1] - center, r))
(points[0], points[1]) = (points[1], points[0]);
// 上の2点を左→右
if (Vector3.Dot(points[2] - center, r) > Vector3.Dot(points[3] - center, r))
(points[2], points[3]) = (points[3], points[2]);
// [左下, 右下, 左上, 右上] → [左下, 右下, 右上, 左上]
(points[2], points[3]) = (points[3], points[2]);
}
(a, b) = (b, a) はC#のタプル代入で、一時変数なしで中身を入れ替えられる。
おわりに
「平面に上下左右は無い、見る主体から決まる」という考え方がまるで地動説ですね。
高校生くらいの数学で実はここまでできてしまうんです。
建設業にもこんなITエンジニアがいるんだなぁと思ってもらえたらうれしいです。
それでは皆様!ご安全に!