Modern OpenGL用Shader拾取VBO內單一圖元的思路和實現


Modern OpenGL用Shader拾取VBO內單一圖元的思路和實現

什么意思?

拾取

最簡單的理解拾取的方式大概是到(http://www.yakergong.net/nehe/course/tutorial_32.html)玩一下NEHE的拾取游戲。用鼠標點擊飛過屏幕的物體就會擊中它,這就是拾取的意義。

 

Legacy OpenGL VS Modern OpenGL

Legacy OpenGL就是使用glTranslate、glRotate、gluScale、gluLookAt、glPerspective等函數的OpenGL程序。NEHE的教程講述的都是Legacy OpenGL。這是以前OpenGL的使用方式,它使用的是固定功能管線。

Modern OpenGL不再使用上面那些函數。它用GLSL語言編寫Shader,由Shader代替上面那些函數的功能,並且Shader能提供更多更強的功能。你必須自行計算投影矩陣、ModelView矩陣。

它還使用VBO把頂點數據放到GPU內存,從而避免了每次渲染時都要把頂點數據從CPU內存上傳到GPU內存。簡單來說,VBO就是一個數組,里面依次保存留每個頂點的某種信息(如位置信息、顏色信息、法線信息)。它代替了顯示列表。所以glVertex也不再使用了。

 

Legacy OpenGL的拾取

在SharpGL(https://github.com/dwmkerr/sharpgl)里有一個HitTestSample項目,演示了Legacy OpenGL里實現拾取的方法。簡單來說,OpenGL在設計的時候就為拾取提供了相關接口,所以拾取功能才得以實現。Legacy OpenGL通過GL_SELECT的渲染模式、預備好的拾取緩存、渲染每個可區分的模型前都用PushName()等一系列動作,實現了拾取屏幕上某一點的模型。

當然,還可以用射線碰撞檢測的方法進行拾取,留待以后再說。

 

Modern OpenGL的拾取

Modern OpenGL使用VBO存儲頂點信息,如果仍舊采用Legacy OpenGL里的拾取機制,最多只能分辨出這個VBO和那個VBO來。如果場景里的模型是由唯一一個VBO渲染的,那么只要點中了VBO里的任何一點,就會拾取到整個VBO。比如一個VBO里保存的是用GL_POINTS代表的星星,那么你點擊任何一個點,Legacy OpenGL的拾取機制都會返回同樣的結果,你是無法拾取單一的星星的。

 

拾取VBO內的單一圖元

如果說用一個VBO只保存一個頂點,那也不行,因為顯卡支持的VBO總數有限,一個VBO只保存一個頂點實在太浪費了。

我在網上搜羅了好多天,只找到一個基於顏色編碼的拾取方法(Color-Coded Picking)。(查看這個足以代表之http://www.lighthouse3d.com/tutorials/opengl-selection-tutorial/)不過這個例子仍然只能分辨出各個VBO來,VBO內部的圖元是無法區分的。不過好歹有點希望了。

萬般無奈之際我在讀OVITO(https://github.com/t-brink/ovito)的代碼時終於找到了它分辨VBO內的單一圖元的原理。順便模仿了一下它的Shader。

 

思路:Color-Coded Picking

這里仍然以(Color-Coded Picking)稱呼這個方法,因為確實就是基於顏色編碼的拾取方式。網上找的那些只不過沒有充分發揮這個方法的能力而已。也可能是人家懶得寫或者不願意寫吧。

用類比的方式說明這個方法的思路。假設張三豐已經收齊了武當七俠,分別給他們編派了法號(應該是道號)為0號俠、1號俠、2號俠、3號俠、4號俠、5號俠、6號俠。這是武當內部編號,從不外傳。武當七俠各自出去闖江湖,每隔1個月都會各自發一封書信給張三豐,為了辨別身份,他們分別用赤、橙、黃、綠、青、藍、紫7個顏色自稱。即"尊師您好,……赤 敬上"就是0號俠寄來的,"恩師萬安,……橙 敬上"就是1號俠發來的,以此類推。張三豐就心里有數了,某一天他又收到了7位徒弟的來信,隨意拿出一封,看到了"藍 敬上"就知道這是4號俠了。

古有張三豐,今有鼠標君。武當七俠就是VBO里的7個頂點信息(看作位置信息吧),GLSL的Shader內置了gl_VertexID這個變量,當Shader處理第1個頂點時,gl_VertexID就是0(號俠),處理第2個頂點時,gl_VertexID就是1(號俠),以此類推。各個頂點位置相互獨立地進行坐標變換,這幫不了我們什么。但是其顏色gl_FragColor可以根據編號gl_VertexID推算;這些頂點按照推算的顏色顯示到屏幕,然后我們(張三豐)用glReadPixel()來獲取鼠標所在位置的顏色,根據此顏色就知道這是哪個頂點了。拾取完成。

總的來說,利用GLSL的內置變量gl_VertexID,設計一個gl_FragColor=f(gl_VertexID)的一一對應的函數關系;再加上我們能夠用glReadPixel()獲取屏幕上任意位置的顏色信息(gl_FragColor),這樣就能夠得到拾取到的頂點的 gl_VertexID,即該頂點在VBO中的位置。

所以,這個思路的核心就是設計一個 gl_VertexID與 gl_FragColor之間的一一對應的函數。 gl_VertexID的范圍是0到Count(頂點數)-1,gl_FragColor由RGBA共4個分量構成,每個分量的范圍都是0到255。所以,這個方法支持的VBO的頂點數上限是(256*256*256*256=232=4294967296)。不過目前的顯卡支持的VBO最大容量據此上限還差很多。所以放心使用好了。

一個很顯然的設計方案如下:(偽代碼,忽略了一些細節)

1     int objectID = gl_VertexID;
2     gl_FragColor = vec4(
3         float(objectID & 0xFF),
4         float((objectID >> 8) & 0xFF),
5         float((objectID >> 16) & 0xFF),
6         float((objectID >> 24) & 0xFF));

就是把gl_VertexID的各個字節上的值分別指派給gl_FragColor的RGBA分量,相當於換一種格式。

gl_FragColor轉換到gl_VertexID的代碼就不用貼了吧。

 

實現:拾取VBO內的點圖元GL_POINTS

設置VBO

這根據你的業務需求來做。加載合適的模型,把頂點信息保存到VBO里。這里給一個測試用的:

 

1             const int length = 256 * 3;
2             vertices = new float[length];
3             Random random = new Random();
4             // points
5             for (int i = 0; i < length; i++)
6             {
7                 vertices[i] = (float)(i) / (float)(length);
8             }

 

這會讓各個頂點按照編號從0到length的順序,從object space的原點到(1,1,1)依次排列。就像武當七俠按照0號俠、1號俠、2號俠、3號俠、4號俠、5號俠、6號俠的順序站在你面前。

 

編寫Shader

Vertex Shader如下:

 

 1 #version 150 core
 2 
 3 in vec3 in_Position;
 4 out vec4 pass_Color;
 5 uniform mat4 projectionMatrix;
 6 uniform mat4 viewMatrix;
 7 uniform mat4 modelMatrix;
 8 
 9 void main(void) {
10     // 坐標變換
11     gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(in_Position, 1.0);
12 
13     // 根據編號計算對應顏色
14     int objectID = gl_VertexID;
15     pass_Color = vec4(
16         float(objectID & 0xFF) / 255.0, 
17         float((objectID >> 8) & 0xFF) / 255.0, 
18         float((objectID >> 16) & 0xFF) / 255.0, 
19         float((objectID >> 24) & 0xFF) / 255.0);
20 }

 

Fragment Shader如下:

 

#version 150 core
in vec4 pass_Color;
out vec4 out_Color;

void main(void) {
    // 顏色值輸出到屏幕,被glReadPixel()獲取
    out_Color = pass_Color;
}

 

Modern OpenGL里的Shader代替里以前固定功能管道里的坐標變換等功能。這里的Vertex Shader還把頂點的編號與顏色值對應了起來。

 

觀察結果

先用GL_POINTS來試驗,根據上文的測試用例,會得到如下的畫面。

可以看到,VBO里的頂點位置從中心到外面,而且是沿着一條直線延伸出來,顏色由黑色變為紅色,這印證了前面的VBO設定和Shader的功能。

再用隨機位置的點試驗。( vertices[i] = (float)(random.NextDouble() * 2 - 1); 

 

看不出問題,但也不能印證什么了。

 

未完待續

再用GL_TRIANGLES試驗。

 

發現一個問題,對於一個三角形,在其三個頂點附近分別得到了3個不同的顏色值(且是相鄰的)。這很正常,頂點所在位置保持gl_FragColor的值,3個頂點之間是用線性插值得到的顏色值。對於GL_TRIANGLES,沒有共用的頂點,所以仍然可以判斷出拾取的是哪個三角形。但是對於GL_TRIANGLE_STRIP這種到處都是共用頂點的情況,就不能區分出拾取的是哪個三角形了。這個問題我們下回分解。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM