OpenTKでオブジェクトを選択する（マウスピッキング）

#■ はじめに

WPFアプリケーションでOpenTKを使ったときのオブジェクト選択を実装しています。
Tao Frameworkのときとほぼ同じ要領で実現できますが、一部対応する関数がなかったりするのでそのあたりの実装が必要になります。

#■1. xaml
OpenTK.GLControlを貼り付けています。

```MainWindow.xaml
<Window x:Class="OpenTKTestApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:OpenTKTestApp"
        xmlns:OpenTK="clr-namespace:OpenTK;assembly=OpenTK.GLControl"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <WindowsFormsHost>
            <OpenTK:GLControl x:Name="glControl" Load="glControl_Load" Resize="glControl_Resize" Paint="glControl_Paint" MouseClick="glControl_MouseClick" />
        </WindowsFormsHost>

    </Grid>
</Window>
```

#■2. ポリゴンデータを作成する

![20180702_TestApp 図面.jpg](https://qiita-image-store.s3.amazonaws.com/0/252253/30c0b2f0-172e-8352-029f-38ca36c53231.jpeg)
画像のようなポリゴンを描画するためのデータを作成します。
頂点データ、エッジデータ、三角形データを作成しています。
頂点データは座標データですが、エッジデータ、三角形データは頂点のインデックスの配列から構成します。

```MainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        private int NDim = 2;
        private double WindowAspect = 1.0;
        private IList<OpenTK.Vector2> Vertexs = new List<OpenTK.Vector2>();
        private double[] VertexCoordArray = null;
        private IList<uint[]> EdgeIndexs = new List<uint[]>();
        private IList<uint[]> TriIndexs = new List<uint[]>();
        private int SelectedPartId = 0;

        public MainWindow()
        {
            InitializeComponent();

            Vertexs.Add(new OpenTK.Vector2(0.0f, 1.0f));
            Vertexs.Add(new OpenTK.Vector2(0.0f, 0.0f));
            Vertexs.Add(new OpenTK.Vector2(1.0f, 0.0f));
            Vertexs.Add(new OpenTK.Vector2(1.0f, -1.0f));
            Vertexs.Add(new OpenTK.Vector2(2.0f, -1.0f));
            Vertexs.Add(new OpenTK.Vector2(2.0f, 1.0f));

            VertexCoordArray = new double[Vertexs.Count * NDim];
            for (int i = 0; i < Vertexs.Count; i++)
            {
                var vec = Vertexs[i];
                VertexCoordArray[i * 2 + 0] = vec.X;
                VertexCoordArray[i * 2 + 1] = vec.Y;
            }

            EdgeIndexs.Add(new uint[2] { 0, 1 });
            EdgeIndexs.Add(new uint[2] { 1, 2 });
            EdgeIndexs.Add(new uint[2] { 2, 3 });
            EdgeIndexs.Add(new uint[2] { 3, 4 });
            EdgeIndexs.Add(new uint[2] { 4, 5 });
            EdgeIndexs.Add(new uint[2] { 5, 0 });

            TriIndexs.Add(new uint[3] { 2, 4, 5 });
            TriIndexs.Add(new uint[3] { 0, 2, 5 });
            TriIndexs.Add(new uint[3] { 2, 3, 4 });
            TriIndexs.Add(new uint[3] { 0, 1, 2});
        }
    }
```
#■3. ロード

```MainWindow.xaml.cs
        /// <summary>
        /// glControlの起動時に実行される。
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void glControl_Load(object sender, EventArgs e)
        {
            GL.Enable(EnableCap.DepthTest);
        }
```

#■4. リサイズ

```MainWindow.xaml.cs
        /// <summary>
        /// glControlのサイズ変更時に実行される。
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void glControl_Resize(object sender, EventArgs e)
        {
            int width = glControl.Size.Width;
            int height = glControl.Size.Height;
            WindowAspect = ((double)width / height);

            GL.Viewport(0, 0, width, height);
            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadIdentity();

            SetProjectionTransform();
        }
```

#■5. 投影変換

```MainWindow.xaml.cs
        private void SetProjectionTransform()
        {
            double windowAspect = WindowAspect;
            double scale = 1.0;
            double halfViewHeight = 1.5;

            double invScale = 1.0 / scale;
            double asp = windowAspect;
            double hH = halfViewHeight * invScale;
            double hW = halfViewHeight * invScale * asp;
            double depth = 2.0 * (hH + hW);

            GL.Ortho(-hW, hW, -hH, hH, -depth, depth);
        }

        private void SetModelViewTransform()
        {
            Vector3 centerPos = new Vector3(0, 0, 0);
            double[] rot = new double[16] { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
            Vector3 objCenterPos = new Vector3(1, 0, 0);

            GL.Translate(centerPos.X, centerPos.Y, centerPos.Z);

            GL.MultMatrix(rot);
            GL.Translate(-objCenterPos.X, -objCenterPos.Y, -objCenterPos.Z);
        }
```

#■6. オブジェクトの描画

点、エッジ、ループを描画します。
ループは領域を三角形をあわせたものです。
```partsId```を割り振ってマウスで選択されたオブジェクトの色を変える処理を実装しています。

```MainWindow.xaml.cs
        /// <summary>
        /// glControlの描画時に実行される。
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void glControl_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
        {
            GL.ClearColor(Color4.White);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
            GL.Enable(EnableCap.PolygonOffsetFill);
            GL.PolygonOffset(1.1f, 4.0f);
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadIdentity();

            SetModelViewTransform();

            // Draw
            Draw();

            glControl.SwapBuffers();
        }

        private void Draw()
        {
            float[] selectedColor = { 1.0f, 1.0f, 0.0f };
            int partId = 2;
            GL.Enable(EnableCap.DepthTest);
            GL.PointSize(5);
            GL.Begin(BeginMode.Points);
            for (int iver = 0; iver < Vertexs.Count; iver++)
            {
                double height = 0.1;
                if (partId == SelectedPartId)
                {
                    GL.Color3(selectedColor);
                }
                else
                {
                    GL.Color3(0.0, 0.0, 0.0);
                }
                GL.Vertex3(
                    VertexCoordArray[iver * NDim + 0],
                    VertexCoordArray[iver * NDim + 1],
                    height);
                partId++;
            }
            GL.End();

            GL.EnableClientState(ArrayCap.VertexArray);
            GL.VertexPointer((int)NDim, VertexPointerType.Double, 0, VertexCoordArray);
            GL.LineWidth(3);
            for (int i = 0; i < EdgeIndexs.Count; i++)
            {
                double height = 0.0;
                if (partId == SelectedPartId)
                {
                    GL.Color3(selectedColor);
                }
                else
                {
                    GL.Color3(0.0f, 0.0f, 0.0f);
                }
                GL.Translate(0.0, 0.0, height);
                var index = EdgeIndexs[i];
                GL.DrawElements(BeginMode.Lines, index.Length, DrawElementsType.UnsignedInt, index);
                GL.Translate(0.0, 0.0, -height);
                partId++;
            }

            for (int i = 0; i < TriIndexs.Count; i++)
            {
                double height = 0.0;
                double dispX = 0.0;
                double dispY = 0.0;
                float[] color = { 0.2f, 0.2f, 0.2f };
                var index = TriIndexs[i];
                GL.Material(MaterialFace.FrontAndBack, MaterialParameter.Diffuse, color);
                if (partId == SelectedPartId)
                {
                    GL.Color3(selectedColor);
                }
                else
                {
                    GL.Color3(color);
                }
                GL.Translate(+dispX, +dispY, +height);
                GL.DrawElements(BeginMode.Triangles, index.Length, DrawElementsType.UnsignedInt, index);
                GL.Translate(-dispX, -dispY, -height);
            }
            partId++;

        }
``` 

#■7. マウスでオブジェクトを選択する（1）マウスピッキング

まずマウスピッキングの開始、終了処理を実装します。
開始時に```GL.RenderMode(RenderingMode.Select)```セレクションモードにしています。
終了時に```GL.RenderMode(RenderingMode.Render)```でレンダリングモードを元に戻しています。このとき、```GL.RenderMode(RenderingMode.Render)```の戻り値にはヒットしたオブジェクトの数が帰ってきます。

```MainWindow.xaml.cs
        private void PickPre(
                int sizeBuffer, int[] selectBuffer,
                uint pointX, uint pointY,
                uint delX, uint delY)
        {
            // Selection初期化
            GL.SelectBuffer(sizeBuffer, selectBuffer);
            GL.RenderMode(RenderingMode.Select);

            // View Port取得
            int[] viewport = new int[4];
            GL.GetInteger(GetPName.Viewport, viewport);

            GL.InitNames();

            // Projection Transform From Here
            GL.MatrixMode(MatrixMode.Projection);
            GL.PushMatrix();
            GL.LoadIdentity();
            //Tao.OpenGl.Glu.gluPickMatrix(pointX, viewport[3] - pointY, delX, delY, viewport);
            GluPickMatrix(pointX, viewport[3] - pointY, delX, delY, viewport);

            SetProjectionTransform();

            // Model-View  Transform From Here
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadIdentity();
            SetModelViewTransform();

        }

        private IList<SelectedObject> PickPost(
            int[] selectBuffer, uint pointX, uint pointY)
        {
            GL.MatrixMode(MatrixMode.Projection);
            GL.PopMatrix();
            GL.MatrixMode(MatrixMode.Modelview);

            IList<SelectedObject> selectedObjs = new List<SelectedObject>();

            int nHits = GL.RenderMode(RenderingMode.Render);    // return value is number of hits
            if (nHits <= 0)
            {
                return selectedObjs;
            }

            IList<PickedObject> pickedObjs = new List<PickedObject>();
            {
                // get picked_object name and its depth
                for (int i = 0; i < nHits; i++)
                {
                    pickedObjs.Add(new PickedObject());
                }
                int iSel = 0;
                for (int i = 0; i < pickedObjs.Count; i++)
                {
                    uint nameDepth = (uint)selectBuffer[iSel];
                    System.Diagnostics.Debug.Assert(nameDepth <= 4);
                    pickedObjs[i].NameDepth = nameDepth;
                    iSel++;
                    pickedObjs[i].MinDepth = (float)selectBuffer[iSel] / 0x7fffffff;
                    iSel++;
                    pickedObjs[i].MaxDepth = (float)selectBuffer[iSel] / 0x7fffffff;
                    iSel++;
                    for (int j = 0; j < nameDepth; j++)
                    {
                        pickedObjs[i].Name[j] = selectBuffer[iSel];
                        iSel++;
                    }
                }
                // sort picked object in the order of min depth
                for (int i = 0; i < pickedObjs.Count; i++)
                {
                    for (int j = i + 1; j < pickedObjs.Count; j++)
                    {
                        if (pickedObjs[i].MinDepth > pickedObjs[j].MinDepth)
                        {
                            PickedObject tmp = pickedObjs[i];
                            pickedObjs[i] = pickedObjs[j];
                            pickedObjs[j] = tmp;
                        }
                    }
                }
            }
            // DEBUG
            System.Diagnostics.Debug.WriteLine("Picked Object nHits = " + nHits);
            for (int i = 0; i < pickedObjs.Count; i++)
            {
                System.Diagnostics.Debug.WriteLine("pickedObjs[" + i + "]");
                System.Diagnostics.Debug.WriteLine("NameDepth = " + pickedObjs[i].NameDepth + " " +
                    "MinDepth = " + pickedObjs[i].MinDepth + " " +
                    "MaxDepth = " + pickedObjs[i].MaxDepth);
                for (int j = 0; j < pickedObjs[i].NameDepth; j++)
                {
                    System.Diagnostics.Debug.Write("Name[" + j + "] = " + pickedObjs[i].Name[j] + " ");
                }
                System.Diagnostics.Debug.WriteLine("");
            }

            selectedObjs.Clear();
            for (int i = 0; i < pickedObjs.Count; i++)
            {
                System.Diagnostics.Debug.Assert(pickedObjs[i].NameDepth <= 4);
                SelectedObject selectedObj = new SelectedObject();
                selectedObj.NameDepth = 3;
                for (int itmp = 0; itmp < 3; itmp++)
                {
                    selectedObj.Name[itmp] = pickedObjs[i].Name[itmp];
                }
                selectedObjs.Add(selectedObj);

                double ox, oy, oz;
                {
                    double[] mvMatrix = new double[16];
                    double[] pjMatrix = new double[16];
                    int[] viewport = new int[4];

                    GL.GetInteger(GetPName.Viewport, viewport);

                    GL.GetDouble(GetPName.ModelviewMatrix, mvMatrix);

                    GL.GetDouble(GetPName.ProjectionMatrix, pjMatrix);

                    //Tao.OpenGl.Glu.gluUnProject(
                    //    (double)pointX,
                    //    (double)viewport[3] - pointY,
                    //    pickedObjs[i].MinDepth * 0.5,
                    //    mvMatrix, pjMatrix, viewport,
                    //    out ox, out oy, out oz);
                    GluUnProject(
                        pointX,
                        viewport[3] - pointY,
                        pickedObjs[i].MinDepth * 0.5,
                        mvMatrix, pjMatrix, viewport,
                        out ox, out oy, out oz);
                }
                selectedObj.PickedPos = new System.Numerics.Vector3((float)ox, (float)oy, (float)oz);
            }
            return selectedObjs;
        }
```

ここで```PickedObject```、```SelectedObject```は次のようなものです。

```PickedObject.cs
    class PickedObject
    {
        public uint NameDepth { get; set; }
        public int[] Name { get; set; } = new int[4];
        public double MinDepth { get; set; }
        public double MaxDepth { get; set; }
    }
```

```SelectedObject.cs
    class SelectedObject
    {
        public uint NameDepth { get; set; } = 0;
        public int[] Name { get; } = new int[4];
        public Vector3 PickedPos { get; set; } = new Vector3();
    }
```

OpenTKには```gluPickMatrix()```、```gluUnProject()```に相当するものがないようです。
Tao Frameworkを使うのも手だと思いますがOpenTKだけですませたいので次のように実装しました。

```MainWindow.xaml.cs
        private void GluPickMatrix(double x, double y, double deltax, double deltay, int[] viewport)
        {
            if (deltax <= 0 || deltay <= 0)
            {
                return;
            }

            GL.Translate((viewport[2] - 2 * (x - viewport[0])) / deltax, (viewport[3] - 2 * (y - viewport[1])) / deltay, 0);
            GL.Scale(viewport[2] / deltax, viewport[3] / deltay, 1.0);
        }

        private int GluProject(float objx, float objy, float objz,
            float[] modelview, float[] projection,
            int[] viewport, float[] windowCoordinate)
        {
            float[] fTempo = new float[8];
            fTempo[0] = modelview[0] * objx + modelview[4] * objy + modelview[8] * objz + modelview[12]; // w is always 1
            fTempo[1] = modelview[1] * objx + modelview[5] * objy + modelview[9] * objz + modelview[13];
            fTempo[2] = modelview[2] * objx + modelview[6] * objy + modelview[10] * objz + modelview[14];
            fTempo[3] = modelview[3] * objx + modelview[7] * objy + modelview[11] * objz + modelview[15];
            fTempo[4] = projection[0] * fTempo[0] + projection[4] * fTempo[1] + projection[8] * fTempo[2] +
                projection[12] * fTempo[3];
            fTempo[5] = projection[1] * fTempo[0] + projection[5] * fTempo[1] + projection[9] * fTempo[2] +
                projection[13] * fTempo[3];
            fTempo[6] = projection[2] * fTempo[0] + projection[6] * fTempo[1] + projection[10] * fTempo[2] +
                projection[14] * fTempo[3];
            fTempo[7] = -fTempo[2];
            if (fTempo[7] == 0.0)
            {
                // w
                return 0;
            }
            fTempo[7] = 1.0f / fTempo[7];
            // Perspective division
            fTempo[4] *= fTempo[7];
            fTempo[5] *= fTempo[7];
            fTempo[6] *= fTempo[7];

            windowCoordinate[0] = (fTempo[4] * 0.5f + 0.5f) * viewport[2] + viewport[0];
            windowCoordinate[1] = (fTempo[5] * 0.5f + 0.5f) * viewport[3] + viewport[1];
            windowCoordinate[2] = (1.0f + fTempo[6]) * 0.5f;
            return 1;
        }

        private static int GluUnProject(double winx, double winy, double winz,
            double[] modelview, double[] projection, int[] viewport,
            out double objectX, out double objectY, out double objectZ)
        {
            objectX = 0;
            objectY = 0;
            objectZ = 0;

            OpenTK.Matrix4d projectionM = new OpenTK.Matrix4d(
                projection[0], projection[4], projection[8], projection[12],
                projection[1], projection[5], projection[9], projection[13],
                projection[2], projection[6], projection[10], projection[14],
                projection[3], projection[7], projection[11], projection[15]);
            OpenTK.Matrix4d modelviewM = new OpenTK.Matrix4d(
                modelview[0], modelview[4], modelview[8], modelview[12],
                modelview[1], modelview[5], modelview[9], modelview[13],
                modelview[2], modelview[6], modelview[10], modelview[14],
                modelview[3], modelview[7], modelview[11], modelview[15]);
            OpenTK.Matrix4d AM = projectionM * modelviewM;
            OpenTK.Matrix4d mM = OpenTK.Matrix4d.Invert(AM);

            OpenTK.Vector4d inV = new OpenTK.Vector4d();
            inV.X = ((winx - viewport[0]) / viewport[2] * 2.0 - 1.0);
            inV.Y = ((winy - viewport[1]) / viewport[3] * 2.0 - 1.0);
            inV.Z = (2.0 * winz - 1.0);
            inV.W = 1.0;
            OpenTK.Vector4d outV;
            MultiplyMatrix4x4ByVector4(out outV, mM, inV);
            if (outV.Z == 0.0)
            {
                return 0;
            }
            outV.W = (1.0 / outV.W);
            objectX = outV.X * outV.W;
            objectY = outV.Y * outV.W;
            objectZ = outV.Z * outV.W;

            /*
            System.Diagnostics.Debug.WriteLine("GluUnProject");
            System.Diagnostics.Debug.WriteLine("objectX = " + objectX);
            System.Diagnostics.Debug.WriteLine("objectY = " + objectY);
            System.Diagnostics.Debug.WriteLine("objectZ = " + objectZ);
            */
            return 1;
        }

        private static void MultiplyMatrix4x4ByVector4(out OpenTK.Vector4d resultvector,
            OpenTK.Matrix4d matrix, OpenTK.Vector4d pvector)
        {
            resultvector.X = matrix.M11 * pvector.X + matrix.M12 * pvector.Y +
                matrix.M13 * pvector.Z + matrix.M14 * pvector.W;
            resultvector.Y = matrix.M21 * pvector.X + matrix.M22 * pvector.Y +
                matrix.M23 * pvector.Z + matrix.M24 * pvector.W;
            resultvector.Z = matrix.M31 * pvector.X + matrix.M32 * pvector.Y +
                matrix.M33 * pvector.Z + matrix.M34 * pvector.W;
            resultvector.W = matrix.M41 * pvector.X + matrix.M42 * pvector.Y +
                matrix.M43 * pvector.Z + matrix.M44 * pvector.W;
        }
```

#■8. マウスでオブジェクトを選択する（2）クリックされたとき

1) ```PickPre()```でセレクトバッファを用意
2) ```DrawSelection()```でセレクション用にオブジェクトの名前を付けて描画
3) ```PickPost()```でクリックされたオブジェクトを特定し、```SelectedPartId```にセットする
という流れになります。

