【Qt OpenGL教程】24:扩展、剪裁和TGA图像文件的手动加载

第24课:扩展、剪裁和TGA图像文件的手动加载 (参照NeHe)

这次教程中,我们将学会如何读取自己电脑显卡支持的OpenGL扩展,并在我们指定的剪裁区域把它显示出来,如何自己手动来加载TGA图像文件(作为补充内容,因为Qt有方法可以直接加载TGA图像文件)。这次教程有一些难度,但它会让你学会很多东西。

如果你在Windows平台下开发OpenGL程序,那么系统中自带的OpenGL库就是1.1的,如果想使用1.2或者更高版本的OpenGL库,那么只能使用OpenGL扩展。但我们在使用扩展之前,总应该知道我们能用哪些扩展吧?这次教程将交你如何读取自己电脑显卡支持的OpenGL扩展方面的内容并显示出来,如此就知道我们显卡支持哪些扩展了。

同时我们会在显示内容时使用剪裁技术,并且我们将学会如何自己根据TGA文件的格式来加载TGA图像文件。TGA图像文件是一种简单并且支持alpha通道的数字图像文件,它可以使我们更容易地创建酷的效果。


程序运行时效果如下:



下面进入教程:


我们这次将在第01课的基础上修改代码,但是其中不少代码与第21课重复,我会给出具体代码,不过就不再次详细解释了,我们会把解释的重点放在这节课新增的内容上。首先打开myglwidget.j文件,将类声明更改如下:

#ifndef MYGLWIDGET_H
#define MYGLWIDGET_H

#include <QWidget>
#include <QGLWidget>

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:
    GLuint loadTGATexture(QString filename);        //加载TGA文件并转为纹理(补充的内容)
    void buildFont();                               //创建字体
    void killFont();                                //删除显示列表
    //输出字符串
    void glPrint(GLuint x, GLuint y, int set, const char *fmt, ...);

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

    int m_Scroll;                                   //用来滚动屏幕
    int m_Maxtokens;                                //记录扩展名的个数
    int m_Swidth;                                   //剪裁宽度
    int m_Sheight;                                  //剪裁高度

    GLuint m_Base;                                  //字符显示列表的开始值
    QString m_FileName;                             //图片的路径及文件名
    GLuint m_Texture;                               //储存一个纹理
};

#endif // MYGLWIDGET_H
我们增加了4个整形变量,依次用来记录滚动屏幕的距离,记录OpenGL扩展名的个数,记录我们进行剪裁的宽度、高度。然后m_Base、m_FileName、m_Texture三个变量相信大家已经很熟悉了,不再解释。最后声明4个新函数,loadTGATexture()、buildFont()、killFont()和glPrint(),后三个大家也都很熟悉了,而loadTGATexture()函数是我们自己手动要用来加载TGA图像文件并转换为纹理的函数(这个函数会放在补充部分讲述,前面部分不会用到它)。


接下来,我们需要打开myglwidget.cpp,加上声明#include <QTimer>、#include <QMessageBox>,在构造函数中对新增变量进行修改并设置窗口的固定大小和增加一个定时器,然后修改析构函数,很简单不多解释,具体代码如下:

MyGLWidget::MyGLWidget(QWidget *parent) :
    QGLWidget(parent)
{
    fullscreen = false;
    setFixedSize(640, 480);                             //设置固定的窗口大小

    m_Scroll = 0;
    m_Maxtokens = 0;
    m_FileName = "D:/QtOpenGL/QtImage/Font1.tga";       //应根据实际存放图片的路径进行修改

    QTimer *timer = new QTimer(this);                   //创建一个定时器
    //将定时器的计时信号与updateGL()绑定
    connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
    timer->start(10);                                   //以10ms为一个计时周期
}
MyGLWidget::~MyGLWidget()
{
    killFont();                                         //删除显示列表
}

下面,我们先来看下我们3个熟悉的函数buildFont()、killFont()和glPrint(),一些数值上的调整只是为了显示效果更好,不多解释,具体代码如下:

