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))就彻底解决这个拾取的问题。
微信扫码,自愿捐赠。天涯同道,共谱新篇。
微信捐赠不显示捐赠者个人信息,如需要,请注明联系方式。 |