```MainWindow.xaml.cs
        /// <summary>
        /// glControl クリックされた
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void glControl_MouseClick(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            int[] viewport = new int[4];
            GL.GetInteger(GetPName.Viewport, viewport);
            int winW = viewport[2];
            int winH = viewport[3];
            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                int sizeBuffer = 2048;
                int[] pickSelectBuffer = new int[sizeBuffer];

                PickPre(
                    sizeBuffer, pickSelectBuffer,
                    (uint)e.X, (uint)e.Y, 5, 5);
                DrawSelection();

                IList<SelectedObject> selectedObjs = PickPost(pickSelectBuffer,
                    (uint)e.X, (uint)e.Y);

                SelectedPartId = 0;
                if (selectedObjs.Count > 0)
                {
                    int[] selectFlg = selectedObjs[0].Name;
                    System.Diagnostics.Debug.WriteLine("selectFlg[1] = " + selectFlg[1]);
                    SelectedPartId = selectFlg[1];
                }
                glControl.Invalidate();
            }
        }
```

```DrawSelection()```は次のようになります。```Draw()```の処理と同じ順番に描画します。各オブジェクトには名前（ここでは番号）を付けています（```GL.PushName()```, ```GL.PopName()```の対）。これによりオブジェクトがクリックされたこと認識できるようになります。