void MyGLWidget::buildFont()                            //创建位图字体
{
    float cx, cy;                                       //储存字符的x、y坐标
    m_Base = glGenLists(256);                           //创建256个显示列表
    glBindTexture(GL_TEXTURE_2D, m_Texture);            //选择字符纹理

    for (int i=0; i<256; i++)                           //循环256个显示列表
    {
        cx = float(i%16)/16.0f;                         //当前字符的x坐标
        cy = float(i/16)/16.0f;                         //当前字符的y坐标

        glNewList(m_Base+i, GL_COMPILE);                //开始创建显示列表
            glBegin(GL_QUADS);                          //使用四边形显示每一个字符
                glTexCoord2f(cx, 1.0f-cy-0.0625f);
                glVertex2i(0, 16);
                glTexCoord2f(cx+0.0625f, 1.0f-cy-0.0625f);
                glVertex2i(16, 16);
                glTexCoord2f(cx+0.0625f, 1.0f-cy);
                glVertex2i(16, 0);
                glTexCoord2f(cx, 1.0f-cy);
                glVertex2i(0, 0);
            glEnd();                                    //四边形字符绘制完成
            glTranslated(14, 0, 0);                     //绘制完一个字符,向右平移14个单位
        glEndList();                                    //字符显示列表完成
    }
}
void MyGLWidget::killFont()                             //删除显示列表
{
    glDeleteLists(m_Base, 256);                         //删除256个显示列表
}
void MyGLWidget::glPrint(GLuint x, GLuint y, int set, const char *fmt, ...)
{
    char text[1024];                                    //保存字符串
    va_list ap;                                         //指向一个变量列表的指针

    if (fmt == NULL)                                    //如果无输入则返回
    {
        return;
    }

    va_start(ap, fmt);                                  //分析可变参数
        vsprintf(text, fmt, ap);                        //把参数值写入字符串
    va_end(ap);                                         //结束分析

    if (set > 1)                                        //如果字符集大于1
    {
        set = 1;                                        //设置其为1
    }
    glEnable(GL_TEXTURE_2D);                            //启用纹理
    glLoadIdentity();                                   //重置模型观察矩阵
    glTranslated(x, y ,0);                              //把字符原点移动到(x,y)位置
    glListBase(m_Base-32+(128*set));                    //选择字符集

    glScalef(1.0f, 2.0f, 1.0f);                         //如果是第一个字符集,放大字体

    glCallLists(strlen(text), GL_BYTE, text);           //把字符串写到屏幕
    glDisable(GL_TEXTURE_2D);                           //禁用纹理
}
注意到除了数值的调整,唯一的变化就是glPrint()函数中glScalef函数调用没有了if语句,是针对每一个字符都有效了(其实我觉得这个也没必要讲的)。


然后我们来修改一下initializeGL()函数和resizeGL()函数,具体代码如下:

void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
{
    m_Texture = bindTexture(QPixmap(m_FileName));
    glEnable(GL_TEXTURE_2D);                            //启用纹理映射
    buildFont();                                        //创建字体

    glClearColor(0.0, 0.0, 0.0, 0.0);                   //黑色背景
    glShadeModel(GL_SMOOTH);                            //启用阴影平滑
    glClearDepth(1.0);                                  //设置深度缓存
    glEnable(GL_BLEND);                                 //启用融合
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  //设置融合因子
}

void MyGLWidget::resizeGL(int w, int h)                 //重置OpenGL窗口的大小
{
    m_Swidth = w;                                       //记录窗口大小用于计算剪切矩形的大小
    m_Sheight = h;

    glViewport(0, 0, (GLint)w, (GLint)h);               //重置当前的视口
    glMatrixMode(GL_PROJECTION);                        //选择投影矩阵
    glLoadIdentity();                                   //重置投影矩阵
    glOrtho(0.0f, 640, 480, 0.0f, -1.0f, 1.0f);         //设置正投影
    glMatrixMode(GL_MODELVIEW);                         //选择模型观察矩阵
    glLoadIdentity();                                   //重置模型观察矩阵
}
initializeGL()中,我们 调用QPixmap的构造函数就可以直接加载TGA图像文件(好吧, 我承认Qt在图像处理上很强大,把NeHe原文中这节课的一大难点,加载TGA图像,直接解决了。这让我很纠结,最后我决定把NeHe提供的加载TGA文件的方法作为补充内容放在文章结尾),然后还是用bindTexture()函数就可以转换为纹理了,这跟我们前面加载bmp图像的方法完全相同。接着我们启用深度测试,并调用buildFont()创建字体。然后由于是2D正投影绘图,我们删掉了深度测试的相关函数,然后设置了融合因子并启动融合。

resizeGL()函数中,我们就是记录下窗口的大小,用于后面计算剪切矩形的大小。


还有,我们该进入重点的paintGL()函数了,具体代码如下:

void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存

    glColor3f(1.0f, 0.5f, 0.5f);                        //设置为红色
    glPrint(50, 16, 1, "Renderer");
    glPrint(80, 48, 1, "Vendor");
    glPrint(66, 80, 1, "Version");

    glColor3f(1.0, 0.7f, 0.4f);                         //设置为橘黄色
    glPrint(200, 16, 1, (char*)glGetString(GL_RENDERER));//显示OpenGL的实现组织
    glPrint(200, 48, 1, (char*)glGetString(GL_VENDOR)); //显示销售商
    glPrint(200, 80, 1, (char*)glGetString(GL_VERSION));//显示当前版本

    glColor3f(0.5f, 0.5f, 1.0f);                        //设置为蓝色
    glPrint(192, 432, 1, "NeHe Productions");           //在屏幕底部显示"NeHe Productions"字符串

    glLoadIdentity();                                   //重置模型观察矩阵
    glColor3f(1.0f, 1.0f, 1.0f);                        //设置为白色
    //绘制边框
    glBegin(GL_LINE_STRIP);
        glVertex2d(639, 417);
        glVertex2d(0, 417);
        glVertex2d(0, 480);
        glVertex2d(639, 480);
        glVertex2d(639, 128);
    glEnd();
    glBegin(GL_LINE_STRIP);
        glVertex2d(0, 128);
        glVertex2d(639, 128);
        glVertex2d(639, 1);
        glVertex2d(0, 1);
        glVertex2d(0, 417);
    glEnd();

    glScissor(1, int(0.135416f*m_Sheight), m_Swidth-2,  //设置剪裁区域
              int(0.597916f*m_Sheight));
    glEnable(GL_SCISSOR_TEST);                          //使用剪裁测试

    //为保存OpenGL扩展名的字符串分配内存空间
    char* text = (char*)malloc(strlen((char*)glGetString(GL_EXTENSIONS)) + 1);
    strcpy(text, (char*)glGetString(GL_EXTENSIONS));    //返回OpenGL扩展名的字符串

    char *token = strtok(text, " ");                    //按空格分割text字符串
    int cnt = 0;                                        //记录字符串的个数
    while (token != NULL)                               //如果token不为NULL
    {
        cnt++;                                          //增加计数器
        if (cnt > m_Maxtokens)
        {
            m_Maxtokens = cnt;                          //记录最大的扩展名数量
        }

        glColor3f(0.5f, 1.0f, 0.5f);                    //设置颜色为绿色
        glPrint(0, 96+(cnt*32)-m_Scroll, 0, "%i", cnt); //绘制第几个扩展名

        glColor3f(1.0f, 1.0f, 0.5f);                    //设置颜色为黄色
        glPrint(50, 96+(cnt*32)-m_Scroll, 0, token);    //输出第cnt个扩展名

        token = strtok(NULL, " ");                      //获得下一个扩展名
    }

    glDisable(GL_SCISSOR_TEST);                         //禁用剪裁测试
    free(text);                                         //释放分配的内存
}
这个函数代码前半部分,也就是到glScissor()函数之前,是用于绘制一个基本的画面边框,并且在顶部打印出OpenGL的实现组织、销售商和当前版本号,在底部打印“NeHe Productions”,打印什么内容都不是什么重点。前面部分大家注意下,绘制边框时用的是GL_LINE_STRIP,这个是用于绘制连续连段的,故我们只需画5个点就能完成一个四边框。

glScissor()函数就是表面我们进入剪裁部分的绘制了。glScissor()函数用来设置剪裁区域,前两个参数分别为起始x、y坐标,后面两个参数分别为宽度和高度。如果启用了GL_SCISSOR_TEST,绘制的内容只能在剪裁区域中显示。接着我们利用glGetString()函数获得扩展名的字符串,为text分配足够的空间后,把字符串保存在text中。然后我们利用while循环和strtok(text, " ")函数,将text字符串按空格分割保存了token指向的地址中。如果token指向的地址不为NULL,我们就打印出这个扩展名的字符串。在一次循环结束前,我们使用strtok(NULL, " ")函数来获得下一个分割得到的字符串,如果还有字符串则token得到其地址,否则为NULL。当返回NULL时循环停止,表示已经显示完所有的扩展名了。

最后我们禁用GL_SCISSOR_TEST,让OpenGL返回到默认的渲染状态,并释放分配的内存资源。


