1. 程式人生 > >OpenGL 紋理入門

OpenGL 紋理入門

OpenGL入門學習[十一]


我們在前一課中,學習了簡單的畫素操作,這意味著我們可以使用各種各樣的BMP檔案來豐富程式的顯示效果,於是我們的OpenGL圖形程式也不再像以前總是隻顯示幾個多邊形那樣單調了。——但是這還不夠。雖然我們可以將畫素資料按照矩形進行縮小和放大,但是還不足以滿足我們的要求。例如要將一幅世界地圖繪製到一個球體表面,只使用glPixelZoom這樣的函式來進行縮放顯然是不夠的。OpenGL紋理對映功能支援將一些畫素資料經過變換(即使是比較不規則的變換)將其附著到各種形狀的多邊形表面。紋理對映功能十分強大,利用它可以實現目前計算機動畫中的大多數效果,但是它也很複雜,我們不可能一次性的完全講解。這裡的課程只是關於二維紋理的簡單使用。但即使是這樣,也會使我們的程式在顯示效果上邁出一大步。
下面幾張圖片說明了紋理的效果。前兩張是我們需要的紋理,後一張是我們使用紋理後,利用OpenGL所產生出的效果。

http://blog.programfan.com/upfile/200707/20070730074740.jpg

http://blog.programfan.com/upfile/200707/20070730074746.jpg
http://blog.programfan.com/upfile/200707/20070730074751.jpg

紋理的使用是非常複雜的。因此即使是入門教程,在編寫時我也多次進行刪改,很多東西都被精簡掉了,但本課的內容仍然較多,大家要有一點心理準備~
1、啟用紋理和載入紋理
就像我們曾經學習過的OpenGL光照、混合等功能一樣。在使用紋理前,必須啟用它。OpenGL支援一維紋理、二維紋理和三維紋理,這裡我們僅介紹二維紋理。可以使用以下語句來啟用和禁用二維紋理:
     glEnable(GL_TEXTURE_2D);   // 啟用二維紋理
     glDisable(GL_TEXTURE_2D); // 禁用二維紋理



使用紋理前,還必須載入紋理。利用glTexImage2D函式可以載入一個二維的紋理,該函式有多達九個引數(雖然某些引數我們可以暫時不去了解),現在分別說明如下:
第一個引數為指定的目標,在我們的入門教材中,這個引數將始終使用GL_TEXTURE_2D。
第二個引數為“多重細節層次”,現在我們並不考慮多重紋理細節,因此這個引數設定為零。
第三個引數有兩種用法。在OpenGL 1.0,即最初的版本中,使用整數來表示顏色分量數目,例如:畫素資料用RGB顏色表示,總共有紅、綠、藍三個值,因此引數設定為3,而如果畫素資料是用RGBA顏色表示,總共有紅、綠、藍、alpha四個值,因此引數設定為4。而在後來的版本中,可以直接使用GL_RGB或GL_RGBA來表示以上情況,顯得更直觀(並帶來其它一些好處,這裡暫時不提)。注意:雖然我們使用Windows的BMP檔案作為紋理時,一般是藍色的畫素在最前,其真實的格式為GL_BGR而不是GL_RGB,在資料的順序上有所不同,但因為同樣是紅、綠、藍三種顏色,因此這裡仍然使用GL_RGB。(如果使用GL_BGR,OpenGL將無法識別這個引數,造成錯誤)
第四、五個引數是二維紋理畫素的寬度和高度。這裡有一個很需要注意的地方:OpenGL在以前的很多版本中,限制紋理的大小必須是2的整數次方,即紋理的寬度和高度只能是16, 32, 64, 128, 256等值,直到最近的新版本才取消了這個限制。而且,一些OpenGL實現(例如,某些PC機上板載顯示卡的驅動程式附帶的OpenGL)並沒有支援到如此高的OpenGL版本。因此在使用紋理時要特別注意其大小。儘量使用大小為2的整數次方的紋理,當這個要求無法滿足時,使用gluScaleImage函式把圖象縮放至所指定的大小(在後面的例子中有用到)。另外,無論舊版本還是新版本,都限制了紋理大小的最大值,例如,某OpenGL實現可能要求紋理最大不能超過1024*1024。可以使用如下的程式碼來獲得OpenGL所支援的最大紋理:

