OpenGL入門學習(十)

OpenGL入門學習[十]


今天我們先簡單介紹Windows中常用的BMP文件格式,然後講OpenGL的像素操作。雖然看起來內容可能有點多,但實際只有少量幾個知識點,如果讀者對諸如“顯示BMP圖象”等內容比較感興趣的話,可能不知不覺就看完了。
像素操作可以很複雜,這裏僅涉及了簡單的部分,讓大家對OpenGL像素操作有初步的印象。
學過多媒體技術的朋友可能知道,計算機保存圖象的方法通常有兩種:一是“矢量圖”,一是“像素圖”。矢量圖保存了圖象中每一幾何物體的位置、形狀、大小等信息,在顯示圖象時,根據這些信息計算得到完整的圖象。“像素圖”是將完整的圖象縱橫分爲若干的行、列,這些行列使得圖象被分割爲很細小的分塊,每一分塊稱爲像素,保存每一像素的顏色也就保存了整個圖象。
這兩種方法各有優缺點。“矢量圖”在圖象進行放大、縮小時很方便,不會失真,但如果圖象很複雜,那麼就需要用非常多的幾何體,數據量和運算量都很龐大。“像素圖”無論圖象多麼複雜,數據量和運算量都不會增加,但在進行放大、縮小等操作時,會產生失真的情況。
前面我們曾介紹瞭如何使用OpenGL來繪製幾何體,我們通過重複的繪製許多幾何體,可以繪製出一幅矢量圖。那麼,應該如何繪製像素圖呢?這就是我們今天要學習的內容了。
1、BMP文件格式簡單介紹
BMP文件是一種像素文件,它保存了一幅圖象中所有的像素。這種文件格式可以保存單色位圖、16色或256色索引模式像素圖、24位真彩色圖象,每種模式種單一像素的大小分別爲1/8字節,1/2字節,1字節和3字節。目前最常見的是256色BMP和24位色BMP。這種文件格式還定義了像素保存的幾種方法,包括不壓縮、RLE壓縮等。常見的BMP文件大多是不壓縮的。
這裏爲了簡單起見,我們僅討論24位色、不使用壓縮的BMP。(如果你使用Windows自帶的畫圖程序,很容易繪製出一個符合以上要求的BMP)
Windows所使用的BMP文件,在開始處有一個文件頭,大小爲54字節。保存了包括文件格式標識、顏色數、圖象大小、壓縮方式等信息,因爲我們僅討論24位色不壓縮的BMP,所以文件頭中的信息基本不需要注意,只有“大小”這一項對我們比較有用。圖象的寬度和高度都是一個32位整數,在文件中的地址分別爲0x0012和0x0016,於是我們可以使用以下代碼來讀取圖象的大小信息:

GLint width, height; // 使用OpenGL的GLint類型,它是32位的。
                      // 而C語言本身的int則不一定是32位的。
FILE* pFile;
// 在這裏進行“打開文件”的操作
fseek(pFile, 0x0012, SEEK_SET);          // 移動到0x0012位置
fread(&width, sizeof(width), 1, pFile); // 讀取寬度
fseek(pFile, 0x0016, SEEK_SET);          // 移動到0x0016位置
                                         // 由於上一句執行後本就應該在0x0016位置
                                         // 所以這一句可省略
fread(&height, sizeof(height), 1, pFile); // 讀取高度

54個字節以後,如果是16色或256色BMP,則還有一個顏色表,但24位色BMP沒有這個,我們這裏不考慮。接下來就是實際的像素數據了。24位色的BMP文件中,每三個字節表示一個像素的顏色。
注意,OpenGL通常使用RGB來表示顏色,但BMP文件則採用BGR,就是說,順序被反過來了。
另外需要注意的地方是:像素的數據量並不一定完全等於圖象的高度乘以寬度乘以每一像素的字節數,而是可能略大於這個值。原因是BMP文件採用了一種“對齊”的機制,每一行像素數據的長度若不是4的倍數,則填充一些數據使它是4的倍數。這樣一來,一個17*15的24位BMP大小就應該是834字節(每行17個像素,有51字節,補充爲52字節,乘以15得到像素數據總長度780,再加上文件開始的54字節,得到834字節)。分配內存時,一定要小心,不能直接使用“圖象的高度乘以寬度乘以每一像素的字節數”來計算分配空間的長度,否則有可能導致分配的內存空間長度不足,造成越界訪問,帶來各種嚴重後果。
一個很簡單的計算數據長度的方法如下:

int LineLength, TotalLength;
LineLength = ImageWidth * BytesPerPixel; // 每行數據長度大致爲圖象寬度乘以
                                          // 每像素的字節數
while( LineLength % 4 != 0 )              // 修正LineLength使其爲4的倍數
     ++LineLenth;
TotalLength = LineLength * ImageHeight;   // 數據總長 = 每行長度 * 圖象高度

這並不是效率最高的方法,但由於這個修正本身運算量並不大,使用頻率也不高,我們就不需要再考慮更快的方法了。
2、簡單的OpenGL像素操作
OpenGL提供了簡潔的函數來操作像素:
glReadPixels:讀取一些像素。當前可以簡單理解爲“把已經繪製好的像素(它可能已經被保存到顯卡的顯存中)讀取到內存”。
glDrawPixels:繪製一些像素。當前可以簡單理解爲“把內存中一些數據作爲像素數據,進行繪製”。
glCopyPixels:複製一些像素。當前可以簡單理解爲“把已經繪製好的像素從一個位置複製到另一個位置”。雖然從功能上看,好象等價於先讀取像素再繪製像素,但實際上它不需要把已經繪製的像素(它可能已經被保存到顯卡的顯存中)轉換爲內存數據,然後再由內存數據進行重新的繪製,所以要比先讀取後繪製快很多。
這三個函數可以完成簡單的像素讀取、繪製和複製任務,但實際上也可以完成更復雜的任務。當前,我們僅討論一些簡單的應用。由於這幾個函數的參數數目比較多,下面我們分別介紹。
3、glReadPixels的用法和舉例
3.1 函數的參數說明
該函數總共有七個參數。前四個參數可以得到一個矩形,該矩形所包括的像素都會被讀取出來。(第一、二個參數表示了矩形的左下角橫、縱座標,座標以窗口最左下角爲零,最右上角爲最大值;第三、四個參數表示了矩形的寬度和高度)
第五個參數表示讀取的內容,例如:GL_RGB就會依次讀取像素的紅、綠、藍三種數據,GL_RGBA則會依次讀取像素的紅、綠、藍、alpha四種數據,GL_RED則只讀取像素的紅色數據(類似的還有GL_GREEN,GL_BLUE,以及GL_ALPHA)。如果採用的不是RGBA顏色模式,而是採用顏色索引模式,則也可以使用GL_COLOR_INDEX來讀取像素的顏色索引。目前僅需要知道這些,但實際上還可以讀取其它內容,例如深度緩衝區的深度數據等。
第六個參數表示讀取的內容保存到內存時所使用的格式,例如:GL_UNSIGNED_BYTE會把各種數據保存爲GLubyte,GL_FLOAT會把各種數據保存爲GLfloat等。
第七個參數表示一個指針,像素數據被讀取後,將被保存到這個指針所表示的地址。注意,需要保證該地址有足夠的可以使用的空間,以容納讀取的像素數據。例如一幅大小爲256*256的圖象,如果讀取其RGB數據,且每一數據被保存爲GLubyte,總大小就是:256*256*3 = 196608字節,即192千字節。如果是讀取RGBA數據,則總大小就是256*256*4 = 262144字節,即256千字節。

注意:glReadPixels實際上是從緩衝區中讀取數據,如果使用了雙緩衝區,則默認是從正在顯示的緩衝(即前緩衝)中讀取,而繪製工作是默認繪製到後緩衝區的。因此,如果需要讀取已經繪製好的像素,往往需要先交換前後緩衝。

再看前面提到的BMP文件中兩個需要注意的地方:
3.2 解決OpenGL常用的RGB像素數據與BMP文件的BGR像素數據順序不一致問題
可以使用一些代碼交換每個像素的第一字節和第三字節,使得RGB的數據變成BGR的數據。當然也可以使用另外的方式解決問題:新版本的OpenGL除了可以使用GL_RGB讀取像素的紅、綠、藍數據外,也可以使用GL_BGR按照相反的順序依次讀取像素的藍、綠、紅數據,這樣就與BMP文件格式相吻合了。即使你的gl/gl.h頭文件中沒有定義這個GL_BGR,也沒有關係,可以嘗試使用GL_BGR_EXT。雖然有的OpenGL實現(尤其是舊版本的實現)並不能使用GL_BGR_EXT,但我所知道的Windows環境下各種OpenGL實現都對GL_BGR提供了支持,畢竟Windows中各種表示顏色的數據幾乎都是使用BGR的順序,而非RGB的順序。這可能與IBM-PC的硬件設計有關。