最后,我们l来修改键盘控制函数就行了,功能很简单,就是控制上下键时,屏幕会滚动,具体代码如下:

void MyGLWidget::keyPressEvent(QKeyEvent *event)
{
    switch (event->key())
    {
    case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
        fullscreen = !fullscreen;
        if (fullscreen)
        {
            showFullScreen();
        }
        else
        {
            showNormal();
        }
        updateGL();
        break;
    case Qt::Key_Escape:                                //ESC为退出键
        close();
        break;
    case Qt::Key_Up:                                    //按下向上字幕向上滚动
        if (m_Scroll > 0)
        {
            m_Scroll -= 8;
        }
        break;
    case Qt::Key_Down:                                  //按下向下字幕向下滚动
        if (m_Scroll < 32*(m_Maxtokens-9))
        {
            m_Scroll += 8;
        }
        break;
    }
}
现在就可以运行程序查看效果了!

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


一点内容的补充:这里我们补充上NeHe提供的加载TGA图片的方法,虽然Qt帮我们解决了这个问题,但是我觉得学习这个加载过程还是收获很多的!(PS:后面的教程加载TGA图时我会用上面简单的方法来加载,毕竟我们没必要放着写好的东西不用,自己来写不一定完整的代码)

我们来看自己手动加载TGA图片的loadTGATexture()函数,我先给代码,后面再详细解释,具体代码如下:

<span style="font-size:12px;">GLuint MyGLWidget::loadTGATexture(QString filename)     //加载TGA文件并转为纹理
{
    GLubyte TGAheader[12] = {0, 0, 2, 0, 0, 0,          //无压缩的TGA文件头
                             0, 0, 0, 0, 0, 0};
    GLubyte TGAcompare[12];                             //保存读入的文件头信息
    GLubyte header[6];                                  //保存最有用的图像信息(宽、高、位深)
    GLuint bytesPerPixel;                               //记录每个颜色所占用的字节数
    GLuint imageSize;                                   //记录文件大小
    GLuint type = GL_RGBA;                              //设置默认的格式为GL_RGBA,即32位图像

    struct TextureImage                                 //创建加载TGA图像文件结构体
    {
        GLubyte *imageData;                             //图像数据指针
        GLuint bpp;                                     //每个数据所占的位数(必须为24或32)
        GLuint width;                                   //图像宽度
        GLuint height;                                  //图像高度
        GLuint texID;                                   //纹理的ID值
    } texture;

    FILE *file = fopen(filename.toUtf8(), "rb");        //打开一个TGA文件
    if (file == NULL                                                            //文件存在么?
        || fread(TGAcompare, 1, sizeof(TGAcompare), file) != sizeof(TGAcompare) //是否包含12个字节的文件头?
        || memcmp(TGAheader, TGAcompare, sizeof(TGAheader)) != 0                //是否是我们需要的格式?
        || fread(header, 1, sizeof(header), file) != sizeof(header))            //如果是读取下面六个图像信息
    {
        if (file == NULL)
        {
            return 0;                                   //文件不存在则返回0
        }
        else
        {
            fclose(file);                               //否则关闭文件,返回0
            return 0;
        }
    }

    texture.width = header[1] * 256 + header[0];        //记录文件高度
    texture.height = header[3] * 256 + header[2];       //记录文件宽度
    if (texture.width <= 0                              //宽度是否小于0
        || texture.height <= 0                          //高度是否小于0
        || (header[4] != 24 && header[4] != 32))        //TGA文件是24位/32位?
    {
        fclose(file);                                   //出错则关闭文件,返回0
        return 0;
    }

    texture.bpp = header[4];                            //记录文件的位深
    bytesPerPixel = texture.bpp / 8;                    //记录每个像素所占的字节数
    //计算TGA文件加载所需要的内存大小
    imageSize = texture.width * texture.height * bytesPerPixel;

    texture.imageData = (GLubyte *)malloc(imageSize);   //分配内存去保存TGA数据
    if (texture.imageData == NULL                                               //是否成功分配内存?
        || fread(texture.imageData, 1, imageSize, file) != imageSize)           //是否成功读入内存?
    {
        if (texture.imageData != NULL)                  //是否有数据被加载
        {
            free(texture.imageData);                    //如果是,则释放载入的数据
        }
        fclose(file);                                   //关闭文件,返回0
        return 0;
    }

    for (int i=0; i<int(imageSize); i+=bytesPerPixel)   //循环所有的像素
    {
        GLuint temp = texture.imageData[i];             //交换R和B的值
        texture.imageData[i] = texture.imageData[i + 2];
        texture.imageData[i + 2] = temp;
    }
    fclose(file);                                       //关闭文件

    glGenTextures(1, &texture.texID);                   //创建纹理空间,并记录其ID
    glBindTexture(GL_TEXTURE_2D, texture.texID);        //绑定纹理
    //设置过滤器为线性过滤
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    if (texture.bpp == 24)                              //是否为24位图像
    {
        type = GL_RGB;                                  //如果是,设置类型为GL_RGB
    }
    //在内存中创建一个纹理
    glTexImage2D(GL_TEXTURE_2D, 0, type, texture.width, texture.height,
                 0, type, GL_UNSIGNED_BYTE, texture.imageData);

    return texture.texID;                               //返回纹理地址
}</span>


