我們?cè)谇耙徽n中,學(xué)習(xí)了簡(jiǎn)單的像素操作,這意味著我們可以使用各種各樣的BMP文件來豐富程序的顯示效果,于是我們的OpenGL圖形程序也不再像以前總是只顯示幾個(gè)多邊形那樣單調(diào)了?!沁@還不夠。雖然我們可以將像素?cái)?shù)據(jù)按照矩形進(jìn)行縮小和放大,但是還不足以滿足我們的要求。例如要將一幅世界地圖繪制到一個(gè)球體表面,只使用glPixelZoom這樣的函數(shù)來進(jìn)行縮放顯然是不夠的。OpenGL紋理映射功能支持將一些像素?cái)?shù)據(jù)經(jīng)過變換(即使是比較不規(guī)則的變換)將其附著到各種形狀的多邊形表面。紋理映射功能十分強(qiáng)大,利用它可以實(shí)現(xiàn)目前計(jì)算機(jī)動(dòng)畫中的大多數(shù)效果,但是它也很復(fù)雜,我們不可能一次性的完全講解。這里的課程只是關(guān)于二維紋理的簡(jiǎn)單使用。但即使是這樣,也會(huì)使我們的程序在顯示效果上邁出一大步。
下面幾張圖片說明了紋理的效果。前兩張是我們需要的紋理,后一張是我們使用紋理后,利用OpenGL所產(chǎn)生出的效果。



