Unityにおけるピッキング・アルゴリズムの研究(1) 概要
ProBuilderはどうやって辺選択しているか?
ピッキング・アルゴリズム
主に二つの方法がある.[1]
- レイキャスティング
- オフスクリーン・フレームバッファ
Unityではオフスクリーン・フレームバッファではなく, RenderTextureやTexture2Dを使うことになる.
以降レイキャスティングを使う場合をレイキャスト法, オフスクリーン・フレームバッファ(に相当するもの)を使う場合をピクセル法と呼ぶことにする.
なぜエッジ選択をなのか?
簡単な実験ツールで必要かなと思ったからです. 辺なのは一番難しそうな気がしたからです.
選択対象を管理するには?
SceneSelectionクラスを使う. シーン内の選択の有無を管理するオブジェクトです.
マウスオーバー時の対象をピッキングするには?
これも一種の選択でしょう. 選択済みとしてキャッシュするのかどうかの違いです. 見ずらいですが黄色になっているのがマウスオーバー時のハイライトです.
ProBuilderEditor.OnSceneGUIに以下のようなコードがあります.
if (s_ShowHoverHighlight
&& selectMode.IsMeshElementMode()
&& (m_CurrentEvent.type == EventType.MouseMove
|| (m_wasSelectingPath != pathSelectionModifier && m_CurrentEvent.isKey)))
{
m_Hovering.CopyTo(m_HoveringPrevious);
if (GUIUtility.hotControl != 0 ||
EditorSceneViewPicker.MouseRayHitTest(m_CurrentEvent.mousePosition, selectMode, m_ScenePickerPreferences, m_Hovering) > ScenePickerPreferences.maxPointerDistance)
m_Hovering.Clear();
if (!m_Hovering.Equals(m_HoveringPrevious))
{
if (pathSelectionModifier)
EditorSceneViewPicker.DoMouseHover(m_Hovering);
SceneView.RepaintAll();
}
}
m_HoveringがSceneSelection型でマウスオーバー時にハイライトされる対象(辺)を表します.
GUIUtility.hotControl != 0 ||
EditorSceneViewPicker.MouseRayHitTest(m_CurrentEvent.mousePosition, selectMode, m_ScenePickerPreferences, m_Hovering) > ScenePickerPreferences.maxPointerDistance
最大範囲にが決まっていますのでこれを超えると位置にある辺ハイライトはされません.
MouseRayHitTest
MouseRayHitTestはモード・フラグに応じて以下の三つから適当なメソッドを呼び出してくれます.
- EdgeRaycast
- VertexRaycast
- FaceRaycast
EdgeRaycast
こいつが本体です. この中身を読めばいいわけですが, HandleUtility.PickGameObjectというUnityのメソッドを使っています. 中身が読めないじゃん!
ただ基本的にはやっていることはGameObject -> Face -> Edgeの順に探索して該当するかどうかを判断するシンプルなものです.
RuntimeではRaycastとかを使えば行けるのかもしれません.
矩形選択するには?
TestEdgePickを読んでいく.
これは矩形選択モードのテストコードの例です.
ProBuilderMesh shape = ShapeFactory.Instantiate<Torus>();
/* ... */
// テストで選択対象となるオブジェクト
selectables = new ProBuilderMesh[]
{
shape
};
この場合トルソーの辺を選択するということになる.
選択対象となるメッシュは?
独自のProBuilderMeshクラスで定義されている. 上のトルソーはまさにそんな感じ.
SelectionPicker
Functions for picking mesh elements in a view. Can either render a texture to test, or cast a ray.
とありメッシュの要素の選択のための関数が定義されている. 条件によって
- テクスチャを使う場合
- レイキャストを使う場合
に分けられる.
SelectionPicker.PickEdgesInRect
SelectionPickerクラスのメソッドで矩形内の辺を選択する.
Pick the edges contained within a rect.
深度テストがある場合テクスチャからDepth Bufferの情報をもらう必要がある. この場合SelectionPicker.PickEdgesInRectはSelectionPIckerRendererに処理を委譲する.
if (options.depthTest && options.rectSelectMode == RectSelectMode.Partial)
{
return SelectionPickerRenderer.PickEdgesInRect(
cam,
rect,
selectable,
true,
(int)(cam.pixelWidth / pixelsPerPoint),
(int)(cam.pixelHeight / pixelsPerPoint));
}
そうでない場合は座標変換やらしてレイキャスト法で遮蔽のチェックしたりして, エッジが矩形領域に含まれるか判断する.
SelectionPickerRenderer
SelectionPickerRenderer.PickEdgesInRectという同名のメソッドがある.
pickerRenderer静的プロパティが適切な実装を動的に返してくれる. このインスタンスに実際の処理は委譲される.
static ISelectionPickerRenderer pickerRenderer
{
get
{
if (s_PickerRenderer == null)
s_PickerRenderer =
ShouldUseHDRP() ?
(ISelectionPickerRenderer)new SelectionPickerRendererHDRP()
: new SelectionPickerRendererStandard();
return s_PickerRenderer;
}
}
がある. URPの場合はSelectionPickerRendererStandardが使われるんだと思う.
テクスチャからデータを取り出すには?
SelectionPickerRenderer.RenderSelectionPickerTextureでピクセル領域を取得する.
RenderSelectionPickerTextureの内部ではGenerateVertexPickingObjects, GenerateEdgePickingObjects, GenerateFacePickingObjectsと選択対象を分けている. これはRenderSelectionPickerTextureに渡されるmapの型で判断しているようである.
// For Edge Selection
Dictionary<uint, SimpleTuple<ProBuilderMesh, Edge>> map;
Texture2D tex = RenderSelectionPickerTexture(camera, selection, doDepthTest, out map, renderTextureWidth, renderTextureHeight);
内部的にはSelectionPickerRendererStandard.RenderLookupTextureを使っている.
以下の部分でピクセルをレンダー・ターゲットから読み込んでimgに読み込んでいます. Applyを呼ぶとこのデータがGPUにアップロードされます.
Texture2D img = new Texture2D(_width, _height, textureFormat, false, false);
img.ReadPixels(new Rect(0, 0, _width, _height), 0, 0);
img.Apply();
ピクセルからIDへの変換するには?
まずTexture2D.GetPixels32でピクセルを取得する.
Color32[] pix = tex.GetPixels32();
ピクセルの色は一意(uint型)で決められている. これを用いて識別子へ変換する.
uint v = DecodeRGBA(pix[y * imageWidth + x]);
この識別子の個数で矩形領域にどの辺が含まれるかを判断する. ピクセルをまたぐ場合はその個数も数えておく.
if (!pixelCount.ContainsKey(v))
pixelCount.Add(v, 1);
else
pixelCount[v] = pixelCount[v] + 1;
Appendix
DoMouseClick
DoMouseClick内のコードが参考になります. が, 少し複雑なので選択アルゴリズムという点では分かりにくいです.
選択モード
選択モードの切り替えに使われていそう.
まとめ
ハイライトのやり方とか良く分かりませんが, 何となくできそうな気がしてきましたね.
-
『初めてのWebGL 2 第2版』 (Farhad Ghayour、Diego Cantor 著、あんどうやすし 訳) ↩︎
Discussion