文章出處

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

上一篇為止,拾取一個VBO里的單個圖元的問題已經徹底解決了。那么來看下一個問題:一個場景里可能會有多個VBO,此時每個VBO的gl_VertexID都是從0開始的,那么如何區分不同VBO里的圖元呢?

 

指定起始編號

其實辦法很簡單。舉個例子,士兵站成一排進行報數,那么每個士兵所報的數值都不同;這時又來了一排士兵,需要兩排都進行報數,且每個士兵所報的數值都不同,怎么辦?讓第二排士兵從第一排所報的最后一個數值后面接著報就行了。

所以,在用gl_VertexID計算給頂點顏色時,需要加上當前已經計算過的頂點總數,記作pickingBaseID,也就是當前VBO的Shader計算頂點顏色時的基礎地址。這樣一來,各個VBO的頂點對應的顏色也就全不相同了。

更新Shader

根據這個思路,只需給Vertex Shader增加一個uniform變量。

 1 #version 150 core
 2 
 3 in vec3 in_Position;
 4 in vec3 in_Color;  
 5 flat out vec4 pass_Color; // glShadeMode(GL_FLAT); in legacy opengl.
 6 uniform mat4 projectionMatrix;
 7 uniform mat4 viewMatrix;
 8 uniform mat4 modelMatrix;
 9 uniform int pickingBaseID; // how many vertices have been coded so far?
