Modern OpenGL用Shader拾取VBO内单一图元的思路和实现(3)

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))就彻底解决这个拾取的问题。

posted @ 2015-05-31 04:28  BIT祝威  阅读(2836)  评论(0编辑  收藏  举报