【Qt OpenGL教程】29:Blitter函数

第29课:Blitter函数 (参照NeHe)

这次教程中,我们将介绍类似于DirectDraw的blit(其实blit函数在许多绘图库都有),我们将用代码自己来实现它。它的作用非常简单,就是把一块纹理的贴到另一块纹理上。想想,有了这个函数,我们就可以自由拼接纹理了,是不是很棒?

这一课中,我们不但会实现blit函数,我们还将讲解如何来加载特定的*.raw图片。raw格式的图片,可以理解为CMOS或者CCD图像感应器将捕捉到的光源信号转化为数字信号的原始数据。可悲的是,Qt并没有提供加载raw图片的方法,所以我们只能自己写了(这是处理图像的一课,与OpenGL关系不大)!


程序运行时效果如下:




下面进入教程:


我们这次将在第06课的基础上修改代码,我们只讲解新的内容,旧的内容到这里大家应该掌握得很好了才对。首先打开myglwidget.h文件,将类声明修改如下:

#ifndef MYGLWIDGET_H
#define MYGLWIDGET_H

#include <QWidget>
#include <QGLWidget>

typedef struct Texture_Image                        //图像结构体
{
    int width;
    int height;
    int format;                                     //格式(图像每一像素内存)
    unsigned char *data;                            //储存图像数据
}* P_TEXTURE_IMAGE;

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:
    //为图像结构体分配内存
    P_TEXTURE_IMAGE allocateTextureBuffer(GLuint w, GLuint h, GLuint f);
    void deallocateTexture(P_TEXTURE_IMAGE t);      //释放图像结构体内存
    //读取图像结构体数据
    void loadRAWData(const char *filename, P_TEXTURE_IMAGE buffer);
    GLuint buildTexture(P_TEXTURE_IMAGE tex);       //建立纹理
    //将一个纹理贴到另一个纹理上
    void blit(P_TEXTURE_IMAGE src, P_TEXTURE_IMAGE dst, int src_xstart,
              int src_ystart, int src_width, int src_height,
              int dst_xstart, int dst_ystart, bool blend, int alpha);

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

    GLfloat m_xRot;                                 //绕x轴旋转的角度
    GLfloat m_yRot;                                 //绕y轴旋转的角度
    GLfloat m_zRot;                                 //绕z轴旋转的角度

    P_TEXTURE_IMAGE t1, t2;                         //图像结构体指针
    GLuint m_Texture;                               //储存一个纹理
};

#endif // MYGLWIDGET_H
首先我们新定义了一个结构体Texture_Image,把其指针重命名为P_TEXTURE_IMAGE。结构体中width、height指图像像素的宽和高;format指每一个像素的位深,也就是每一个像素所占的内存大小;data指向用于储存图像数据的内存。接着我们定义了该结构体的两个指针t1、t2。最后,我们声明了5个新的函数,函数的作用大家先看注释,后面定义的时候再一起解释。


接下来,我们需要打开myglwidget.cpp,加上声明#include <QMessageBox>,对构造函数进行修改,很简单不多解释,具体代码如下:

MyGLWidget::MyGLWidget(QWidget *parent) :
    QGLWidget(parent)
{
    fullscreen = false;
    m_xRot = 0.0f;
    m_yRot = 0.0f;
    m_zRot = 0.0f;

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

下面,我们来看allocateTextureBuffer()、deallocateTexture()函数的定义,具体代码如下:

P_TEXTURE_IMAGE MyGLWidget::allocateTextureBuffer(GLuint w, GLuint h, GLuint f)
{
    P_TEXTURE_IMAGE ti = NULL;
    unsigned char *c = NULL;

    ti = (P_TEXTURE_IMAGE)malloc(sizeof(Texture_Image));//分配图像结构体内存
    if (ti != NULL)
    {
        ti->width = w;                                  //设置宽度
        ti->height = h;                                 //设置高度
        ti->format = f;                                 //设置格式(位深/8)

        c = (unsigned char *)malloc(w * h *f);          //分配w*h*f字节来存放图像数据
        if (c != NULL )
        {
            ti->data = c;
        }
        else
        {
            QMessageBox::warning(this, "内存不足", "分配图像内存错误", QMessageBox::Ok);
            exit(1);
        }
    }
    else
    {
        QMessageBox::warning(this, "内存不足", "分配图像结构体内存错误", QMessageBox::Ok);
        exit(1);
    }

    return ti;                                          //返回图像结构体指针
}
void MyGLWidget::deallocateTexture(P_TEXTURE_IMAGE t)
{
    if (t != NULL)
    {
        if (t->data != NULL)
        {
            free(t->data);                              //释放存放图像数据的内存
        }

        free(t);                                        //释放图像结构体的内存
    }
}

首先是allocateTextureBuffer()函数,这个函数用来为图像结构体分配内存。函数中,我们为ti和c分别分配内存,其中为c分配时,其大小是w*h*f;然后如果分配不成功,则利用QMessageBox来报告错误,并退出程序。
然后是deallocateTexture()函数,这个函数用来释放我们分配的内存。函数中,我们把t和data指向的内存都释放掉。


继续,我们来看loadRAWData()和buildTexture()函数的定义,具体代码如下:

void MyGLWidget::loadRAWData(const char *filename, P_TEXTURE_IMAGE buffer)
{
    int stride = buffer->width * buffer->format;        //记录每一行的字节数
    FILE *f = fopen(filename, "rb");                    //打开文件

    if (f != NULL)                                      //如果文件存在
    {
        for (int i=buffer->height-1; i>=0; i--)         //循环所有的行,从最下面的行开始读入
        {
            unsigned char *p = buffer->data + (i*stride);
            for (int j=0; j<buffer->width; j++)         //读取每一行的数据
            {
                for (int k=0; k<buffer->format-1; k++, p++)
                {
                    *p = fgetc(f);                      //读取一个字节
                }
                *p = 255;                               //把255储存在alpha通道中
                p++;
            }
        }
        fclose(f);                                      //关闭文件
    }
    else
    {
        QMessageBox::warning(this, "不能打开文件", "图像错误", QMessageBox::Ok);
        exit(1);
    }
}
GLuint MyGLWidget::buildTexture(P_TEXTURE_IMAGE tex)
{
    GLuint texture;

    glGenTextures(1, &texture);                         //创建纹理空间,并记录其地址
    glBindTexture(GL_TEXTURE_2D, texture);              //绑定纹理
    //设置过滤器为线性过滤
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    //在内存中创建一个纹理
    gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB, tex->width, tex->height,
                      GL_RGBA, GL_UNSIGNED_BYTE, tex->data);
    return texture;                                     //返回纹理地址
}

