質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.63%

UnityでMesh上のクリックされた頂点を取得したい。

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 542

ta93san

score 8

前提・実現したいこと

  • 最終的に実現したいことはUnityで点群を表示し、マウスでクリックした点を取得したいです。
  • 具体的にはUnityでMeshToporogy.PointsのMeshを持ったGameObjectのクリック検知をしたいです。

発生している問題・エラーメッセージ

MeshToporogy.PointsのMeshに対してMeshColliderを設定できないため、クリック検出でよくやるRayCastIPointerClickHandlervoid OnPointerClick(PointerEventData eventData)も使えません(試したことを参照)。

該当のソースコード

100万点の点群を生成し、表示するスクリプトです。これで生成されるGameObjectをゲーム画面上でクリックしたときクリックした点が取得できれば目的達成です。

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

public class PointCloudCreater : MonoBehaviour
{

    int N = 1000000;    // 頂点数
    int N_sub = 100000;     // 1つのGameObject当たりの頂点数
    List<GameObject> objs = new List<GameObject>();

    void Start()
    {
        Vector3[] vertices = new Vector3[N_sub];
        int[] indecies = new int[N_sub];

        for(int i=0;i<N;i++){
            if(i%N_sub==0){
                vertices = new Vector3[N_sub];
                indecies = new int[N_sub];
            }
            float offset = ((int)(i/N_sub))*100;
            float x = Random.Range(offset,100+offset);
            float y = Random.Range(0,100);
            float z = Random.Range(0,100);
            vertices[i%N_sub].Set(x,y,z);
            indecies[i%N_sub] = i%N_sub;
            if((i+1)%N_sub==0){
                GameObject obj = new GameObject();
                obj.AddComponent<PointCloudPartBehaviour>();
                MeshRenderer renderer = obj.AddComponent<MeshRenderer>();
                MeshCollider collider = obj.AddComponent<MeshCollider>();
                Material material = renderer.material;
                Shader shader = Shader.Find("Mobile/Particles/VertexLit Blended");
                material.shader = shader;
                Mesh mesh = obj.AddComponent<MeshFilter>().mesh;
                mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
                mesh.vertices = vertices;
                mesh.SetIndices(indecies,MeshTopology.Points,0);
                objs.Add(obj);
            }
        }
    }
}

試したこと

① IPointerClickHandlerを継承したスクリプトコンポーネントをアタッチする。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class PointCloudPartBehaviour : MonoBehaviour, IPointerClickHandler
{
     // Detect when the Event System of Unity has detected a click on the target
    public void OnPointerClick(PointerEventData eventData)
    {
        Debug.Log(this.name + "が押された。");
    }
}

② RayCastで検出する。

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

public class ClickMonitor : MonoBehaviour
{
    void Update()
    {
        if(Input.GetMouseButtonDown(0)){
            RaycastHit hit;
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray, out hit)){
                if (hit.collider.gameObject != null){
                    GameObject obj = hit.collider.gameObject;
                    Debug.Log(obj.name + "がおされたよ。");
                }
            }
        }
    }
}


①②いずれもMeshColliderがついてないと検出できません。

補足情報(FW/ツールのバージョンなど)

Unityのバージョンは20191.5f1を使っています。
下記スクショのように、シーン画面ではMeshToporogyが何だろうがクリックしたGameObjectを検出できていますので、技術的に不可能ではないと思っているのですが、どのように実装すれば良いのか全く分かりません。
イメージ説明

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • sakura_hana

    2019/10/16 10:42

    最適解じゃないと思うのでコメントだけ。
    Collider系のクリック検出が使えないなら、「クリックされたスクリーン座標をワールド座標に変換→その座標と合致した座標を持つメッシュはあるか」を探せば一応出来る気がします。但し負荷がヤバそうです。

    大まかな形が分かっているならMeshColliderではなくBoxCollider等を付けるという手が考えられますが、実際はもっと複雑な形になるんですかね?

    キャンセル

  • ta93san

    2019/10/16 17:36

    コメントありがとうございます。
    ご指摘されている通り、スクリーン座標から写っているものを特定する方法は負荷が高すぎました。すべての点についてスクリーン上の座標を計算する必要があるため、1000万点程度が限界でした。
    ポリゴンメッシュのように、ある程度点がまとまっていれば良いのですが、恐らくそれも期待できないかと思います。

    キャンセル

  • sakura_hana

    2019/10/17 12:00

    100万個も点があれば何かしらの形を作る(そしてその形があるならBoxCollider等を付与出来る/簡単な形の組み合わせならColliderを複数追加すればいい)と思うのですが、「実際はこんな形になる」というイメージ図がありますか?

    キャンセル

  • ta93san

    2019/10/17 16:52

    特定の点群データを読み込むというより、汎用的なツールを作ることが目的となるので、具体的なイメージ図は無いです。

    キャンセル

