【Qt OpenGL教程】29:Blitter函式
第29課:Blitter函式 (參照NeHe)
這次教程中,我們將介紹類似於DirectDraw的blit(其實blit函式在許多繪相簿都有),我們將用程式碼自己來實現它。它的作用非常簡單,就是把一塊紋理的貼到另一塊紋理上。想想,有了這個函式,我們就可以自由拼接紋理了,是不是很棒?
這一課中,我們不但會實現blit函式,我們還將講解如何來載入特定的*.raw圖片。raw格式的圖片,可以理解為CMOS或者CCD影象感應器將捕捉到的光源訊號轉化為數字訊號的原始資料。可悲的是,Qt並沒有提供載入raw圖片的方法,所以我們只能自己寫了(這是處理影象的一課,與OpenGL關係不大)!
程式執行時效果如下:
下面進入教程:
我們這次將在第06課的基礎上修改程式碼,我們只講解新的內容,舊的內容到這裡大家應該掌握得很好了才對。首先開啟myglwidget.h檔案,將類宣告修改如下:
首先我們新定義了一個結構體Texture_Image,把其指標重新命名為P_TEXTURE_IMAGE。結構體中width、height指影象畫素的寬和高;format指每一個畫素的位深,也就是每一個畫素所佔的記憶體大小;data指向用於儲存影象資料的記憶體。接著我們定義了該結構體的兩個指標t1、t2。最後,我們聲明瞭5個新的函式,函式的作用大家先看註釋,後面定義的時候再一起解釋。#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
接下來,我們需要開啟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); //告訴系統對透視進行修正
}
函式只是做了小的改動,我們自己呼叫自己寫的函式為影象分配記憶體,讀入資料,完成拼接,轉換為紋理後,我們釋放了之前分配的記憶體。並不難理解,大家自己看吧。
現在就可以執行程式檢視效果了!