KENTEM TechBlog

建設業のDXを実現するKENTEMの技術ブログです。

【Unity】多角形の内外判定を行う方法 #1 (外積を用いる)

Unityのようなゲームエンジンを用いて作られたアプリケーションでは多角形(ポリゴン)を作成し、さまざまな処理を行うことがあります。

本シリーズでは任意の点が多角形の内部に存在しているかを判定する手法を4種類紹介いたします。

  • [1] 外積を用いる方法
  • [2] 角度を用いる方法
  • [3] 平面を用いる方法
  • [4] Ray Casting Algorithmを用いる方法

本記事では[1]ベクトルの外積を用い、多角形の内外判定を行う方法の仕組みとソースコードを示します。

シリーズ目次

  1. 【Unity】多角形の内外判定を行う方法 #1 (外積を用いる) ←本記事 tech.kentem.jp
  2. 【Unity】多角形の内外判定を行う方法 #2 (角度を用いる) tech.kentem.jp
  3. 【Unity】多角形の内外判定を行う方法 #3 (平面を用いる) tech.kentem.jp
  4. 【Unity】多角形の内外判定を行う方法 #4 (Ray Casting Algorithmを用いる) tech.kentem.jp

これから作るもの

  • 再生ボタンを押すと、設定された多角形の辺が赤色で描画されます。
  • マウスのポインターが示している場所(緑色)が多角形の内側にある場合は「内側」、外側にある場合は「外側」という文字列を表示します。
  • 三角形や五角形など他の形でも判定可能です。

注意点

  • 凸多角形を対象としています。
  • 多角形の頂点の位置は反時計回りで設定する必要があります。
  • Unityを用いた開発であるため左手座標系で説明します。
  • 本シリーズで掲載する写真およびプログラムはUnityのXY平面での動作を示していますが、カメラの向きや多角形の頂点を適切に設定することで3次元に拡張することが可能です。

事前準備

Hierarchyの作成

① SampleSceneを作成し、下記のとおりにGameObjectを作成します。

CameraはScene作成時からあるものを、Canvas, EventSystemは Create > UI > Canvas で作成できるものを用います。

② PolygonとMousePositionManagerには後で示すPolygon.csとMousePositionManager.csをアタッチします。Textにはスクリプトをアタッチしていません。

多角形とポインターの描画

① 多角形の描画には下記に示すPolygon.csを使用します。

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class Polygon : MonoBehaviour
{
    // ① 多角形の頂点の位置をVector3形式で設定
    [SerializeField] private List<Vector3> _vertices = new List<Vector3>();
    public List<Vector3> Vertices => _vertices;

    // ② _verticesの位置をもとに3D空間上に多角形の辺と点を描画
    private void Start()
    {
        // ③多角形の辺を描画
        CreatePolygonLines();
        // ④多角形の頂点を描画
        CreatePolygonVertices();
    }

    private void CreatePolygonLines()
    {
        var lineRenderer = gameObject.AddComponent<LineRenderer>();
        var polygon = _vertices.Append(_vertices.First()).ToArray();
        var count = polygon.Count();
        lineRenderer.positionCount = count;
        lineRenderer.SetPositions(polygon);
        lineRenderer.startWidth = 0.1f;
        lineRenderer.endWidth = 0.1f;
        lineRenderer.material = new Material(Shader.Find("Sprites/Default"));
        lineRenderer.startColor = Color.red;
        lineRenderer.endColor = Color.red;
    }

    private void CreatePolygonVertices()
    {
        for (int i = 0; i < _vertices.Count; i++)
        {
            var obj = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            obj.transform.position = _vertices[i];
            obj.GetComponent<MeshRenderer>().material.color = new Color32(0, 0, 0, 255);
        }
    }
}

SerializeFieldの_verticesには多角形の頂点の位置を反時計回りで設定します。

② ポインターの描画には下記に示すMousePositionManager.csを使用します。

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(LineRenderer))]
public class MousePositionManager : MonoBehaviour
{
    [SerializeField] private Polygon _polygon;
    [SerializeField] private Camera _camera;
    [SerializeField] private Text _text;

    // マウスの位置を可視化するGameObject
    private GameObject _mousePointer;

    private void Start()
    {
        _mousePointer = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        _mousePointer.GetComponent<MeshRenderer>().material.color = Color.green;
    }

