🥢

Unityにおけるピッキング・アルゴリズムの研究(1) 概要

2022/07/02に公開

ProBuilderはどうやって辺選択しているか?

ピッキング・アルゴリズム

主に二つの方法がある.[1]

  • レイキャスティング
  • オフスクリーン・フレームバッファ

Unityではオフスクリーン・フレームバッファではなく, RenderTextureやTexture2Dを使うことになる.

Picking with an OpenGL hack

以降レイキャスティングを使う場合をレイキャスト法, オフスクリーン・フレームバッファ(に相当するもの)を使う場合をピクセル法と呼ぶことにする.

なぜエッジ選択をなのか?

簡単な実験ツールで必要かなと思ったからです. 辺なのは一番難しそうな気がしたからです.

選択対象を管理するには?

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とかを使えば行けるのかもしれません.

EdgeRaycast

矩形選択するには?

TestEdgePickを読んでいく.

これは矩形選択モードのテストコードの例です.

ProBuilderMesh shape = ShapeFactory.Instantiate<Torus>();

/* ... */

// テストで選択対象となるオブジェクト
selectables = new ProBuilderMesh[]
{
    shape
};

この場合トルソーの辺を選択するということになる.

選択対象となるメッシュは?

独自のProBuilderMeshクラスで定義されている. 上のトルソーはまさにそんな感じ.

Class ProBuilderMesh

SelectionPicker

SelectionPicker class

Functions for picking mesh elements in a view. Can either render a texture to test, or cast a ray.

とありメッシュの要素の選択のための関数が定義されている. 条件によって

  • テクスチャを使う場合
  • レイキャストを使う場合

に分けられる.

SelectionPicker

SelectionPicker.PickEdgesInRect

SelectionPickerクラスのメソッドで矩形内の辺を選択する.

Pick the edges contained within a rect.

PIckEdgesInReact

深度テストがある場合テクスチャから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();

Texture2D.ReadPixels

ピクセルから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内のコードが参考になります. が, 少し複雑なので選択アルゴリズムという点では分かりにくいです.

選択モード

選択モードの切り替えに使われていそう.

Enum SelectMode

まとめ

ハイライトのやり方とか良く分かりませんが, 何となくできそうな気がしてきましたね.

脚注
  1. 『初めてのWebGL 2 第2版』 (Farhad Ghayour、Diego Cantor 著、あんどうやすし 訳) ↩︎

Discussion