大家好。現(xiàn)在因?yàn)閰⒓庸ぷ鞯年P(guān)系,又是長(zhǎng)時(shí)間沒有更新。趁著國慶的空閑,總算是又寫出了一課。我感覺入門的知識(shí)已經(jīng)快要介紹完畢,這課之后再有一課,就可以告一段落了。以后我可能會(huì)寫一些自己在這方面的體會(huì),做一份進(jìn)階課程。
現(xiàn)在即將放出的是第十二課的內(nèi)容。
首先還是以前課程的連接:
第一課,編寫第一個(gè)OpenGL程序
第二課,繪制幾何圖形
第三課,繪制幾何圖形的一些細(xì)節(jié)問題
第四課,顏色的選擇
第五課,三維的空間變換
第六課,動(dòng)畫的制作
第七課,使用光照來表現(xiàn)立體感
第八課,使用顯示列表
第九課,使用混合來實(shí)現(xiàn)半透明效果
第十課,BMP文件與像素操作
第十一課,紋理的使用入門
第十二課,OpenGL片斷測(cè)試 ——→ 本次課程的內(nèi)容
片斷測(cè)試其實(shí)就是測(cè)試每一個(gè)像素,只有通過測(cè)試的像素才會(huì)被繪制,沒有通過測(cè)試的像素則不進(jìn)行繪制。OpenGL提供了多種測(cè)試操作,利用這些操作可以實(shí)現(xiàn)一些特殊的效果。
我們?cè)谇懊娴恼n程中,曾經(jīng)提到了“深度測(cè)試”的概念,它在繪制三維場(chǎng)景的時(shí)候特別有用。在不使用深度測(cè)試的時(shí)候,如果我們先繪制一個(gè)距離較近的物體,再繪制距離較遠(yuǎn)的物體,則距離遠(yuǎn)的物體因?yàn)楹罄L制,會(huì)把距離近的物體覆蓋掉,這樣的效果并不是我們所希望的。
如果使用了深度測(cè)試,則情況就會(huì)有所不同:每當(dāng)一個(gè)像素被繪制,OpenGL就記錄這個(gè)像素的“深度”(深度可以理解為:該像素距離觀察者的距離。深度值越大,表示距離越遠(yuǎn)),如果有新的像素即將覆蓋原來的像素時(shí),深度測(cè)試會(huì)檢查新的深度是否會(huì)比原來的深度值小。如果是,則覆蓋像素,繪制成功;如果不是,則不會(huì)覆蓋原來的像素,繪制被取消。這樣一來,即使我們先繪制比較近的物體,再繪制比較遠(yuǎn)的物體,則遠(yuǎn)的物體也不會(huì)覆蓋近的物體了。
實(shí)際上,只要存在深度緩沖區(qū),無論是否啟用深度測(cè)試,OpenGL在像素被繪制時(shí)都會(huì)嘗試將深度數(shù)據(jù)寫入到緩沖區(qū)內(nèi),除非調(diào)用了glDepthMask(GL_FALSE)來禁止寫入。這些深度數(shù)據(jù)除了用于常規(guī)的測(cè)試外,還可以有一些有趣的用途,比如繪制陰影等等。
除了深度測(cè)試,OpenGL還提供了剪裁測(cè)試、Alpha測(cè)試和模板測(cè)試。
1、剪裁測(cè)試
剪裁測(cè)試用于限制繪制區(qū)域。我們可以指定一個(gè)矩形的剪裁窗口,當(dāng)啟用剪裁測(cè)試后,只有在這個(gè)窗口之內(nèi)的像素才能被繪制,其它像素則會(huì)被丟棄。換句話說,無論怎么繪制,剪裁窗口以外的像素將不會(huì)被修改。
有的朋友可能玩過《魔獸爭(zhēng)霸3》這款游戲。游戲時(shí)如果選中一個(gè)士兵,則畫面下方的一個(gè)方框內(nèi)就會(huì)出現(xiàn)該士兵的頭像。為了保證該頭像無論如何繪制都不會(huì)越界而覆蓋到外面的像素,就可以使用剪裁測(cè)試。
可以通過下面的代碼來啟用或禁用剪裁測(cè)試:
glEnable(GL_SCISSOR_TEST); // 啟用剪裁測(cè)試
glDisable(GL_SCISSOR_TEST); // 禁用剪裁測(cè)試
可以通過下面的代碼來指定一個(gè)位置在(x, y),寬度為width,高度為height的剪裁窗口。
glScissor(x, y, width, height);
注意,OpenGL窗口坐標(biāo)是以左下角為(0, 0),右上角為(width, height)的,這與Windows系統(tǒng)窗口有所不同。
還有一種方法可以保證像素只繪制到某一個(gè)特定的矩形區(qū)域內(nèi),這就是視口變換(在第五課第3節(jié)中有介紹)。但視口變換和剪裁測(cè)試是不同的。視口變換是將所有內(nèi)容縮放到合適的大小后,放到一個(gè)矩形的區(qū)域內(nèi);而剪裁測(cè)試不會(huì)進(jìn)行縮放,超出矩形范圍的像素直接忽略掉。
2、Alpha測(cè)試
在前面的課程中,我們知道像素的Alpha值可以用于混合操作。其實(shí)Alpha值還有一個(gè)用途,這就是Alpha測(cè)試。當(dāng)每個(gè)像素即將繪制時(shí),如果啟動(dòng)了Alpha測(cè)試,OpenGL會(huì)檢查像素的Alpha值,只有Alpha值滿足條件的像素才會(huì)進(jìn)行繪制(嚴(yán)格的說,滿足條件的像素會(huì)通過本項(xiàng)測(cè)試,進(jìn)行下一種測(cè)試,只有所有測(cè)試都通過,才能進(jìn)行繪制),不滿足條件的則不進(jìn)行繪制。這個(gè)“條件”可以是:始終通過(默認(rèn)情況)、始終不通過、大于設(shè)定值則通過、小于設(shè)定值則通過、等于設(shè)定值則通過、大于等于設(shè)定值則通過、小于等于設(shè)定值則通過、不等于設(shè)定值則通過。
如果我們需要繪制一幅圖片,而這幅圖片的某些部分又是透明的(想象一下,你先繪制一幅相片,然后繪制一個(gè)相框,則相框這幅圖片有很多地方都是透明的,這樣就可以透過相框看到下面的照片),這時(shí)可以使用Alpha測(cè)試。將圖片中所有需要透明的地方的Alpha值設(shè)置為0.0,不需要透明的地方Alpha值設(shè)置為1.0,然后設(shè)置Alpha測(cè)試的通過條件為:“大于0.5則通過”,這樣便能達(dá)到目的。當(dāng)然也可以設(shè)置需要透明的地方Alpha值為1.0,不需要透明的地方Alpha值設(shè)置為0.0,然后設(shè)置條件為“小于0.5則通過”。Alpha測(cè)試的設(shè)置方式往往不只一種,可以根據(jù)個(gè)人喜好和實(shí)際情況需要進(jìn)行選擇。
可以通過下面的代碼來啟用或禁用Alpha測(cè)試:
glEnable(GL_ALPHA_TEST); // 啟用Alpha測(cè)試
glDisable(GL_ALPHA_TEST); // 禁用Alpha測(cè)試
可以通過下面的代碼來設(shè)置Alpha測(cè)試條件為“大于0.5則通過”:
glAlphaFunc(GL_GREATER, 0.5f);
該函數(shù)的第二個(gè)參數(shù)表示設(shè)定值,用于進(jìn)行比較。第一個(gè)參數(shù)是比較方式,除了GL_LESS(小于則通過)外,還可以選擇:
GL_ALWAYS(始終通過),
GL_NEVER(始終不通過),
GL_LESS(小于則通過),
GL_LEQUAL(小于等于則通過),
GL_EQUAL(等于則通過),
GL_GEQUAL(大于等于則通過),
GL_NOTEQUAL(不等于則通過)。
現(xiàn)在我們來看一個(gè)實(shí)際例子。一幅照片圖片,一幅相框圖片,如何將它們組合在一起呢?為了簡(jiǎn)單起見,我們使用前面兩課一直使用的24位BMP文件來作為圖片格式。(因?yàn)榘l(fā)布到網(wǎng)絡(luò)上,為了節(jié)約容量,我所發(fā)布的是JPG格式。大家下載后可以用Windows XP自帶的畫圖工具打開,并另存為24位BMP格式)