10 
11 void main(void) {
12     gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(in_Position, 1.0);
13 
14     int objectID = pickingBaseID + 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則保持不變。

 

階段狀態信息

為了保存渲染各個VBO的中間過程里的pickingBaseID,我們先給出如下一個存儲階段性計算狀態的類型。

 1     /// <summary>
 2     /// This type's instance is used in <see cref="MyScene.Draw(RenderMode.HitTest)"/>
 3     /// by <see cref="IColorCodedPicking"/> so that sceneElements can get their updated PickingBaseID.
 4     /// </summary>
 5     public class SharedStageInfo
 6     {
 7         /// <summary>
 8         /// Gets or sets how many vertices have been rendered during hit test.
 9         /// </summary>
10         public virtual int RenderedVertexCount { get; set; }
11 
12         /// <summary>
13         /// Reset this instance's fields' values to initial state so that it can be used again during rendering.
14         /// </summary>
15         public virtual void Reset()
16         {
17             RenderedVertexCount = 0;
18         }
19 
20         public override string ToString()
21         {
22             return string.Format("rendered {0} vertexes during hit test(picking).", RenderedVertexCount);
23             //return base.ToString();
24         }
25     }

稍后,我們將在每次渲染完一個VBO時就更新此類型的實例的狀態,并在每次渲染下一個VBO前為其指定PickingBaseID

 

可拾取的場景元素

為了實現拾取功能,我們首先做的就是用這幾篇文章介紹的方法渲染場景。當然,渲染出來的效果并不展示到屏幕上,只在OpenGL內部緩存中存在。其實想展示出來也很容易,在SharpGL中只需用如下幾行代碼:

1     //    Blit our offscreen bitmap.
2     IntPtr handleDeviceContext = e.Graphics.GetHdc();
3     OpenGL.Blit(handleDeviceContext);
4     e.Graphics.ReleaseHdc(handleDeviceContext);

其大意就是把OpenGL緩存中的圖形貼到屏幕上。

我們設計一個接口IColorCodedPicking,只有實現了此接口的場景元素類型,才能參與拾取過程。

 

 1     /// <summary>
 2     /// Scene element that implemented this interface will take part in color-coded picking when using <see cref="MyScene.Draw(RenderMode.HitTest);"/>.
 3     /// </summary>
 4     public interface IColorCodedPicking
 5     {
 6         /// <summary>
 7         /// Gets or internal sets how many primitived have been rendered till now during hit test.
 8         /// <para>This will be set up by <see cref="MyScene.Draw(RenderMode.HitTest)"/>, so just use the get method.</para>
 9         /// </summary>
10         int PickingBaseID { get; set; }
11 
12         /// <summary>
13         /// Gets Primitive's count of this element.
14         /// </summary>
15         int VertexCount { get; }
16 
17         /// <summary>
18         /// Get the primitive according to vertex's id.
19         /// <para>Note: the <paramref name="stageVertexID"/> refers to the last vertex that constructs the primitive.</para>
20         /// </summary>
21         /// <param name="stageVertexID"></param>
22         /// <returns></returns>
23         IPickedPrimitive Pick(int stageVertexID);
24     }

 

 

渲染場景

接下來就是實施渲染了。注意在為了拾取而渲染時,我們讓gl.ClearColor(1, 1, 1, 1);,這樣一來,如果鼠標所在位置沒有任何圖元,其"顏色編號"就是4294967295。這是color-coded picking在理論上能分辨的圖元數量的上限,所以可以用來判定是否拾取到了圖元。

 1         /// <summary>
 2         /// Draw the scene.
 3         /// </summary>
 4         /// <param name="renderMode">Use Render for normal rendering and HitTest for picking.</param>
 5         /// <param name="camera">Keep this to null if <see cref="CurrentCamera"/> is already set up.</param>
 6         public void Draw(RenderMode renderMode = RenderMode.Render)
 7         {
 8             var gl = OpenGL;
 9             if (gl == null) { return; }
10 
11             if (renderMode == RenderMode.HitTest)
12             {
13                 // When picking on a position that no model exists, 
14                 // the picked color would be
15                 // =255
16                 // +255 << 8
17                 // +255 << 16
18                 // +255 << 24
19                 // =255
20                 // +65280
21                 // +16711680
22                 // +4278190080
23                 // =4294967295
24                 // This makes it easier to determin whether we picked something or not.
25                 gl.ClearColor(1, 1, 1, 1);
26             }
27             else
28             {
29                 //    Set the clear color.
30                 float[] clear = (SharpGL.SceneGraph.GLColor)ClearColor;
31 
32                 gl.ClearColor(clear[0], clear[1], clear[2], clear[3]);
33             }
34 
35             //  Reproject.
36             if (camera != null)
37                 camera.Project(gl);
38 
39             //    Clear.
40             gl.Clear(OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT |
41                 OpenGL.GL_STENCIL_BUFFER_BIT);
42 
43             SharedStageInfo info = this.StageInfo;
44             info.Reset();
45 
46             //  Render the root element, this will then render the whole
47             //  of the scene tree.
48             MyRenderElement(SceneContainer, gl, renderMode, info);
49 
50             gl.Flush();
51         }
52 
53         /// <summary>
54         /// Renders the element.
55         /// </summary>
56         /// <param name="gl">The gl.</param>
57         /// <param name="renderMode">The render mode.</param>
58         public void MyRenderElement(SceneElement sceneElement, OpenGL gl, RenderMode renderMode, SharedStageInfo info)
59         {
60             // ...
61             if (renderMode == RenderMode.HitTest) // Do color coded picking if we are in HitTest mode.
62             {
63                 IColorCodedPicking picking = sceneElement as IColorCodedPicking;
64                 if (picking != null)// This element should take part in color coded picking.
65                 {
66                     picking.PickingBaseID = info.RenderedVertexCount;// set up picking base id to transform to shader.
67 
68                     //  If the element can be rendered, render it.
69                     IRenderable renderable = sceneElement as IRenderable;
70                     if (renderable != null) renderable.Render(gl, renderMode);
71 
72                     info.RenderedVertexCount += picking.VertexCount;// update stage info for next element's picking process.
73                 }
74             }
75             else // Normally render the scene.
76             {
77                 //  If the element can be rendered, render it.
78                 IRenderable renderable = sceneElement as IRenderable;
79                 if (renderable != null) renderable.Render(gl, renderMode);
80             }
81             
82             //  Recurse through the children.
83             foreach (var childElement in sceneElement.Children)
84                 MyRenderElement(childElement, gl, renderMode, info);
85                 
86             // ...
87         }

 

獲取頂點編號

場景渲染完畢,那么就可以獲取鼠標所在位置的顏色,進而獲取頂點編號了。

 1         private IPickedPrimitive Pick(int x, int y)
 2         {
 3             // render the scene for color-coded picking.
 4             this.Scene.Draw(RenderMode.HitTest);
 5             // get coded color.
 6             byte[] codedColor = new byte[4];
 7             this.OpenGL.ReadPixels(x, this.Height - y - 1, 1, 1,
 8                 OpenGL.GL_RGBA, OpenGL.GL_UNSIGNED_BYTE, codedColor);
 9 
10             // get vertexID from coded color.
11             // the vertexID is the last vertex that constructs the primitive.
12             // see http://www.cnblogs.com/bitzhuwei/p/modern-opengl-picking-primitive-in-VBO-2.html
13             var shiftedR = (uint)codedColor[0];
14             var shiftedG = (uint)codedColor[1] << 8;
15             var shiftedB = (uint)codedColor[2] << 16;
16             var shiftedA = (uint)codedColor[3] << 24;
17             var stageVertexID = shiftedR + shiftedG + shiftedB + shiftedA;
18 
19             // get picked primitive.
20             IPickedPrimitive picked = null;
21             picked = this.Scene.Pick((int)stageVertexID);
22 
23             return picked;
24         }

 

獲取圖元

這個頂點編號是在所有VBO中的唯一編號,所以需要遍歷所有實現了IColorCodedPicking接口的場景元素來找到此編號對應的圖元。

 1         /// <summary>
 2         /// Get picked primitive by <paramref name="stageVertexID"/> as the last vertex that constructs the primitive.
 3         /// </summary>
 4         /// <param name="stageVertexID">The last vertex that constructs the primitive.</param>
 5         /// <returns></returns>
 6         public IPickedPrimitive Pick(int stageVertexID)
 7         {
 8             if (stageVertexID < 0) { return null; }
 9 
10             IPickedPrimitive picked = null;
11 
12             SceneElement element = this.SceneContainer;
13             picked = Pick(element, stageVertexID);
14 
15             return picked;
16         }
17 
18         private IPickedPrimitive Pick(SceneElement element, int stageVertexID)
19         {
20             IPickedPrimitive result = null;
21             IColorCodedPicking picking = element as IColorCodedPicking;
22             if (picking != null)
23             {
24                 result = picking.Pick(stageVertexID);
25                 if (result != null)
26                 {
27                     result.Element = picking;
28                     result.StageVertexID = stageVertexID;
29                 }
30             }
31 
32             if (result == null)
33             {
34                 foreach (var item in element.Children)
35                 {
36                     result = Pick(item, stageVertexID);
37                     if (result != null)
38                     { break; }
39                 }
40             }
41 
42             return result;
43         }

 

至于每個場景元素是如何實現IColorCodedPicking的Pick方法的,就比較自由了,下面是一種可參考的方式:

 1         IPickedPrimitive IColorCodedPicking.Pick(int stageVertexID)
 2         {
 3             ScientificModel model = this.Model;
 4             if (model == null) { return null; }
 5 
 6             IColorCodedPicking picking = this;
 7 
 8             int lastVertexID = picking.GetLastVertexIDOfPickedPrimitive(stageVertexID);
 9             if (lastVertexID < 0) { return null; }
10 
11             PickedPrimitive primitive = new PickedPrimitive();
12 
13             primitive.Type = BeginModeHelper.ToPrimitiveType(model.Mode);
14 
15             int vertexCount = PrimitiveTypeHelper.GetVertexCount(primitive.Type);
16             if (vertexCount == -1) { vertexCount = model.VertexCount; }
17 
18             float[] positions = new float[vertexCount * 3];
19             float[] colors = new float[vertexCount * 3];
20 
21             // copy primitive's position and color to result.
22             {
23                 float[] modelPositions = model.Positions;
24                 float[] modelColors = model.Colors;
25                 for (int i = lastVertexID * 3 + 2, j = positions.Length - 1; j >= 0; i--, j--)
26                 {
27                     if (i < 0)
28                     { i += modelPositions.Length; }
29                     positions[j] = modelPositions[i];
30                     colors[j] = modelColors[i];
31                 }
32             }
33            
34             primitive.positions = positions;
35             primitive.colors = colors;
36 
37             return primitive;
38         }
39         /// <summary>
40         /// Get last vertex's id of picked Primitive if it belongs to this <paramref name="picking"/> instance.
41         /// <para>Returns -1 if <paramref name="stageVertexID"/> is an illigal number or the <paramref name="stageVertexID"/> is in some other element.</para>
42         /// </summary>
43         /// <param name="picking"></param>
44         /// <param name="stageVertexID"></param>
45         /// <returns></returns>
46         public static int GetLastVertexIDOfPickedPrimitive(this IColorCodedPicking picking, int stageVertexID)
47         {
48             int lastVertexID = -1;
49 
50             if (picking == null) { return lastVertexID; }
51 
52             if (stageVertexID < 0) // Illigal ID.
53             { return lastVertexID; }
54 
55             if (stageVertexID < picking.PickingBaseID) // ID is in some previous element.
56             { return lastVertexID; }
57 
58             if (picking.PickingBaseID + picking.VertexCount <= stageVertexID) // ID is in some subsequent element.
59             { return lastVertexID; }
60 
61             lastVertexID = stageVertexID - picking.PickingBaseID;
62 
63             return lastVertexID;
64         }

 

至此,終于找到了要拾取的圖元。

 

有圖有真相

折騰了3篇,現在終于算解決所有的問題了。

這里以GL_POINTS為例,如圖所示,有3個VBO,每個VBO各有1000個頂點。我們可以分別拾取各個頂點,并得知其位置、顏色、ID號、從屬哪個VBO這些信息。可以說能得到所拾取的圖元的所有信息。

 

綜上所述

總結起來,Modern OpenGL可以利用GLSL內置變量gl_VertexID的存在,借助一點小技巧,實現拾取多個VBO內的任一圖元的功能。不過這個方法顯然只能拾取一個圖元,就是Z緩沖中離屏幕最近的那個圖元,不像射線一樣能穿透過去拾取多個。

 

本系列到此結束,今后如果需要拾取鼠標所在位置下的所有圖元,再續后話吧。

2016-04-24

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

推薦CSharpGL(18)分別處理glDrawArrays()和glDrawElements()兩種方式下的拾取(ColorCodedPicking))就徹底解決這個拾取的問題。


文章列表


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

IT工程師數位筆記本

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