最近利用opengl+QT实现了一个3D点云,并且点击点云的时候可以画出一个以选中点为原点的三维坐标系,可以实现移动和旋转,大概效果如下:
(图中的小黄圆是截屏软件导致的,并非实际效果。)
在这里分享几个关键函数。
将屏幕坐标转换为opengl的世界坐标:
QVector3D GlDisplayWidget::mousePosToWorldPos(QPoint _pos)
{
double modelview[ 16 ], projection[ 16 ];
int viewport[ 4 ];
float x = _pos.x();
float y = _pos.y();
GLfloat z = 0 ;
double objx, objy, objz;
/*Read the projection, modelview and viewport matrices using the glGet functions.*/
glGetIntegerv( GL_VIEWPORT, viewport );
glGetDoublev( GL_MODELVIEW_MATRIX, modelview );
glGetDoublev( GL_PROJECTION_MATRIX, projection );
//Read the window z value from the z-buffer
glReadBuffer(GL_FRONT);
glReadPixels( x, viewport[ 3 ]-y, 1 , 1 , GL_DEPTH_COMPONENT, GL_FLOAT, &z );
//Use the gluUnProject to get the world co-ordinates of
//the point the user clicked and save in objx, objy, objz.
gluUnProject( x, viewport[ 3 ]-y, z, modelview, projection, viewport, &objx, &objy, &objz );
return QVector3D(objx,objy,objz);
}
我们知道,在opengl中,从世界坐标到屏幕坐标的顺序是:
所以我们只要使用glReadPixels获取该屏幕坐标的深度值,然后读取最后进行的变换矩阵,就可以通过gluUnProject获取屏幕坐标对应的世界坐标,就可以实现点云的选中功能了。由于QT屏幕坐标系与opengl上下相反,所以调用的时候需要将y改成viewport[3]-y。
将世界坐标转换为屏幕坐标:
QPointF GlDisplayWidget::worldPosToScreenPos(QVector3D pos)
{
double modelview[ 16 ], projection[ 16 ];
int viewport[ 4 ];
//double objx, objy, objz;
double x,y,z;
/*Read the projection, modelview and viewport matrices using the glGet functions.*/
glGetIntegerv( GL_VIEWPORT, viewport );
glGetDoublev( GL_MODELVIEW_MATRIX, modelview );
glGetDoublev( GL_PROJECTION_MATRIX, projection );
gluProject(pos.x(),pos.y(),pos.z(),modelview, projection, viewport,&x,&y,&z);
return QPointF(x,viewport[ 3 ]-y);
}
类似的,我们可以使用gluProject将世界坐标转换为屏幕坐标。
接下来是坐标系的绘制,由于我使用的是,z-y-x欧拉角变换,所以旋转都是先绕z轴旋转,再绕y轴,最后绕x轴。
坐标轴渲染的接口:
void GlDisplayWidget::draw3DCoord(customPoint point)
{
double x,y,z,rx,ry,rz;
x = point.x;
y = point.y;
z = point.z;
rx = point.rotateX;
ry = point.rotateY;
rz = point.rotateZ;
const double len = 2.0;
const double k = 0.75;
const double vlen = len*(1-k);
const double step =0.707/25.0;
GLUquadric* obj = gluNewQuadric();
glColor3f(0.745,0.745,0.745);
glPushMatrix();
glTranslated(x,y,z);
gluSphere(obj,0.1,20,20);
glPopMatrix();
glPushMatrix();
//x轴
glTranslated(x,y,z);
glRotated(rz,0.0,0.0,1.0);
glRotated(ry,0.0,1.0,0.0);
glRotated(rx,1.0,0.0,0.0);
if(curClickType == rxCenter)
{
glRotated(rotateAngel,1.0,0.0,0.0);
}
else if(curClickType == ryCenter)
{
glRotated(rotateAngel,0.0,1.0,0.0);
}
else if(curClickType == rzCenter)
{
glRotated(rotateAngel,0.0,0.0,1.0);
}
{
//圆柱
if(curClickType == txCenter)
{
glColor3f(1.0,0.0,1.0);
}
else
{
glColor3f(1.0,0.0,0.0);
}
glPushMatrix();
glRotatef(90, 0.0, 1.0, 0.0);
//glTranslated(x,y,z);
gluCylinder(obj,0.02,0.02,len*k,30,30);
glTranslated(0.0,0.0,len-vlen);
gluCylinder(obj,0.1,0.00,0.5,30,30);
//z旋转球
glRotatef(-90, 0.0, 1.0, 0.0);
glTranslated(vlen-len+0.5,0.5,0.0);
if(curClickType == rzCenter)
{
glColor3f(1.0,0.0,1.0);
}
else
{
glColor3f(0.0,0.0,1.0);
}
gluSphere(obj,0.12,20,20);
glPopMatrix();
//绘制弧线x-y
glLineWidth(1);
glBegin(GL_LINE_STRIP);
for(double x1 = 0.0;x1<=0.707;x1 += step)
{
double y1 = sqrt(0.50 - x1*x1);
glVertex3d(x1,y1,0.0);
}
glEnd();
}
//y轴
{
//圆柱
if(curClickType == tyCenter)
{
glColor3f(1.0,0.0,1.0);
}
else
{
glColor3f(0.0,1.0,0.0);
}
glPushMatrix();
glRotatef(90, -1.0, 0.0, 0.0);
//glTranslated(x,y,z);
gluCylinder(obj,0.02,0.02,1.8,30,30);
glTranslated(0.0,0.0,len-vlen);
gluCylinder(obj,0.1,0.00,0.5,30,30);
//x旋转球
glRotatef(-90, -1.0, 0.0, 0.0);
glTranslated(0.0,vlen-len+0.5,0.5);
if(curClickType == rxCenter)
{
glColor3f(1.0,0.0,1.0);
}
else
{
glColor3f(1.0,0.0,0.0);
}
gluSphere(obj,0.12,20,20);
glPopMatrix();
//绘制弧线y-z
glLineWidth(1);
glBegin(GL_LINE_STRIP);
for(double y1 = 0.0;y1<=0.707;y1 += step)
{
double z1 = sqrt(0.50 - y1*y1);
glVertex3d(0.0,y1,z1);
}
glEnd();
}
//z轴
{
//圆柱
if(curClickType == tzCenter)
{
glColor3f(1.0,0.0,1.0);
}
else
{
glColor3f(0.0,0.0,1.0);
}
glPushMatrix();
//glTranslated(x,y,z);
gluCylinder(obj,0.02,0.02,1.8,30,30);
glTranslated(0.0,0.0,len-vlen);
gluCylinder(obj,0.1,0.00,0.5,30,30);
//y旋转球
glTranslated(0.5,0.0,vlen-len+0.5);
if(curClickType == ryCenter)
{
glColor3f(1.0,0.0,1.0);
}
else
{
glColor3f(0.0,1.0,0.0);
}
gluSphere(obj,0.12,20,20);
glPopMatrix();
//绘制弧线x-z
glLineWidth(1);
glBegin(GL_LINE_STRIP);
for(double x1 = 0.0;x1<=0.707;x1 += step)
{
double z1 = sqrt(0.50 - x1*x1);
glVertex3d(x1,0.0,z1);
}
glEnd();
}
glPopMatrix();
}
在函数中,我们使用了opengl绘制图元的方式画作标轴,使用gluSphere绘制球体,gluCylinder绘制圆柱来当坐标轴,gluCylinder来绘制圆锥。绘制圆弧的时候,计算一部分圆弧上的点,再将这些点连起来,就是弧线,效果如下图:
坐标轴的移动比较简单,但是旋转因为改变旋转顺序会导致结果不同,在这里,我才用了欧拉角旋转公式和逆转公式实现了旋转。
点绕指定轴旋转a的函数:
QVector3D GlDisplayWidget::Rotate(QVector3D p1, QVector3D p2, double angle)
{
double old_x,old_y,old_z;
double vx,vy,vz;
old_x = p1.x();
old_y = p1.y();
old_z = p1.z();
vx = p2.x();
vy = p2.y();
vz = p2.z();
double c = cos(angle/180.0*3.14);
double s = sin(angle/180.0*3.14);
double new_x = (vx*vx*(1 - c) + c) * old_x + (vx*vy*(1 - c) - vz*s) * old_y + (vx*vz*(1 - c) + vy*s) * old_z;
double new_y = (vy*vx*(1 - c) + vz*s) * old_x + (vy*vy*(1 - c) + c) * old_y + (vy*vz*(1 - c) - vx*s) * old_z;
double new_z = (vx*vz*(1 - c) - vy*s) * old_x + (vy*vz*(1 - c) + vx*s) * old_y + (vz*vz*(1 - c) + c) * old_z;
return QVector3D(new_x,new_y,new_z);
}
已知坐标轴从状态a到状态b,获取从a到b旋转角度的函数:
QVector3D GlDisplayWidget::fromRotate(QVector3D x,QVector3D y,QVector3D z)
{
double r11,r12,r13,r21,r22,r23,r31,r32,r33;
r11 = x.x();
r12 = y.x();
r13 = z.x();
r21 = x.y();
r22 = y.y();
r23 = z.y();
r31 = x.z();
r32 = y.z();
r33 = z.z();
QVector3D result;
double cb = sqrt(r11*r11+r21*r21);
if(cb != 0)
{
result.setY(atan2(0-r31,sqrt(r11*r11+r21*r21))/3.14*180.0);
result.setZ(atan2(r21,r11)/3.14*180.0);
result.setX(atan2(r32,r33)/3.14*180.0);
return result;
}
else
{
result.setY(90.0);
result.setZ(0);
result.setX(atan2(r21,r22)/3.14*180.0);
return result;
}
}
这个接口得出来的角度一定是从z轴旋转c,y轴旋转b,x轴旋转a,得到了角度a,b,c。具体原理大家可以去了解欧拉角和RPY角。
如果想给窗口加一个背景图,可以先切换正交视角,关闭深度测试,加载图片,然后再切换投影视角,开启深度测试,再进行渲染。