#■ はじめに
WPFアプリケーションでOpenTKを使ったときのオブジェクト選択を実装しています。
Tao Frameworkのときとほぼ同じ要領で実現できますが、一部対応する関数がなかったりするのでそのあたりの実装が必要になります。
#■1. xaml
OpenTK.GLControlを貼り付けています。
<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. ポリゴンデータを作成する
画像のようなポリゴンを描画するためのデータを作成します。
頂点データ、エッジデータ、三角形データを作成しています。
頂点データは座標データですが、エッジデータ、三角形データは頂点のインデックスの配列から構成します。
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. ロード
/// <summary>
/// glControlの起動時に実行される。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void glControl_Load(object sender, EventArgs e)
{
GL.Enable(EnableCap.DepthTest);
}
#■4. リサイズ
/// <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. 投影変換
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
を割り振ってマウスで選択されたオブジェクトの色を変える処理を実装しています。
/// <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)
の戻り値にはヒットしたオブジェクトの数が帰ってきます。
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
は次のようなものです。
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; }
}
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だけですませたいので次のように実装しました。
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)クリックされたとき
-
PickPre()
でセレクトバッファを用意 -
DrawSelection()
でセレクション用にオブジェクトの名前を付けて描画 -
PickPost()
でクリックされたオブジェクトを特定し、SelectedPartId
にセットする
という流れになります。
/// <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()
の対)。これによりオブジェクトがクリックされたこと認識できるようになります。
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. 実行結果
#■ まとめ
WPFアプリケーションでOpenTKを使って点、エッジ、ループの3種類のオブジェクトを選択できるようにしました。
なお、ループではなくて三角形メッシュを選択させたいときは、Draw()
およびDrawSelection()
で各三角形に名前を付けるように変更すればできます。