第25课:变形和从文件中加载3D物体 (参照NeHe)
这次教程中,我们将学会如何从文件中加载3D模型,并且平滑的从一个模型变形为另一个模型。在这一课里,我们将介绍如何实现模型的变形过程,这将会是效果很棒的一课!
程序运行时效果如下:
下面进入教程:
我们这次将在第01课的基础上修改代码,其中一些在前面教程中反复出现的,我就不会多解释了。首先打开myglwidget.h文件,将类声明更改如下:
#ifndef MYGLWIDGET_H
#define MYGLWIDGET_H
#include
#include
struct VERTEX //顶点结构体
{
float x, y, z;
};
struct OBJECT //物体结构体
{
int verts; //物体中顶点的个数
QVector vPoints; //包含顶点数据的向量
};
class MyGLWidget : public QGLWidget
{
Q_OBJECT
public:
explicit MyGLWidget(QWidget *parent = 0);
~MyGLWidget();
protected:
//对3个纯虚函数的重定义
void initializeGL();
void resizeGL(int w, int h);
void paintGL();
void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
private:
void loadObject(QString filename, OBJECT *k); //从文件加载一个模型
VERTEX calculate(int i); //计算第i个顶点变形过程每一步的位移
private:
bool fullscreen; //是否全屏显示
GLfloat m_xRot; //x轴旋转角度
GLfloat m_yRot; //y轴旋转角度
GLfloat m_zRot; //z轴旋转角度
GLfloat m_xSpeed; //x轴旋转速度
GLfloat m_ySpeed; //y轴旋转速度
GLfloat m_zSpeed; //z轴旋转速度
GLfloat m_xPos; //x轴坐标
GLfloat m_yPos; //y轴坐标
GLfloat m_zPos; //z轴坐标
int m_Key; //物体的标示符
int m_Step; //当前变形步数
int m_Steps; //变形的总步数
bool m_MorphOrNot; //是否在变形过程
OBJECT m_Morph1; //要绘制的4个物体
OBJECT m_Morph2;
OBJECT m_Morph3;
OBJECT m_Morph4;
OBJECT m_Helper; //协助绘制变形过程的物体(中间模型)
OBJECT *m_Src; //变形的源物体
OBJECT *m_Dest; //变形的目标物体
};
#endif // MYGLWIDGET_H
可以看到我们定义了2个结构体,依次表示顶点和物体模型。由于我们是通过点来绘制物体模型的(不使用纹理),因此一个物体模型包含许多顶点,并且我们的顶点数据只需要空间坐标x、y、z的值而不需要纹理坐标。定义完后,我们定义OBJECT对象m_Morph1、m_Morph2、m_Morph3、m_Morph4来储存我们要绘制的四个物体模型的数据,m_Helper来储存中间模型(变形过程)的数据,OBJECT指针来指定变形过程的源物体和目标物体。
其他增加的变量,前9个与x、y、z相关的变量是用于控制平移和旋转的,整形变量m_Key表示当前的模型类型,m_Step储存当前变形过程的步数,m_Steps储存变形过程需要的总步数,布尔变量m_MorphOrNot表示当前是否在变形过程。
然后是两个新增的函数loadObject()和calculate(),前一个函数用于从文件中向目标物体模型加载数据,后一个函数用于计算第i个顶点在变换过程中每一步的位移。
接下来,我们需要打开myglwidget.cpp,加上声明#include
void MyGLWidget::loadObject(QString filename, OBJECT *k)//从文件加载一个模型
{
QFile file(filename);
file.open(QIODevice::ReadOnly | QIODevice::Text); //将要读入数据的文本打开
QTextStream in(&file);
QString line = in.readLine(); //读入第一行
sscanf(line.toUtf8(), "Vertices: %d\n", &k->verts); //获取物体顶点的个数
for (int i=0; iverts; i++) //循环保存每个顶点的数据
{
do //读入数据并保证数据有效
{
line = in.readLine();
}
while (line[0] == '/' || line == "");
VERTEX object;
QTextStream inLine(&line);
inLine >> object.x
>> object.y
>> object.z;
k->vPoints.push_back(object);
}
file.close();
}
MyGLWidget::MyGLWidget(QWidget *parent) :
QGLWidget(parent)
{
fullscreen = false;
m_xRot = 0.0f;
m_yRot = 0.0f;
m_zRot = 0.0f;
m_xSpeed = 0.0f;
m_ySpeed = 0.0f;
m_zSpeed = 0.0f;
m_xPos = 0.0f;
m_yPos = 0.0f;
m_zPos = -10.0f;
m_Key = 1; //当前模型为球
m_Step = 0;
m_Steps = 200;
m_MorphOrNot = false;
loadObject("D:/QtOpenGL/QtImage/Sphere.txt", &m_Morph1);//加载球模型
loadObject("D:/QtOpenGL/QtImage/Torus.txt", &m_Morph2); //加载圆环模型
loadObject("D:/QtOpenGL/QtImage/Tube.txt", &m_Morph3); //加载立方体模型
m_Morph4.verts = 486;
for (int i=0; istart(10); //以10ms为一个计时周期
}
首先是loadObject()函数。首先将文件打开,再利用Qt的文本流(QTextStream)先读取第一行,由于我们文件的第一行是预先按照“Vertices: x”(x为一个整数)的格式写好的,我们利用sscanf()函数,读取并储存该物体模型k的顶点个数。然后利用循环,一行一行的读取数据并保证读入的数据是有效的。每当成功读入一个数据时,就创建一个顶点结构体来储存这些数据,并在最后把顶点放入物体模型k中。最后录完数据后,关上文件。
再来看构造函数。前面一堆变量的值得初始化我觉得没什么好说的,写完程序大家也就明白了,我们直接看调用loadObject()函数的部分。我们直接调用了三次loadObject()函数,把三个文件中的模型数据分别储存到m_Morph1、m_Morph2和m_Morph3中。然后第四个模型我们不从文件读取,我们在(-7, -7, -7)到(7, 7, 7)之间随机生成模型点,当然它和我们前面加载的模型都一样具有486个顶点。最后我们把中间模型初始化为球体,并源模型和目标模型都设置为球体(这代表初设状态为球体,因此m_Key应该初始化为1)。
接下来,我们把calculate()函数和initializeGL()函数放在一起解释,具体代码如下:
void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
{
glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
glShadeModel(GL_SMOOTH); //启用阴影平滑
glClearDepth(1.0); //设置深度缓存
glEnable(GL_DEPTH_TEST); //启用深度测试
glDepthFunc(GL_LESS); //所作深度测试的类型
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
}
VERTEX MyGLWidget::calculate(int i) //计算第i个顶点变形过程每一步的位移
{
VERTEX a;
a.x = (m_Src->vPoints[i].x - m_Dest->vPoints[i].x) / m_Steps;
a.y = (m_Src->vPoints[i].y - m_Dest->vPoints[i].y) / m_Steps;
a.z = (m_Src->vPoints[i].z - m_Dest->vPoints[i].z) / m_Steps;
return a;
}
先是initializeGL()函数。注意到唯一的改动就是glDepthFunc()函数的参数由GL_LEQUAL变为GL_LESS,两者的区别在于,当深度相同时LEQUAL显示的是最先绘制的像素,而GL_LESS显示的是最新绘制的像素。具体为什么要修改,下面会讲到。
然后是calculate()函数。我们定义了一个VERTEX对象a,然后用源对象和目标对象的x、y、z坐标的差值除以我们变形过程的总步数保存于a中,再将a返回。这样a中就储存了从源模型变形到目标模型每一步应该进行的位移。我们通过指针m_Src和m_Dest知道哪个是源模型,哪个是目标模型。
然后我们来进入重点的paintGL()函数,但其实它并不难,具体代码如下:
void MyGLWidget::paintGL() //从这里开始进行所以的绘制
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
glLoadIdentity(); //重置当前的模型观察矩阵
glTranslatef(m_xPos, m_yPos, m_zPos); //平移和旋转
glRotatef(m_xRot, 1.0f, 0.0f, 0.0f);
glRotatef(m_yRot, 0.0f, 1.0f, 0.0f);
glRotatef(m_zRot, 0.0f, 0.0f, 1.0f);
GLfloat tx, ty, tz; //顶点坐标临时变量
VERTEX q; //保存计算的临时顶点
glBegin(GL_POINTS); //点绘制开始
for (int i=0; i
首先我们就走常规步骤,清除屏幕和缓存,重置模型观察矩阵,平移和旋转。接下来,我们循环绘制模型的每一个点,如果在变形过程,则计算得到变形过程每一步的应该进行的位移,保存在q中,否则q中的各方向位移量均设置为0.0f。然后我们让m_Helper,移动q对应的位移,如果在变形过程,则m_Helper绘制出来的是下一步移动后的样子,否则由于q各方向位移量均设置为0.0f,不会移动(此时是变形完成的模型,当然不需要移动)。
下面我们设置颜色为蓝色,绘制顶点;然后把颜色变蓝一些,把前面一个点的各方向坐标均减去2倍的位移量,如果在变形过程就会得到2步后的位置,否则还是在原位置,接着就绘制这个顶点。同样再重复一次相似的工作,就是把颜色再变蓝一些。上面的3次绘制可以看到,如果在变形过程,则三个点会错开,形成一个比较长的点,颜色从蓝绿色到蓝色渐变(其实就看得到头尾颜色不一样);如果不在变形过程,3次绘制其实是在同一个地方绘制的,那会显示哪种颜色呢?我们上面讲到我们把glDepthFunc()函数的参数设为GL_LESS,当绘制深度相同时显示的是最先绘制的,所以模型会显示出蓝绿色(当然你可以把参数改回GL_LEQUAL就显示成蓝色,全凭个人喜好)。
最后我们当前是否为变形过程,如果是判断变形过程是否完成(否就保持原状没什么解释的),如果未完成则增加当前的步数,等待下一次绘制;如果完成了就设置m_MorphOrNot为false,m_Step为0,m_Src与m_Dest相同。函数结束前,我们根据旋转速度增加旋转角度,让物体模型自动旋转起来。
最后,我们来修改我们的键盘控制函数,不会很难,不过加的控制键真不少,具体代码如下:
void MyGLWidget::keyPressEvent(QKeyEvent *event)
{
switch (event->key())
{
case Qt::Key_F1: //F1为全屏和普通屏的切换键
fullscreen = !fullscreen;
if (fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
break;
case Qt::Key_Escape: //ESC为退出键
close();
break;
case Qt::Key_PageUp: //PageUp按下增加m_zSpeed
m_zSpeed += 0.1f;
break;
case Qt::Key_PageDown: //PageDown按下减少m_zSpeed
m_zSpeed -= 0.1f;
break;
case Qt::Key_Down: //Down按下增加m_xSpeed
m_xSpeed += 0.1f;
break;
case Qt::Key_Up: //Up按下减少m_xSpeed
m_xSpeed -= 0.1f;
break;
case Qt::Key_Right: //Right按下增加m_ySpeed
m_ySpeed += 0.1f;
break;
case Qt::Key_Left: //Left按下减少m_ySpeed
m_ySpeed -= 0.1f;
break;
case Qt::Key_Q: //Q按下放大物体
m_zPos -= 0.1f;
break;
case Qt::Key_Z: //Z按下缩小物体
m_zPos += 0.1f;
break;
case Qt::Key_W: //W按下上移物体
m_yPos -= 0.1f;
break;
case Qt::Key_S: //S按下下移物体
m_yPos += 0.1f;
break;
case Qt::Key_D: //D按下右移物体
m_xPos -= 0.1f;
break;
case Qt::Key_A: //A按下左移物体
m_xPos += 0.1f;
break;
case Qt::Key_1: //1按下进入变形过程,变形到模型1
if ((m_Key != 1) && !m_MorphOrNot)
{
m_Key = 1;
m_MorphOrNot = true;
m_Dest = &m_Morph1;
}
break;
case Qt::Key_2: //2按下进入变形过程,变形到模型2
if ((m_Key != 2) && !m_MorphOrNot)
{
m_Key = 2;
m_MorphOrNot = true;
m_Dest = &m_Morph2;
}
break;
case Qt::Key_3: //3按下进入变形过程,变形到模型3
if ((m_Key != 3) && !m_MorphOrNot)
{
m_Key = 3;
m_MorphOrNot = true;
m_Dest = &m_Morph3;
}
break;
case Qt::Key_4: //4按下进入变形过程,变形到模型4
if ((m_Key != 4) && !m_MorphOrNot)
{
m_Key = 4;
m_MorphOrNot = true;
m_Dest = &m_Morph4;
}
break;
}
}
新增的前12个关于旋转和平移的我就不解释了,我们看下最后4个键。当按下数字1、2、3或4时,我们会判断是否为当前状态以及当前是否在变形过程,如果都不是,则允许进行变形,进入变形过程(修改m_Key和m_Dest并设置m_MorphOrNot为true)。
现在就可以运行程序查看效果了!
全部教程中需要的资源文件点此下载