GLint max;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);


這樣max的值就是當前OpenGL實現中所支援的最大紋理。
在很長一段時間內,很多圖形程式都喜歡使用256*256大小的紋理,不僅因為256是2的整數次方,也因為某些硬體可以使用8位的整數來表示紋理座標,2的8次方正好是256,這一巧妙的組合為處理紋理座標時的硬體優化創造了一些不錯的條件。

第六個引數是紋理邊框的大小,我們沒有使用紋理邊框,因此這裡設定為零。
最後三個引數與glDrawPixels函式的最後三個引數的使用方法相同,其含義可以參考glReadPixels的最後三個引數。大家可以複習一下第10課的相關內容,這裡不再重複。
舉個例子,如果有一幅大小為width*height,格式為Windows系統中使用最普遍的24位BGR,儲存在pixels中的畫素圖象。則把這樣一幅圖象載入為紋理可使用以下程式碼:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, pixels);



注意,載入紋理的過程可能比較慢,原因是紋理資料通常比較大,例如一幅512*512的BGR格式的圖象,大小為0.75M。把這些畫素資料從主記憶體傳送到專門的圖形硬體,這個過程中還可能需要把程式中所指定的畫素格式轉化為圖形硬體所能識別的格式(或最能發揮圖形硬體效能的格式),這些操作都需要較多時間。
2、紋理座標
我們先來回憶一下之前學過的一點內容:
當我們繪製一個三角形時,只需要指定三個頂點的顏色。三角形中其它各點的顏色不需要我們指定,這些點的顏色是OpenGL自己通過計算得到的。
在我們學習OpneGL光照時,法線向量、材質的指定,都是隻需要在頂點處指定一下就可以了,其它地方的法線向量和材質都是OpenGL自己通過計算去獲得。

紋理的使用方法也與此類似。只要指定每一個頂點在紋理圖象中所對應的畫素位置,OpenGL就會自動計算頂點以外的其它點在紋理圖象中所對應的畫素位置。
這聽起來比較令人迷惑。我們可以這樣類比一下:
在繪製一條線段時,我們設定其中一個端點為紅色,另一個端點為綠色,則OpenGL會自動計算線段中其它各畫素的顏色,如果是使用glShadeMode(GL_SMOOTH);,則最終會形成一種漸變的效果(例如線段中點,就是紅色和綠色的中間色)。
類似的,在繪製一條線段時,我們設定其中一個端點使用“紋理圖象中最左下角的顏色”作為它的顏色,另一個端點使用“紋理圖象中最右上角的顏色”作為它的顏色,則OpenGL會自動在紋理圖象中選擇合適位置的顏色,填充到線段的各個畫素(例如線段中點,可能就是選擇紋理圖象中央的那個畫素的顏色)。

我們在類比時,使用了“紋理圖象中最左下角的顏色”這種說法。但這種說法在很多時候不夠精確,我們需要一種精確的方式來表示我們究竟使用紋理中的哪個畫素。紋理座標也就是因為這樣的要求而產生的。以二維紋理為例,規定紋理最左下角的座標為(0, 0),最右上角的座標為(1, 1),於是紋理中的每一個畫素的位置都可以用兩個浮點數來表示(三維紋理會用三個浮點數表示,一維紋理則只用一個即可)。
使用glTexCoord*系列函式來指定紋理座標。這些函式的用法與使用glVertex*系列函式來指定頂點座標十分相似。例如:glTexCoord2f(0.0f, 0.0f);指定使用(0, 0)紋理座標。
通常,每個頂點使用不同的紋理,於是下面這樣形式的程式碼是比較常見的。