我们先来看一个TGA图像的文件格式,具体格式如下:


现在我们来看我们的函数代码,我们会根据上面的图进行讲解。一开始我们定义了一些变量,具体作用大家看注释,后面都会讲解到。然后我们还定义了一个结构体TextrueImage用于记录我们将要创建的纹理的数据。

定义完变量后,首先我们用fopen()函数打开文件,“rb”指以只读方式打开二进制文件。接着我们用if语句和||符号的特性先看文件是否成功打开;如果成功打开,读入前12个字节,并检查字节是否读入成功;如果读入成功,再比较这前12个字节是否为规定格式(TGA无颜色表RBG图像前12个字节一般为0,0,2,0,0,0,0,0,0,0,0,0);如果为规定格式,则读入偏移量为12到17的字节,并检查是否读入成功。上面的过程中,如果有其中一环失败了,就会进入if语句的执行语句,根据情况看是否关闭文件,然后返回0。

如果文件规格是正确的,我们就会在header[]中获得我们要的数据(偏移量12到17的字节),然后我们可以求得纹理的宽度和高度。下面我们用if语句检查宽度和高度是否合法以及位深(偏移量为16的字节读入的数据,存放在header[4])是否合法(为24或32),如果不合法就关闭文件并返回0。如果合法,我们就记录文件的位深并计算加载图像需要的总内存大小imageSize。然后我们为图像数据分配足够的内存,保证分配成功后,我们从文件中读入图像数据保存于texture的imageData中。

由于TGA文件中,颜色的储存顺序为BGR,而OpenGL中的颜色顺序为RGB,所以我们需要交换每个像素中R和B的值,我们通过循环来完成这一步操作。如果一切顺利,TGA文件中的图像数据将按照OpenGL的要求储存在内存中了,我们就可以关闭文件了。

最后我们用获得的数据来创建纹理,glGenTextures为纹理分配的空间,然后绑定纹理,设置过滤器,判断位数为24还是32,来设置类型为GL_RGB还是GL_RGBA。最后用glTexImage2D函数完成一个纹理的创建。到此我们完成了TGA文件的加载并转换成纹理的操作,这个过程真的不简单,但是我们一路下来还是学到不少的东西的,很开心的(比起直接用bindTexture()函数,我们收获了新的知识,这不是一件令人愉悦的事情吗?)。


当然为了用上这个函数,我们的initializeGL函数就要修改一下了,具体代码如下:

void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
{
    if ((m_Texture = loadTGATexture(m_FileName)) == 0)  //加载TGA图像文件并转换为纹理
    {
        //出错则发出警告并退出
        QMessageBox::warning(this, "警告", "TGA文件加载出错!", QMessageBox::Ok);
        delete this;
    }
    glEnable(GL_TEXTURE_2D);                            //启用纹理映射
    buildFont();                                        //创建字体

    glClearColor(0.0, 0.0, 0.0, 0.0);                   //黑色背景
    glShadeModel(GL_SMOOTH);                            //启用阴影平滑
    glClearDepth(1.0);                                  //设置深度缓存
    glEnable(GL_BLEND);                                 //启用融合
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  //设置融合因子
}
我们把bindTexture()函数换成我们自己的loadTGATexture()函数,并检查其返回值。如果为0,说明加载出错,弹出警告对话框,并退出程序;成功则继续执行程序。


猜你喜欢

转载自blog.csdn.net/cly116/article/details/47418285
今日推荐