文章出處

CSharpGL(18)分別處理glDrawArrays()和glDrawElements()兩種方式下的拾取(ColorCodedPicking)

我在(Modern OpenGL用Shader拾取VBO內單一圖元的思路和實現)記錄了基于Color-Coded-Picking的拾取方法。

最近在整理CSharpGL時發現了一個問題:我只解決了用glDrawArrays();渲染時的拾取問題。如果是用glDrawElements();進行渲染,就會得到錯誤的圖元。

本文就分別解決這兩種情況下的拾取的問題。

下載

CSharpGL已在GitHub開源,歡迎對OpenGL有興趣的同學加入(https://github.com/bitzhuwei/CSharpGL

兩種Index Buffer

ZeroIndexBuffer

用 glDrawArrays(uint mode, int first, int count); 進行渲染時,本質上是用這樣一個(特殊索引+  glDrawElements(uint mode, int count, uint type, void* indices); )進行渲染:

uint[] index = { 0, 1, 2, 3, 4, 5, 6, 7, 8, … }
 

這個特殊索引的特點就是(i == index[i])且(index buffer的長度==position buffer的長度)。

所以我們可以把這個索引看做一個經過優化的VertexBufferObject(VBO)。優化的效果就是:此VBO占用的GPU內存空間(幾乎)為。所以我把這種索引buffer命名為ZeroIndexBuffer。

之前的文章里,我拾取到了圖元的最后一個頂點在position buffer里的索引值。由于index的特殊性質,position buffer前方(左側)的連續幾個頂點就屬于拾取到的圖元。所以glDrawArrays方式下的拾取問題就解決了。

像下面這個BigDipper的模型,是用glDrawArrays方式渲染的。其拾取功能完全正常。

 

OneIndexBuffer

我把用glDrawElements進行渲染的index buffer命名為OneIndexBuffer。(因為實在想不出合適的名字了,就模仿一下編譯原理里的0型文法、1型文法的命名方式)

lastVertexID

為便于說明,以下面的模型為例:

此模型描述了一個立方體,每個面都由4個頂點組成,共24個頂點。其索引(index buffer)用GL_TRIANGLES方式渲染,索引內容如上圖如下:

index = { 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23 };
 

此index buffer的長度為36個uint(36*sizeof(uint)個字節)。

這個模型的position buffer長度(24)不等于index buffer的長度(36)。

所以,繼續用上面的拾取方式,只能拾取到圖元的最后一個頂點(此例為三角形的第3個頂點)在position buffer中的索引值

假設拾取到的是第二個三角形,如下圖所示,那么拾取到的圖元的最后一個頂點在position buffer的索引值就是3。(此圖只渲染了前2個三角形)

如果像之前那樣,連續向前(向左)取3個頂點,就會得到position[1],position[2],position[3]。但是,如圖所見,正確的3個頂點應該是position[0],position[2],position[3]。

就是說,由于index buffer內容是任意的,導致描述一個圖元的各個頂點在position buffer中并非連續排列。

lastVertexID -> lastIndexIDList

繼續這個例子,現在已經找到了lastVertexID為3。為了找到這個三角形所有的頂點,我們先在index buffer里找到內容為3的索引。

index = { 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23 };

只需遍歷一下就會發現index[5] = 3

所以,拾取到的三角形的三個頂點就是position[ index[5 – 2] ],position[ index[5 – 1] ],position[ index[5 – 0] ]。(index buffer中描述同一個圖元的索引值是緊挨著排列的)

PrimitiveRecognizer

這個例子里,要識別的是三角形。實際上可能會識別點(Points)、線段(Lines、LineStrip、LineLoop)、四邊形(Quads、QuadStrip)、多邊形(Polygon)。所以我需用一個簡單的工廠來提供各種PrimitiveRecognizer。

用于識別三角形的TriangleRecognizer如下:

 1     class TrianglesRecognizer : PrimitiveRecognizer
 2     {
 3         public override List<RecognizedPrimitiveIndex> Recognize(
 4             uint lastVertexID, IntPtr pointer, int length)
 5         {
 6             var lastIndexIDList = new List<RecognizedPrimitiveIndex>();
 7             unsafe
 8             {
 9                 var array = (uint*)pointer.ToPointer();
10                 for (uint i = 2; i < length; i += 3)
11                 {
12                     if (array[i] == lastVertexID)
13                     {
14                         var item = new RecognizedPrimitiveIndex(lastVertexID);
15                         item.IndexIDList.Add(array[i - 2]);
16                         item.IndexIDList.Add(array[i - 1]);
17                         item.IndexIDList.Add(array[i - 0]);
18                         lastIndexIDList.Add(item);
19                     }
20                 }
21             }
22 
23             return lastIndexIDList;
24         }
25 }
26 
27     class RecognizedPrimitiveIndex
28     {
29         public RecognizedPrimitiveIndex(uint lastIndexID, params uint[] indexIDs)
30         {
31             this.LastIndexID = lastIndexID;
32             this.IndexIDList = new List<uint>();
33             this.IndexIDList.AddRange(indexIDs);
34         }
35 
36         public uint LastIndexID { get; set; }
37 
38         public List<uint> IndexIDList { get; set; }
39     }
TrianglesRecognizer

lastIndexIDList -> lastIndexID

這個例子里,只有一個index[5]=3。實際上可能會有多個index[i]=索引值

所以要想辦法從這些候選圖元中找到真正拾取到的那個。

那么,什么時候會出現多個候選圖元?就是這幾個圖元共享最后一個頂點的時候。例如下面的例子:在鼠標所在位置執行拾取時,會找到[0 1 3]、[0 2 3]和[1 2 3]這三組lastIndexID。

那么如何分辨出我們拾取到的是[0 1 3]而不是另2個?

我想到的方法是,將共享點前移,然后重新渲染、拾取。在這個例子里,就是把[0 1 3]和[0 2 3]變成[3 0 1]和[3 0 2],然后渲染[3 0 1 3 0 2]這個小小的index buffer(即僅渲染這2個圖元)。這樣是能夠拾取到[3 0 1]的,這就排除了[3 0 2]。然后繼續用同樣的方法排除[1 2 3]。這就找到了[0 1 3]這個正確的目標。

 1         /// <summary>
 2         /// 在所有可能的圖元(<see cref="lastVertexId"/>匹配)中,
 3         /// 逐個測試,找到最接近攝像機的那個圖元,
 4         /// 返回此圖元的最后一個索引在<see cref="indexBufferPtr"/>中的索引(位置)。
 5         /// </summary>
 6         /// <param name="lastIndexIdList"></param>
 7         /// <returns></returns>
 8         private RecognizedPrimitiveIndex GetLastIndexId(
 9             ICamera camera,
10             List<RecognizedPrimitiveIndex> lastIndexIdList,
11             int x, int y, int canvasWidth, int canvasHeight)
12         {
13             if (lastIndexIdList.Count == 0) { throw new ArgumentException(); }
14 
15             int current = 0;
16             foreach (var item in lastIndexIdList[0].IndexIdList)
17             {
18                 if (item == uint.MaxValue) { throw new Exception(); }
19             }
20             for (int i = 1; i < lastIndexIdList.Count; i++)
21             {
22                 foreach (var item in lastIndexIdList[i].IndexIdList)
23                 {
24                     if (item == uint.MaxValue) { throw new Exception(); }
25                 }
26                 OneIndexBufferPtr twoPrimitivesIndexBufferPtr;
27                 uint lastIndex0, lastIndex1;
28                 AssembleIndexBuffer(
29                     lastIndexIdList[current], lastIndexIdList[i], this.indexBufferPtr.Mode,
30                     out twoPrimitivesIndexBufferPtr, out lastIndex0, out lastIndex1);
31                 uint pickedIndex = Pick(camera, twoPrimitivesIndexBufferPtr, x, y, canvasWidth, canvasHeight);
32                 if (pickedIndex == lastIndex1)
33                 { current = i; }
34                 else if (pickedIndex == lastIndex0)
35                 { /* nothing to do */}
36                 else if (pickedIndex == uint.MaxValue)
37                 { /* nothing to do */}
38                 else
39                 { throw new Exception("This should not happen!"); }
40             }
41 
42             return lastIndexIdList[current];
43         }
GetLastIndexId

lastIndexID -> PickedGeometry

現在得到了圖元的所有頂點在position buffer中的索引(上面的例子中,是[0 1 3]),只需一步就可以找到頂點了。(上面的例子中,是position[ index[0] ],position[ index[1] ],position[ index[3] ])

 1         private PickedGeometry GetGeometry(RecognizedPrimitiveIndex lastIndexId, uint stageVertexId)
 2         {
 3             var pickedGeometry = new PickedGeometry();
 4             pickedGeometry.GeometryType = this.indexBufferPtr.Mode.ToPrimitiveMode().ToGeometryType();
 5             pickedGeometry.StageVertexId = stageVertexId;
 6             pickedGeometry.From = this;
 7             pickedGeometry.Indexes = lastIndexId.IndexIdList.ToArray();
 8             GL.BindBuffer(BufferTarget.ArrayBuffer, this.positionBufferPtr.BufferId);
 9             IntPtr pointer = GL.MapBuffer(BufferTarget.ArrayBuffer, MapBufferAccess.ReadOnly);
10             unsafe
11             {
12                 var array = (vec3*)pointer.ToPointer();
13                 List<vec3> list = new List<vec3>();
14                 for (int i = 0; i < lastIndexId.IndexIdList.Count; i++)
15                 {
16                     list.Add(array[lastIndexId.IndexIdList[i]]);
17                 }
18                 pickedGeometry.Positions = list.ToArray();
19             }
20             GL.UnmapBuffer(BufferTarget.ArrayBuffer);
21             GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
22 
23             return pickedGeometry;
24         }
GetGeometry

測試用例

ZeroIndexBuffer

這個情況屬于早就解決了的,可以在(CSharpGL(17)重構CSharpGL)中查看。

OneIndexBuffer

Cube

下圖中的Cube模型就可以用來測試OneIndexBuffer的拾取功能。

下面12個測試用例測試了拾取CubeModel的12個三角形的情況。結果顯示完全符合對Cube的定義。

 

最后一個面在背面,所以需要旋轉過來。

Sphere

當然,Cube是不足以完全測試OneIndexBuffer的拾取的。因為Cube里不存在共享最后一個頂點的情況。

Sphere里就有。

Teapot

Teapot的頂點組織方式我沒有查看,權且充個數吧。

2016-04-26

為了嚴格測試OneIndexBuffer時存在“多個圖元共享同一個最后的頂點”的情況,我制作了下面這個四邊形模型。這驗證了下圖的情況。

Tetrahedron

根據上圖,我設計了這樣的模型數據:

首先看一下這個四邊形的結構。通過設置GLSwitch里的PolygonModeSwtich為Lines,就可以看到這確實是個四邊形。

(白色部分是geometry shader制造的法線,不必理會)

然后恢復到Filled模式下開始測試。下圖中右邊標明了各個頂點的索引(白色的0 1 2 3)。

可以看到這正是本文示例中描述的情況。結果完全符合預期。

然后我們在其他位置都試試看。

 其余位置就不貼圖了。

總結

解決拾取問題的過程也是整理ModernRenderer的過程。由于兩種渲染方式的巨大差異,我設計了對應的ModernRenderer(即ZeroIndexModernRenderer和OneIndexModernRenderer)。再配合工廠模式,既封裝了細節,實現了功能,又易于使用。

原CSharpGL的其他功能(UI、3ds解析器、TTF2Bmp、CSSL等),我將逐步加入新CSharpGL。

歡迎對OpenGL有興趣的同學關注(https://github.com/bitzhuwei/CSharpGL


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()