glBegin( /* ... */ );
     glTexCoord2f( /* ... */ );   glVertex3f( /* ... */ );
     glTexCoord2f( /* ... */ );   glVertex3f( /* ... */ );
     /* ... */
glEnd();



當我們用一個座標表示頂點在三維空間的位置時,可以使用glRotate*等函式來對座標進行轉換。紋理座標也可以進行這種轉換。只要使用glMatrixMode(GL_TEXTURE);,就可以切換到紋理矩陣(另外還有透視矩陣GL_PROJECTION和模型檢視矩陣GL_MODELVIEW,詳細情況在第五課有講述),然後glRotate*,glScale*,glTranslate*等操作矩陣的函式就可以用來處理“對紋理座標進行轉換”的工作了。在簡單應用中,可能不會對矩陣進行任何變換,這樣考慮問題會比較簡單。
3、紋理引數
到這裡,入門所需要掌握的所有難點都被我們掌握了。但是,我們的知識仍然是不夠的,如果僅利用現有的知識去使用紋理的話,你可能會發現紋理完全不起作用。這是因為在使用紋理前還有某些引數是必須設定的。
使用glTexParameter*系列函式來設定紋理引數。通常需要設定下面四個引數:
GL_TEXTURE_MAG_FILTER:指當紋理圖象被使用到一個大於它的形狀上時(即:有可能紋理圖象中的一個畫素會被應用到實際繪製時的多個畫素。例如將一幅256*256的紋理圖象應用到一個512*512的正方形),應該如何處理。可選擇的設定有GL_NEAREST和GL_LINEAR,前者表示“使用紋理中座標最接近的一個畫素的顏色作為需要繪製的畫素顏色”,後者表示“使用紋理中座標最接近的若干個顏色,通過加權平均演算法得到需要繪製的畫素顏色”。前者只經過簡單比較,需要運算較少,可能速度較快,後者需要經過加權平均計算,其中涉及除法運算,可能速度較慢(但如果有專門的處理硬體,也可能兩者速度相同)。從視覺效果上看,前者效果較差,在一些情況下鋸齒現象明顯,後者效果會較好(但如果紋理圖象本身比較大,則兩者在視覺效果上就會比較接近)。
GL_TEXTURE_MIN_FILTER:指當紋理圖象被使用到一個小於(或等於)它的形狀上時(即有可能紋理圖象中的多個畫素被應用到實際繪製時的一個畫素。例如將一幅256*256的紋理圖象應用到一個128*128的正方形),應該如何處理。可選擇的設定有GL_NEAREST,GL_LINEAR,GL_NEAREST_MIPMAP_NEAREST,GL_NEAREST_MIPMAP_LINEAR,GL_LINEAR_MIPMAP_NEAREST和GL_LINEAR_MIPMAP_LINEAR。其中後四個涉及到mipmap,現在暫時不需要了解。前兩個選項則和GL_TEXTURE_MAG_FILTER中的類似。此引數似乎是必須設定的(在我的計算機上,不設定此引數將得到錯誤的顯示結果,但我目前並沒有找到根據)。
GL_TEXTURE_WRAP_S:指當紋理座標的第一維座標值大於1.0或小於0.0時,應該如何處理。基本的選項有GL_CLAMP和GL_REPEAT,前者表示“截斷”,即超過1.0的按1.0處理,不足0.0的按0.0處理。後者表示“重複”,即對座標值加上一個合適的整數(可以是正數或負數),得到一個在[0.0, 1.0]範圍內的值,然後用這個值作為新的紋理座標。例如:某二維紋理,在繪製某形狀時,一畫素需要得到紋理中座標為(3.5, 0.5)的畫素的顏色,其中第一維的座標值3.5超過了1.0,則在GL_CLAMP方式中將被轉化為(1.0, 0.5),在GL_REPEAT方式中將被轉化為(0.5, 0.5)。在後來的OpenGL版本中,又增加了新的處理方式,這裡不做介紹。如果不指定這個引數,則預設為GL_REPEAT。
GL_TEXTURE_WRAP_T:指當紋理座標的第二維座標值大於1.0或小於0.0時,應該如何處理。選項與GL_TEXTURE_WRAP_S類似,不再重複。如果不指定這個引數,則預設為GL_REPEAT。

