1. 程式人生 > >【Qt OpenGL教程】29:Blitter函式

【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);  //告訴系統對透視進行修正
}
函式只是做了小的改動,我們自己呼叫自己寫的函式為影象分配記憶體,讀入資料,完成拼接,轉換為紋理後,我們釋放了之前分配的記憶體。並不難理解,大家自己看吧。

現在就可以執行程式檢視效果了!