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)
文章列表