設定引數的程式碼如下所示:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);

4、紋理物件
前面已經提到過,載入一幅紋理所需要的時間是比較多的。因此應該儘量減少載入紋理的次數。如果只有一幅紋理,則應該在第一次繪製前就載入它,以後就不需要再次載入了。這點與glDrawPixels函式很不相同。每次使用glDrawPixels函式,都需要把畫素資料重新載入一次,因此用glDrawPixels函式來反覆繪製圖象的效率是較低的(如果只繪製一次,則不會有此問題),使用紋理來反覆繪製圖象是可取的做法。
但是,在每次繪製時要使用兩幅或更多幅的紋理時,這個辦法就行不通了。你可能會編寫下面的程式碼:

glTexImage2D( /* ... */ ); // 載入第一幅紋理
// 使用第一幅紋理
glTexImage2D( /* ... */ ); // 載入第二幅紋理
// 使用第二幅紋理
// 當紋理的數量增加時,這段程式碼會變得更加複雜。



在繪製動畫時,由於每秒鐘需要將畫面繪製數十次,因此如果使用上面的程式碼,就會反覆載入紋理,這對計算機是非常大的負擔,以目前的個人計算機配置來說,根本就無法讓動畫能夠流暢的執行。因此,需要有一種機制,能夠在不同的紋理之間進行快速的切換。

紋理物件正是這樣一種機制。我們可以把每一幅紋理(包括紋理的畫素資料、紋理大小等資訊,也包括了前面所講的紋理引數)放到一個紋理物件中,通過建立多個紋理物件來達到同時儲存多幅紋理的目的。這樣一來,在第一次使用紋理前,把所有的紋理都載入,然後在繪製時只需要指明究竟使用哪一個紋理物件就可以了。

使用紋理物件和使用顯示列表有相似之處:使用一個正整數來作為紋理物件的編號。在使用前,可以呼叫glGenTextures來分配紋理物件。該函式有兩種比較常見的用法:

GLuint texture_ID;
glGenTextures(1, &texture_ID); // 分配一個紋理物件的編號


或者:

GLuint texture_ID_list[5];
glGenTextures(5, texture_ID_list); // 分配5個紋理物件的編號



零是一個特殊的紋理物件編號,表示“預設的紋理物件”,在分配正確的情況下,glGenTextures不會分配這個編號。與glGenTextures對應的是glDeleteTextures,用於銷燬一個紋理物件。

在分配了紋理物件編號後,使用glBindTexture函式來指定“當前所使用的紋理物件”。然後就可以使用glTexImage*系列函式來指定紋理畫素、使用glTexParameter*系列函式來指定紋理引數、使用glTexCoord*系列函式來指定紋理座標了。如果不使用glBindTexture函式,那麼glTexImage*、glTexParameter*、glTexCoord*系列函式預設在一個編號為0的紋理物件上進行操作。glBindTexture函式有兩個引數,第一個引數是需要使用紋理的目標,因為我們現在只學習二維紋理,所以指定為GL_TEXTURE_2D,第二個引數是所使用的紋理的編號。
使用多個紋理物件,就可以使OpenGL同時儲存多個紋理。在使用時只需要呼叫glBindTexture函式,在不同紋理之間進行切換,而不需要反覆載入紋理,因此動畫的繪製速度會有非常明顯的提升。典型的程式碼如下所示:

// 在程式開始時:分配好紋理編號,並載入紋理
glGenTextures( /* ... */ );
glBindTexture(GL_TEXTURE_2D, texture_ID_1);
// 載入第一幅紋理
glBindTexture(GL_TEXTURE_2D, texture_ID_2);
// 載入第二幅紋理 // 在繪製時,切換並使用紋理,不需要再進行載入
glBindTexture(GL_TEXTURE_2D, texture_ID_1); // 指定第一幅紋理
// 使用第一幅紋理
glBindTexture(GL_TEXTURE_2D, texture_ID_2); // 指定第二幅紋理
// 使用第二幅紋理



