CSharpGL(21)用鼠标拾取、拖拽VBO图元内的点、线或本身
效果图
以最常见的三角形网格(用GL_TRIANGLES方式进行渲染)为例。
在拾取模式为GeometryType.Point时,你可以拾取单个的顶点。

在拾取模式为GeometryType.Line时,你可以拾取任意一个三角形里的任意一条线。即同时拾取此线段的两个顶点。

在拾取模式为GeometryType.Triangle时,你可以拾取任意一个三角形。即同时拾取此三角形的三个顶点。

实际上,CSharpGL实现了在所有渲染模式下拾取Point、Line、Triangle、Quad和Polygon的功能。(当然,你可以想象,如果想在一个GL_TRIANGLES渲染方式下拾取一个Quad,那是什么都拾取不到的)下面是描述这一功能的图示。由于我的白板小,就没有列出GL_TRIANGLES_ADJACENCY、GL_TRIANGLE_STRIP_ADJACENCY、GL_LINES_ADJACENCY、GL_LINE_STRIP_ADJCANCEY这几个情况。

下载
CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
规定
为了简便描述,我用GL_LINE*代表GL_LINES、GL_LINE_STRIP、GL_LINES_ADJACENCY、GL_LINE_STRIP_ADJACENCY,用GL_TRIANGLE*代表GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN、GL_TRIANGLES_ADJACENCY、GL_TRIANGLE_STRIP_ADJACENCY,用GL_QUAD*代表GL_QUADS、GL_QUAD_STRIP。
如何使用
使用方式十分简单,只需给RenderEventArgs传入如下的参数:
1 GeometryType PickingGeometryType = Geometry.Point; 2 var arg = new RenderEventArgs( 3 // 为了拾取而进行的渲染 4 RenderModes.ColorCodedPicking, 5 this.glCanvas1.ClientRectangle, 6 this.camera, 7 // 我想拾取的类型(Geometry) 8 this.PickingGeometryType); 9 // 要拾取的位置(鼠标位置) 10 Point mousePostion = GetMousePosition(); 11 // 支持Picking的Renderer列表 12 PickableRenderer[] pickableElements = GetRenderersInScene(); 13 // 执行拾取操作 14 PickedGeometry pickedGeometry = ColorCodedPicking.Pick(arg, mousePostion, pickableElements);
具体用法详见(CSharpGL(20)用unProject和Project实现鼠标拖拽图元)
如何实现
在GL_POINTS时拾取Point,在GL_LINE*时拾取Line,在GL_TRIANGL*时拾取Triangle,在GL_QUAD*时拾取Quad,在GL_POLYGON时拾取Polygon,这都是已经实现了的(CSharpGL(18)分别处理glDrawArrays()和glDrawElements()两种方式下的拾取(ColorCodedPicking))。这些不再详述。
拾取Point
ZeroIndexRenderer
在除了GL_POINTS时,想拾取一个Point,只能用 glDrawArrays(GL_POINTS, ..); 来代替原有的 glDrawArrays(OriginalMode, ..); 。但这会渲染所有的顶点。而在OriginalMode下,未必渲染所有的顶点。所以在拾取到一个Point后要判断一下是否真的应该拾取到它。

1 /// <summary>
2 /// 现在,已经判定了鼠标在某个点上。
3 /// 我需要判定此点是否出现在图元上。
4 /// now that I know the mouse is picking on some point,
5 /// I need to make sure that point should appear.
6 /// </summary>
7 /// <param name="lastVertexId"></param>
8 /// <param name="mode"></param>
9 /// <returns></returns>
10 private bool OnPrimitiveTest(uint lastVertexId, DrawMode mode)
11 {
12 bool result = false;
13 int first = this.zeroIndexBufferPtr.FirstVertex;
14 if (first < 0) { return false; }
15 int vertexCount = this.zeroIndexBufferPtr.VertexCount;
16 if (vertexCount <= 0) { return false; }
17 int last = first + vertexCount - 1;
18 switch (mode)
19 {
20 case DrawMode.Points:
21 result = true;
22 break;
23 case DrawMode.LineStrip:
24 result = vertexCount > 1;
25 break;
26 case DrawMode.LineLoop:
27 result = vertexCount > 1;
28 break;
29 case DrawMode.Lines:
30 if (vertexCount > 1)
31 {
32 if (vertexCount % 2 == 0)
33 {
34 result = (first <= lastVertexId && lastVertexId <= last);
35 }
36 else
37 {
38 result = (first <= lastVertexId && lastVertexId <= last - 1);
39 }
40 }
41 break;
42 case DrawMode.LineStripAdjacency:
43 if (vertexCount > 3)
44 {
45 result = (first < lastVertexId && lastVertexId < last);
46 }
47 break;
48 case DrawMode.LinesAdjacency:
49 if (vertexCount > 3)
50 {
51 var lastPart = last - (last + 1 - first) % 4;
52 if (first <= lastVertexId && lastVertexId <= lastPart)
53 {
54 var m = (lastVertexId - first) % 4;
55 result = (m == 1 || m == 2);
56 }
57 }
58 break;
59 case DrawMode.TriangleStrip:
60 if (vertexCount > 2)
61 {
62 result = vertexCount > 2;
63 }
64 break;
65 case DrawMode.TriangleFan:
66 if (vertexCount > 2)
67 {
68 result = vertexCount > 2;
69 }
70 break;
71 case DrawMode.Triangles:
72 if (vertexCount > 2)
73 {
74 if (first <= lastVertexId)
75 {
76 result = ((vertexCount % 3 == 0) && (lastVertexId <= last))
77 || ((vertexCount % 3 == 1) && (lastVertexId < last))
78 || ((vertexCount % 3 == 2) && (lastVertexId + 1 < last));
79 }
80 }
81 break;
82 case DrawMode.TriangleStripAdjacency:
83 if (vertexCount > 5)
84 {
85 var lastPart = last - (last + 1 - first) % 2;
86 if (first <= lastVertexId && lastVertexId <= lastPart)
87 {
88 result = (lastVertexId - first) % 2 == 0;
89 }
90 }
91 break;
92 case DrawMode.TrianglesAdjacency:
93 if (vertexCount > 5)
94 {
95 var lastPart = last - (last + 1 - first) % 6;
96 if (first <= lastVertexId && lastVertexId <= lastPart)
97 {
98 result = (lastVertexId - first) % 2 == 0;
99 }
100 }
101 break;
102 case DrawMode.Patches:
103 // not know what to do for now
104 break;
105 case DrawMode.QuadStrip:
106 if (vertexCount > 3)
107 {
108 if (first <= lastVertexId && lastVertexId <= last)
109 {
110 result = (vertexCount % 2 == 0)
111 || (lastVertexId < last);
112 }
113 }
114 break;
115 case DrawMode.Quads:
116 if (vertexCount > 3)
117 {
118 if (first <= lastVertexId && lastVertexId <= last)
119 {
120 var m = vertexCount % 4;
121 if (m == 0) { result = true; }
122 else if (m == 1) { result = lastVertexId + 0 < last; }
123 else if (m == 2) { result = lastVertexId + 1 < last; }
124 else if (m == 3) { result = lastVertexId + 2 < last; }
125 else { throw new Exception("This should never happen!"); }
126 }
127 }
128 break;
129 case DrawMode.Polygon:
130 if (vertexCount > 2)
131 {
132 result = (first <= lastVertexId && lastVertexId <= last);
133 }
134 break;
135 default:
136 throw new NotImplementedException();
137 }
138
139 return result;
140 }
OneIndexBuffer
如果是用glDrawElements(OriginalMode, ..);渲染,此时想拾取一个Point,那么我就不做类似的OnPrimitiveTest了。因为情况太复杂,且必须用MapBufferRange来检测大量的顶点情况。而这仅仅是因为导入的IBufferable模型本身没有使用某些顶点。没用你就删了它啊!这我就不管了。
1 /// <summary>
2 /// I don't know how to implement this method in a high effitiency way.
3 /// So keep it like this.
4 /// Also, why would someone use glDrawElements() when rendering GL_POINTS?
5 /// </summary>
6 /// <param name="lastVertexId"></param>
7 /// <param name="mode"></param>
8 /// <returns></returns>
9 private bool OnPrimitiveTest(uint lastVertexId, DrawMode mode)
10 {
11 return true;
12 }
拾取Line
ZeroIndexRenderer
如果是在GL_LINE*下拾取线,那么这是上一篇文章已经实现了的情况。如果是想在GL_TRIANGLE*、GL_QUAD*、GL_POLYGON模式下拾取其某个图元的某条Line,那么就分两部走:第一,像上一篇一样拾取图元;第二,设计一个新的小小的索引,即用GL_LINES模式渲染此图元(三角形、四边形、多边形)的所有边的索引。用此索引重新执行渲染、拾取,那么就可以找到鼠标所在位置的Line了。

例如,下面是在一个三角形图元中找到那个你想要的Line的过程。
1 class ZeroIndexLineInTriangleSearcher : ZeroIndexLineSearcher
2 {
3 /// <summary>
4 /// 在三角形图元中拾取指定位置的Line
5 /// </summary>
6 /// <param name="arg">渲染参数</param>
7 /// <param name="x">指定位置</param>
8 /// <param name="y">指定位置</param>
9 /// <param name="lastVertexId">三角形图元的最后一个顶点</param>
10 /// <param name="modernRenderer">目标Renderer</param>
11 /// <returns></returns>
12 internal override uint[] Search(RenderEventArgs arg,
13 int x, int y,
14 uint lastVertexId, ZeroIndexRenderer modernRenderer)
15 {
16 // 创建临时索引
17 OneIndexBufferPtr indexBufferPtr = null;
18 using (var buffer = new OneIndexBuffer<uint>(DrawMode.Lines, BufferUsage.StaticDraw))
19 {
20 buffer.Alloc(6);
21 unsafe
22 {
23 var array = (uint*)buffer.FirstElement();
24 array[0] = lastVertexId - 1; array[1] = lastVertexId - 0;
25 array[2] = lastVertexId - 2; array[3] = lastVertexId - 1;
26 array[4] = lastVertexId - 0; array[5] = lastVertexId - 2;
27 }
28
29 indexBufferPtr = buffer.GetBufferPtr() as OneIndexBufferPtr;
30 }
31
32 // 用临时索引渲染此三角形图元(仅渲染此三角形图元)
33 modernRenderer.Render4InnerPicking(arg, indexBufferPtr);
34 // id是拾取到的Line的Last Vertex Id
35 uint id = ColorCodedPicking.ReadPixel(x, y, arg.CanvasRect.Height);
36
37 indexBufferPtr.Dispose();
38
39 // 对比临时索引,找到那个Line
40 if (id + 2 == lastVertexId)
41 { return new uint[] { id + 2, id, }; }
42 else
43 { return new uint[] { id - 1, id, }; }
44 }
45 }
OneIndexBuffer
用glDrawElements()时,实现思路与上面一样,只不过Index参数变化一下而已。
在(CSharpGL(18)分别处理glDrawArrays()和glDrawElements()两种方式下的拾取(ColorCodedPicking)),已经能够找到目标图元的所有顶点,所以就简单了。

继续用"在一个三角形图元中找到那个你想要的Line的过程"来举例。
1 class OneIndexLineInTrianglesSearcher : OneIndexLineSearcher
2 {
3 internal override uint[] Search(RenderEventArgs arg,
4 int x, int y,
5 RecognizedPrimitiveIndex lastIndexId,
6 OneIndexRenderer modernRenderer)
7 {
8 if (lastIndexId.IndexIdList.Count != 3) { throw new ArgumentException(); }
9 List<uint> indexList = lastIndexId.IndexIdList;
10 if (indexList[0] == indexList[1]) { return new uint[] { indexList[0], indexList[2], }; }
11 else if (indexList[0] == indexList[2]) { return new uint[] { indexList[0], indexList[1], }; }
12 else if (indexList[1] == indexList[2]) { return new uint[] { indexList[1], indexList[0], }; }
13
14 OneIndexBufferPtr indexBufferPtr = null;
15 using (var buffer = new OneIndexBuffer<uint>(DrawMode.Lines, BufferUsage.StaticDraw))
16 {
17 buffer.Alloc(6);
18 unsafe
19 {
20 var array = (uint*)buffer.FirstElement();
21 array[0] = indexList[0]; array[1] = indexList[1];
22 array[2] = indexList[1]; array[3] = indexList[2];
23 array[4] = indexList[2]; array[5] = indexList[0];
24 }
25
26 indexBufferPtr = buffer.GetBufferPtr() as OneIndexBufferPtr;
27 }
28
29 modernRenderer.Render4InnerPicking(arg, indexBufferPtr);
30 uint id = ColorCodedPicking.ReadPixel(x, y, arg.CanvasRect.Height);
31
32 indexBufferPtr.Dispose();
33
34 if (id == indexList[1])
35 { return new uint[] { indexList[0], indexList[1], }; }
36 else if (id == indexList[2])
37 { return new uint[] { indexList[1], indexList[2], }; }
38 else if (id == indexList[0])
39 { return new uint[] { indexList[2], indexList[0], }; }
40 else
41 { throw new Exception("This should not happen!"); }
42 }
43 }
Polygon
这里顺便提一下GL_POLYGON,这是个特别的图元,因为它的顶点数是不确定的。它产生的临时小索引就可能不再小。但神奇的是,它不再需要OneIndexBufferPtr类型的临时索引,而只需一个几乎不占空间的ZeroIndexBufferPtr。
1 class ZeroIndexLineInPolygonSearcher : ZeroIndexLineSearcher
2 {
3 internal override uint[] Search(RenderEventArgs arg,
4 int x, int y,
5 uint lastVertexId, ZeroIndexRenderer modernRenderer)
6 {
7 var zeroIndexBufferPtr = modernRenderer.GetIndexBufferPtr() as ZeroIndexBufferPtr;
8 ZeroIndexBufferPtr indexBufferPtr = null;
9 // when the temp index buffer could be long, it's no longer needed.
10 // what a great OpenGL API design!
11 using (var buffer = new ZeroIndexBuffer(DrawMode.LineLoop,
12 zeroIndexBufferPtr.FirstVertex, zeroIndexBufferPtr.VertexCount))
13 {
14 indexBufferPtr = buffer.GetBufferPtr() as ZeroIndexBufferPtr;
15 }
16 modernRenderer.Render4InnerPicking(arg, indexBufferPtr);
17 uint id = ColorCodedPicking.ReadPixel(x, y, arg.CanvasRect.Height);
18
19 indexBufferPtr.Dispose();
20
21 if (id == zeroIndexBufferPtr.FirstVertex)
22 { return new uint[] { (uint)(zeroIndexBufferPtr.FirstVertex + zeroIndexBufferPtr.VertexCount - 1), id, }; }
23 else
24 { return new uint[] { id - 1, id, }; }
25 }
26 }
拾取本身
所谓拾取本身,就是:如果用GL_TRIANGLE*进行渲染,就拾取一个Triangle;如果用GL_QUAD*进行渲染,就拾取一个Quad;如果用GL_POLYGON进行渲染,就拾取一个Polygon。这都是在(CSharpGL(18)分别处理glDrawArrays()和glDrawElements()两种方式下的拾取(ColorCodedPicking))中已经实现了的功能。
整合
三种情况都解决了,下面整合进来就行了。
ZeroIndexRenderer
这是对ZeroIndexRenderer的Pick。

1 public override PickedGeometry Pick(RenderEventArgs arg, uint stageVertexId,
2 int x, int y)
3 {
4 uint lastVertexId;
5 if (!this.GetLastVertexIdOfPickedGeometry(stageVertexId, out lastVertexId))
6 { return null; }
7
8 GeometryType geometryType = arg.PickingGeometryType;
9
10 if (geometryType == GeometryType.Point)
11 {
12 DrawMode mode = this.GetIndexBufferPtr().Mode;
13 if (this.OnPrimitiveTest(lastVertexId, mode))
14 { return PickPoint(stageVertexId, lastVertexId); }
15 else
16 { return null; }
17 }
18 else if (geometryType == GeometryType.Line)
19 {
20 DrawMode mode = this.GetIndexBufferPtr().Mode;
21 GeometryType typeOfMode = mode.ToGeometryType();
22 if (geometryType == typeOfMode)
23 { return PickWhateverItIs(stageVertexId, lastVertexId, mode, typeOfMode); }
24 else
25 {
26 ZeroIndexLineSearcher searcher = GetLineSearcher(mode);
27 if (searcher != null)// line is from triangle, quad or polygon
28 { return SearchLine(arg, stageVertexId, x, y, lastVertexId, searcher); }
29 else if (mode == DrawMode.Points)// want a line when rendering GL_POINTS
30 { return null; }
31 else
32 { throw new Exception(string.Format("Lack of searcher for [{0}]", mode)); }
33 }
34 }
35 else
36 {
37 DrawMode mode = this.GetIndexBufferPtr().Mode;
38 GeometryType typeOfMode = mode.ToGeometryType();
39 if (typeOfMode == geometryType)// I want what it is
40 { return PickWhateverItIs(stageVertexId, lastVertexId, mode, typeOfMode); }
41 else
42 { return null; }
43 //{ throw new Exception(string.Format("Lack of searcher for [{0}]", mode)); }
44 }
45 }
OneIndexRenderer
这是对OneIndexRenderer的Pick。

1 public override PickedGeometry Pick(RenderEventArgs arg, uint stageVertexId,
2 int x, int y)
3 {
4 uint lastVertexId;
5 if (!this.GetLastVertexIdOfPickedGeometry(stageVertexId, out lastVertexId))
6 { return null; }
7
8 GeometryType geometryType = arg.PickingGeometryType;
9
10 if (geometryType == GeometryType.Point)
11 {
12 DrawMode mode = this.GetIndexBufferPtr().Mode;
13 if (this.OnPrimitiveTest(lastVertexId, mode))
14 { return PickPoint(stageVertexId, lastVertexId); }
15 else
16 { return null; }
17 }
18 else if (geometryType == GeometryType.Line)
19 {
20 // 找到 lastIndexId
21 RecognizedPrimitiveIndex lastIndexId = this.GetLastIndexIdOfPickedGeometry(
22 arg, lastVertexId, x, y);
23 if (lastIndexId == null)
24 {
25 Debug.WriteLine(
26 "Got lastVertexId[{0}] but no lastIndexId! Params are [{1}] [{2}] [{3}] [{4}]",
27 lastVertexId, arg, stageVertexId, x, y);
28 { return null; }
29 }
30 else
31 {
32 // 获取pickedGeometry
33 DrawMode mode = this.GetIndexBufferPtr().Mode;
34 GeometryType typeOfMode = mode.ToGeometryType();
35 if (geometryType == typeOfMode)
36 { return PickWhateverItIs(stageVertexId, lastIndexId, typeOfMode); }
37 else
38 {
39 OneIndexLineSearcher searcher = GetLineSearcher(mode);
40 if (searcher != null)// line is from triangle, quad or polygon
41 { return SearchLine(arg, stageVertexId, x, y, lastVertexId, lastIndexId, searcher); }
42 else if (mode == DrawMode.Points)// want a line when rendering GL_POINTS
43 { return null; }
44 else
45 { throw new Exception(string.Format("Lack of searcher for [{0}]", mode)); }
46 }
47 }
48 }
49 else
50 {
51 // 找到 lastIndexId
52 RecognizedPrimitiveIndex lastIndexId = this.GetLastIndexIdOfPickedGeometry(
53 arg, lastVertexId, x, y);
54 if (lastIndexId == null)
55 {
56 Debug.WriteLine(
57 "Got lastVertexId[{0}] but no lastIndexId! Params are [{1}] [{2}] [{3}] [{4}]",
58 lastVertexId, arg, stageVertexId, x, y);
59 { return null; }
60 }
61 else
62 {
63 DrawMode mode = this.GetIndexBufferPtr().Mode;
64 GeometryType typeOfMode = mode.ToGeometryType();
65 if (typeOfMode == geometryType)// I want what it is
66 { return PickWhateverItIs(stageVertexId, lastIndexId, typeOfMode); }
67 else
68 { return null; }
69 //{ throw new Exception(string.Format("Lack of searcher for [{0}]", mode)); }
70 }
71 }
72 }
总结
在完成后,我以为彻底解决了拾取问题。等完成本文后,我不再这么想了。还是谦虚点好。
原CSharpGL的其他功能(3ds解析器、TTF2Bmp、CSSL等),我将逐步加入新CSharpGL。
欢迎对OpenGL有兴趣的同学关注(https://github.com/bitzhuwei/CSharpGL)
来源:https://www.cnblogs.com/bitzhuwei/p/picking-and-dragging-point-line-or-primitive-inside-any-VBO.html