3.3 消除BMP文件中“對齊”帶來的影響
實際上OpenGL也支持使用了這種“對齊”方式的像素數據。只要通過glPixelStore修改“像素保存時對齊的方式”就可以了。像這樣:
int alignment = 4;
glPixelStorei(GL_UNPACK_ALIGNMENT, alignment);
第一個參數表示“設置像素的對齊值”,第二個參數表示實際設置爲多少。這裏像素可以單字節對齊(實際上就是不使用對齊)、雙字節對齊(如果長度爲奇數,則再補一個字節)、四字節對齊(如果長度不是四的倍數,則補爲四的倍數)、八字節對齊。分別對應alignment的值爲1, 2, 4, 8。實際上,默認的值是4,正好與BMP文件的對齊方式相吻合。
glPixelStorei也可以用於設置其它各種參數。但我們這裏並不需要深入討論了。


現在,我們已經可以把屏幕上的像素讀取到內存了,如果需要的話,我們還可以將內存中的數據保存到文件。正確的對照BMP文件格式,我們的程序就可以把屏幕中的圖象保存爲BMP文件,達到屏幕截圖的效果。
我們並沒有詳細介紹BMP文件開頭的54個字節的所有內容,不過這無傷大雅。從一個正確的BMP文件中讀取前54個字節,修改其中的寬度和高度信息,就可以得到新的文件頭了。假設我們先建立一個1*1大小的24位色BMP,文件名爲dummy.bmp,又假設新的BMP文件名稱爲grab.bmp。則可以編寫如下代碼:

FILE* pOriginFile = fopen("dummy.bmp", "rb);
FILE* pGrabFile = fopen("grab.bmp", "wb");
char   BMP_Header[54];
GLint width, height;

/* 先在這裏設置好圖象的寬度和高度,即width和height的值,並計算像素的總長度 */

// 讀取dummy.bmp中的頭54個字節到數組
fread(BMP_Header, sizeof(BMP_Header), 1, pOriginFile);
// 把數組內容寫入到新的BMP文件
fwrite(BMP_Header, sizeof(BMP_Header), 1, pGrabFile);

// 修改其中的大小信息
fseek(pGrabFile, 0x0012, SEEK_SET);
fwrite(&width, sizeof(width), 1, pGrabFile);
fwrite(&height, sizeof(height), 1, pGrabFile);

// 移動到文件末尾,開始寫入像素數據
fseek(pGrabFile, 0, SEEK_END);

/* 在這裏寫入像素數據到文件 */

fclose(pOriginFile);
fclose(pGrabFile);
我們給出完整的代碼,演示如何把整個窗口的圖象抓取出來並保存爲BMP文件。

#define WindowWidth   400
#define WindowHeight 400

#include <stdio.h>
#include <stdlib.h>

/* 函數grab
* 抓取窗口中的像素
* 假設窗口寬度爲WindowWidth,高度爲WindowHeight
*/
#define BMP_Header_Length 54
void grab(void)
{
     FILE*     pDummyFile;
     FILE*     pWritingFile;
     GLubyte* pPixelData;
     GLubyte   BMP_Header[BMP_Header_Length];
     GLint     i, j;
     GLint     PixelDataLength;

     // 計算像素數據的實際長度
     i = WindowWidth * 3;    // 得到每一行的像素數據長度
     while( i%4 != 0 )       // 補充數據,直到i是的倍數
         ++i;                // 本來還有更快的算法,
                            // 但這裏僅追求直觀,對速度沒有太高要求
     PixelDataLength = i * WindowHeight;

     // 分配內存和打開文件
     pPixelData = (GLubyte*)malloc(PixelDataLength);
     if( pPixelData == 0 )
         exit(0);

     pDummyFile = fopen("dummy.bmp", "rb");
     if( pDummyFile == 0 )
         exit(0);

     pWritingFile = fopen("grab.bmp", "wb");
     if( pWritingFile == 0 )
         exit(0);

     // 讀取像素
     glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
     glReadPixels(0, 0, WindowWidth, WindowHeight,
         GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);

     // 把dummy.bmp的文件頭複製爲新文件的文件頭
     fread(BMP_Header, sizeof(BMP_Header), 1, pDummyFile);
     fwrite(BMP_Header, sizeof(BMP_Header), 1, pWritingFile);
     fseek(pWritingFile, 0x0012, SEEK_SET);
     i = WindowWidth;
     j = WindowHeight;
     fwrite(&i, sizeof(i), 1, pWritingFile);
     fwrite(&j, sizeof(j), 1, pWritingFile);

     // 寫入像素數據
     fseek(pWritingFile, 0, SEEK_END);
     fwrite(pPixelData, PixelDataLength, 1, pWritingFile);

     // 釋放內存和關閉文件
     fclose(pDummyFile);
     fclose(pWritingFile);
     free(pPixelData);
}



把這段代碼複製到以前任何課程的樣例程序中,在繪製函數的最後調用grab函數,即可把圖象內容保存爲BMP文件了。(在我寫這個教程的時候,不少地方都用這樣的代碼進行截圖工作,這段代碼一旦寫好,運行起來是很方便的。)
4、glDrawPixels的用法和舉例
glDrawPixels函數與glReadPixels函數相比,參數內容大致相同。它的第一、二、三、四個參數分別對應於glReadPixels函數的第三、四、五、六個參數,依次表示圖象寬度、圖象高度、像素數據內容、像素數據在內存中的格式。兩個函數的最後一個參數也是對應的,glReadPixels中表示像素讀取後存放在內存中的位置,glDrawPixels則表示用於繪製的像素數據在內存中的位置。
注意到glDrawPixels函數比glReadPixels函數少了兩個參數,這兩個參數在glReadPixels中分別是表示圖象的起始位置。在glDrawPixels中,不必顯式的指定繪製的位置,這是因爲繪製的位置是由另一個函數glRasterPos*來指定的。glRasterPos*函數的參數與glVertex*類似,通過指定一個二維/三維/四維座標,OpenGL將自動計算出該座標對應的屏幕位置,並把該位置作爲繪製像素的起始位置。
很自然的,我們可以從BMP文件中讀取像素數據,並使用glDrawPixels繪製到屏幕上。我們選擇Windows XP默認的桌面背景Bliss.bmp作爲繪製的內容(如果你使用的是Windows XP系統,很可能可以在硬盤中搜索到這個文件。當然你也可以使用其它BMP文件來代替,只要它是24位的BMP文件。注意需要修改代碼開始部分的FileName的定義),先把該文件複製一份放到正確的位置,我們在程序開始時,就讀取該文件,從而獲得圖象的大小後,根據該大小來創建合適的OpenGL窗口,並繪製像素。
繪製像素本來是很簡單的過程,但是這個程序在骨架上與前面的各種示例程序稍有不同,所以我還是打算給出一份完整的代碼。

#include <gl/glut.h>

#define FileName "Bliss.bmp"

static GLint     ImageWidth;
static GLint     ImageHeight;
static GLint     PixelLength;
static GLubyte* PixelData;

#include <stdio.h>
#include <stdlib.h>

void display(void)
{
     // 清除屏幕並不必要
     // 每次繪製時,畫面都覆蓋整個屏幕
     // 因此無論是否清除屏幕,結果都一樣
     // glClear(GL_COLOR_BUFFER_BIT);

     // 繪製像素
     glDrawPixels(ImageWidth, ImageHeight,
         GL_BGR_EXT, GL_UNSIGNED_BYTE, PixelData);

     // 完成繪製
     glutSwapBuffers();
}

int main(int argc, char* argv[])
{
     // 打開文件
     FILE* pFile = fopen("Bliss.bmp", "rb");
     if( pFile == 0 )
         exit(0);

     // 讀取圖象的大小信息
     fseek(pFile, 0x0012, SEEK_SET);
     fread(&ImageWidth, sizeof(ImageWidth), 1, pFile);
     fread(&ImageHeight, sizeof(ImageHeight), 1, pFile);

     // 計算像素數據長度
     PixelLength = ImageWidth * 3;
     while( PixelLength % 4 != 0 )
         ++PixelLength;
     PixelLength *= ImageHeight;

     // 讀取像素數據
     PixelData = (GLubyte*)malloc(PixelLength);
     if( PixelData == 0 )
         exit(0);

     fseek(pFile, 54, SEEK_SET);
     fread(PixelData, PixelLength, 1, pFile);

     // 關閉文件
     fclose(pFile);

     // 初始化GLUT並運行
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
     glutInitWindowPosition(100, 100);
     glutInitWindowSize(ImageWidth, ImageHeight);
     glutCreateWindow(FileName);
     glutDisplayFunc(&display);
     glutMainLoop();

     // 釋放內存
     // 實際上,glutMainLoop函數永遠不會返回,這裏也永遠不會到達
     // 這裏寫釋放內存只是出於一種個人習慣
     // 不用擔心內存無法釋放。在程序結束時操作系統會自動回收所有內存
     free(PixelData);

     return 0;
}



這裏僅僅是一個簡單的顯示24位BMP圖象的程序,如果讀者對BMP文件格式比較熟悉,也可以寫出適用於各種BMP圖象的顯示程序,在像素處理時,它們所使用的方法是類似的。
OpenGL在繪製像素之前,可以對像素進行若干處理。最常用的可能就是對整個像素圖象進行放大/縮小。使用glPixelZoom來設置放大/縮小的係數,該函數有兩個參數,分別是水平方向係數和垂直方向係數。例如設置glPixelZoom(0.5f, 0.8f);則表示水平方向變爲原來的50%大小,而垂直方向變爲原來的80%大小。我們甚至可以使用負的係數,使得整個圖象進行水平方向或垂直方向的翻轉(默認像素從左繪製到右,但翻轉後將從右繪製到左。默認像素從下繪製到上,但翻轉後將從上繪製到下。因此,glRasterPos*函數設置的“開始位置”不一定就是矩形的左下角)。
5、glCopyPixels的用法和舉例
從效果上看,glCopyPixels進行像素複製的操作,等價於把像素讀取到內存,再從內存繪製到另一個區域,因此可以通過glReadPixels和glDrawPixels組合來實現複製像素的功能。然而我們知道,像素數據通常數據量很大,例如一幅1024*768的圖象,如果使用24位BGR方式表示,則需要至少1024*768*3字節,即2.25兆字節。這麼多的數據要進行一次讀操作和一次寫操作,並且因爲在glReadPixels和glDrawPixels中設置的數據格式不同,很可能涉及到數據格式的轉換。這對CPU無疑是一個不小的負擔。使用glCopyPixels直接從像素數據複製出新的像素數據,避免了多餘的數據的格式轉換,並且也可能減少一些數據複製操作(因爲數據可能直接由顯卡負責複製,不需要經過主內存),因此效率比較高。
glCopyPixels函數也通過glRasterPos*系列函數來設置繪製的位置,因爲不需要涉及到主內存,所以不需要指定數據在內存中的格式,也不需要使用任何指針。
glCopyPixels函數有五個參數,第一、二個參數表示複製像素來源的矩形的左下角座標,第三、四個參數表示複製像素來源的舉行的寬度和高度,第五個參數通常使用GL_COLOR,表示複製像素的顏色,但也可以是GL_DEPTH或GL_STENCIL,分別表示複製深度緩衝數據或模板緩衝數據。
值得一提的是,glDrawPixels和glReadPixels中設置的各種操作,例如glPixelZoom等,在glCopyPixels函數中同樣有效。
下面看一個簡單的例子,繪製一個三角形後,複製像素,並同時進行水平和垂直方向的翻轉,然後縮小爲原來的一半,並繪製。繪製完畢後,調用前面的grab函數,將屏幕中所有內容保存爲grab.bmp。其中WindowWidth和WindowHeight是表示窗口寬度和高度的常量。

void display(void)
{
     // 清除屏幕
     glClear(GL_COLOR_BUFFER_BIT);

     // 繪製
     glBegin(GL_TRIANGLES);
         glColor3f(1.0f, 0.0f, 0.0f);     glVertex2f(0.0f, 0.0f);
         glColor3f(0.0f, 1.0f, 0.0f);     glVertex2f(1.0f, 0.0f);
         glColor3f(0.0f, 0.0f, 1.0f);     glVertex2f(0.5f, 1.0f);
     glEnd();
     glPixelZoom(-0.5f, -0.5f);
     glRasterPos2i(1, 1);
     glCopyPixels(WindowWidth/2, WindowHeight/2,
         WindowWidth/2, WindowHeight/2, GL_COLOR);

     // 完成繪製,並抓取圖象保存爲BMP文件
     glutSwapBuffers();
     grab();
}



http://blog.programfan.com/upfile/200704/20070419202924.jpg
小結:
本課結合Windows系統常見的BMP圖象格式,簡單介紹了OpenGL的像素處理功能。包括使用glReadPixels讀取像素、glDrawPixels繪製像素、glCopyPixels複製像素。
本課僅介紹了像素處理的一些簡單應用,但相信大家已經可以體會到,圍繞這三個像素處理函數,還存在一些“外圍”函數,比如glPixelStore*,glRasterPos*,以及glPixelZoom等。我們僅使用了這些函數的一少部分功能。
本課內容並不多,例子足夠豐富,三個像素處理函數都有例子,大家可以結合例子來體會。

轉自http://www.cppblog.com/doing5552/archive/2009/01/08/71532.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章