注:第一幅圖片是著名網(wǎng)絡(luò)游戲《魔獸世界》的一幅桌面背景,用在這里希望沒有涉及版權(quán)問題。如果有什么不妥,請(qǐng)及時(shí)指出,我會(huì)立即更換。
在24位的BMP文件格式中,BGR三種顏色各占8位,沒有保存Alpha值,因此無法直接使用Alpha測(cè)試。注意到相框那幅圖片中,所有需要透明的位置都是白色,所以我們?cè)诔绦蛑性O(shè)置所有白色(或很接近白色)的像素Alpha值為0.0,設(shè)置其它像素Alpha值為1.0,然后設(shè)置Alpha測(cè)試的條件為“大于0.5則通過”即可。這種使用某種特殊顏色來代表透明顏色的技術(shù),有時(shí)又被成為Color Key技術(shù)。
利用前面第11課的一段代碼,將圖片讀取為紋理,然后利用下面這個(gè)函數(shù)來設(shè)置“當(dāng)前紋理”中每一個(gè)像素的Alpha值。
/* 將當(dāng)前紋理BGR格式轉(zhuǎn)換為BGRA格式
* 紋理中像素的RGB值如果與指定rgb相差不超過absolute,則將Alpha設(shè)置為0.0,否則設(shè)置為1.0
*/
void texture_colorkey(GLubyte r, GLubyte g, GLubyte b, GLubyte absolute)
{
GLint width, height;
GLubyte* pixels = 0;
// 獲得紋理的大小信息
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height);
// 分配空間并獲得紋理像素
pixels = (GLubyte*)malloc(width*height*4);
if( pixels == 0 )
return;
glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);
// 修改像素中的Alpha值
// 其中pixels[i*4], pixels[i*4+1], pixels[i*4+2], pixels[i*4+3]
// 分別表示第i個(gè)像素的藍(lán)、綠、紅、Alpha四種分量,0表示最小,255表示最大
{
GLint i;
GLint count = width * height;
for(i=0; i<count; ++i)
{
if( abs(pixels[i*4] - b) <= absolute
&& abs(pixels[i*4+1] - g) <= absolute
&& abs(pixels[i*4+2] - r) <= absolute )
pixels[i*4+3] = 0;
else
pixels[i*4+3] = 255;
}
}
// 將修改后的像素重新設(shè)置到紋理中,釋放內(nèi)存
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);
free(pixels);
}
有了紋理后,我們開啟紋理,指定合適的紋理坐標(biāo)并繪制一個(gè)矩形,這樣就可以在屏幕上將圖片繪制出來。我們先繪制相片的紋理,再繪制相框的紋理。程序代碼如下:
void display(void)
{
static int initialized = 0;
static GLuint texWindow = 0;
static GLuint texPicture = 0;
// 執(zhí)行初始化操作,包括:讀取相片,讀取相框,將相框由BGR顏色轉(zhuǎn)換為BGRA,啟用二維紋理
if( !initialized )
{
texPicture = load_texture("pic.bmp");
texWindow = load_texture("window.bmp");
glBindTexture(GL_TEXTURE_2D, texWindow);
texture_colorkey(255, 255, 255, 10);
glEnable(GL_TEXTURE_2D);
initialized = 1;
}
// 清除屏幕
glClear(GL_COLOR_BUFFER_BIT);
// 繪制相片,此時(shí)不需要進(jìn)行Alpha測(cè)試,所有的像素都進(jìn)行繪制
glBindTexture(GL_TEXTURE_2D, texPicture);
glDisable(GL_ALPHA_TEST);
glBegin(GL_QUADS);
glTexCoord2f(0, 0); glVertex2f(-1.0f, -1.0f);
glTexCoord2f(0, 1); glVertex2f(-1.0f, 1.0f);
glTexCoord2f(1, 1); glVertex2f( 1.0f, 1.0f);
glTexCoord2f(1, 0); glVertex2f( 1.0f, -1.0f);
glEnd();
// 繪制相框,此時(shí)進(jìn)行Alpha測(cè)試,只繪制不透明部分的像素
glBindTexture(GL_TEXTURE_2D, texWindow);
glEnable(GL_ALPHA_TEST);
glAlphaFunc(GL_GREATER, 0.5f);
glBegin(GL_QUADS);
glTexCoord2f(0, 0); glVertex2f(-1.0f, -1.0f);
glTexCoord2f(0, 1); glVertex2f(-1.0f, 1.0f);
glTexCoord2f(1, 1); glVertex2f( 1.0f, 1.0f);
glTexCoord2f(1, 0); glVertex2f( 1.0f, -1.0f);
glEnd();
// 交換緩沖
glutSwapBuffers();
}
其中:load_texture函數(shù)是從第11課中照搬過來的(該函數(shù)還使用了一個(gè)power_of_two函數(shù),一個(gè)BMP_Header_Length常數(shù),同樣照搬),無需進(jìn)行修改。main函數(shù)跟其它課程的基本相同,不再重復(fù)。
程序運(yùn)行后,會(huì)發(fā)現(xiàn)相框與相片的銜接有些不自然,這是因?yàn)橄嗫蚰承┻吘壊糠蛛m然肉眼看上去是白色,但其實(shí)RGB值與純白色相差并不少,因此程序計(jì)算其Alpha值時(shí)認(rèn)為其不需要透明。解決辦法是仔細(xì)處理相框中的每個(gè)像素,在需要透明的地方涂上純白色,這也許是一件很需要耐心的工作。
大家可能會(huì)想:前面我們學(xué)習(xí)過混合操作,混合可以實(shí)現(xiàn)半透明,自然也可以通過設(shè)定實(shí)現(xiàn)全透明。也就是說,Alpha測(cè)試可以實(shí)現(xiàn)的效果幾乎都可以通過OpenGL混合功能來實(shí)現(xiàn)。那么為什么還需要一個(gè)Alpha測(cè)試呢?答案就是,這與性能相關(guān)。Alpha測(cè)試只要簡(jiǎn)單的比較大小就可以得到最終結(jié)果,而混合操作一般需要進(jìn)行乘法運(yùn)算,性能有所下降。另外,OpenGL測(cè)試的順序是:剪裁測(cè)試、Alpha測(cè)試、模板測(cè)試、深度測(cè)試。如果某項(xiàng)測(cè)試不通過,則不會(huì)進(jìn)行下一步,而只有所有測(cè)試都通過的情況下才會(huì)執(zhí)行混合操作。因此,在使用Alpha測(cè)試的情況下,透明的像素就不需要經(jīng)過模板測(cè)試和深度測(cè)試了;而如果使用混合操作,即使透明的像素也需要進(jìn)行模板測(cè)試和深度測(cè)試,性能會(huì)有所下降。還有一點(diǎn):對(duì)于那些“透明”的像素來說,如果使用Alpha測(cè)試,則“透明”的像素不會(huì)通過測(cè)試,因此像素的深度值不會(huì)被修改;而使用混合操作時(shí),雖然像素的顏色沒有被修改,但它的深度值則有可能被修改掉了。
因此,如果所有的像素都是“透明”或“不透明”,沒有“半透明”時(shí),應(yīng)該盡量采用Alpha測(cè)試而不是采用混合操作。當(dāng)需要繪制半透明像素時(shí),才采用混合操作。
3、模板測(cè)試
模板測(cè)試是所有OpenGL測(cè)試中比較復(fù)雜的一種。
首先,模板測(cè)試需要一個(gè)模板緩沖區(qū),這個(gè)緩沖區(qū)是在初始化OpenGL時(shí)指定的。如果使用GLUT工具包,可以在調(diào)用glutInitDisplayMode函數(shù)時(shí)在參數(shù)中加上GLUT_STENCIL,例如:
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_STENCIL);
在Windows操作系統(tǒng)中,即使沒有明確要求使用模板緩沖區(qū),有時(shí)候也會(huì)分配模板緩沖區(qū)。但為了保證程序的通用性,最好還是明確指定使用模板緩沖區(qū)。如果確實(shí)沒有分配模板緩沖區(qū),則所有進(jìn)行模板測(cè)試的像素全部都會(huì)通過測(cè)試。
通過glEnable/glDisable可以啟用或禁用模板測(cè)試。
glEnable(GL_STENCIL_TEST); // 啟用模板測(cè)試
glDisable(GL_STENCIL_TEST); // 禁用模板測(cè)試
OpenGL在模板緩沖區(qū)中為每個(gè)像素保存了一個(gè)“模板值”,當(dāng)像素需要進(jìn)行模板測(cè)試時(shí),將設(shè)定的模板參考值與該像素的“模板值”進(jìn)行比較,符合條件的通過測(cè)試,不符合條件的則被丟棄,不進(jìn)行繪制。
條件的設(shè)置與Alpha測(cè)試中的條件設(shè)置相似。但注意Alpha測(cè)試中是用浮點(diǎn)數(shù)來進(jìn)行比較,而模板測(cè)試則是用整數(shù)來進(jìn)行比較。比較也有八種情況:始終通過、始終不通過、大于則通過、小于則通過、大于等于則通過、小于等于則通過、等于則通過、不等于則通過。
glStencilFunc(GL_LESS, 3, mask);
這段代碼設(shè)置模板測(cè)試的條件為:“小于3則通過”。glStencilFunc的前兩個(gè)參數(shù)意義與glAlphaFunc的兩個(gè)參數(shù)類似,第三個(gè)參數(shù)的意義為:如果進(jìn)行比較,則只比較mask中二進(jìn)制為1的位。例如,某個(gè)像素模板值為5(二進(jìn)制101),而mask的二進(jìn)制值為00000011,因?yàn)橹槐容^最后兩位,5的最后兩位為01,其實(shí)是小于3的,因此會(huì)通過測(cè)試。
如何設(shè)置像素的“模板值”呢?glClear函數(shù)可以將所有像素的模板值復(fù)位。代碼如下:
glClear(GL_STENCIL_BUFFER_BIT);
可以同時(shí)復(fù)位顏色值和模板值:
glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
正如可以使用glClearColor函數(shù)來指定清空屏幕后的顏色那樣,也可以使用glClearStencil函數(shù)來指定復(fù)位后的“模板值”。
每個(gè)像素的“模板值”會(huì)根據(jù)模板測(cè)試的結(jié)果和深度測(cè)試的結(jié)果而進(jìn)行改變。
glStencilOp(fail, zfail, zpass);
該函數(shù)指定了三種情況下“模板值”該如何變化。第一個(gè)參數(shù)表示模板測(cè)試未通過時(shí)該如何變化;第二個(gè)參數(shù)表示模板測(cè)試通過,但深度測(cè)試未通過時(shí)該如何變化;第三個(gè)參數(shù)表示模板測(cè)試和深度測(cè)試均通過時(shí)該如何變化。如果沒有起用模板測(cè)試,則認(rèn)為模板測(cè)試總是通過;如果沒有啟用深度測(cè)試,則認(rèn)為深度測(cè)試總是通過)
變化可以是:
GL_KEEP(不改變,這也是默認(rèn)值),
GL_ZERO(回零),
GL_REPLACE(使用測(cè)試條件中的設(shè)定值來代替當(dāng)前模板值),
GL_INCR(增加1,但如果已經(jīng)是最大值,則保持不變),
GL_INCR_WRAP(增加1,但如果已經(jīng)是最大值,則從零重新開始),
GL_DECR(減少1,但如果已經(jīng)是零,則保持不變),
GL_DECR_WRAP(減少1,但如果已經(jīng)是零,則重新設(shè)置為最大值),
GL_INVERT(按位取反)。
在新版本的OpenGL中,允許為多邊形的正面和背面使用不同的模板測(cè)試條件和模板值改變方式,于是就有了glStencilFuncSeparate函數(shù)和glStencilOpSeparate函數(shù)。這兩個(gè)函數(shù)分別與glStencilFunc和glStencilOp類似,只在最前面多了一個(gè)參數(shù)face,用于指定當(dāng)前設(shè)置的是哪個(gè)面。可以選擇GL_FRONT, GL_BACK, GL_FRONT_AND_BACK。
注意:模板緩沖區(qū)與深度緩沖區(qū)有一點(diǎn)不同。無論是否啟用深度測(cè)試,當(dāng)有像素被繪制時(shí),總會(huì)重新設(shè)置該像素的深度值(除非設(shè)置glDepthMask(GL_FALSE);)。而模板測(cè)試如果不啟用,則像素的模板值會(huì)保持不變,只有啟用模板測(cè)試時(shí)才有可能修改像素的模板值。(這一結(jié)論是我自己的實(shí)驗(yàn)得出的,暫時(shí)沒發(fā)現(xiàn)什么資料上是這樣寫。如果有不正確的地方,歡迎指正)
另外,模板測(cè)試雖然是從OpenGL 1.0就開始提供的功能,但是對(duì)于個(gè)人計(jì)算機(jī)而言,硬件實(shí)現(xiàn)模板測(cè)試的似乎并不多,很多計(jì)算機(jī)系統(tǒng)直接使用CPU運(yùn)算來完成模板測(cè)試。因此在一些老的顯卡,或者是多數(shù)集成顯卡上,大量而頻繁的使用模板測(cè)試可能造成程序運(yùn)行效率低下。即使是當(dāng)前配置比較高端的個(gè)人計(jì)算機(jī),也盡量不要使用glStencilFuncSeparate和glStencilOpSeparate函數(shù)。
從前面所講可以知道,使用剪裁測(cè)試可以把繪制區(qū)域限制在一個(gè)矩形的區(qū)域內(nèi)。但如果需要把繪制區(qū)域限制在一個(gè)不規(guī)則的區(qū)域內(nèi),則需要使用模板測(cè)試。
例如:繪制一個(gè)湖泊,以及周圍的樹木,然后繪制樹木在湖泊中的倒影。為了保證倒影被正確的限制在湖泊表面,可以使用模板測(cè)試。具體的步驟如下:
(1) 關(guān)閉模板測(cè)試,繪制地面和樹木。
(2) 開啟模板測(cè)試,使用glClear設(shè)置所有像素的模板值為0。
(3) 設(shè)置glStencilFunc(GL_ALWAYS, 1, 1); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);繪制湖泊水面。這樣一來,湖泊水面的像素的“模板值”為1,而其它地方像素的“模板值”為0。
(4) 設(shè)置glStencilFunc(GL_EQUAL, 1, 1); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);繪制倒影。這樣一來,只有“模板值”為1的像素才會(huì)被繪制,因此只有“水面”的像素才有可能被倒影的像素替換,而其它像素則保持不變。
我們?nèi)匀粊砜匆粋€(gè)實(shí)際的例子。這是一個(gè)比較簡(jiǎn)單的場(chǎng)景:空間中有一個(gè)球體,一個(gè)平面鏡。我們站在某個(gè)特殊的觀察點(diǎn),可以看到球體在平面鏡中的鏡像,并且鏡像處于平面鏡的邊緣,有一部分因?yàn)槠矫骁R大小的限制,而無法顯示出來。整個(gè)場(chǎng)景的效果如下圖:

繪制這個(gè)場(chǎng)景的思路跟前面提到的湖面倒影是接近的。
假設(shè)平面鏡所在的平面正好是X軸和Y軸所確定的平面,則球體和它在平面鏡中的鏡像是關(guān)于這個(gè)平面對(duì)稱的。我們用一個(gè)draw_sphere函數(shù)來繪制球體,先調(diào)用該函數(shù)以繪制球體本身,然后調(diào)用glScalef(1.0f, 1.0f, -1.0f); 再調(diào)用draw_sphere函數(shù),就可以繪制球體的鏡像。
另外需要注意的地方就是:因?yàn)槭抢L制三維的場(chǎng)景,我們開啟了深度測(cè)試。但是站在觀察者的位置,球體的鏡像其實(shí)是在平面鏡的“背后”,也就是說,如果按照常規(guī)的方式繪制,平面鏡會(huì)把鏡像覆蓋掉,這不是我們想要的效果。解決辦法就是:設(shè)置深度緩沖區(qū)為只讀,繪制平面鏡,然后設(shè)置深度緩沖區(qū)為可寫的狀態(tài),繪制平面鏡“背后”的鏡像。
有的朋友可能會(huì)問:如果在繪制鏡像的時(shí)候關(guān)閉深度測(cè)試,那鏡像不就不會(huì)被平面鏡遮擋了嗎?為什么還要開啟深度測(cè)試,又需要把深度緩沖區(qū)設(shè)置為只讀呢?實(shí)際情況是:雖然關(guān)閉深度測(cè)試確實(shí)可以讓鏡像不被平面鏡遮擋,但是鏡像本身會(huì)出現(xiàn)若干問題。我們看到的鏡像是一個(gè)球體,但實(shí)際上這個(gè)球體是由很多的多邊形所組成的,這些多邊形有的代表了我們所能看到的“正面”,有的則代表了我們不能看到的“背面”。如果關(guān)閉深度測(cè)試,而有的“背面”多邊形又比“正面”多邊形先繪制,就會(huì)造成球體的背面反而把正面擋住了,這不是我們想要的效果。為了確保正面可以擋住背面,應(yīng)該開啟深度測(cè)試。
繪制部分的代碼如下:
void draw_sphere()
{
// 設(shè)置光源
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
{
GLfloat
pos[] = {5.0f, 5.0f, 0.0f, 1.0f},
ambient[] = {0.0f, 0.0f, 1.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, pos);
glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
}
// 繪制一個(gè)球體
glColor3f(1, 0, 0);
glPushMatrix();
glTranslatef(0, 0, 2);
glutSolidSphere(0.5, 20, 20);
glPopMatrix();
}
void display(void)
{
// 清除屏幕
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 設(shè)置觀察點(diǎn)
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60, 1, 5, 25);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(5, 0, 6.5, 0, 0, 0, 0, 1, 0);
glEnable(GL_DEPTH_TEST);
// 繪制球體
glDisable(GL_STENCIL_TEST);
draw_sphere();
// 繪制一個(gè)平面鏡。在繪制的同時(shí)注意設(shè)置模板緩沖。
// 另外,為了保證平面鏡之后的鏡像能夠正確繪制,在繪制平面鏡時(shí)需要將深度緩沖區(qū)設(shè)置為只讀的。
// 在繪制時(shí)暫時(shí)關(guān)閉光照效果
glClearStencil(0);
glClear(GL_STENCIL_BUFFER_BIT);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glEnable(GL_STENCIL_TEST);
glDisable(GL_LIGHTING);
glColor3f(0.5f, 0.5f, 0.5f);
glDepthMask(GL_FALSE);
glRectf(-1.5f, -1.5f, 1.5f, 1.5f);
glDepthMask(GL_TRUE);
// 繪制一個(gè)與先前球體關(guān)于平面鏡對(duì)稱的球體,注意光源的位置也要發(fā)生對(duì)稱改變
// 因?yàn)槠矫骁R是在X軸和Y軸所確定的平面,所以只要Z坐標(biāo)取反即可實(shí)現(xiàn)對(duì)稱
// 為了保證球體的繪制范圍被限制在平面鏡內(nèi)部,使用模板測(cè)試
glStencilFunc(GL_EQUAL, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glScalef(1.0f, 1.0f, -1.0f);
draw_sphere();
// 交換緩沖
glutSwapBuffers();
// 截圖
grab();
}
其中display函數(shù)的末尾調(diào)用了一個(gè)grab函數(shù),它保存當(dāng)前的圖象到一個(gè)BMP文件。這個(gè)函數(shù)本來是在第十課和第十一課中都有所使用的。但是我發(fā)現(xiàn)它有一個(gè)bug,現(xiàn)在進(jìn)行了修改:在函數(shù)最開頭的部分加上一句:glReadBuffer(GL_FRONT);即可。注意這個(gè)函數(shù)最好是在繪制完畢后(如果是使用雙緩沖,則應(yīng)該在交換緩沖后)立即調(diào)用。
大家可能會(huì)有這樣的感覺:模板測(cè)試的設(shè)置是如此復(fù)雜,它可以實(shí)現(xiàn)的功能應(yīng)該很多,肯定不止這樣一個(gè)“限制像素的繪制范圍”。事實(shí)上也是如此,不過現(xiàn)在我們暫時(shí)只講這些。
其實(shí),如果不需要繪制半透明效果,有時(shí)候可以用混合功能來代替模板測(cè)試。就繪制鏡像這個(gè)例子來說,可以采用下面的步驟:
(1) 清除屏幕,在glClearColor中設(shè)置合適的值確保清除屏幕后像素的Alpha值為0.0
(2) 關(guān)閉混合功能,繪制球體本身,設(shè)置合適的顏色(或者光照與材質(zhì))以確保所有被繪制的像素的Alpha值為0.0
(3) 繪制平面鏡,設(shè)置合適的顏色(或者光照與材質(zhì))以確保所有被繪制的像素的Alpha值為1.0
(4) 啟用混合功能,用GL_DST_ALPHA作為源因子,GL_ONE_MINUS_DST_ALPHA作為目標(biāo)因子,這樣就實(shí)現(xiàn)了只有原來Alpha為1.0的像素才能被修改,而原來Alpha為0.0的像素則保持不變。這時(shí)再繪制鏡像物體,注意確保所有被繪制的像素的Alpha值為1.0。
在有的OpenGL實(shí)現(xiàn)中,模板測(cè)試是軟件實(shí)現(xiàn)的,而混合功能是硬件實(shí)現(xiàn)的,這時(shí)候可以考慮這樣的代替方法以提高運(yùn)行效率。但是并非所有的模板測(cè)試都可以用混合功能來代替,并且這樣的代替顯得不自然,復(fù)雜而且容易出錯(cuò)。
另外始終注意:使用混合來模擬時(shí),即使某個(gè)像素原來的Alpha值為0.0,以致于在繪制后其顏色不會(huì)有任何變化,但是這個(gè)像素的深度值有可能會(huì)被修改,而如果是使用模板測(cè)試,沒有通過測(cè)試的像素其深度值不會(huì)發(fā)生任何變化。而且,模板測(cè)試和混合功能中,像素模板值的修改方式是不一樣的。
4、深度測(cè)試
在本課的開頭,已經(jīng)簡(jiǎn)單的敘述了深度測(cè)試。這里是完整的內(nèi)容。
深度測(cè)試需要深度緩沖區(qū),跟模板測(cè)試需要模板緩沖區(qū)是類似的。如果使用GLUT工具包,可以在調(diào)用glutInitDisplayMode函數(shù)時(shí)在參數(shù)中加上GLUT_DEPTH,這樣來明確指定要求使用深度緩沖區(qū)。
深度測(cè)試和模板測(cè)試的實(shí)現(xiàn)原理很類似,都是在一個(gè)緩沖區(qū)保存像素的某個(gè)值,當(dāng)需要進(jìn)行測(cè)試時(shí),將保存的值與另一個(gè)值進(jìn)行比較,以確定是否通過測(cè)試。兩者的區(qū)別在于:模板測(cè)試是設(shè)定一個(gè)值,在測(cè)試時(shí)用這個(gè)設(shè)定值與像素的“模板值”進(jìn)行比較,而深度測(cè)試是根據(jù)頂點(diǎn)的空間坐標(biāo)計(jì)算出深度,用這個(gè)深度與像素的“深度值”進(jìn)行比較。也就是說,模板測(cè)試需要指定一個(gè)值作為比較參考,而深度測(cè)試中,這個(gè)比較用的參考值是OpenGL根據(jù)空間坐標(biāo)自動(dòng)計(jì)算的。
通過glEnable/glDisable函數(shù)可以啟用或禁用深度測(cè)試。
glEnable(GL_DEPTH_TEST); // 啟用深度測(cè)試
glDisable(GL_DEPTH_TEST); // 禁用深度測(cè)試
至于通過測(cè)試的條件,同樣有八種,與Alpha測(cè)試中的條件設(shè)置相同。條件設(shè)置是通過glDepthFunc函數(shù)完成的,默認(rèn)值是GL_LESS。
glDepthFunc(GL_LESS);
與模板測(cè)試相比,深度測(cè)試的應(yīng)用要頻繁得多。幾乎所有的三維場(chǎng)景繪制都使用了深度測(cè)試。正因?yàn)檫@樣,幾乎所有的OpenGL實(shí)現(xiàn)都對(duì)深度測(cè)試提供了硬件支持,所以雖然兩者的實(shí)現(xiàn)原理類似,但深度測(cè)試很可能會(huì)比模板測(cè)試快得多。當(dāng)然了,兩種測(cè)試在應(yīng)用上很少有交集,一般不會(huì)出現(xiàn)使用一種測(cè)試去代替另一種測(cè)試的情況。
小結(jié):
本次課程介紹了OpenGL所提供的四種測(cè)試,分別是剪裁測(cè)試、Alpha測(cè)試、模板測(cè)試、深度測(cè)試。OpenGL會(huì)對(duì)每個(gè)即將繪制的像素進(jìn)行以上四種測(cè)試,每個(gè)像素只有通過一項(xiàng)測(cè)試后才會(huì)進(jìn)入下一項(xiàng)測(cè)試,而只有通過所有測(cè)試的像素才會(huì)被繪制,沒有通過測(cè)試的像素會(huì)被丟棄掉,不進(jìn)行繪制。每種測(cè)試都可以單獨(dú)的開啟或者關(guān)閉,如果某項(xiàng)測(cè)試被關(guān)閉,則認(rèn)為所有像素都可以順利通過該項(xiàng)測(cè)試。
剪裁測(cè)試是指:只有位于指定矩形內(nèi)部的像素才能通過測(cè)試。
Alpha測(cè)試是指:只有Alpha值與設(shè)定值相比較,滿足特定關(guān)系條件的像素才能通過測(cè)試。
模板測(cè)試是指:只有像素模板值與設(shè)定值相比較,滿足特定關(guān)系條件的像素才能通過測(cè)試。
深度測(cè)試是指:只有像素深度值與新的深度值比較,滿足特定關(guān)系條件的像素才能通過測(cè)試。
上面所說的特定關(guān)系條件可以是大于、小于、等于、大于等于、小于等于、不等于、始終通過、始終不通過這八種。
模板測(cè)試需要模板緩沖區(qū),深度測(cè)試需要深度緩沖區(qū)。這些緩沖區(qū)都是在初始化OpenGL時(shí)指定的。如果使用GLUT工具包,則可以在glutInitDisplayMode函數(shù)中指定。無論是否開啟深度測(cè)試,OpenGL在像素被繪制時(shí)都會(huì)嘗試修改像素的深度值;而只有開啟模板測(cè)試時(shí),OpenGL才會(huì)嘗試修改像素的模板值,模板測(cè)試被關(guān)閉時(shí),OpenGL在像素被繪制時(shí)也不會(huì)修改像素的模板值。
利用這些測(cè)試操作可以控制像素被繪制或不被繪制,從而實(shí)現(xiàn)一些特殊效果。利用混合功能可以實(shí)現(xiàn)半透明,通過設(shè)置也可以實(shí)現(xiàn)完全透明,因而可以模擬像素顏色的繪制或不繪制。但注意,這里僅僅是顏色的模擬。OpenGL可以為像素保存顏色、深度值和模板值,利用混合實(shí)現(xiàn)透明時(shí),像素顏色不發(fā)生變化,但深度值則會(huì)可能變化,模板值受glStencilFunc函數(shù)中第三個(gè)參數(shù)影響;利用測(cè)試操作實(shí)現(xiàn)透明時(shí),像素顏色不發(fā)生變化,深度值也不發(fā)生變化,模板值受glStencilFunc函數(shù)中前兩個(gè)參數(shù)影響。
此外,修正了第十課、第十一課中的一個(gè)函數(shù)中的bug。在grab函數(shù)中,應(yīng)該在最開頭加上一句glReadBuffer(GL_FRONT);以保證讀取到的內(nèi)容正好就是顯示的內(nèi)容。