回答 1

checkベストアンサー

+2

確かに、シーンビュー上だと正確にポイントクラウドの形に沿ったクリック判定ができていますね。Unityがどのように実現しているかは未調査ですが、コライダーとのレイキャスト的な方法ではなさそうに思います。
おそらく今回の場合にはレイキャストは効果的ではないでしょう。お試しのとおり、メッシュコライダーは普通のポリゴンメッシュのように表面を持つ物体を前提としているはずで、ポイントクラウドのように点だけで構成されたオブジェクトで使用するのは困難だろうと予想されます。

方法としては、まずはご質問者さんが試されたように素朴に頂点座標を一つひとつ調べていく手があるかと思いますが、やはり相当な高負荷になるでしょうね(しかも実際には100万点どころかもっと多数になる可能性があるのでしょうか?)。
処理コスト削減案としては、粗い判定と精密な判定の二段構えはどうでしょう。ご質問者さんの場合100万点を10万点ずつ10個のオブジェクトに分割していますが、これをもっと細かくしてそれぞれのオブジェクトにはBoxColliderをつけ、まずRaycastAllでヒットする可能性のあるオブジェクトだけを抜き出してから精密な頂点単位の判定を行うようにすれば、100万点すべてを処理しなくても済むようになるでしょうから効率的なんじゃないかと思います。

別の案として、以前別の方のご質問(unity マウスクリックした部分の色を抽出する)で検討した方法を利用できそうでしたので実験してみました。あちらではレンダーテクスチャ上にアルベド色を出力していますが、代わりにオブジェクト番号と頂点番号を出力すればいいんじゃないかと思います(あるいは、もし「何番目の頂点か」という情報が不要ならば、頂点座標そのものを出力してしまってもいいでしょう)。

インデックス出力シェーダーは下記のようにして...

Shader "Unlit/VertexIndex"
{
    Properties
    {
        _ObjectIndex ("Object Index", Int) = -1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Cull Off
            ZWrite On
            ZTest LEqual

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                uint index : SV_VertexID;
            };

            struct v2f
            {
                float index : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.index = v.index;
                return o;
            }

            int _ObjectIndex;

            float4 frag (v2f i) : SV_Target
            {
                float4 col = float4(_ObjectIndex, i.index, 0.0, 1.0);
                return col;
            }
            ENDCG
        }
    }
}

ポイントクラウド生成スクリプトは下記のようにし、さしあたりクリック判定もこれに担当させることにしました。

using System.Collections.Generic;
using UnityEngine;

public class PointCloudCreator : MonoBehaviour
{
    static readonly int ObjectIndexProperty = Shader.PropertyToID("_ObjectIndex"); // 効率化のためプロパティIDを求めておく

    [SerializeField] private Shader vertexIndexShader; // インスペクター上でUnlit/VertexIndexをセットしておく

    int N = 10000000; // 頂点数
    int N_sub = 1000000; // 1つのGameObject当たりの頂点数
    List<GameObject> objs = new List<GameObject>();
    List<MeshRenderer> renderers = new List<MeshRenderer>(); // アクセス効率化のために、レンダラーをリスト化しておく
    List<Mesh> meshes = new List<Mesh>(); // アクセス効率化のために、メッシュをリスト化しておく
    List<Vector3[]> vertices = new List<Vector3[]>(); // アクセス効率化のために、頂点をリスト化しておく
    Material vertexIndexMaterial; // インデックス描画用マテリアル
    Camera mainCamera; // メインカメラ
    Texture2D pixelTexture; // インデックス描画結果を受け取るテクスチャ