    // マウスの位置を_mousePointerに反映する。
    // マウスの位置が多角形の内側か外側かを判定し、結果をテキストに表示する。
    private void Update()
    {
        // マウスのスクリーン座標を取得する
        var mouseScreenPosition = Input.mousePosition;

        // カメラからマウスのスクリーン座標に向かうRayを取得する
        var ray = _camera.ScreenPointToRay(mouseScreenPosition);

        // 多角形の面を含む平面を取得する
        // 第一引数には面の法線べクトル、第二引数には平面上の点を指定する
        var plane = new Plane(_camera.transform.forward, _polygon.Vertices[0]);

        // planeに向かってRayを飛ばし、交差した点をenterで受け取る
        if (plane.Raycast(ray, out var enter))
        {
            // マウスの位置をワールド座標に変換する
            var mouseWorldPosition = ray.GetPoint(enter);

            // マウスの位置が多角形の内側か外側かを判定し、結果をテキストに表示する
            _text.text = IsPolygonInside(mouseWorldPosition) ? "内側" : "外側";

            // マウスの位置を可視化するGameObjectの位置に反映する
            _mousePointer.transform.position = mouseWorldPosition;
        }
    }

   // マウスの位置が多角形の内側かを判定する
    private bool IsPolygonInside(Vector3 mousePosition)
    {
       // 戻り値は仮のもの。詳細は後で示す。
        return true;
    }
}

SerializeFieldでは変数名と同名のGameObjectをアタッチします。

Updateメソッド内はマウスの位置を描画し、マウスの位置が多角形の内側か外側かを判定しています。

これ以降は本記事のテーマであるマウスの位置が多角形の内側か外側かを判定する部分を行うIsPolygonInsideメソッドについて解説します。

このメソッドはマウスの位置を引数に指定し、多角形の内部に存在していればtrueを返すようにしています。

冒頭で紹介した多角形の内外判定の手法ごとに記事を分け、こちらのメソッドを実装していきます。

外積を用いた多角形の内外判定

仕組みやアルゴリズムは末尾に示したURLを参考に行いました。

仕組み

① 下記を満たすベクトルabを定義します。

  • aベクトル...多角形の各頂点を始点とし、始点から見て反時計回りに1つ隣にある頂点を終点とするベクトル
  • bベクトル...多角形の各頂点を始点とし、判定対象の点(=マウスの位置)を終点とするベクトル

② この2本の外積を頂点ごとに求めます。

多角形の内部に判定対象の点が存在していれば、外積の性質により各頂点のベクトルの外積はすべて同じ向きになります。

一方、多角形の外部に判定対象の点が存在していれば、各ベクトルの外積の中に向きが異なるものが含まれています。

③ 外積同士の内積を求めます。

内積は2本のベクトルの長さとそのベクトルを挟む角の余弦(=\cos\theta)を用いるため、同じ向きであれば正の値となります。

外積同士の内積がすべて正であれば多角形の内部にあると判定できます。

ソースコード

private bool IsPolygonInside(Vector3 mousePosition)
{
    var count = _polygon.Vertices.Count;
    var crossList = new List<Vector3>();

    for (int i = 0; i < count; i++)
    {
        var currentIndex = i;
        var nextIndex = (i + 1) % count;

        // 始点を多角形の各頂点とし、終点を始点から見て反時計回りに1つ隣にある頂点とするベクトル
        var a = _polygon.Vertices[nextIndex] - _polygon.Vertices[currentIndex];

        // 始点を多角形の各頂点とし、終点を判定対象の点(=マウスの位置)とするベクトル
        var b = mousePosition - _polygon.Vertices[currentIndex];

        // aとbの外積を求める 
        var cross = Vector3.Cross(a, b);
        crossList.Add(cross);
    }
    
    // 外積が同じ向きを向いているか判定
    return crossList.All(v => Vector3.Dot(v, crossList[0]) >= 0);
}

ここまで示した方法を用いることで、特定の点が多角形の内側に存在するのかを判定できました。

次の記事では別の方法で実装した内容について解説します。

参考URL

kunassy.com

おわりに

KENTEMでは、様々な拠点でエンジニアを大募集しています! 建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。 recruit.kentem.jp career.kentem.jp