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

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

新規登録して質問してみよう
ただいま回答率
85.50%
Unity3D

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

Unity

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

Q&A

解決済

1回答

2008閲覧

Unity Mesh.verticesでとった頂点とワールド空間の一点との距離の比較

torano

総合スコア92

Unity3D

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

Unity

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

0グッド

1クリップ

投稿2019/06/02 06:22

やりたいこと

Mesh.verticesでとってきた頂点(ローカル座標)とワールド空間の一点の座標をワールド空間の距離で比較し、一定以下のものをとりだしたいです。イメージとしては、transfrom.lossyScaleが(radius, radius, radius)の球と衝突している頂点を取り出すような感じです。ただし、Meshのtransform.rotationやtransform.lossyScaleは変わることがあり、また頂点数は数万個あります。(10万個以上はないです。テストでは30000頂点少しあるstanford bunnyでやっています。)

比較は全頂点に対し行うので、Job Systemで並列に処理したいと思っています。NativeArrayの頂点を調べ条件に合うものをNativeListとして取得する感じです。

試したこと

以下のコードのように、まずIJobParallelForで並列に、basePosからの距離の2乗を比較しNativeQueueにいれて、そのあとIJobでNativeListに入れるようにしてみました。ここでは、基準となるワールド空間の一点をInverseTransformPointでローカル空間の座標になおしてbasePosに格納、またmaxSqrDistanceには上記のradiusの二乗を入れています。ただしこのコードでは、頂点や基準の点がローカル空間のものにかかわらず、maxSqrDistanceはワールド空間の距離なのでモデルの回転やスケールが一定でないとうまくいきません。

C#

1 [Unity.Burst.BurstCompile] 2 struct SelectVertexQueueJob : IJobParallelFor 3 { 4 [ReadOnly] 5 public NativeArray<Vector3> vertices; 6 [WriteOnly] 7 public NativeQueue<int>.Concurrent queue; 8 9 public float3 basePos; 10 public float maxSqrDistance; 11 12 public void Execute(int index) 13 { 14 var v = vertices[index]; 15 var sqrDistance = math.distancesq(basePos, v); 16 17 if (sqrDistance < maxSqrDistance) 18 { 19 queue.Enqueue(index); 20 } 21 } 22 } 23 24 [Unity.Burst.BurstCompile] 25 struct QueueToListJob : IJob 26 { 27 [WriteOnly] 28 public NativeQueue<int> source; 29 [WriteOnly] 30 public NativeList<int> result; 31 32 public void Execute() 33 { 34 while (source.TryDequeue(out int value)) 35 { 36 result.Add(value); 37 } 38 } 39 }

他の方法として、頂点をワールド空間基準に直せば簡単にできてしまいますが、数万個の頂点に対しワールド空間に変換するのはちょっと重そうでやりたくないです。

となると最初の方法ですが、どうすればワールド空間の距離をローカル空間に変換するのかがわからないです。

よろしくお願いします。

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

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

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

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

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

guest

回答1

0

ベストアンサー

スケールが一様でない可能性があるのなら、ご質問者さんのおっしゃる「モデルの頂点座標をそれぞれワールド座標に直す」という作戦がいいと思いますよ。

たとえば下図はモデルがX軸だけ2倍に拡大されたものですが(ウサギの背中の上にある赤い球が取り出したい領域のつもりです)...

図1

これをモデルのローカル空間から見ると下図のように見えるはずです。

図2

赤い球の領域がX軸だけ半分に縮んでしまう...つまり、radiusが方向によって変化してしまうので、正しく領域内の点を求めるには結局頂点座標をワールド空間に直すのと同等の計算量が必要になってしまいそうです。

モデル頂点をそれぞれワールド座標に変換する方式を採用したとしても、Burst User GuideのUnity.Mathematicsの節によればベクトル演算はSIMD命令が使用されるらしいので(しかもマニュアルの注釈にあるように、3次元空間の座標変換のような4要素ベクトル演算は一番得意そうです)、パフォーマンスも結構いいんじゃないでしょうか。
それでもまだ足りないようでしたら、コンピュートシェーダーを使う作戦に切り替えたほうがいいかもしれません。これならさらに並列化され、数万点の座標変換ぐらいなら一瞬で処理できそうな気がします。

追記
Burstの効果がどの程度か試してみようと思い実験してみました。
メッシュは34834頂点のスタンフォードバニーで、下図のように球範囲内の頂点に頂点カラーを設定するというシチュエーションです。