首先是loadRAWData()函数,这个函数用于加载raw图像的数据。我在网上查到raw图像的格式似乎根据硬件的不同,是由差别的,所以NeHe给的这个函数应该是只针对他给的图片是有效的(虽然有点可惜,但是我们还是有收获的)。函数中,我们先定义了变量stride来保存每一行数据的字节数,接着打开文件,如果文件不存在则报错退出程序;如果文件存在,我们通过一个循环来读取我们的数据,我们从图像的最下面一行开始,一向上行一行地读取每一行的数据。然后我们再利用一个循环来读取每一行的数据,并在内层再加一个循环来读取每一个像素的数据,并把alpha设置为255。
然后是buildTexture()函数,这个函数利用我们定义的图像结构体里的数据来创建纹理。一开始调用glGenTextures为纹理分配空间,然后绑定纹理,设置过滤器,然后利用结构体里的数据创建一个纹理,最后返回纹理的地址。


还有,我们来定义blit函数,这才是我们这节课的重点,前面4个函数都是直接或间接为了这个函数作铺垫,具体代码如下:

/*blit函数将一个纹理贴到另一个纹理上
 * src为源图像,dst为目标图像
 * src_xstart,src_ystart为要复制的部分在源图像中的位置
 * src_width,src_height为复制的部分的宽度和高度
 * dst_xstart,dst_ystart为复制到目标图像时的起始位置
 * blend设置是否启用混合,true为启用,false为不启用
 * alpha设置源图像中在混合时所占的百分比
 */
void MyGLWidget::blit(P_TEXTURE_IMAGE src, P_TEXTURE_IMAGE dst, int src_xstart,
                      int src_ystart, int src_width, int src_height,
                      int dst_xstart, int dst_ystart, bool blend, int alpha)
{
    if (alpha > 255)                                    //保证alpha的值有效
    {
        alpha = 255;
    }
    if (alpha < 0)
    {
        alpha = 0;
    }

    //计算要复制的像素在源图像数据中的开始行
    unsigned char *s = src->data + (src_ystart*src->width*src->format);
    //计算要复制的像素在目标图像数据中的开始行
    unsigned char *d = dst->data + (dst_ystart*dst->width*dst->format);

    for (int i=0; i<src_height; i++)                    //循环每一行
    {
        s = s + (src_xstart*src->format);               //移动到这一行要复制像素的开始位置
        d = d + (dst_xstart*dst->format);
        for (int j=0; j<src_width; j++)                 //循环复制一行
        {
            for (int k=0; k<src->format; k++, d++, s++) //复制每一个字节
            {
                if (blend)                              //如果启用了混合
                {
                    //根据alpha值计算颜色值
                    *d = ((*s * alpha) + (*d * (255-alpha))) >> 8;
                }
                else
                {
                    *d = *s;                            //否则直接复制
                }
            }
        }
        //移动到下一行
        d = d + (dst->width - (src_width+dst_xstart))*dst->format;
        s = s + (src->width - (src_width+src_xstart))*src->format;
    }
}
函数的参数有点多,在函数开头有各个参数的注释,大家自己看。一开始,我们先保证传进来的alpha值为0~255,接着我们让指针s和d分别指向源图像和目标图像要复制的起始行。进入循环,我们让s和d指向当前行要复制的像素的开始位置,然后循环复制每一行。复制过程,我们检查blend是否为true,如果为true,则根据alpha的值计算颜色值,否则直接复制颜色值,一次循环的结束,把指针s和d移动到需要要复制的下一行。


最后,对于initializeGL()、resizeGL()、paintGL()函数,后两个函数可以继续用第06课的,我们只需要修改initializeGL()函数,具体代码如下:

void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
{
    t1 = allocateTextureBuffer(256, 256, 4);            //为t1分配内存
    loadRAWData("D:/QtOpenGL/QtImage/Monitor.raw", t1); //读入t1的数据
    t2 = allocateTextureBuffer(256, 256, 4);            //为t2分配内存
    loadRAWData("D:/QtOpenGL/QtImage/GL.raw", t2);      //读入t2的数据
    blit(t2, t1, 127, 127, 128, 128, 64, 64, true, 127);//调用blit函数实现图像的拼接
    m_Texture = buildTexture(t1);                       //创建纹理
    deallocateTexture(t1);                              //释放t1的内存
    deallocateTexture(t2);                              //释放t2的内存
    glEnable(GL_TEXTURE_2D);                            //启用纹理映射

    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);  //告诉系统对透视进行修正
}
函数只是做了小的改动,我们自己调用自己写的函数为图像分配内存,读入数据,完成拼接,转换为纹理后,我们释放了之前分配的内存。并不难理解,大家自己看吧。

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


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


猜你喜欢

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