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

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

新規登録して質問してみよう
ただいま回答率
85.48%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Unity3D

Unity3Dは、ゲームや対話式の3Dアプリケーション、トレーニングシュミレーション、そして医学的・建築学的な技術を可視化する、商業用の開発プラットフォームです。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

Q&A

解決済

1回答

9553閲覧

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

ta93san

総合スコア23

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Unity3D

Unity3Dは、ゲームや対話式の3Dアプリケーション、トレーニングシュミレーション、そして医学的・建築学的な技術を可視化する、商業用の開発プラットフォームです。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

0グッド

0クリップ

投稿2019/10/15 09:10

編集2019/10/15 09:12

前提・実現したいこと

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

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

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

該当のソースコード

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

cs

1using System.Collections; 2using System.Collections.Generic; 3using UnityEngine; 4 5public class PointCloudCreater : MonoBehaviour 6{ 7 8 int N = 1000000; // 頂点数 9 int N_sub = 100000; // 1つのGameObject当たりの頂点数 10 List<GameObject> objs = new List<GameObject>(); 11 12 void Start() 13 { 14 Vector3[] vertices = new Vector3[N_sub]; 15 int[] indecies = new int[N_sub]; 16 17 for(int i=0;i<N;i++){ 18 if(i%N_sub==0){ 19 vertices = new Vector3[N_sub]; 20 indecies = new int[N_sub]; 21 } 22 float offset = ((int)(i/N_sub))*100; 23 float x = Random.Range(offset,100+offset); 24 float y = Random.Range(0,100); 25 float z = Random.Range(0,100); 26 vertices[i%N_sub].Set(x,y,z); 27 indecies[i%N_sub] = i%N_sub; 28 if((i+1)%N_sub==0){ 29 GameObject obj = new GameObject(); 30 obj.AddComponent<PointCloudPartBehaviour>(); 31 MeshRenderer renderer = obj.AddComponent<MeshRenderer>(); 32 MeshCollider collider = obj.AddComponent<MeshCollider>(); 33 Material material = renderer.material; 34 Shader shader = Shader.Find("Mobile/Particles/VertexLit Blended"); 35 material.shader = shader; 36 Mesh mesh = obj.AddComponent<MeshFilter>().mesh; 37 mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; 38 mesh.vertices = vertices; 39 mesh.SetIndices(indecies,MeshTopology.Points,0); 40 objs.Add(obj); 41 } 42 } 43 } 44}

試したこと

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

cs

1using System.Collections; 2using System.Collections.Generic; 3using UnityEngine; 4using UnityEngine.EventSystems; 5 6public class PointCloudPartBehaviour : MonoBehaviour, IPointerClickHandler 7{ 8 // Detect when the Event System of Unity has detected a click on the target 9 public void OnPointerClick(PointerEventData eventData) 10 { 11 Debug.Log(this.name + "が押された。"); 12 } 13}

② 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を検出できていますので、技術的に不可能ではないと思っているのですが、どのように実装すれば良いのか全く分かりません。
イメージ説明

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sakura_hana

2019/10/16 01:42

最適解じゃないと思うのでコメントだけ。 Collider系のクリック検出が使えないなら、「クリックされたスクリーン座標をワールド座標に変換→その座標と合致した座標を持つメッシュはあるか」を探せば一応出来る気がします。但し負荷がヤバそうです。 大まかな形が分かっているならMeshColliderではなくBoxCollider等を付けるという手が考えられますが、実際はもっと複雑な形になるんですかね?
ta93san

2019/10/16 08:36

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

2019/10/17 03:00

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

2019/10/17 07:52

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

回答1

0

ベストアンサー

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

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

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

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

ShaderLab

1Shader "Unlit/VertexIndex" 2{ 3 Properties 4 { 5 _ObjectIndex ("Object Index", Int) = -1 6 } 7 SubShader 8 { 9 Tags { "RenderType"="Opaque" } 10 11 Pass 12 { 13 Cull Off 14 ZWrite On 15 ZTest LEqual 16 17 CGPROGRAM 18 #pragma vertex vert 19 #pragma fragment frag 20 21 #include "UnityCG.cginc" 22 23 struct appdata 24 { 25 float4 vertex : POSITION; 26 uint index : SV_VertexID; 27 }; 28 29 struct v2f 30 { 31 float index : TEXCOORD0; 32 float4 vertex : SV_POSITION; 33 }; 34 35 v2f vert (appdata v) 36 { 37 v2f o; 38 o.vertex = UnityObjectToClipPos(v.vertex); 39 o.index = v.index; 40 return o; 41 } 42 43 int _ObjectIndex; 44 45 float4 frag (v2f i) : SV_Target 46 { 47 float4 col = float4(_ObjectIndex, i.index, 0.0, 1.0); 48 return col; 49 } 50 ENDCG 51 } 52 } 53}

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

C#

1using System.Collections.Generic; 2using UnityEngine; 3 4public class PointCloudCreator : MonoBehaviour 5{ 6 static readonly int ObjectIndexProperty = Shader.PropertyToID("_ObjectIndex"); // 効率化のためプロパティIDを求めておく 7 8 [SerializeField] private Shader vertexIndexShader; // インスペクター上でUnlit/VertexIndexをセットしておく 9 10 int N = 10000000; // 頂点数 11 int N_sub = 1000000; // 1つのGameObject当たりの頂点数 12 List<GameObject> objs = new List<GameObject>(); 13 List<MeshRenderer> renderers = new List<MeshRenderer>(); // アクセス効率化のために、レンダラーをリスト化しておく 14 List<Mesh> meshes = new List<Mesh>(); // アクセス効率化のために、メッシュをリスト化しておく 15 List<Vector3[]> vertices = new List<Vector3[]>(); // アクセス効率化のために、頂点をリスト化しておく 16 Material vertexIndexMaterial; // インデックス描画用マテリアル 17 Camera mainCamera; // メインカメラ 18 Texture2D pixelTexture; // インデックス描画結果を受け取るテクスチャ 19 20 void Start() 21 { 22 this.mainCamera = Camera.main; // メインカメラの参照を得る 23 this.pixelTexture = new Texture2D(1, 1, TextureFormat.RGBAFloat, false); // インデックス描画結果を受け取るテクスチャを作る 24 this.vertexIndexMaterial = new Material(this.vertexIndexShader); // インデックス描画用マテリアルを作る 25 26 Vector3[] vertices = new Vector3[this.N_sub]; 27 int[] indecies = new int[this.N_sub]; 28 29 for(int i=0;i<this.N;i++){ 30 if(i%this.N_sub==0){ 31 vertices = new Vector3[this.N_sub]; 32 indecies = new int[this.N_sub]; 33 } 34 float offset = ((int)(i/this.N_sub))*100; 35 float x = Random.Range(offset,100+offset); 36 float y = Random.Range(0,100); 37 float z = Random.Range(0,100); 38 vertices[i%this.N_sub].Set(x,y,z); 39 indecies[i%this.N_sub] = i%this.N_sub; 40 if((i+1)%this.N_sub==0){ 41 GameObject obj = new GameObject(); 42 // obj.AddComponent<PointCloudPartBehaviour>(); // PointCloudPartBehaviourは使用しない 43 MeshRenderer renderer = obj.AddComponent<MeshRenderer>(); 44 // MeshCollider collider = obj.AddComponent<MeshCollider>(); // コライダーは使用しない 45 Material material = renderer.material; 46 Shader shader = Shader.Find("Mobile/Particles/VertexLit Blended"); 47 material.shader = shader; 48 Mesh mesh = obj.AddComponent<MeshFilter>().mesh; 49 mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; 50 mesh.vertices = vertices; 51 mesh.SetIndices(indecies,MeshTopology.Points,0); 52 this.objs.Add(obj); 53 this.renderers.Add(renderer); // レンダラーをリストに追加 54 this.meshes.Add(mesh); // メッシュをリストに追加 55 this.vertices.Add(vertices); // 頂点をリストに追加 56 } 57 } 58 } 59 60 void Update() 61 { 62 if (Input.GetMouseButtonDown(0)) 63 { 64 // 描画パラメーターをセットアップ、テクスチャを準備 65 Vector2 screenScale = new Vector2(Screen.width, Screen.height); 66 Vector2 pixelPosition = ((Vector2)Input.mousePosition * 2.0f) - screenScale; 67 Matrix4x4 pixelMatrix = Matrix4x4.TRS( 68 -pixelPosition, 69 Quaternion.identity, 70 new Vector3(screenScale.x, screenScale.y, 1.0f)); 71 Matrix4x4 viewMatrix = this.mainCamera.worldToCameraMatrix; 72 RenderTexture renderTexture = RenderTexture.GetTemporary(1, 1, 24, RenderTextureFormat.ARGBFloat); 73 renderTexture.filterMode = FilterMode.Point; 74 RenderTexture activeTexture = RenderTexture.active; 75 RenderTexture.active = renderTexture; 76 GL.PushMatrix(); 77 78 // プレイモードだとCamera.currentはnullだが、ビルド後の場合はnullではなくDrawMeshNowの時に 79 // 余計な影響を及ぼしてくるようだったため、Camera.currentのビュー行列を無効化して干渉を防ぐ 80 var currentCamera = Camera.current; 81 if (currentCamera != null) 82 { 83 currentCamera.worldToCameraMatrix = Matrix4x4.identity; 84 } 85 86 GL.LoadProjectionMatrix(pixelMatrix * this.mainCamera.projectionMatrix); 87 88 // まずインデックスの初期値はオブジェクト・頂点ともに-1とでもしておき... 89 GL.Clear(true, true, new Color(-1.0f, -1.0f, 0.0f, 1.0f)); 90 91 // 各メッシュを描画していく 92 // 最終的に最も手前にあるオブジェクトのインデックスが書き込まれるはず 93 for (int i = 0; i < this.objs.Count; i++) 94 { 95 this.vertexIndexMaterial.SetInt(ObjectIndexProperty, i); 96 this.vertexIndexMaterial.SetPass(0); 97 Graphics.DrawMeshNow(this.meshes[i], viewMatrix * this.renderers[i].localToWorldMatrix); 98 } 99 100 if (currentCamera != null) 101 { 102 currentCamera.ResetWorldToCameraMatrix(); 103 } 104 105 GL.PopMatrix(); 106 107 // renderTextureの内容をpixelTextureに読み取り、さらにpixelTextureから色を取り出す 108 this.pixelTexture.ReadPixels(new Rect(0, 0, 1, 1), 0, 0); 109 RenderTexture.active = activeTexture; 110 RenderTexture.ReleaseTemporary(renderTexture); 111 Color result = this.pixelTexture.GetPixel(0, 0); 112 113 // 結果を整理 114 // もしいずれかの頂点をクリックできていれば、インデックスが0以上の値になるはず 115 int objectIndex = (int)result.r; 116 int vertexIndex = (int)result.g; 117 if (objectIndex >= 0 && vertexIndex >= 0) 118 { 119 Vector3 vertexLocalPosition = this.vertices[objectIndex][vertexIndex]; 120 Vector3 vertexWorldPosition = this.renderers[objectIndex].localToWorldMatrix.MultiplyPoint3x4(vertexLocalPosition); 121 Debug.Log($"Vertex {vertexIndex} of object {objectIndex} : {vertexWorldPosition}"); 122 123 // さしあたり頂点の位置に球体を生成してみました 124 // ポイントクラウドが巨大なので、目立つように球体も大きめにしています 125 GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); 126 sphere.transform.position = vertexWorldPosition; 127 sphere.transform.localScale = Vector3.one * 10.0f; 128 } 129 } 130 } 131}

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

図

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

C#

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

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

投稿2019/10/17 13:43

編集2019/10/21 22:49
Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ta93san

2019/10/17 23:47

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

2019/10/17 23:54

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

2019/10/21 05:45

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

2019/10/21 22:50

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問