紋理的使用是非常復(fù)雜的。因此即使是入門教程,在編寫時(shí)我也多次進(jìn)行刪改,很多東西都被精簡(jiǎn)掉了,但本課的內(nèi)容仍然較多,大家要有一點(diǎn)心理準(zhǔn)備~
1、啟用紋理和載入紋理
就像我們?cè)?jīng)學(xué)習(xí)過的OpenGL光照、混合等功能一樣。在使用紋理前,必須啟用它。OpenGL支持一維紋理、二維紋理和三維紋理,這里我們僅介紹二維紋理??梢允褂靡韵抡Z句來啟用和禁用二維紋理:
glEnable(GL_TEXTURE_2D); // 啟用二維紋理
glDisable(GL_TEXTURE_2D); // 禁用二維紋理
使用紋理前,還必須載入紋理。利用glTexImage2D函數(shù)可以載入一個(gè)二維的紋理,該函數(shù)有多達(dá)九個(gè)參數(shù)(雖然某些參數(shù)我們可以暫時(shí)不去了解),現(xiàn)在分別說明如下:
第一個(gè)參數(shù)為指定的目標(biāo),在我們的入門教材中,這個(gè)參數(shù)將始終使用GL_TEXTURE_2D。
第二個(gè)參數(shù)為“多重細(xì)節(jié)層次”,現(xiàn)在我們并不考慮多重紋理細(xì)節(jié),因此這個(gè)參數(shù)設(shè)置為零。
第三個(gè)參數(shù)有兩種用法。在OpenGL 1.0,即最初的版本中,使用整數(shù)來表示顏色分量數(shù)目,例如:像素?cái)?shù)據(jù)用RGB顏色表示,總共有紅、綠、藍(lán)三個(gè)值,因此參數(shù)設(shè)置為3,而如果像素?cái)?shù)據(jù)是用RGBA顏色表示,總共有紅、綠、藍(lán)、alpha四個(gè)值,因此參數(shù)設(shè)置為4。而在后來的版本中,可以直接使用GL_RGB或GL_RGBA來表示以上情況,顯得更直觀(并帶來其它一些好處,這里暫時(shí)不提)。注意:雖然我們使用Windows的BMP文件作為紋理時(shí),一般是藍(lán)色的像素在最前,其真實(shí)的格式為GL_BGR而不是GL_RGB,在數(shù)據(jù)的順序上有所不同,但因?yàn)橥瑯邮羌t、綠、藍(lán)三種顏色,因此這里仍然使用GL_RGB。(如果使用GL_BGR,OpenGL將無法識(shí)別這個(gè)參數(shù),造成錯(cuò)誤)
第四、五個(gè)參數(shù)是二維紋理像素的寬度和高度。這里有一個(gè)很需要注意的地方:OpenGL在以前的很多版本中,限制紋理的大小必須是2的整數(shù)次方,即紋理的寬度和高度只能是16, 32, 64, 128, 256等值,直到最近的新版本才取消了這個(gè)限制。而且,一些OpenGL實(shí)現(xiàn)(例如,某些PC機(jī)上板載顯卡的驅(qū)動(dòng)程序附帶的OpenGL)并沒有支持到如此高的OpenGL版本。因此在使用紋理時(shí)要特別注意其大小。盡量使用大小為2的整數(shù)次方的紋理,當(dāng)這個(gè)要求無法滿足時(shí),使用gluScaleImage函數(shù)把圖象縮放至所指定的大小(在后面的例子中有用到)。另外,無論舊版本還是新版本,都限制了紋理大小的最大值,例如,某OpenGL實(shí)現(xiàn)可能要求紋理最大不能超過1024*1024??梢允褂萌缦碌拇a來獲得OpenGL所支持的最大紋理:
GLint max;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
這樣max的值就是當(dāng)前OpenGL實(shí)現(xiàn)中所支持的最大紋理。
在很長(zhǎng)一段時(shí)間內(nèi),很多圖形程序都喜歡使用256*256大小的紋理,不僅因?yàn)?56是2的整數(shù)次方,也因?yàn)槟承┯布梢允褂?位的整數(shù)來表示紋理坐標(biāo),2的8次方正好是256,這一巧妙的組合為處理紋理坐標(biāo)時(shí)的硬件優(yōu)化創(chuàng)造了一些不錯(cuò)的條件。
第六個(gè)參數(shù)是紋理邊框的大小,我們沒有使用紋理邊框,因此這里設(shè)置為零。
最后三個(gè)參數(shù)與glDrawPixels函數(shù)的最后三個(gè)參數(shù)的使用方法相同,其含義可以參考glReadPixels的最后三個(gè)參數(shù)。大家可以復(fù)習(xí)一下第10課的相關(guān)內(nèi)容,這里不再重復(fù)。
舉個(gè)例子,如果有一幅大小為width*height,格式為Windows系統(tǒng)中使用最普遍的24位BGR,保存在pixels中的像素圖象。則把這樣一幅圖象載入為紋理可使用以下代碼:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, pixels);
注意,載入紋理的過程可能比較慢,原因是紋理數(shù)據(jù)通常比較大,例如一幅512*512的BGR格式的圖象,大小為0.75M。把這些像素?cái)?shù)據(jù)從主內(nèi)存?zhèn)魉偷綄iT的圖形硬件,這個(gè)過程中還可能需要把程序中所指定的像素格式轉(zhuǎn)化為圖形硬件所能識(shí)別的格式(或最能發(fā)揮圖形硬件性能的格式),這些操作都需要較多時(shí)間。
.
2、紋理坐標(biāo)
我們先來回憶一下之前學(xué)過的一點(diǎn)內(nèi)容:
當(dāng)我們繪制一個(gè)三角形時(shí),只需要指定三個(gè)頂點(diǎn)的顏色。三角形中其它各點(diǎn)的顏色不需要我們指定,這些點(diǎn)的顏色是OpenGL自己通過計(jì)算得到的。
在我們學(xué)習(xí)OpneGL光照時(shí),法線向量、材質(zhì)的指定,都是只需要在頂點(diǎn)處指定一下就可以了,其它地方的法線向量和材質(zhì)都是OpenGL自己通過計(jì)算去獲得。
紋理的使用方法也與此類似。只要指定每一個(gè)頂點(diǎn)在紋理圖象中所對(duì)應(yīng)的像素位置,OpenGL就會(huì)自動(dòng)計(jì)算頂點(diǎn)以外的其它點(diǎn)在紋理圖象中所對(duì)應(yīng)的像素位置。
這聽起來比較令人迷惑。我們可以這樣類比一下:
在繪制一條線段時(shí),我們?cè)O(shè)置其中一個(gè)端點(diǎn)為紅色,另一個(gè)端點(diǎn)為綠色,則OpenGL會(huì)自動(dòng)計(jì)算線段中其它各像素的顏色,如果是使用glShadeMode(GL_SMOOTH);,則最終會(huì)形成一種漸變的效果(例如線段中點(diǎn),就是紅色和綠色的中間色)。
類似的,在繪制一條線段時(shí),我們?cè)O(shè)置其中一個(gè)端點(diǎn)使用“紋理圖象中最左下角的顏色”作為它的顏色,另一個(gè)端點(diǎn)使用“紋理圖象中最右上角的顏色”作為它的顏色,則OpenGL會(huì)自動(dòng)在紋理圖象中選擇合適位置的顏色,填充到線段的各個(gè)像素(例如線段中點(diǎn),可能就是選擇紋理圖象中央的那個(gè)像素的顏色)。
我們?cè)陬惐葧r(shí),使用了“紋理圖象中最左下角的顏色”這種說法。但這種說法在很多時(shí)候不夠精確,我們需要一種精確的方式來表示我們究竟使用紋理中的哪個(gè)像素。紋理坐標(biāo)也就是因?yàn)檫@樣的要求而產(chǎn)生的。以二維紋理為例,規(guī)定紋理最左下角的坐標(biāo)為(0, 0),最右上角的坐標(biāo)為(1, 1),于是紋理中的每一個(gè)像素的位置都可以用兩個(gè)浮點(diǎn)數(shù)來表示(三維紋理會(huì)用三個(gè)浮點(diǎn)數(shù)表示,一維紋理則只用一個(gè)即可)。
使用glTexCoord*系列函數(shù)來指定紋理坐標(biāo)。這些函數(shù)的用法與使用glVertex*系列函數(shù)來指定頂點(diǎn)坐標(biāo)十分相似。例如:glTexCoord2f(0.0f, 0.0f);指定使用(0, 0)紋理坐標(biāo)。
通常,每個(gè)頂點(diǎn)使用不同的紋理,于是下面這樣形式的代碼是比較常見的。
glBegin( /* ... */ );
glTexCoord2f( /* ... */ ); glVertex3f( /* ... */ );
glTexCoord2f( /* ... */ ); glVertex3f( /* ... */ );
/* ... */
glEnd();
當(dāng)我們用一個(gè)坐標(biāo)表示頂點(diǎn)在三維空間的位置時(shí),可以使用glRotate*等函數(shù)來對(duì)坐標(biāo)進(jìn)行轉(zhuǎn)換。紋理坐標(biāo)也可以進(jìn)行這種轉(zhuǎn)換。只要使用glMatrixMode(GL_TEXTURE);,就可以切換到紋理矩陣(另外還有透視矩陣GL_PROJECTION和模型視圖矩陣GL_MODELVIEW,詳細(xì)情況在第五課有講述),然后glRotate*,glScale*,glTranslate*等操作矩陣的函數(shù)就可以用來處理“對(duì)紋理坐標(biāo)進(jìn)行轉(zhuǎn)換”的工作了。在簡(jiǎn)單應(yīng)用中,可能不會(huì)對(duì)矩陣進(jìn)行任何變換,這樣考慮問題會(huì)比較簡(jiǎn)單。
3、紋理參數(shù)
到這里,入門所需要掌握的所有難點(diǎn)都被我們掌握了。但是,我們的知識(shí)仍然是不夠的,如果僅利用現(xiàn)有的知識(shí)去使用紋理的話,你可能會(huì)發(fā)現(xiàn)紋理完全不起作用。這是因?yàn)樵谑褂眉y理前還有某些參數(shù)是必須設(shè)置的。
使用glTexParameter*系列函數(shù)來設(shè)置紋理參數(shù)。通常需要設(shè)置下面四個(gè)參數(shù):
GL_TEXTURE_MAG_FILTER:指當(dāng)紋理圖象被使用到一個(gè)大于它的形狀上時(shí)(即:有可能紋理圖象中的一個(gè)像素會(huì)被應(yīng)用到實(shí)際繪制時(shí)的多個(gè)像素。例如將一幅256*256的紋理圖象應(yīng)用到一個(gè)512*512的正方形),應(yīng)該如何處理。可選擇的設(shè)置有GL_NEAREST和GL_LINEAR,前者表示“使用紋理中坐標(biāo)最接近的一個(gè)像素的顏色作為需要繪制的像素顏色”,后者表示“使用紋理中坐標(biāo)最接近的若干個(gè)顏色,通過加權(quán)平均算法得到需要繪制的像素顏色”。前者只經(jīng)過簡(jiǎn)單比較,需要運(yùn)算較少,可能速度較快,后者需要經(jīng)過加權(quán)平均計(jì)算,其中涉及除法運(yùn)算,可能速度較慢(但如果有專門的處理硬件,也可能兩者速度相同)。從視覺效果上看,前者效果較差,在一些情況下鋸齒現(xiàn)象明顯,后者效果會(huì)較好(但如果紋理圖象本身比較大,則兩者在視覺效果上就會(huì)比較接近)。
GL_TEXTURE_MIN_FILTER:指當(dāng)紋理圖象被使用到一個(gè)小于(或等于)它的形狀上時(shí)(即有可能紋理圖象中的多個(gè)像素被應(yīng)用到實(shí)際繪制時(shí)的一個(gè)像素。例如將一幅256*256的紋理圖象應(yīng)用到一個(gè)128*128的正方形),應(yīng)該如何處理。可選擇的設(shè)置有GL_NEAREST,GL_LINEAR,GL_NEAREST_MIPMAP_NEAREST,GL_NEAREST_MIPMAP_LINEAR,GL_LINEAR_MIPMAP_NEAREST和GL_LINEAR_MIPMAP_LINEAR。其中后四個(gè)涉及到mipmap,現(xiàn)在暫時(shí)不需要了解。前兩個(gè)選項(xiàng)則和GL_TEXTURE_MAG_FILTER中的類似。此參數(shù)似乎是必須設(shè)置的(在我的計(jì)算機(jī)上,不設(shè)置此參數(shù)將得到錯(cuò)誤的顯示結(jié)果,但我目前并沒有找到根據(jù))。
GL_TEXTURE_WRAP_S:指當(dāng)紋理坐標(biāo)的第一維坐標(biāo)值大于1.0或小于0.0時(shí),應(yīng)該如何處理?;镜倪x項(xiàng)有GL_CLAMP和GL_REPEAT,前者表示“截?cái)?#8221;,即超過1.0的按1.0處理,不足0.0的按0.0處理。后者表示“重復(fù)”,即對(duì)坐標(biāo)值加上一個(gè)合適的整數(shù)(可以是正數(shù)或負(fù)數(shù)),得到一個(gè)在[0.0, 1.0]范圍內(nèi)的值,然后用這個(gè)值作為新的紋理坐標(biāo)。例如:某二維紋理,在繪制某形狀時(shí),一像素需要得到紋理中坐標(biāo)為(3.5, 0.5)的像素的顏色,其中第一維的坐標(biāo)值3.5超過了1.0,則在GL_CLAMP方式中將被轉(zhuǎn)化為(1.0, 0.5),在GL_REPEAT方式中將被轉(zhuǎn)化為(0.5, 0.5)。在后來的OpenGL版本中,又增加了新的處理方式,這里不做介紹。如果不指定這個(gè)參數(shù),則默認(rèn)為GL_REPEAT。
GL_TEXTURE_WRAP_T:指當(dāng)紋理坐標(biāo)的第二維坐標(biāo)值大于1.0或小于0.0時(shí),應(yīng)該如何處理。選項(xiàng)與GL_TEXTURE_WRAP_S類似,不再重復(fù)。如果不指定這個(gè)參數(shù),則默認(rèn)為GL_REPEAT。
設(shè)置參數(shù)的代碼如下所示:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
4、紋理對(duì)象
前面已經(jīng)提到過,載入一幅紋理所需要的時(shí)間是比較多的。因此應(yīng)該盡量減少載入紋理的次數(shù)。如果只有一幅紋理,則應(yīng)該在第一次繪制前就載入它,以后就不需要再次載入了。這點(diǎn)與glDrawPixels函數(shù)很不相同。每次使用glDrawPixels函數(shù),都需要把像素?cái)?shù)據(jù)重新載入一次,因此用glDrawPixels函數(shù)來反復(fù)繪制圖象的效率是較低的(如果只繪制一次,則不會(huì)有此問題),使用紋理來反復(fù)繪制圖象是可取的做法。
但是,在每次繪制時(shí)要使用兩幅或更多幅的紋理時(shí),這個(gè)辦法就行不通了。你可能會(huì)編寫下面的代碼:
glTexImage2D( /* ... */ ); // 載入第一幅紋理
// 使用第一幅紋理
glTexImage2D( /* ... */ ); // 載入第二幅紋理
// 使用第二幅紋理
// 當(dāng)紋理的數(shù)量增加時(shí),這段代碼會(huì)變得更加復(fù)雜。
在繪制動(dòng)畫時(shí),由于每秒鐘需要將畫面繪制數(shù)十次,因此如果使用上面的代碼,就會(huì)反復(fù)載入紋理,這對(duì)計(jì)算機(jī)是非常大的負(fù)擔(dān),以目前的個(gè)人計(jì)算機(jī)配置來說,根本就無法讓動(dòng)畫能夠流暢的運(yùn)行。因此,需要有一種機(jī)制,能夠在不同的紋理之間進(jìn)行快速的切換。
紋理對(duì)象正是這樣一種機(jī)制。我們可以把每一幅紋理(包括紋理的像素?cái)?shù)據(jù)、紋理大小等信息,也包括了前面所講的紋理參數(shù))放到一個(gè)紋理對(duì)象中,通過創(chuàng)建多個(gè)紋理對(duì)象來達(dá)到同時(shí)保存多幅紋理的目的。這樣一來,在第一次使用紋理前,把所有的紋理都載入,然后在繪制時(shí)只需要指明究竟使用哪一個(gè)紋理對(duì)象就可以了。
使用紋理對(duì)象和使用顯示列表有相似之處:使用一個(gè)正整數(shù)來作為紋理對(duì)象的編號(hào)。在使用前,可以調(diào)用glGenTextures來分配紋理對(duì)象。該函數(shù)有兩種比較常見的用法:
GLuint texture_ID;
glGenTextures(1, &texture_ID); // 分配一個(gè)紋理對(duì)象的編號(hào)
或者:
GLuint texture_ID_list[5];
glGenTextures(5, texture_ID_list); // 分配5個(gè)紋理對(duì)象的編號(hào)
零是一個(gè)特殊的紋理對(duì)象編號(hào),表示“默認(rèn)的紋理對(duì)象”,在分配正確的情況下,glGenTextures不會(huì)分配這個(gè)編號(hào)。與glGenTextures對(duì)應(yīng)的是glDeleteTextures,用于銷毀一個(gè)紋理對(duì)象。
在分配了紋理對(duì)象編號(hào)后,使用glBindTexture函數(shù)來指定“當(dāng)前所使用的紋理對(duì)象”。然后就可以使用glTexImage*系列函數(shù)來指定紋理像素、使用glTexParameter*系列函數(shù)來指定紋理參數(shù)、使用glTexCoord*系列函數(shù)來指定紋理坐標(biāo)了。如果不使用glBindTexture函數(shù),那么glTexImage*、glTexParameter*、glTexCoord*系列函數(shù)默認(rèn)在一個(gè)編號(hào)為0的紋理對(duì)象上進(jìn)行操作。glBindTexture函數(shù)有兩個(gè)參數(shù),第一個(gè)參數(shù)是需要使用紋理的目標(biāo),因?yàn)槲覀儸F(xiàn)在只學(xué)習(xí)二維紋理,所以指定為GL_TEXTURE_2D,第二個(gè)參數(shù)是所使用的紋理的編號(hào)。
使用多個(gè)紋理對(duì)象,就可以使OpenGL同時(shí)保存多個(gè)紋理。在使用時(shí)只需要調(diào)用glBindTexture函數(shù),在不同紋理之間進(jìn)行切換,而不需要反復(fù)載入紋理,因此動(dòng)畫的繪制速度會(huì)有非常明顯的提升。典型的代碼如下所示:
// 在程序開始時(shí):分配好紋理編號(hào),并載入紋理
glGenTextures( /* ... */ );
glBindTexture(GL_TEXTURE_2D, texture_ID_1);
// 載入第一幅紋理
glBindTexture(GL_TEXTURE_2D, texture_ID_2);
// 載入第二幅紋理
// 在繪制時(shí),切換并使用紋理,不需要再進(jìn)行載入
glBindTexture(GL_TEXTURE_2D, texture_ID_1); // 指定第一幅紋理
// 使用第一幅紋理
glBindTexture(GL_TEXTURE_2D, texture_ID_2); // 指定第二幅紋理
// 使用第二幅紋理
提示:紋理對(duì)象是從OpenGL 1.1版開始才有的,最舊版本的OpenGL 1.0并沒有處理紋理對(duì)象的功能。不過,我想各位的機(jī)器不會(huì)是比OpenGL 1.1更低的版本(Windows 95就自帶了OpenGL 1.1版本,遺憾的是,Microsoft對(duì)OpenGL的支持并不積極,Windows XP也還采用1.1版本。據(jù)說Vista使用的是OpenGL 1.4版。當(dāng)然了,如果安裝顯卡驅(qū)動(dòng)的話,現(xiàn)在的主流顯卡一般都附帶了適用于該顯卡的OpenGL 1.4版或更高版本),所以這個(gè)問題也就不算是問題了。
5、示例程序
紋理入門所需要掌握的知識(shí)點(diǎn)就介紹到這里了。但是如果不實(shí)際動(dòng)手操作的話,也是不可能真正掌握的。下面我們來看看本課開頭的那個(gè)紋理效果是如何實(shí)現(xiàn)的吧。
因?yàn)榇a比較長(zhǎng),我把它拆分成了三段,大家如果要編譯的話,應(yīng)該把三段代碼按順序連在一起編譯。如果要運(yùn)行的話,除了要保證有一個(gè)名稱為dummy.bmp,圖象大小為1*1的24位BMP文件,還要把本課開始的兩幅紋理圖片保存到正確位置(一幅名叫g(shù)round.bmp,另一幅名叫wall.bmp。注意:我為了節(jié)省網(wǎng)絡(luò)空間,把兩幅圖片都轉(zhuǎn)成jpg格式了,讀者把圖片保存到本地后,需要把它們?cè)俎D(zhuǎn)化為BMP格式??梢允褂肳indows XP帶的畫圖程序中的“另存為”功能完成這一轉(zhuǎn)換)。
第一段代碼如下。其中的主體——grab函數(shù),是我們?cè)诘谑n介紹過的,這里僅僅是抄過來用一下,目的是為了將最終效果圖保存到一個(gè)名字叫g(shù)rab.bmp的文件中。(當(dāng)然了,為了保證程序的正確運(yùn)行,那個(gè)大小為1*1的dummy.bmp文件仍然是必要的,參見第十課)
#define WindowWidth 400
#define WindowHeight 400
#define WindowTitle "OpenGL紋理測(cè)試"
#include <gl/glut.h>
#include <stdio.h>
#include <stdlib.h>
/* 函數(shù)grab
* 抓取窗口中的像素
* 假設(shè)窗口寬度為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;
// 計(jì)算像素?cái)?shù)據(jù)的實(shí)際長(zhǎng)度
i = WindowWidth * 3; // 得到每一行的像素?cái)?shù)據(jù)長(zhǎng)度
while( i%4 != 0 ) // 補(bǔ)充數(shù)據(jù),直到i是的倍數(shù)
++i; // 本來還有更快的算法,
// 但這里僅追求直觀,對(duì)速度沒有太高要求
PixelDataLength = i * WindowHeight;
// 分配內(nèi)存和打開文件
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的文件頭復(fù)制為新文件的文件頭
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);
// 寫入像素?cái)?shù)據(jù)
fseek(pWritingFile, 0, SEEK_END);
fwrite(pPixelData, PixelDataLength, 1, pWritingFile);
// 釋放內(nèi)存和關(guān)閉文件
fclose(pDummyFile);
fclose(pWritingFile);
free(pPixelData);
}
第二段代碼是我們的重點(diǎn)。它包括兩個(gè)函數(shù)。其中power_of_two比較簡(jiǎn)單,雖然實(shí)現(xiàn)手段有點(diǎn)奇特,但也并非無法理解(即使真的無法理解,讀者也可以給出自己的解決方案,用一些循環(huán)以及多使用一些位操作也沒關(guān)系。反正,這里不是重點(diǎn)啦)。另一個(gè)load_texture函數(shù)卻是重頭戲:打開BMP文件、讀取其中的高度和寬度信息、計(jì)算像素?cái)?shù)據(jù)所占的字節(jié)數(shù)、為像素?cái)?shù)據(jù)分配空間、讀取像素?cái)?shù)據(jù)、對(duì)像素圖象進(jìn)行縮放(如果必要的話)、分配新的紋理編號(hào)、填寫紋理參數(shù)、載入紋理,所有的功能都在同一個(gè)函數(shù)里面完成了。為了敘述方便,我把所有的解釋都放在了注釋里。
/* 函數(shù)power_of_two
* 檢查一個(gè)整數(shù)是否為2的整數(shù)次方,如果是,返回1,否則返回0
* 實(shí)際上只要查看其二進(jìn)制位中有多少個(gè),如果正好有1個(gè),返回1,否則返回0
* 在“查看其二進(jìn)制位中有多少個(gè)”時(shí)使用了一個(gè)小技巧
* 使用n &= (n-1)可以使得n中的減少一個(gè)(具體原理大家可以自己思考)
*/
int power_of_two(int n)
{
if( n <= 0 )
return 0;
return (n & (n-1)) == 0;
}
/* 函數(shù)load_texture
* 讀取一個(gè)BMP文件作為紋理
* 如果失敗,返回0,如果成功,返回紋理編號(hào)
*/
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);
// 計(jì)算每行像素所占字節(jié)數(shù),并根據(jù)此數(shù)據(jù)計(jì)算總像素字節(jié)數(shù)
{
GLint line_bytes = width * 3;
while( line_bytes % 4 != 0 )
++line_bytes;
total_bytes = line_bytes * height;
}
// 根據(jù)總像素字節(jié)數(shù)分配內(nèi)存
pixels = (GLubyte*)malloc(total_bytes);
if( pixels == 0 )
{
fclose(pFile);
return 0;
}
// 讀取像素?cái)?shù)據(jù)
if( fread(pixels, total_bytes, 1, pFile) <= 0 )
{
free(pixels);
fclose(pFile);
return 0;
}
// 在舊版本的OpenGL中
// 如果圖象的寬度和高度不是的整數(shù)次方,則需要進(jìn)行縮放
// 這里并沒有檢查OpenGL版本,出于對(duì)版本兼容性的考慮,按舊版本處理
// 另外,無論是舊版本還是新版本,
// 當(dāng)圖象的寬度和高度超過當(dāng)前OpenGL實(shí)現(xiàn)所支持的最大值時(shí),也要進(jìn)行縮放
{
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; // 規(guī)定縮放后新的大小為邊長(zhǎng)的正方形
GLint new_line_bytes, new_total_bytes;
GLubyte* new_pixels = 0;
// 計(jì)算每行需要的字節(jié)數(shù)和總字節(jié)數(shù)
new_line_bytes = new_width * 3;
while( new_line_bytes % 4 != 0 )
++new_line_bytes;
new_total_bytes = new_line_bytes * new_height;
// 分配內(nèi)存
new_pixels = (GLubyte*)malloc(new_total_bytes);
if( new_pixels == 0 )
{
free(pixels);
fclose(pFile);
return 0;
}
// 進(jìn)行像素縮放
gluScaleImage(GL_RGB,
width, height, GL_UNSIGNED_BYTE, pixels,
new_width, new_height, GL_UNSIGNED_BYTE, new_pixels);
// 釋放原來的像素?cái)?shù)據(jù),把pixels指向新的像素?cái)?shù)據(jù),并重新設(shè)置width和height
free(pixels);
pixels = new_pixels;
width = new_width;
height = new_height;
}
}
// 分配一個(gè)新的紋理編號(hào)
glGenTextures(1, &texture_ID);
if( texture_ID == 0 )
{
free(pixels);
fclose(pFile);
return 0;
}
// 綁定新的紋理,載入紋理并設(shè)置紋理參數(shù)
// 在綁定前,先獲得原來綁定的紋理編號(hào),以便在最后進(jìn)行恢復(fù)
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分配的內(nèi)存可在使用glTexImage2D以后釋放
// 因?yàn)榇藭r(shí)像素?cái)?shù)據(jù)已經(jīng)被OpenGL另行保存了一份(可能被保存到專門的圖形硬件中)
free(pixels);
return texture_ID;
}
第三段代碼是關(guān)于顯示的部分,以及main函數(shù)。注意,我們只在main函數(shù)中讀取了兩幅紋理,并把它們保存在各自的紋理對(duì)象中,以后就再也不載入紋理。每次繪制時(shí)使用glBindTexture在不同的紋理對(duì)象中切換。另外,我們使用了超過1.0的紋理坐標(biāo),由于GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T參數(shù)都被設(shè)置為GL_REPEAT,所以得到的效果就是紋理像素的重復(fù),有點(diǎn)向地板磚的花紋那樣。讀者可以試著修改“墻”的紋理坐標(biāo),將5.0修改為10.0,看看效果有什么變化。
/* 兩個(gè)紋理對(duì)象的編號(hào)
*/
GLuint texGround;
GLuint texWall;
void display(void)
{
// 清除屏幕
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 設(shè)置視角
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();
// 旋轉(zhuǎn)后再繪制一個(gè)
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();
// 交換緩沖區(qū),并保存像素?cái)?shù)據(jù)到文件
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;
}
小結(jié):
本課介紹了OpenGL紋理的入門知識(shí)。
利用紋理可以進(jìn)行比glReadPixels和glDrawPixels更復(fù)雜的像素繪制,因此可以實(shí)現(xiàn)很多精彩的效果。
本課只涉及了二維紋理。OpenGL還支持一維和三維紋理,其原理是類似的。
在使用紋理前,要啟用紋理。并且,還需要將像素?cái)?shù)據(jù)載入到紋理中。注意紋理的寬度和高度,目前很多OpenGL的實(shí)現(xiàn)都還要求其值為2的整數(shù)次方,如果紋理圖象本身并不滿足這個(gè)條件,可以使用gluScaleImage函數(shù)來進(jìn)行縮放。為了正確的使用紋理,需要設(shè)置紋理參數(shù)。
載入紋理所需要的系統(tǒng)開銷是比較大的,應(yīng)該盡可能減少載入紋理的次數(shù)。如果程序中只使用一幅紋理,則只在第一次使用前載入,以后不必重新載入。如果程序中要使用多幅紋理,不應(yīng)該反復(fù)載入它們,而應(yīng)該將每個(gè)紋理都用一個(gè)紋理對(duì)象來保存,并使用glBindTextures在各個(gè)紋理之間進(jìn)行切換。
本課還給出了一個(gè)程序(到目前為止,它是這個(gè)OpenGL教程系列中所給出的程序中最長(zhǎng)的)。該程序演示了紋理的基本使用方法,本課程涉及到的幾乎所有內(nèi)容都被包括其中,這是對(duì)本課中文字說明的一個(gè)補(bǔ)充。如果讀者有什么不明白的地方,也可以以這個(gè)程序作為參考。