    void Start()
    {
        this.mainCamera = Camera.main; // メインカメラの参照を得る
        this.pixelTexture = new Texture2D(1, 1, TextureFormat.RGBAFloat, false); // インデックス描画結果を受け取るテクスチャを作る
        this.vertexIndexMaterial = new Material(this.vertexIndexShader); // インデックス描画用マテリアルを作る

        Vector3[] vertices = new Vector3[this.N_sub];
        int[] indecies = new int[this.N_sub];

        for(int i=0;i<this.N;i++){
            if(i%this.N_sub==0){
                vertices = new Vector3[this.N_sub];
                indecies = new int[this.N_sub];
            }
            float offset = ((int)(i/this.N_sub))*100;
            float x = Random.Range(offset,100+offset);
            float y = Random.Range(0,100);
            float z = Random.Range(0,100);
            vertices[i%this.N_sub].Set(x,y,z);
            indecies[i%this.N_sub] = i%this.N_sub;
            if((i+1)%this.N_sub==0){
                GameObject obj = new GameObject();
                // obj.AddComponent<PointCloudPartBehaviour>(); // PointCloudPartBehaviourは使用しない
                MeshRenderer renderer = obj.AddComponent<MeshRenderer>();
                // MeshCollider collider = obj.AddComponent<MeshCollider>(); // コライダーは使用しない
                Material material = renderer.material;
                Shader shader = Shader.Find("Mobile/Particles/VertexLit Blended");
                material.shader = shader;
                Mesh mesh = obj.AddComponent<MeshFilter>().mesh;
                mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
                mesh.vertices = vertices;
                mesh.SetIndices(indecies,MeshTopology.Points,0);
                this.objs.Add(obj);
                this.renderers.Add(renderer); // レンダラーをリストに追加
                this.meshes.Add(mesh); // メッシュをリストに追加
                this.vertices.Add(vertices); // 頂点をリストに追加
            }
        }
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            // 描画パラメーターをセットアップ、テクスチャを準備
            Vector2 screenScale = new Vector2(Screen.width, Screen.height);
            Vector2 pixelPosition = ((Vector2)Input.mousePosition * 2.0f) - screenScale;
            Matrix4x4 pixelMatrix = Matrix4x4.TRS(
                -pixelPosition,
                Quaternion.identity,
                new Vector3(screenScale.x, screenScale.y, 1.0f));
            Matrix4x4 viewMatrix = this.mainCamera.worldToCameraMatrix;
            RenderTexture renderTexture = RenderTexture.GetTemporary(1, 1, 24, RenderTextureFormat.ARGBFloat);
            renderTexture.filterMode = FilterMode.Point;
            RenderTexture activeTexture = RenderTexture.active;
            RenderTexture.active = renderTexture;
            GL.PushMatrix();

            // プレイモードだとCamera.currentはnullだが、ビルド後の場合はnullではなくDrawMeshNowの時に
            // 余計な影響を及ぼしてくるようだったため、Camera.currentのビュー行列を無効化して干渉を防ぐ
            var currentCamera = Camera.current;
            if (currentCamera != null)
            {
                currentCamera.worldToCameraMatrix = Matrix4x4.identity;
            }

            GL.LoadProjectionMatrix(pixelMatrix * this.mainCamera.projectionMatrix);

            // まずインデックスの初期値はオブジェクト・頂点ともに-1とでもしておき...
            GL.Clear(true, true, new Color(-1.0f, -1.0f, 0.0f, 1.0f));

            // 各メッシュを描画していく
            // 最終的に最も手前にあるオブジェクトのインデックスが書き込まれるはず
            for (int i = 0; i < this.objs.Count; i++)
            {
                this.vertexIndexMaterial.SetInt(ObjectIndexProperty, i);
                this.vertexIndexMaterial.SetPass(0);
                Graphics.DrawMeshNow(this.meshes[i], viewMatrix * this.renderers[i].localToWorldMatrix);
            }

            if (currentCamera != null)
            {
                currentCamera.ResetWorldToCameraMatrix();
            }

            GL.PopMatrix();

            // renderTextureの内容をpixelTextureに読み取り、さらにpixelTextureから色を取り出す
            this.pixelTexture.ReadPixels(new Rect(0, 0, 1, 1), 0, 0);
            RenderTexture.active = activeTexture;
            RenderTexture.ReleaseTemporary(renderTexture);
            Color result = this.pixelTexture.GetPixel(0, 0);

            // 結果を整理
            // もしいずれかの頂点をクリックできていれば、インデックスが0以上の値になるはず
            int objectIndex = (int)result.r;
            int vertexIndex = (int)result.g;
            if (objectIndex >= 0 && vertexIndex >= 0)
            {
                Vector3 vertexLocalPosition = this.vertices[objectIndex][vertexIndex];
                Vector3 vertexWorldPosition = this.renderers[objectIndex].localToWorldMatrix.MultiplyPoint3x4(vertexLocalPosition);
                Debug.Log($"Vertex {vertexIndex} of object {objectIndex} : {vertexWorldPosition}");

                // さしあたり頂点の位置に球体を生成してみました
                // ポイントクラウドが巨大なので、目立つように球体も大きめにしています
                GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                sphere.transform.position = vertexWorldPosition;
                sphere.transform.localScale = Vector3.one * 10.0f;
            }
        }
    }
}