```MainWindow.xaml.cs
        private void DrawSelection()
        {
            int idraw = 1;
            GL.PushName(idraw);
            int partId = 2;

            GL.PointSize(5);
            for (int iver = 0; iver < Vertexs.Count; iver++)
            {
                GL.PushName(partId);
                GL.Begin(BeginMode.Points);
                double height = 0.1;
                GL.Vertex3(
                    VertexCoordArray[iver * NDim + 0],
                    VertexCoordArray[iver * NDim + 1],
                    height);
                GL.End();
                GL.PopName();
                partId++;
            }

            GL.EnableClientState(ArrayCap.VertexArray);
            GL.VertexPointer((int)NDim, VertexPointerType.Double, 0, VertexCoordArray);
            GL.LineWidth(3);
            for (int i = 0; i < EdgeIndexs.Count; i++)
            {
                GL.PushName(partId);
                double height = 0.0;
                GL.Translate(0.0, 0.0, height);
                var index = EdgeIndexs[i];
                GL.DrawElements(BeginMode.Lines, index.Length, DrawElementsType.UnsignedInt, index);
                GL.Translate(0.0, 0.0, -height);
                GL.PopName();
                partId++;
            }

            GL.PushName(partId);
            for (int i = 0; i < TriIndexs.Count; i++)
            {
                double height = 0.0;
                double dispX = 0.0;
                double dispY = 0.0;
                var index = TriIndexs[i];
                GL.Translate(+dispX, +dispY, +height);
                GL.DrawElements(BeginMode.Triangles, index.Length, DrawElementsType.UnsignedInt, index);
                GL.Translate(-dispX, -dispY, -height);
            }
            GL.PopName();
            partId++;

            GL.PopName();
        }
```
#■9. 実行結果

点を選択したとき
![20180702_TestApp 点選択.jpg](https://qiita-image-store.s3.amazonaws.com/0/252253/044e29e5-b047-c10f-17d4-602df9e559c2.jpeg)

エッジを選択したとき
![20180702_TestApp 辺選択.jpg](https://qiita-image-store.s3.amazonaws.com/0/252253/c5792065-983f-7c61-0c32-c8300c8cf722.jpeg)

ループを選択したとき
![20180702_TestApp 領域選択.jpg](https://qiita-image-store.s3.amazonaws.com/0/252253/cc0eb635-618f-ea61-b32b-64335d640eec.jpeg)

#■ まとめ
WPFアプリケーションでOpenTKを使って点、エッジ、ループの3種類のオブジェクトを選択できるようにしました。
なお、ループではなくて三角形メッシュを選択させたいときは、```Draw()```および```DrawSelection()```で各三角形に名前を付けるように変更すればできます。