第十一課,紋理的使用入門

轉自:http://www.programfan.com/club/post-244703.html

我們在前一課中,學習了簡單的像素操作,這意味着我們可以使用各種各樣的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;
    }

    // 讀取像素數據
    iffread(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教程系列中所給出的程序中最長的)。該程序演示了紋理的基本使用方法,本課程涉及到的幾乎所有內容都被包括其中,這是對本課中文字說明的一個補充。如果讀者有什麼不明白的地方,也可以以這個程序作爲參考。

 

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