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

  • 相关阅读:
    P1536 村村通 题解
    P1551 亲戚题解
    P1185 绘制二叉树 题解
    P3884 [JLOI2009]二叉树问题
    P1087 [NOIP2004 普及组] FBI 树
    P1305 新二叉树题解
    P1229 遍历问题
    P1030 [NOIP2001 普及组] 求先序排列题解
    P1827 [USACO3.4]美国血统 American Heritage 题解
    深度优先搜索dfs 讲解教程
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/modern-opengl-picking-primitive-in-VBO-3.html
Copyright © 2020-2023  润新知