三维拾取

原理

​ 让每一个像素都有自己独特的索引,当鼠标点击时获取鼠标点击处的像素和像素索引(一般glReadPixels函数是获取鼠标点击处的指定的数据,比如说3d拾取就选择获取rgb颜色数据 ,但是此处并不是真正的获取颜色数据,而是在颜色纹理中填充的是三元索引,所以从颜色纹理中获取的是三元索引),就可知道点击的像素属于哪个对象 ,就可以知道点击到了哪个对象。

​ 通过创建帧缓冲, 将三元索引代替每个顶点像素的颜色rgb存储进入帧缓冲的颜色缓冲,从而使像素位置和索引相对应。这样在点击的时候就可以知道点击到了哪个索引。

三元索引:

  1. 第一级是像素所在物体的索引值,场景中的每一个物体都会得到一个唯一的索引;
  2. 物体的draw call的索引,这个索引会在开始渲染新物体时重置;
  3. 每个draw call中图元的索引值,每次新的draw call开始时该索引会重置;

拾取阶段

把模型相应数据传进拾取着色器,绘制一遍模型,把相应的顶点和索引绘制进帧缓冲。

step1:创建帧缓冲,绑定颜色和深度纹理

把模型的各个顶点放入帧缓冲的深度纹理,把模型的对象索引,绘制索引,原始索引放入帧缓冲的颜色纹理。下面只显示了基于阴影贴图init代码(只用了深度纹理)后添加的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Init()
{
//注意颜色纹理的参数设置GL_RGB32F、GL_RGB
glGenTextures(1, &pickingTexture);
glBindTexture(GL_TEXTURE_2D, pickingTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, 1024, 1024,0, GL_RGB, GL_FLOAT, NULL);
//将此纹理附加到FBO的GL_COLOR_ATTACHMENT0目标,这将使其成为片段着色器输出的目标。
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,pickingTexture, 0);

glDrawBuffer(GL_COLOR_ATTACHMENT0);
glBindTexture(GL_TEXTURE_2D, 0);
//重置读取缓冲区和帧缓冲区
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

}
step2:拾取着色器

顶点着色器进行正常的MVP变换,片段着色器代码如下:

​ 在同一次绘图调用中对象索引和绘图索引对于所有像素都是相同的,因此它们来自统一的变量。为了获得原始索引,使用内置变量gl_PimitiveID(渲染过程中的图元数量)。这是系统自动维护的原语的运行索引。系统在绘图开始时将gl_PimitiveID重置为零。这使得很难区分“背景”像素和实际被对象覆盖的像素。为了克服这个问题,在将索引写入输出之前先将其递增一。这意味着可以识别背景像素,因为它们的图元ID为零,而对象覆盖的像素具有1…n作为图元ID。

1
2
3
4
5
6
7
8
#version 330
uniform uint gDrawIndex;
uniform uint gObjectIndex;
out vec3 FragColor;
void main()
{
FragColor = vec3(float(gObjectIndex), float(gDrawIndex),float(gl_PrimitiveID + 1));
}
step3:写入帧缓冲

主循环中:开启拾取帧缓冲-开启拾取着色器-把模型相应数据传进拾取着色器-绘制一遍模型-恢复默认缓冲

1
2
3
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
/*绘制图形*/
glBindFramebuffer(GL_FRAMEBUFFER, 0);

绘制阶段

如果左键点击了,获取鼠标点击处的像素的三元索引(此处将点击处改成了光标位置处)
如果该像素不是背景,通过得到的该像素的绘制索引和原始索引绘制出相应片段(此处将绘制出相应片段改成了设置不同的颜色)

step1:从点击的像素中获取三元索引

​ 函数参数为鼠标点击的屏幕上的xy坐标。要读取FBO,必须将其绑定到GL_read_FRAMEBUFFER目标。然后需要使用函数glReadBuffer()指定从哪个颜色缓冲区读取,原因是FBO可以包含多个颜色缓冲区。函数glReadPixels执行实际读取,它使用左下角(第一对参数)和宽度/高度(第二对参数)指定的矩形,并将结果读入最后一个参数给出的地址,此处矩形的大小是一个纹素。因为需要原始数据,因此使用GL_RGB作为格式,使用GL_FLOAT作为类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct PixelInfo {
float ObjectID;float DrawID;float PrimID;
PixelInfo() {ObjectID = 0.0f;DrawID = 0.0f;PrimID = 0.0f;}};
PixelInfo ReadPixel(unsigned int x, unsigned int y)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glReadBuffer(GL_COLOR_ATTACHMENT0);
PixelInfo Pixel;
glReadPixels(x, y, 1, 1, GL_RGB, GL_FLOAT, &Pixel);
//必须重置读取缓冲区和帧缓冲区
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
return Pixel;
}
step2:绘制

​ 此处绘制一个物体正方体,但是利用不同的MVP矩阵将其draw两次。两次的gObjectIndex都为0,第一次draw的DrawIndex是0,第二次draw的DrawIndex是1。

1
2
glUniform1ui(gObjectIndexID, 0);
glUniform1ui(gDrawIndexID, 1);

glfwGetCursorPos获取窗口上光标的位置,存入xy中。注意y轴的计算方式:height-int(y)+1(屏幕y轴与世界空间的y轴相反)。通过如下代码:如果光标在第一个物体的第一个三角形,则为红色;第一个物体的第二个三角形,则为墨绿色;如果光标在第二个物体的第一个三角形,则为蓝色;第二个物体的第二个三角形,则为灰色;光标不在物体上则为绿色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    glfwGetCursorPos(window,&x,&y);
PixelInfo Pixel = ReadPixel(int(x), 768-int(y)+1);
if (Pixel.PrimID != 0) {
glm::uint a = (uint)Pixel.DrawID;
glm::uint b = (uint)Pixel.PrimID;
if (a == 0) {
if(b == 1){glColor4f(0.4f, 0.0f, 0.0f, 1.0f);}
if (b == 2) {glColor4f(0.4f, 0.4f, 0.0f, 1.0f);}
}
else if (a == 1) {
if (b == 1) {glColor4f(0.0f, 0.0f, 0.4f, 1.0f);}
if (b == 2) {glColor4f(0.4f, 0.4f, 0.4f, 1.0f);}
}
}
else {
glColor4f(0.0f, 0.4f, 0.0f, 1.0f);
}

真正绘制阶段

主循环中正常绘制,使物体显示在屏幕。

效果:

img image-20230128185721248
image-20230128185707432 image-20230128185729121
image-20230128185734563
Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码

请我喝杯咖啡吧~

支付宝
微信