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

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 RenderedPrimitiveCount { 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 RenderedPrimitiveCount = 0;
18 }
19
20 public override string ToString()
21 {
22 return string.Format(rendered {0} primitives during hit test(picking)., RenderedPrimitiveCount);
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 PrimitiveCount { 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.RenderedPrimitiveCount;// 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.RenderedPrimitiveCount += picking.PrimitiveCount;// 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.PrimitiveCount <= 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这些信息。可以说能得到所拾取的图元的所有信息。

您可以在此下载此Demo慢慢体验。

 

综上所述

总结起来,Modern OpenGL可以利用gl_VertexID的存在,借助一点小技巧,实现拾取多个VBO内的任一图元的功能。不过这个方法显然只能拾取一个图元,就是Z缓冲中离屏幕最近的那个图元,不像射线一样能穿透过去拾取多个。

 

本系列到此结束,今后如果需要拾取鼠标所在位置下的所有图元,再续后话吧。

本文链接:Modern OpenGL用Shader拾取VBO内单一图元的思路和实现(3),转载请注明。



You must enable javascript to see captcha here!

Copyright © All Rights Reserved · Green Hope Theme by Sivan & schiy · Proudly powered by WordPress

无觅相关文章插件,快速提升流量