提示:紋理物件是從OpenGL 1.1版開始才有的,最舊版本的OpenGL 1.0並沒有處理紋理物件的功能。不過,我想各位的機器不會是比OpenGL 1.1更低的版本(Windows 95就自帶了OpenGL 1.1版本,遺憾的是,Microsoft對OpenGL的支援並不積極,Windows XP也還採用1.1版本。據說Vista使用的是OpenGL 1.4版。當然了,如果安裝顯示卡驅動的話,現在的主流顯示卡一般都附帶了適用於該顯示卡的OpenGL 1.4版或更高版本),所以這個問題也就不算是問題了。
5、示例程式
紋理入門所需要掌握的知識點就介紹到這裡了。但是如果不實際動手操作的話,也是不可能真正掌握的。下面我們來看看本課開頭的那個紋理效果是如何實現的吧。
因為程式碼比較長,我把它拆分成了三段,大家如果要編譯的話,應該把三段程式碼按順序連在一起編譯。如果要執行的話,除了要保證有一個名稱為dummy.bmp,圖象大小為1*1的24位BMP檔案,還要把本課開始的兩幅紋理圖片儲存到正確位置(一幅名叫ground.bmp,另一幅名叫wall.bmp。注意:我為了節省網路空間,把兩幅圖片都轉成jpg格式了,讀者把圖片儲存到本地後,需要把它們再轉化為BMP格式。可以使用Windows XP帶的畫圖程式中的“另存為”功能完成這一轉換)。
第一段程式碼如下。其中的主體——grab函式,是我們在第十課介紹過的,這裡僅僅是抄過來用一下,目的是為了將最終效果圖儲存到一個名字叫grab.bmp的檔案中。(當然了,為了保證程式的正確執行,那個大小為1*1的dummy.bmp檔案仍然是必要的,參見第十課)

#define WindowWidth   400
#define WindowHeight 400
#define WindowTitle  "OpenGL紋理測試"

#include <gl/glut.h>
#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);
}

第二段程式碼是我們的重點。它包括兩個函式。其中power_of_two比較簡單,雖然實現手段有點奇特,但也並非無法理解(即使真的無法理解,讀者也可以給出自己的解決方案,用一些迴圈以及多使用一些位操作也沒關係。反正,這裡不是重點啦)。另一個load_texture函式卻是重頭戲:開啟BMP檔案、讀取其中的高度和寬度資訊、計算畫素資料所佔的位元組數、為畫素資料分配空間、讀取畫素資料、對畫素圖象進行縮放(如果必要的話)、分配新的紋理編號、填寫紋理引數、載入紋理,所有的功能都在同一個函式裡面完成了。為了敘述方便,我把所有的解釋都放在了註釋裡。

/* 函式power_of_two
* 檢查一個整數是否為2的整數次方,如果是,返回1,否則返回0
* 實際上只要檢視其二進位制位中有多少個,如果正好有1個,返回1,否則返回0
* 在“檢視其二進位制位中有多少個”時使用了一個小技巧
* 使用n &= (n-1)可以使得n中的減少一個(具體原理大家可以自己思考)
*/
int power_of_two(int n)
{
    if( n <= 0 )
        return 0;
    return (n & (n-1)) == 0;
}

