【Qt OpenGL教程】25:变形和从文件中加载3D物体

第25课:变形和从文件中加载3D物体 (参照NeHe)

这次教程中,我们将学会如何从文件中加载3D模型,并且平滑的从一个模型变形为另一个模型。在这一课里,我们将介绍如何实现模型的变形过程,这将会是效果很棒的一课!


程序运行时效果如下:



下面进入教程:


我们这次将在第01课的基础上修改代码,其中一些在前面教程中反复出现的,我就不会多解释了。首先打开myglwidget.h文件,将类声明更改如下:

#ifndef MYGLWIDGET_H
#define MYGLWIDGET_H

#include <QWidget>
#include <QGLWidget>

struct VERTEX                                       //顶点结构体
{
    float x, y, z;
};

struct OBJECT                                       //物体结构体
{
    int verts;                                      //物体中顶点的个数
    QVector<VERTEX> 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 <QTimer>、#include <QTextStream>,由于loadObejct()函数需要在构造函数中被调用,我们把两者放在一起讲,具体代码如下:

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; i<k->verts; 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; i<m_Morph4.verts; i++)                    //随机设置486个顶点在[-7,7]
    {
        VERTEX object;
        object.x = ((float)(rand()%14000)/1000)-7;
        object.y = ((float)(rand()%14000)/1000)-7;
        object.z = ((float)(rand()%14000)/1000)-7;
        m_Morph4.vPoints.push_back(object);
    }

    loadObject("D:/QtOpenGL/QtImage/Sphere.txt", &m_Helper);//初始化中间模型为球
    m_Src = m_Dest = &m_Morph1;                             //源模型和目标模型都设置为球

    QTimer *timer = new QTimer(this);                       //创建一个定时器
    //将定时器的计时信号与updateGL()绑定
    connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
    timer->start(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<m_Morph1.verts; i++)            //循环绘制每一个顶点
        {
            if (m_MorphOrNot)
            {
                q = calculate(i);
            }
            else
            {
                q.x = 0.0f;
                q.y = 0.0f;
                q.z = 0.0f;
            }

            m_Helper.vPoints[i].x -= q.x;               //如果在变形过程,则计算中间模型
            m_Helper.vPoints[i].y -= q.y;
            m_Helper.vPoints[i].z -= q.z;
            tx = m_Helper.vPoints[i].x;                 //保存计算结果到tx、ty、tz中
            ty = m_Helper.vPoints[i].y;
            tz = m_Helper.vPoints[i].z;

            glColor3f(0.0f, 1.0f, 1.0f);                //设置颜色
            glVertex3f(tx, ty, tz);                     //绘制顶点

            glColor3f(0.0f, 0.5f, 1.0f);                //把颜色变蓝一些
            tx -= 2*q.x;                                //如果在变形过程,则绘制2步后的顶点
            ty -= 2*q.y;
            tz -= 2*q.z;
            glVertex3f(tx, ty, tz);

            glColor3f(0.0f, 0.0f, 1.0f);                //把颜色变蓝一些
            tx -= 2*q.x;                                //如果在变形过程,则绘制2步后的顶点
            ty -= 2*q.y;
            tz -= 2*q.z;
            glVertex3f(tx, ty, tz);
        }
    glEnd();                                            //绘制结束

    if (m_MorphOrNot && (m_Step <= m_Steps))
    {
        m_Step++;                                       //如果在变形过程则把当前变形步数增加
    }
    else
    {
        m_MorphOrNot = false;                           //当前变形步数大于总步数时,退出变形过程
        m_Src = m_Dest;
        m_Step = 0;
    }

    m_xRot += m_xSpeed;                                 //自动增加旋转角度
    m_yRot += m_ySpeed;
    m_zRot += m_zSpeed;
}
首先我们就走常规步骤,清除屏幕和缓存,重置模型观察矩阵,平移和旋转。接下来,我们循环绘制模型的每一个点,如果在变形过程,则计算得到变形过程每一步的应该进行的位移,保存在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)。

现在就可以运行程序查看效果了!


全部教程中需要的资源文件点此下载


猜你喜欢

转载自blog.csdn.net/cly116/article/details/47450485