阅读本篇文章需要提前掌握OpenGL顶点和着色器及摄像机的相关知识。
前面复现篇的两篇文章中介绍了Qt+OpenGL框架下顶点和着色器及摄像机的知识,接下来我们用这两个知识来实现3D领域非常常见的任务—点云显示和交互。
3D领域常见的一个需求是将点云显示出来给用户,这个功能乍一看好像还比较复杂,实则不然,只要我们学会OpenGL的顶点和着色器的知识就能轻松搞定。
原理很简单,只要直接将点云当成是OpenGL的"点"图元进行处理就可以了。在顶点和着色器的复现文章中,我们绘制的图元是三角形,代码如下:
f->glDrawArrays(GL_TRIANGLES, 0, 3);
只要我们提前将点云全部当成顶点并放到VBO中,然后将图元类型改成点,问题就完美解决了:
f->glDrawArrays(GL_POINTS, 0, 3);//最后一个参数可以改为点云的个数
在很多情况下我们需要放大和缩小对象,以更好的对3D点云进行观测,这个功能可以通过修改摄像机的视角大小来实现。详细的介绍可以看这里摄像机,这里给出实现代码片段:
将摄像头视角大小以变量进行表示:
mvp.perspective(field, aspectRatio, 0.1f, 100.0f);//field是视角大小的变量
在鼠标滚轮响应函数中,修改视角的大小
void MyOpenGLWidget::wheelEvent(QWheelEvent *event)// 滚轮事件
{
if(event->delta() > 0)// 当滚轮远离使用者时
{
if(field>1)
{
field--;
}
}
else// 当滚轮向使用者方向旋转时
{
if(field<170)
{
field++;
}
}
this->update();
}
点云的平移其实就相当于移动世界的中心观察点,可以通过mvp对象的translate函数来实现
QVector3D offset;//偏移量
mvp.translate(offset);//图像平移
点云的旋转可以使用摄像机文章中介绍的rotate函数来实现,这里以自动驾驶的点云处理为例做简单的介绍。
在自动驾驶中,使用激光雷达获得的点云投影到世界坐标中,一般是以坐标系的xoy为地平面的,而z轴为高度。因此,出于直观的原因,大部分的软件会提供用户两种点云的旋转观测方式:一是将点云绕着z轴进行旋转(保持地面不变,车子周围的物体绕车中轴旋转),二是将点云绕着x轴进行旋转(相当于车子上下俯仰的视角)。
以ROS中的Rviz为例,当鼠标左键按住然后向左移动时,程序根据移动的幅度将点云绕着z轴顺时针旋转,向右则绕着z轴逆时针旋转。当左键按住然后向上移动时,程序会根据移动的幅度将点云绕着x轴顺时针旋转,向下则为绕着x轴逆时针旋转。
因此我们只需要在鼠标按下时记住鼠标指针的位置以及当前点云的旋转状态:
void MyOpenGLWidget::mousePressEvent(QMouseEvent *event) // 鼠标按下事件
{
if(event->button() == Qt::LeftButton)
{
offset = event->globalPos();// 获取指针位置
oldrotatex=rotatex;
oldrotatez=rotatez;
}
}
然后在鼠标移动响应函数中判断左键是否被按下,如果是计算偏差值并设置点云绕轴旋转的对应参数即可:
void MyOpenGLWidget::mouseMoveEvent(QMouseEvent *event) // 鼠标移动事件
{
if(event->buttons() & Qt::LeftButton)//按下左键并且移动
{
QPoint temp;
temp = event->globalPos() - offset;//获取移动值
rotatex=oldrotatex+temp.y()/20.0;
rotatez=oldrotatez+temp.x()/20.0;
}
this->update();
}
旋转函数修改如下:
mvp.rotate(10.0f * rotatex, QVector3D(1.0f, 0.0f, 0.0f));//绕x轴旋转
mvp.rotate(10.0f * rotatez, QVector3D(0.0f, 0.0f, 1.0f));//绕z轴旋转
在rviz中我们通过鼠标中键平移点云,之后旋转点云对象会发现旋转中心还在原来的位置,因此操作起来非常别扭。要实现那样的效果只要结合我们上面写的平移和旋转就可以了:
QMatrix4x4 mvp;
mvp.translate(worldcenter);//通过鼠标中键等操作点云的平移,这里不再赘述
mvp.rotate(10.0f * rotatex, QVector3D(1.0f, 0.0f, 0.0f)); //绕x轴旋转
mvp.rotate(10.0f * rotatez, QVector3D(0.0f, 0.0f, 1.0f)); //绕z轴旋转
但这不是我们理想中的操作,一般习惯上我们是希望移动后以新的中心进行旋转的,因此我们需要把代码修改成下面这样:
QMatrix4x4 mvp;
mvp.rotate(10.0f * rotatex, QVector3D(1.0f, 0.0f, 0.0f)); //绕x轴旋转
mvp.rotate(10.0f * rotatez, QVector3D(0.0f, 0.0f, 1.0f)); //绕z轴旋转
mvp.translate(worldcenter);//通过鼠标中键等操作点云的平移,这里不再赘述
事实上就是换了个顺序,咋一看非常奇怪,似乎之前的那个才是对的,因为平移写在了旋转之前。这样写的原因是QMatrix4x4 对象操作的内部实现是从下往上的,因此才有这个问题。
这个坑让本人一度放弃这个实现的方式,而用曲线救国的方法来实现点云的平移和旋转。在最近一次修改的时候突然想起来之前一篇已经忘了名字的博客上有提到这个问题,一式才恍然大悟。。。
在initializeGL()
中加入:
f->glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);//开启点大小调节功能
然后在顶点着色器中加入点大小设置代码即可:
gl_PointSize = 10.0f;//设置点的大小为10
点云的更新,本人采取的办法是不断的更新VBO的顶点数据,代码如下:
m_painting->m_vbo->bind();
for(int i=0;i<ptcloud->points.size();i++)
{
vertices[i * 3 + 0] = ptcloud->points[i].x;
vertices[i * 3 + 1] = ptcloud->points[i].y;
vertices[i * 3 + 2] = ptcloud->points[i].z;
}
m_painting->m_vbo->allocate(vertices, ptcloud->points.size() * 3 * sizeof(GLfloat));
m_painting->f->glEnableVertexAttribArray(0);
m_painting->f->glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,3*sizeof(GLfloat), 0);
m_painting->m_vbo->release();
m_painting->update();
然而个人感觉这么写应该不是最好的,每帧数据更新都会去内存中拿数据之后更新到显存,这里应该有将内存和显存进行更为高效绑定的方法,后面再研究。