/* 函式load_texture
* 讀取一個BMP檔案作為紋理
* 如果失敗,返回0,如果成功,返回紋理編號
*/
GLuint load_texture(const char* file_name)
{
     GLint width, height, total_bytes;
     GLubyte* pixels = 0;
     GLuint last_texture_ID, texture_ID = 0;

     // 開啟檔案,如果失敗,返回
     FILE* pFile = fopen(file_name, "rb");
    if( pFile == 0 )
        return 0;

     // 讀取檔案中圖象的寬度和高度
    fseek(pFile, 0x0012, SEEK_SET);
    fread(&width, 4, 1, pFile);
    fread(&height, 4, 1, pFile);
    fseek(pFile, BMP_Header_Length, SEEK_SET);

     // 計算每行畫素所佔位元組數,並根據此資料計算總畫素位元組數
     {
         GLint line_bytes = width * 3;
        while( line_bytes % 4 != 0 )
             ++line_bytes;
         total_bytes = line_bytes * height;
     }

     // 根據總畫素位元組數分配記憶體
     pixels = (GLubyte*)malloc(total_bytes);
    if( pixels == 0 )
     {
        fclose(pFile);
        return 0;
     }

     // 讀取畫素資料
    if( fread(pixels, total_bytes, 1, pFile) <= 0 )
     {
        free(pixels);
        fclose(pFile);
        return 0;
     }

     // 在舊版本的OpenGL中
     // 如果圖象的寬度和高度不是的整數次方,則需要進行縮放
     // 這裡並沒有檢查OpenGL版本,出於對版本相容性的考慮,按舊版本處理
     // 另外,無論是舊版本還是新版本,
     // 當圖象的寬度和高度超過當前OpenGL實現所支援的最大值時,也要進行縮放
     {
         GLint max;
         glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
        if( !power_of_two(width)
          || !power_of_two(height)
          || width > max
          || height > max )
         {
            const GLint new_width = 256;
            const GLint new_height = 256; // 規定縮放後新的大小為邊長的正方形
             GLint new_line_bytes, new_total_bytes;
             GLubyte* new_pixels = 0;

             // 計算每行需要的位元組數和總位元組數
             new_line_bytes = new_width * 3;
            while( new_line_bytes % 4 != 0 )
                 ++new_line_bytes;
             new_total_bytes = new_line_bytes * new_height;

             // 分配記憶體
             new_pixels = (GLubyte*)malloc(new_total_bytes);
            if( new_pixels == 0 )
             {
                free(pixels);
                fclose(pFile);
                return 0;
             }

             // 進行畫素縮放
             gluScaleImage(GL_RGB,
                 width, height, GL_UNSIGNED_BYTE, pixels,
                 new_width, new_height, GL_UNSIGNED_BYTE, new_pixels);

             // 釋放原來的畫素資料,把pixels指向新的畫素資料,並重新設定width和height
            free(pixels);
             pixels = new_pixels;
             width = new_width;
             height = new_height;
         }
     }

     // 分配一個新的紋理編號
     glGenTextures(1, &texture_ID);
    if( texture_ID == 0 )
     {
        free(pixels);
        fclose(pFile);
        return 0;
     }

     // 繫結新的紋理,載入紋理並設定紋理引數
     // 在繫結前,先獲得原來繫結的紋理編號,以便在最後進行恢復
     glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture_ID);
     glBindTexture(GL_TEXTURE_2D, texture_ID);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
     glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
     glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
         GL_BGR_EXT, GL_UNSIGNED_BYTE, pixels);
     glBindTexture(GL_TEXTURE_2D, last_texture_ID);

     // 之前為pixels分配的記憶體可在使用glTexImage2D以後釋放
     // 因為此時畫素資料已經被OpenGL另行儲存了一份(可能被儲存到專門的圖形硬體中)
    free(pixels);
    return texture_ID;
}

第三段程式碼是關於顯示的部分,以及main函式。注意,我們只在main函式中讀取了兩幅紋理,並把它們儲存在各自的紋理物件中,以後就再也不載入紋理。每次繪製時使用glBindTexture在不同的紋理物件中切換。另外,我們使用了超過1.0的紋理座標,由於GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T引數都被設定為GL_REPEAT,所以得到的效果就是紋理畫素的重複,有點向地板磚的花紋那樣。讀者可以試著修改“牆”的紋理座標,將5.0修改為10.0,看看效果有什麼變化。

/* 兩個紋理物件的編號
*/
GLuint texGround;
GLuint texWall;