ただし、この方法だと最前面の頂点(または最背面の頂点...プロジェクション行列の前後を反転させる)だけなら効果的に調べられるかと思いますが、RaycastAllのようにレイ上の頂点をすべて抜き出すのは難しいと思います。
パフォーマンスについてはさしあたり1000万点でやってみたところ、これだけの数の頂点でも私の古いノートPCにしてはわりといいレスポンスではありましたが、やはりもっさり感は否めませんでした。こちらの場合でも多段階判定を組み合わせた方がいいかもしれません(そもそも画面のレンダリングがけっこうきついです...遠くの頂点を事前レンダリングしたビルボードでごまかすような対策も欲しいところです)。

図

※ちなみに、ご提示のコード中の下記の部分で...

           float offset = ((int)(i/N_sub))*100;
           float x = Random.Range(offset,100+offset);
           float y = Random.Range(0,100);
           float z = Random.Range(0,100);

xだけfloat版のRangeが使われているようです。そのため、生成されるランダム座標のうちX成分だけ端数が生じているようでした。もしそれで意図通り、あるいは実験目的なのでさほど重要でないということでしたら問題ないでしょうが、X座標も整数にしたい場合はoffsetint型にするのがいいかと思います。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/10/18 08:47

    回答有難うございます。
    Shaderは触ったことがないので、目から鱗です。
    色以外の情報をレンダリング(?)することもできるんですね。
    現実的な解決策と思われるのでベストアンサーとさせていただきます。
    Shaderについては素人なのでまたわからないことを質問するかもしれませんが、よろしくお願いいたします。
    ありがとうございました。

    キャンセル

  • 2019/10/18 08:54

    ※ちなみにxだけfloatになってることについてはシンプルにミスです。実験用のコードなので実害はありませんが、ご指摘有難うございます。

    キャンセル

  • 2019/10/21 14:45

    解決済みとした後で申し訳ありません。
    UnityEditor上でのデバック実行なら動作するのですが、Windows用にビルドすると点上をクリックしても初期状態のColor(-1.0f, -1.0f, 0.0f, 1.0f)が取得されてしまうようです。
    Shaderはビルド時に含むようにしていますし、原因がわかりません。
    GLやGraphicsなどのAPIでUnityEditor特有の動きをしているものがあるのでしょうか?
    お教えいただけるとありがたいです。

    キャンセル

  • 2019/10/22 07:50

    ご指摘ありがとうございます。試してみたところ、私の環境でも失敗しました...
    いろいろ試行錯誤したところ、どうやら描画処理自体は実行されているものの、変換行列が狂っていて正しい位置に描画できていなかったようです。
    プレイモードとビルド後では描画プロセスに微妙な違いがあるらしく、ビルド後だとどうもCamera.currentに残っていたビュー変換行列が悪さをしていたようですので、それをMatrix4x4.identityに書き換えるようにスクリプトを修正してみました(シェーダーコードの側には変更はありません)。これならどうでしょうか?

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.63%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる