【Qt OpenGL教程】23:球面映射

第23课:球面映射 (参照NeHe)

这次教程中,我们将学会如何把环境纹理包裹在我们的3D模型上,让它看起来像反射了周围的景象一样,我们把这种纹理映射的方式称为球体映射。球体映射是一种创建金属反射效果的方法,虽然它并不像真实世界里那么精确,但效果还是很不错的!

红宝书中,球体映射的定义为,把一幅位于无限远的图像映射到球面上。当然我们需要自己来创建一幅球体环境映射图,方法如下:

打开Photoshop,并在Photoshop中打开我们要转换成球体环境映射图的原图,选择所有的像素点,创建它的一个复制。接着我们把图像变为2的幂次方大小,一般为128×128或256×256.最后使用扭曲滤镜,并应用球体效果,然后把得到的图像保存为 *.bmp文件(PS:我给大家的资源文件中球体映射图是处理好的了,命名为Reflect.bmp)。


程序运行时效果如下:

【Qt OpenGL教程】23:球面映射_第1张图片


下面进入教程:


我们这次将在第18课的基础上修改代码,这次相比前两次简单太多了,我只会对代码的变化作出解释。首先打开myglwidget.h文件,将类声明更改如下:

#ifndef MYGLWIDGET_H
#define MYGLWIDGET_H

#include 
#include 

class GLUquadric;
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 glDrawCube();                              //绘制立方体

private:
    bool fullscreen;                                //是否全屏显示

    QString m_FileName[2];                          //图片的路径及文件名
    GLuint m_Texture[2];                            //储存两个纹理

    bool m_Light;                                   //光源的开/关
    GLfloat m_xRot;                                 //x旋转角度
    GLfloat m_yRot;                                 //y旋转角度
    GLfloat m_xSpeed;                               //x旋转速度
    GLfloat m_ySpeed;                               //y旋转速度
    GLfloat m_Deep;                                 //深入屏幕的距离

    GLUquadric *m_Quadratic;                        //二次几何体
    GLuint m_Object;                                //绘制对象标示符
};

#endif // MYGLWIDGET_H
由于我们程序的背景图和用于纹理映射的图并不是同一张(后者是PS处理过的那张),所以我们需要储存两个不同的纹理,m_FileName和m_Texture就都变成长度为2的数组啦。然后我们删掉了m_Part1、m_Part2、m_P1、m_P2等变量,由于我们后面的绘制过程并不打算绘制圆盘和部分圆盘,这是因为它们绘制出来的映射效果并不好,所以我就不绘制它们了。


接下来,我们需要打开myglwidget.cpp,对构造函数进行修改(更换图片途径名和删掉不存在的变量),很简单不多解释,具体代码如下:

MyGLWidget::MyGLWidget(QWidget *parent) :
    QGLWidget(parent)
{
    fullscreen = false;
    m_FileName[0] = "D:/QtOpenGL/QtImage/BG.bmp";       //应根据实际存放图片的路径进行修改
    m_FileName[1] = "D:/QtOpenGL/QtImage/Reflect.bmp";
    m_Light = false;

    m_xRot = 0.0f;
    m_yRot = 0.0f;
    m_xSpeed = 0.0f;
    m_ySpeed = 0.0f;
    m_Deep = -10.0f;
    m_Object = 0;

    QTimer *timer = new QTimer(this);                   //创建一个定时器
    //将定时器的计时信号与updateGL()绑定
    connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
    timer->start(10);                                   //以10ms为一个计时周期
}


继续,我们要对glDrawCube()函数作一些小改动,具体代码如下:

void MyGLWidget::glDrawCube()
{
    glBegin(GL_QUADS);                                  //开始绘制立方体
        glNormal3f(0.0f, 0.5f, 0.0f);
        glTexCoord2f(1.0f, 1.0f);
        glVertex3f(1.0f, 1.0f, -1.0f);                  //右上(顶面)
        glTexCoord2f(0.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f, -1.0f);                 //左上(顶面)
        glTexCoord2f(0.0f, 0.0f);
        glVertex3f(-1.0f, 1.0f, 1.0f);                  //左下(顶面)
        glTexCoord2f(1.0f, 0.0f);
        glVertex3f(1.0f, 1.0f, 1.0f);                   //右下(顶面)

        glNormal3f(0.0f, -0.5f, 0.0f);
        glTexCoord2f(0.0f, 0.0f);
        glVertex3f(1.0f, -1.0f, 1.0f);                  //右上(底面)
        glTexCoord2f(1.0f, 0.0f);
        glVertex3f(-1.0f, -1.0f, 1.0f);                 //左上(底面)
        glTexCoord2f(1.0f, 1.0f);
        glVertex3f(-1.0f, -1.0f, -1.0f);                //左下(底面)
        glTexCoord2f(0.0f, 1.0f);
        glVertex3f(1.0f, -1.0f, -1.0f);                 //右下(底面)

        glNormal3f(0.0f, 0.0f, 0.5f);
        glTexCoord2f(1.0f, 1.0f);
        glVertex3f(1.0f, 1.0f, 1.0f);                   //右上(前面)
        glTexCoord2f(0.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f, 1.0f);                  //左上(前面)
        glTexCoord2f(0.0f, 0.0f);
        glVertex3f(-1.0f, -1.0f, 1.0f);                 //左下(前面)
        glTexCoord2f(1.0f, 0.0f);
        glVertex3f(1.0f, -1.0f, 1.0f);                  //右下(前面)

        glNormal3f(0.0f, 0.0f, -0.5f);
        glTexCoord2f(0.0f, 0.0f);
        glVertex3f(1.0f, -1.0f, -1.0f);                 //右上(后面)
        glTexCoord2f(1.0f, 0.0f);
        glVertex3f(-1.0f, -1.0f, -1.0f);                //左上(后面)
        glTexCoord2f(1.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f, -1.0f);                 //左下(后面)
        glTexCoord2f(0.0f, 1.0f);
        glVertex3f(1.0f, 1.0f, -1.0f);                  //右下(后面)

        glNormal3f(-0.5f, 0.0f, 0.0f);
        glTexCoord2f(1.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f, 1.0f);                  //右上(左面)
        glTexCoord2f(0.0f, 1.0f);
        glVertex3f(-1.0f, 1.0f, -1.0f);                 //左上(左面)
        glTexCoord2f(0.0f, 0.0f);
        glVertex3f(-1.0f, -1.0f, -1.0f);                //左下(左面)
        glTexCoord2f(1.0f, 0.0f);
        glVertex3f(-1.0f, -1.0f, 1.0f);                 //右下(左面)

        glNormal3f(0.5f, 0.0f, 0.0f);
        glTexCoord2f(1.0f, 1.0f);
        glVertex3f(1.0f, 1.0f, -1.0f);                  //右上(右面)
        glTexCoord2f(0.0f, 1.0f);
        glVertex3f(1.0f, 1.0f, 1.0f);                   //左上(右面)
        glTexCoord2f(0.0f, 0.0f);
        glVertex3f(1.0f, -1.0f, 1.0f);                  //左下(右面)
        glTexCoord2f(1.0f, 0.0f);
        glVertex3f(1.0f, -1.0f, -1.0f);                 //右下(右面)
    glEnd();                                            //立方体绘制结束
}
几乎没有什么改动,就是把法线(glNormal)的范围从[-1, 1]缩放到[-0.5, 0.5]。如果法向量太大的话,会产生一些块状效果,影响视觉效果。


然后我们需要修改一下initializeGL()函数,我们添加一些新的函数(glTexGeni)来使用球体纹理映射,具体代码如下:

void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
{
    m_Texture[0] = bindTexture(QPixmap(m_FileName[0])); //载入位图并转换成纹理
    m_Texture[1] = bindTexture(QPixmap(m_FileName[1]));
    glEnable(GL_TEXTURE_2D);                            //启用纹理映射
    glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);//设置s方向的纹理坐标自动生成
    glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);//设置t方向的纹理坐标自动生成

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景
    glShadeModel(GL_SMOOTH);                            //启用阴影平滑

    glClearDepth(1.0);                                  //设置深度缓存
    glEnable(GL_DEPTH_TEST);                            //启用深度测试
    glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正

    m_Quadratic = gluNewQuadric();                        //创建二次几何体
    gluQuadricNormals(m_Quadratic, GLU_SMOOTH);           //使用平滑法线
    gluQuadricTexture(m_Quadratic, GL_TRUE);              //使用纹理

    //光源部分
    GLfloat LightAmbient[] = {0.5f, 0.5f, 0.5f, 1.0f};  //环境光参数
    GLfloat LightDiffuse[] = {1.0f, 1.0f, 1.0f, 1.0f};  //漫散光参数
    GLfloat LightPosition[] = {0.0f, 0.0f, 2.0f, 1.0f}; //光源位置
    glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);     //设置环境光
    glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);     //设置漫射光
    glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);   //设置光源位置
    glEnable(GL_LIGHT1);                                //启动一号光源
}
注意到,我们除了加载了两个位图并转换成纹理外,我们还调用了glTexGeni()函数。这个函数在之前第15课有提到过,就是让OpenGL为我们在指定的方向上(S、T方向),自动生成纹理映射的坐标。然后函数的第三个参数设置为GL_SPHERE_MAP,使得创建出一种有金属质感的物体(大家不记得这个函数了的话,请参照前面 第15课)。


还有就是paintGL()函数,我们也是作了部分的修改,我只解释增加的部分,具体代码如下:

void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
    glLoadIdentity();                                   //重置模型观察矩阵
    glTranslatef(0.0f, 0.0f, m_Deep);                   //移入屏幕

    glEnable(GL_TEXTURE_GEN_S);                         //启用自动生成s方向纹理坐标
    glEnable(GL_TEXTURE_GEN_T);                         //启用自动生成t方向纹理坐标

    glBindTexture(GL_TEXTURE_2D, m_Texture[1]);         //选择球体映射纹理
    glPushMatrix();                                     //保存模型观察矩阵
    glRotatef(m_xRot, 1.0f, 0.0f, 0.0f);                //绕x轴旋转
    glRotatef(m_yRot, 0.0f, 1.0f, 0.0f);                //绕y轴旋转
    switch(m_Object)
    {
    case 0:                                             //绘制立方体
        glDrawCube();
        break;
    case 1:                                             //绘制圆柱体
        glTranslatef(0.0f, 0.0f, -1.5f);
        gluCylinder(m_Quadratic, 1.0f, 1.0f, 3.0f, 64, 64);
        break;
    case 2:                                             //绘制球
        gluSphere(m_Quadratic, 1.3f, 64, 64);
        break;
    case 3:                                             //绘制圆锥
        glTranslatef(0.0f, 0.0f, -1.5f);
        gluCylinder(m_Quadratic, 1.0f, 0.0f, 3.0f, 64, 64);
        break;
    }

    glPopMatrix();                                      //恢复模型观察矩阵
    glDisable(GL_TEXTURE_GEN_S);                        //禁用自动生成纹理坐标
    glDisable(GL_TEXTURE_GEN_T);

    glBindTexture(GL_TEXTURE_2D, m_Texture[0]);         //选择平面纹理
    glPushMatrix();                                     //保存模型观察矩阵
    glTranslatef(0.0f, 0.0f, -24.0);                    //移入屏幕24.0单位
    glBegin(GL_QUADS);                                  //绘制背景四边形
        glNormal3f(0.0f, 0.0f, 1.0f);
        glTexCoord2f(0.0f, 0.0f);
        glVertex3f(-13.3f, -10.0f, 10.0f);
        glTexCoord2f(1.0f, 0.0f);
        glVertex3f(13.3f, -10.0f, 10.0f);
        glTexCoord2f(1.0f, 1.0f);
        glVertex3f(13.3f, 10.0f, 10.0f);
        glTexCoord2f(0.0f, 1.0f);
        glVertex3f(-13.3f, 10.0f, 10.0f);
    glEnd();
    glPopMatrix();                                      //恢复模型观察矩阵

    m_xRot += m_xSpeed;                                 //x轴旋转
    m_yRot += m_ySpeed;                                 //y轴旋转
}
首先在移入屏幕(glTranslate)后,我们开启自动生成纹理坐标,要注意必须自己手动开启,不然前面initializeGL()函数中的设置是不会生效的。接着我们选择球面映射的纹理,保存模型观察矩阵(glPushMatrix)后,进行旋转后绘制会我们的3D模型。绘制完3D模型后,我们恢复模型观察矩阵(glPopMatrix)并禁用自动生成纹理坐标。正如前面提到,由于绘制出来的圆盘和部分圆盘效果并不好,所以我们把绘制它们的部分删掉了,对switch语句作了修改,现在我们将会绘制的3D模型有立方体、圆柱、球和圆锥。

然后我们选择作为背景的平面纹理,同样保存模型观察矩阵,平移后绘制出背景四边形,绘制完同样恢复模型观察矩阵。


最后我们修改一下键盘控制函数,不多解释了,就是改了一下m_Object的最大值,具体代码如下:

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_L:                                     //L为开启关闭光源的切换键
        m_Light = !m_Light;
        if (m_Light)
        {
            glEnable(GL_LIGHTING);                      //开启光源
        }
        else
        {
            glDisable(GL_LIGHTING);                     //关闭光源
        }
        break;
    case Qt::Key_Space:                                 //空格为物体的切换键
        m_Object++;
        if (m_Object == 4)
        {
            m_Object = 0;
        }
        break;
    case Qt::Key_PageUp:                                //PageUp按下使木箱移向屏幕内部
        m_Deep -= 0.1f;
        break;
    case Qt::Key_PageDown:                              //PageDown按下使木箱移向观察者
        m_Deep += 0.1f;
        break;
    case Qt::Key_Up:                                    //Up按下减少m_xSpeed
        m_xSpeed -= 0.1f;
        break;
    case Qt::Key_Down:                                  //Down按下增加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;
    }
}
现在就可以运行程序查看效果了!


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


一点内容的补充:这里我准备解释一下上面glDrawCube()函数中法线范围修改的问题,大家先看下图(左侧为修改前,右面为修改后):

【Qt OpenGL教程】23:球面映射_第2张图片  【Qt OpenGL教程】23:球面映射_第3张图片

很明显左侧立方体中,对于环境的映射有块状现象,模糊了很多。这是由于glNormal()函数会根据参数对纹理坐标进行一定比例的放大缩小,1.0时保持原状,大于1.0时为放大,小于1.0时为缩小,所以我们把参数方位调整为[-0.5, 0.5]实际上缩小纹理图像(这里说的放大缩小是针对图像的,实际纹理坐标范围是相反的;可以想象,纹理坐标范围缩小,映射出来的图像就像被放大了一样)

那为什么右侧的图里立方体上的纹理看起来还是比背景图大呢?大家不要忘了,我们在绘制背景图时还向屏幕里移入24.0单位呢,看起来小没什么问题吧。

你可能感兴趣的:(Qt,OpenGL)