void display(void)
{
     // 清除螢幕
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     // 設定視角
     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(75, 1, 1, 21);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(1, 5, 5, 0, 0, 0, 0, 0, 1);

     // 使用“地”紋理繪製土地
     glBindTexture(GL_TEXTURE_2D, texGround);
     glBegin(GL_QUADS);
         glTexCoord2f(0.0f, 0.0f); glVertex3f(-8.0f, -8.0f, 0.0f);
         glTexCoord2f(0.0f, 5.0f); glVertex3f(-8.0f, 8.0f, 0.0f);
         glTexCoord2f(5.0f, 5.0f); glVertex3f(8.0f, 8.0f, 0.0f);
         glTexCoord2f(5.0f, 0.0f); glVertex3f(8.0f, -8.0f, 0.0f);
     glEnd();
     // 使用“牆”紋理繪製柵欄
     glBindTexture(GL_TEXTURE_2D, texWall);
     glBegin(GL_QUADS);
         glTexCoord2f(0.0f, 0.0f); glVertex3f(-6.0f, -3.0f, 0.0f);
         glTexCoord2f(0.0f, 1.0f); glVertex3f(-6.0f, -3.0f, 1.5f);
         glTexCoord2f(5.0f, 1.0f); glVertex3f(6.0f, -3.0f, 1.5f);
         glTexCoord2f(5.0f, 0.0f); glVertex3f(6.0f, -3.0f, 0.0f);
     glEnd();

     // 旋轉後再繪製一個
     glRotatef(-90, 0, 0, 1);
     glBegin(GL_QUADS);
         glTexCoord2f(0.0f, 0.0f); glVertex3f(-6.0f, -3.0f, 0.0f);
         glTexCoord2f(0.0f, 1.0f); glVertex3f(-6.0f, -3.0f, 1.5f);
         glTexCoord2f(5.0f, 1.0f); glVertex3f(6.0f, -3.0f, 1.5f);
         glTexCoord2f(5.0f, 0.0f); glVertex3f(6.0f, -3.0f, 0.0f);
     glEnd();

     // 交換緩衝區,並儲存畫素資料到檔案
     glutSwapBuffers();
     grab();
}

int main(int argc, char* argv[])
{
     // GLUT初始化
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
     glutInitWindowPosition(100, 100);
     glutInitWindowSize(WindowWidth, WindowHeight);
     glutCreateWindow(WindowTitle);
     glutDisplayFunc(&display);

     // 在這裡做一些初始化
     glEnable(GL_DEPTH_TEST);
     glEnable(GL_TEXTURE_2D);
     texGround = load_texture("ground.bmp");
     texWall = load_texture("wall.bmp");

     // 開始顯示
     glutMainLoop();

    return 0;
}

小結:
本課介紹了OpenGL紋理的入門知識。
利用紋理可以進行比glReadPixels和glDrawPixels更復雜的畫素繪製,因此可以實現很多精彩的效果。
本課只涉及了二維紋理。OpenGL還支援一維和三維紋理,其原理是類似的。
在使用紋理前,要啟用紋理。並且,還需要將畫素資料載入到紋理中。注意紋理的寬度和高度,目前很多OpenGL的實現都還要求其值為2的整數次方,如果紋理圖象本身並不滿足這個條件,可以使用gluScaleImage函式來進行縮放。為了正確的使用紋理,需要設定紋理引數。
載入紋理所需要的系統開銷是比較大的,應該儘可能減少載入紋理的次數。如果程式中只使用一幅紋理,則只在第一次使用前載入,以後不必重新載入。如果程式中要使用多幅紋理,不應該反覆載入它們,而應該將每個紋理都用一個紋理物件來儲存,並使用glBindTextures在各個紋理之間進行切換。
本課還給出了一個程式(到目前為止,它是這個OpenGL教程系列中所給出的程式中最長的)。該程式演示了紋理的基本使用方法,本課程涉及到的幾乎所有內容都被包括其中,這是對本課中文字說明的一個補充。如果讀者有什麼不明白的地方,也可以以這個程式作為參考