Modern OpenGL用Shader拾取VBO内单一图元的思路和实现
2015-05-28 13:06
711 查看
[b]Modern OpenGL用Shader拾取VBO内单一图元的思路和实现 [/b]
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/78208742000d82dbe98ec02e53b0e9c5.png)
最简单的理解拾取的方式大概是到(http://www.yakergong.net/nehe/course/tutorial_32.html)玩一下NEHE的拾取游戏。用鼠标点击飞过屏幕的物体就会击中它,这就是拾取的意义。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/8d0e2fd3dc708e6b997108c89cf6f6d0.png)
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/23e248fea405130c3d1ebf0a933e3731.png)
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/ef189d39d0acee4225cce3f917a66cb2.png)
Legacy OpenGL就是使用glTranslate、glRotate、gluScale、gluLookAt、glPerspective等函数的OpenGL程序。NEHE的教程讲述的都是Legacy OpenGL。这是以前OpenGL的使用方式,它使用的是固定功能管线。
Modern OpenGL不再使用上面那些函数。它用GLSL语言编写Shader,由Shader代替上面那些函数的功能,并且Shader能提供更多更强的功能。你必须自行计算投影矩阵、ModelView矩阵。
它还使用VBO把顶点数据放到GPU内存,从而避免了每次渲染时都要把顶点数据从CPU内存上传到GPU内存。简单来说,VBO就是一个数组,里面依次保存留每个顶点的某种信息(如位置信息、颜色信息、法线信息)。它代替了显示列表。所以glVertex也不再使用了。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/2edbdbe9e4bd299ded3d914649932c3d.gif)
在SharpGL(https://github.com/dwmkerr/sharpgl)里有一个HitTestSample项目,演示了Legacy OpenGL里实现拾取的方法。简单来说,OpenGL在设计的时候就为拾取提供了相关接口,所以拾取功能才得以实现。Legacy OpenGL通过GL_SELECT的渲染模式、预备好的拾取缓存、渲染每个可区分的模型前都用PushName()等一系列动作,实现了拾取屏幕上某一点的模型。
当然,还可以用射线碰撞检测的方法进行拾取,留待以后再说。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/70a6ed1418857db1ffcedd4a5b21dd6f.gif)
Modern OpenGL使用VBO存储顶点信息,如果仍旧采用Legacy OpenGL里的拾取机制,最多只能分辨出这个VBO和那个VBO来。如果场景里的模型是由唯一一个VBO渲染的,那么只要点中了VBO里的任何一点,就会拾取到整个VBO。比如一个VBO里保存的是用GL_POINTS代表的星星,那么你点击任何一个点,Legacy OpenGL的拾取机制都会返回同样的结果,你是无法拾取单一的星星的。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/3a6be3f6114d8a361f60d93a4dfedf43.jpg)
如果说用一个VBO只保存一个顶点,那也不行,因为显卡支持的VBO总数有限,一个VBO只保存一个顶点实在太浪费了。
我在网上搜罗了好多天,只找到一个基于颜色编码的拾取方法(Color-Coded Picking)。(查看这个足以代表之http://www.lighthouse3d.com/tutorials/opengl-selection-tutorial/)不过这个例子仍然只能分辨出各个VBO来,VBO内部的图元是无法区分的。不过好歹有点希望了。
万般无奈之际我在读OVITO(https://github.com/t-brink/ovito)的代码时终于找到了它分辨VBO内的单一图元的原理。顺便模仿了一下它的Shader。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/5ed05b613da688be0cff625593f32c52.png)
这里仍然以(Color-Coded Picking)称呼这个方法,因为确实就是基于颜色编码的拾取方式。网上找的那些只不过没有充分发挥这个方法的能力而已。也可能是人家懒得写或者不愿意写吧。
用类比的方式说明这个方法的思路。假设张三丰已经收齐了武当七侠,分别给他们编派了法号(应该是道号)为0号侠、1号侠、2号侠、3号侠、4号侠、5号侠、6号侠。这是武当内部编号,从不外传。武当七侠各自出去闯江湖,每隔1个月都会各自发一封书信给张三丰,为了辨别身份,他们分别用赤、橙、黄、绿、青、蓝、紫7个颜色自称。即"尊师您好,……赤 敬上"就是0号侠寄来的,"恩师万安,……橙 敬上"就是1号侠发来的,以此类推。张三丰就心里有数了,某一天他又收到了7位徒弟的来信,随意拿出一封,看到了"蓝 敬上"就知道这是4号侠了。
古有张三丰,今有鼠标君。武当七侠就是VBO里的7个顶点信息(看作位置信息吧),GLSL的Shader内置了gl_VertexID这个变量,当Shader处理第1个顶点时,gl_VertexID就是0(号侠),处理第2个顶点时,gl_VertexID就是1(号侠),以此类推。各个顶点位置相互独立地进行坐标变换,这帮不了我们什么。但是其颜色gl_FragColor可以根据编号gl_VertexID推算;这些顶点按照推算的颜色显示到屏幕,然后我们(张三丰)用glReadPixel()来获取鼠标所在位置的颜色,根据此颜色就知道这是哪个顶点了。拾取完成。
总的来说,利用GLSL的内置变量gl_VertexID,设计一个gl_FragColor=f(gl_VertexID)的一一对应的函数关系;再加上我们能够用glReadPixel()获取屏幕上任意位置的颜色信息(gl_FragColor),这样就能够得到拾取到的顶点的 gl_VertexID,即该顶点在VBO中的位置。
所以,这个思路的核心就是设计一个 gl_VertexID与 gl_FragColor之间的一一对应的函数。 gl_VertexID的范围是0到Count(顶点数)-1,gl_FragColor由RGBA共4个分量构成,每个分量的范围都是0到255。所以,这个方法支持的VBO的顶点数上限是(256*256*256*256=232=4294967296)。不过目前的显卡支持的VBO最大容量据此上限还差很多。所以放心使用好了。
一个很显然的设计方案如下:(伪代码,忽略了一些细节)
就是把gl_VertexID的各个字节上的值分别指派给gl_FragColor的RGBA分量,相当于换一种格式。
从gl_FragColor转换到gl_VertexID的代码就不用贴了吧。
这会让各个顶点按照编号从0到length的顺序,从object space的原点到(1,1,1)依次排列。就像武当七侠按照0号侠、1号侠、2号侠、3号侠、4号侠、5号侠、6号侠的顺序站在你面前。
Fragment Shader如下:
Modern OpenGL里的Shader代替里以前固定功能管道里的坐标变换等功能。这里的Vertex Shader还把顶点的编号与颜色值对应了起来。
.png]
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/2833f4d4ff51acebdf907e45eb9018b0.png)
可以看到,VBO里的顶点位置从中心到外面,而且是沿着一条直线延伸出来,颜色由黑色变为红色,这印证了前面的VBO设定和Shader的功能。
再用随机位置的点试验。( vertices[i] = (float)(random.NextDouble() * 2 - 1); )
.png]
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/e2a1a6f918f595260f59a40051f33d90.png)
看不出问题,但也不能印证什么了。
.png]
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/be8bf8724f99e603ad4ced7014fcc2f0.png)
发现一个问题,对于一个三角形,在其三个顶点附近分别得到了3个不同的颜色值(且是相邻的)。这很正常,顶点所在位置保持gl_FragColor的值,3个顶点之间是用线性插值得到的颜色值。对于GL_TRIANGLES,没有共用的顶点,所以仍然可以判断出拾取的是哪个三角形。但是对于GL_TRIANGLE_STRIP这种到处都是共用顶点的情况,就不能区分出拾取的是哪个三角形了。这个问题我们下回分解。
什么意思?
拾取
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/78208742000d82dbe98ec02e53b0e9c5.png)
最简单的理解拾取的方式大概是到(http://www.yakergong.net/nehe/course/tutorial_32.html)玩一下NEHE的拾取游戏。用鼠标点击飞过屏幕的物体就会击中它,这就是拾取的意义。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/8d0e2fd3dc708e6b997108c89cf6f6d0.png)
Legacy OpenGL VS Modern OpenGL
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/23e248fea405130c3d1ebf0a933e3731.png)
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/ef189d39d0acee4225cce3f917a66cb2.png)
Legacy OpenGL就是使用glTranslate、glRotate、gluScale、gluLookAt、glPerspective等函数的OpenGL程序。NEHE的教程讲述的都是Legacy OpenGL。这是以前OpenGL的使用方式,它使用的是固定功能管线。
Modern OpenGL不再使用上面那些函数。它用GLSL语言编写Shader,由Shader代替上面那些函数的功能,并且Shader能提供更多更强的功能。你必须自行计算投影矩阵、ModelView矩阵。
它还使用VBO把顶点数据放到GPU内存,从而避免了每次渲染时都要把顶点数据从CPU内存上传到GPU内存。简单来说,VBO就是一个数组,里面依次保存留每个顶点的某种信息(如位置信息、颜色信息、法线信息)。它代替了显示列表。所以glVertex也不再使用了。
Legacy OpenGL的拾取
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/2edbdbe9e4bd299ded3d914649932c3d.gif)
在SharpGL(https://github.com/dwmkerr/sharpgl)里有一个HitTestSample项目,演示了Legacy OpenGL里实现拾取的方法。简单来说,OpenGL在设计的时候就为拾取提供了相关接口,所以拾取功能才得以实现。Legacy OpenGL通过GL_SELECT的渲染模式、预备好的拾取缓存、渲染每个可区分的模型前都用PushName()等一系列动作,实现了拾取屏幕上某一点的模型。
当然,还可以用射线碰撞检测的方法进行拾取,留待以后再说。
Modern OpenGL的拾取
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/70a6ed1418857db1ffcedd4a5b21dd6f.gif)
Modern OpenGL使用VBO存储顶点信息,如果仍旧采用Legacy OpenGL里的拾取机制,最多只能分辨出这个VBO和那个VBO来。如果场景里的模型是由唯一一个VBO渲染的,那么只要点中了VBO里的任何一点,就会拾取到整个VBO。比如一个VBO里保存的是用GL_POINTS代表的星星,那么你点击任何一个点,Legacy OpenGL的拾取机制都会返回同样的结果,你是无法拾取单一的星星的。
拾取VBO内的单一图元
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/3a6be3f6114d8a361f60d93a4dfedf43.jpg)
如果说用一个VBO只保存一个顶点,那也不行,因为显卡支持的VBO总数有限,一个VBO只保存一个顶点实在太浪费了。
我在网上搜罗了好多天,只找到一个基于颜色编码的拾取方法(Color-Coded Picking)。(查看这个足以代表之http://www.lighthouse3d.com/tutorials/opengl-selection-tutorial/)不过这个例子仍然只能分辨出各个VBO来,VBO内部的图元是无法区分的。不过好歹有点希望了。
万般无奈之际我在读OVITO(https://github.com/t-brink/ovito)的代码时终于找到了它分辨VBO内的单一图元的原理。顺便模仿了一下它的Shader。
思路:Color-Coded Picking
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/5ed05b613da688be0cff625593f32c52.png)
这里仍然以(Color-Coded Picking)称呼这个方法,因为确实就是基于颜色编码的拾取方式。网上找的那些只不过没有充分发挥这个方法的能力而已。也可能是人家懒得写或者不愿意写吧。
用类比的方式说明这个方法的思路。假设张三丰已经收齐了武当七侠,分别给他们编派了法号(应该是道号)为0号侠、1号侠、2号侠、3号侠、4号侠、5号侠、6号侠。这是武当内部编号,从不外传。武当七侠各自出去闯江湖,每隔1个月都会各自发一封书信给张三丰,为了辨别身份,他们分别用赤、橙、黄、绿、青、蓝、紫7个颜色自称。即"尊师您好,……赤 敬上"就是0号侠寄来的,"恩师万安,……橙 敬上"就是1号侠发来的,以此类推。张三丰就心里有数了,某一天他又收到了7位徒弟的来信,随意拿出一封,看到了"蓝 敬上"就知道这是4号侠了。
古有张三丰,今有鼠标君。武当七侠就是VBO里的7个顶点信息(看作位置信息吧),GLSL的Shader内置了gl_VertexID这个变量,当Shader处理第1个顶点时,gl_VertexID就是0(号侠),处理第2个顶点时,gl_VertexID就是1(号侠),以此类推。各个顶点位置相互独立地进行坐标变换,这帮不了我们什么。但是其颜色gl_FragColor可以根据编号gl_VertexID推算;这些顶点按照推算的颜色显示到屏幕,然后我们(张三丰)用glReadPixel()来获取鼠标所在位置的颜色,根据此颜色就知道这是哪个顶点了。拾取完成。
总的来说,利用GLSL的内置变量gl_VertexID,设计一个gl_FragColor=f(gl_VertexID)的一一对应的函数关系;再加上我们能够用glReadPixel()获取屏幕上任意位置的颜色信息(gl_FragColor),这样就能够得到拾取到的顶点的 gl_VertexID,即该顶点在VBO中的位置。
所以,这个思路的核心就是设计一个 gl_VertexID与 gl_FragColor之间的一一对应的函数。 gl_VertexID的范围是0到Count(顶点数)-1,gl_FragColor由RGBA共4个分量构成,每个分量的范围都是0到255。所以,这个方法支持的VBO的顶点数上限是(256*256*256*256=232=4294967296)。不过目前的显卡支持的VBO最大容量据此上限还差很多。所以放心使用好了。
一个很显然的设计方案如下:(伪代码,忽略了一些细节)
int objectID = gl_VertexID; gl_FragColor = vec4( float(objectID & 0xFF), float((objectID >> 8) & 0xFF), float((objectID >> 16) & 0xFF), float((objectID >> 24) & 0xFF));
就是把gl_VertexID的各个字节上的值分别指派给gl_FragColor的RGBA分量,相当于换一种格式。
从gl_FragColor转换到gl_VertexID的代码就不用贴了吧。
实现:拾取VBO内的点图元GL_POINTS
设置VBO
这根据你的业务需求来做。加载合适的模型,把顶点信息保存到VBO里。这里给一个测试用的:const int length = 256 * 3; vertices = new float[length]; Random random = new Random(); // points for (int i = 0; i < length; i++) { vertices[i] = (float)(i) / (float)(length); }
这会让各个顶点按照编号从0到length的顺序,从object space的原点到(1,1,1)依次排列。就像武当七侠按照0号侠、1号侠、2号侠、3号侠、4号侠、5号侠、6号侠的顺序站在你面前。
编写Shader
Vertex Shader如下:#version 150 core in vec3 in_Position; out vec4 pass_Color; uniform mat4 projectionMatrix; uniform mat4 viewMatrix; uniform mat4 modelMatrix; void main(void) { // 坐标变换 gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(in_Position, 1.0); // 根据编号计算对应颜色 int objectID = gl_VertexID; pass_Color = vec4( float(objectID & 0xFF) / 255.0, float((objectID >> 8) & 0xFF) / 255.0, float((objectID >> 16) & 0xFF) / 255.0, float((objectID >> 24) & 0xFF) / 255.0); }
Fragment Shader如下:
#version 150 core in vec4 pass_Color; out vec4 out_Color; void main(void) { // 颜色值输出到屏幕,被glReadPixel()获取 out_Color = pass_Color; }
Modern OpenGL里的Shader代替里以前固定功能管道里的坐标变换等功能。这里的Vertex Shader还把顶点的编号与颜色值对应了起来。
观察结果
先用GL_POINTS来试验,根据上文的测试用例,会得到如下的画面。.png]
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/2833f4d4ff51acebdf907e45eb9018b0.png)
可以看到,VBO里的顶点位置从中心到外面,而且是沿着一条直线延伸出来,颜色由黑色变为红色,这印证了前面的VBO设定和Shader的功能。
再用随机位置的点试验。( vertices[i] = (float)(random.NextDouble() * 2 - 1); )
.png]
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/e2a1a6f918f595260f59a40051f33d90.png)
看不出问题,但也不能印证什么了。
未完待续
再用GL_TRIANGLES试验。.png]
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/12/be8bf8724f99e603ad4ced7014fcc2f0.png)
发现一个问题,对于一个三角形,在其三个顶点附近分别得到了3个不同的颜色值(且是相邻的)。这很正常,顶点所在位置保持gl_FragColor的值,3个顶点之间是用线性插值得到的颜色值。对于GL_TRIANGLES,没有共用的顶点,所以仍然可以判断出拾取的是哪个三角形。但是对于GL_TRIANGLE_STRIP这种到处都是共用顶点的情况,就不能区分出拾取的是哪个三角形了。这个问题我们下回分解。
相关文章推荐
- Modern OpenGL用Shader拾取VBO内单一图元的思路和实现(3)
- Modern OpenGL用Shader拾取VBO内单一图元的思路和实现(2)
- 【转】Direct3D中实现图元的鼠标拾取
- 用OpenGL shader 实现将YUV转RGB(直接调用GPU实现)
- Opengl及D3D以及Shader实现的特效
- OpenGL学习笔记(八):进一步理解VAO、VBO和SHADER,并使用VAO、VBO和SHADER绘制一个三角形
- OpenGL12-shader(GLSL)着色语言4-广告版的实现
- Direct3D 中实现图元的鼠标拾取
- OpenGL(十四)环境反射 环境折射 的shader实现
- OpenGL中shader读取实现
- Opengl及D3D以及Shader实现的特效
- 使用OpenGL Shader实现放大镜效果
- 使用OpenGL Shader实现放大镜效果
- OpenGL实现瀑布图的一些思路
- Direct3D中实现图元的鼠标拾取
- OPENGL光源(shader部分)整体实现思想
- OpenGL(十三) 天空盒 的 shader 实现
- OpenGL(十九) 阴影 通过ShadowMap的shader实现
- OpenGL(十五)雾效 的 shader 实现
- [OpenGL] 利用Shader实现复杂地形的渲染