図1

このような効果を表現するだけなら、ウサギのマテリアルのバーテックスシェーダー内で距離判定・頂点カラー決定を行う方式にすればもっと効率的な気はしますが、それだと抜き出された頂点インデックスをC#側で利用するのが困難になるでしょうから、そういう場合はご質問者さんのようにJobを使うのはいい選択だと思います。

下記のようなスクリプトを使い...

C#

1using System.Linq; 2using Unity.Burst; 3using Unity.Collections; 4using Unity.Jobs; 5using Unity.Mathematics; 6using UnityEngine; 7using UnityEngine.Profiling; 8 9namespace Scenes.FindPoints 10{ 11 public class PointFinder : MonoBehaviour 12 { 13 [SerializeField] private Transform model; // スタンフォードバニーのメッシュを持つオブジェクト 14 [SerializeField] private Transform region; // 抜き出し範囲を表す球オブジェクト 15 16 private SphereCollider regionCollider; 17 private Mesh mesh; 18 private Vector3[] meshVertices; 19 private Color32[] meshColors; 20 private NativeArray<float3> vertices; 21 private NativeArray<Color32> colors; 22 private NativeArray<Color32> initialColors; 23 24 private void Start() 25 { 26 if ((this.model == null) || (this.region == null)) 27 { 28 Destroy(this); 29 return; 30 } 31 32 var meshFilter = this.model.GetComponent<MeshFilter>(); 33 this.regionCollider = this.region.GetComponent<SphereCollider>(); 34 if ((meshFilter == null) || (this.regionCollider == null)) 35 { 36 Destroy(this); 37 return; 38 } 39 40 this.mesh = meshFilter.mesh; 41 if (this.mesh == null) 42 { 43 Destroy(this); 44 return; 45 } 46 47 this.meshVertices = this.mesh.vertices; 48 if (this.meshVertices == null) 49 { 50 Destroy(this); 51 return; 52 } 53 54 this.meshColors = this.mesh.colors32; 55 if ((this.meshColors == null) || (this.meshColors.Length != this.meshVertices.Length)) 56 { 57 this.meshColors = Enumerable.Repeat(new Color32(255, 255, 255, 255), this.meshVertices.Length).ToArray(); 58 } 59 60 this.vertices = new NativeArray<float3>(this.meshVertices.Select(v => (float3)v).ToArray(), Allocator.Persistent); 61 this.colors = new NativeArray<Color32>(this.meshColors, Allocator.Persistent); 62 this.initialColors = new NativeArray<Color32>(this.meshColors, Allocator.Persistent); 63 NativeArray<Color32>.Copy(this.meshColors, this.initialColors); 64 } 65 66 private void OnDestroy() 67 { 68 if (this.vertices.IsCreated) 69 { 70 this.vertices.Dispose(); 71 } 72 73 if (this.colors.IsCreated) 74 { 75 this.colors.Dispose(); 76 } 77 78 if (this.initialColors.IsCreated) 79 { 80 this.initialColors.Dispose(); 81 } 82 } 83 84 private void Update() 85 { 86 // 範囲内の頂点のインデックスを抜き出す作業 87 Profiler.BeginSample("Find indices"); 88 var center = this.region.TransformPoint(this.regionCollider.center); 89 var scales = this.region.lossyScale; 90 var scale = Mathf.Max(Mathf.Max(scales.x, scales.y), scales.z); 91 var radius = scale * this.regionCollider.radius; 92 var indices = new NativeList<int>(Allocator.TempJob); 93 var indicesQueue = new NativeQueue<int>(Allocator.TempJob); 94 var selectVertexQueueJob = new SelectVertexQueueJob 95 { 96 vertices = this.vertices, 97 queue = indicesQueue.ToConcurrent(), 98 basePos = center, 99 maxSqrDistance = radius * radius, 100 transform = this.model.localToWorldMatrix 101 }.Schedule(this.vertices.Length, 0); 102 new QueueToListJob 103 { 104 source = indicesQueue, 105 result = indices 106 }.Schedule(selectVertexQueueJob).Complete(); 107 indicesQueue.Dispose(); 108 Profiler.EndSample(); 109 110 // 抜き出された頂点に頂点カラーを設定、メッシュデータを更新する作業 111 Profiler.BeginSample("Apply vertex colors"); 112 NativeArray<Color32>.Copy(this.initialColors, this.colors); 113 new ApplyColorJob 114 { 115 indices = indices, 116 colors = this.colors, 117 color = new Color32(255, 0, 0, 255) 118 }.Schedule().Complete(); 119 indices.Dispose(); 120 NativeArray<Color32>.Copy(this.colors, this.meshColors); 121 this.mesh.colors32 = this.meshColors; 122 this.mesh.UploadMeshData(false); 123 Profiler.EndSample(); 124 } 125 126 [BurstCompile] 127 private struct SelectVertexQueueJob : IJobParallelFor 128 { 129 [ReadOnly] public NativeArray<float3> vertices; 130 [WriteOnly] public NativeQueue<int>.Concurrent queue; 131 132 public float3 basePos; 133 public float maxSqrDistance; 134 public float4x4 transform; 135 136 public void Execute(int index) 137 { 138 // ワールド座標に変換して球中心との2乗距離を調べ... 139 var v = math.mul(this.transform, new float4(this.vertices[index], 1.0f)).xyz; 140 var sqrDistance = math.distancesq(this.basePos, v); 141 if (sqrDistance < this.maxSqrDistance) 142 { 143 // 範囲内ならキューにインデックスを追加 144 this.queue.Enqueue(index); 145 } 146 } 147 } 148 149 [BurstCompile] 150 private struct QueueToListJob : IJob 151 { 152 [WriteOnly] public NativeQueue<int> source; 153 [WriteOnly] public NativeList<int> result; 154 155 public void Execute() 156 { 157 while (this.source.TryDequeue(out var value)) 158 { 159 this.result.Add(value); 160 } 161 } 162 } 163 164 [BurstCompile] 165 private struct ApplyColorJob : IJob 166 { 167 [ReadOnly] public NativeList<int> indices; 168 [WriteOnly] public NativeArray<Color32> colors; 169 170 public Color32 color; 171 172 public void Execute() 173 { 174 for (var i = 0; i < this.indices.Length; i++) 175 { 176 this.colors[this.indices[i]] = this.color; 177 } 178 } 179 } 180 } 181}

Burstオフで実行すると頂点抜き出し作業にかなりの時間がかかりますが...

図2

Burstをオンにすると、抜き出し作業時間が大幅に短縮されました。

図3

使ったのは数年前に入手した比較的廉価なノートパソコンでIntel Core i3-4030Uが載ったマシンですが、それでもこれだけの速度が出るならワールド座標変換方式でも悪くないんじゃないでしょうか?

投稿2019/06/02 11:17

編集2019/06/03 22:08
Bongo

総合スコア10807

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

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

torano

2019/06/03 08:02

ありがとうございます。スケールxyzが同じでない場合、結局全頂点ごとに変換しなければならないということですね。 他の理由でワールド空間の座標を使用する必要がでてきたので、速度でるかわからないのですが全頂点をワールド空間に直す方式でやってみたいと思います。
torano

2019/06/10 12:11

追記されていたんですね、ありがとうございます。そうですね、自分もBurst使ってIL2CPPでビルドしてみたのですがエディタに比べてすごく高速化できていたので驚きました。 自分の場合は、ずっと毎フレームこの頂点選択を実行するのではなく一定の期間毎フレーム行うのと、Meshが変更の可能性があることもあり、実行のタイミングになったら毎フレームNativeArrayをAllocator.TempJobでnewし、使い終わったらDisposeするようにしていました。逆に、NativeListとNativeQueueはClearとdequeueすれば使いまわせるのでPersistentです。 また毎フレームnewする際Linqで要素いれるのはあまり速くないと思い(Linqはたぶんforループでひとつずつ入れてるのと同じ?間違っていたらすいません)、NativeArray<Vector3>(T[], Allocator)のコンストラクタでいれるようにしました。コンストラクタですが、内部ではNativeArray.CopyFromを使用していて、これは以前にはそんな早くなかったようですが調べてみたら今はけっこう高速になっているようでした。またここでfloat3ではなくVector3になっていますが、Job内部でfloat3に変換後計算はBurstの恩恵を受けれているようで、以上のように変更しても同じくらい高速です。 というわけで頂点ごとに座標変換しても問題なさそうです。Burst&Job System使うといろんなことが高速化できて楽しいですね。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.50%

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

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

質問する

関連した質問