模板測試(Stencil testing)
當(dāng)片段著色器處理完片段之后,模板測試(stencil test) 就開始執(zhí)行了,和深度測試一樣,它能丟棄一些片段。仍然保留下來的片段進入深度測試階段,深度測試可能丟棄更多。模板測試基于另一個緩沖,這個緩沖叫做模板緩沖(stencil buffer),我們被允許在渲染時更新它來獲取有意思的效果。
模板緩沖中的模板值(stencil value)通常是8位的,因此每個片段(像素)共有256種不同的模板值。這樣我們就能將這些模板值設(shè)置為我們鏈接的,然后在模板測試時根據(jù)這個模板值,我們就可以決定丟棄或保留它了。
每個窗口庫都需要為你設(shè)置模板緩沖。GLFW自動做了這件事,所以你不必告訴GLFW去創(chuàng)建它,但是其他庫可能沒默認創(chuàng)建模板庫,所以一定要查看你使用的庫的文檔。
下面是一個模板緩沖的簡單例子:

先通過設(shè)置所有片段的模板值為0清空模板緩沖,然后開啟矩形片段用1填充。場景中的模板值為1的那些片段才會被渲染(其他的都被丟棄)。
無論我們在渲染哪里的片段,模板緩沖操作都允許我們把模板緩沖設(shè)置為一個特定值。改變模板緩沖的內(nèi)容實際上就是對模板緩沖進行寫入。在同一次(或接下來的)渲染迭代我們可以讀取這些值來決定丟棄還是保留這些片段。當(dāng)使用模板緩沖的時候,你可以隨心所欲,通常是這樣的:
- 開啟模板緩沖寫入。
- 渲染物體,更新模板緩沖。
- 關(guān)閉模板緩沖寫入。
- 渲染(其他)物體,這次基于模板緩沖內(nèi)容丟棄特定片段。
使用模板緩沖我們可以基于場景中已經(jīng)繪制的片段,來決定是否丟棄特定的片段。
你可以開啟GL_STENCIL_TEST來開啟模板測試。接著所有渲染函數(shù)調(diào)用都會以這樣或那樣的方式影響到模板緩沖。
glEnable(GL_STENCIL_TEST);
要注意的是,像顏色和深度緩沖一樣,在每次循環(huán),你也得清空模板緩沖。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
同時,和深度測試的glDepthMask函數(shù)一樣,模板緩沖也有一個相似函數(shù)。glStencilMask允許我們給模板值設(shè)置一個位遮罩(bitmask),它與模板值進行按位與(and)運算決定緩沖是否可寫。默認設(shè)置的位遮罩都是1,這樣就不會影響輸出,但是如果我們設(shè)置為0x00,所有寫入深度緩沖最后都是0。這和深度緩沖的glDepthMask(GL_FALSE)很類似:
// 0xFF == 0b11111111
//此時,模板值與它進行按位與運算結(jié)果是模板值,模板緩沖可寫
glStencilMask (0xFF);
// 0x00 == 0b00000000 == 0
//此時,模板值與它進行按位與運算結(jié)果是0,模板緩沖不可寫
glStencilMask (0x00);
大多數(shù)情況你的模板遮罩(stencil mask)寫為0x00或0xFF就行,但是最好知道有一個選項可以自定義位遮罩。
模板函數(shù)(stencil functions)
和深度測試一樣,我們也有幾個不同控制權(quán),決定何時模板測試通過或失敗以及它怎樣影響模板緩沖。一共有兩種函數(shù)可供我們使用去配置模板測試:glStencilFunc和glStencilOp。
void glStencilFunc(GLenum func, GLint ref, GLuint mask)函數(shù)有三個參數(shù):
func:設(shè)置模板測試操作。這個測試操作應(yīng)用到已經(jīng)儲存的模板值和glStencilFunc的ref值上,可用的選項是:GL_NEVER、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL、GL_ALWAYS。它們的語義和深度緩沖的相似。
ref:指定模板測試的引用值。模板緩沖的內(nèi)容會與這個值對比。
mask:指定一個遮罩,在模板測試對比引用值和儲存的模板值前,對它們進行按位與(and)操作,初始設(shè)置為1。
在上面簡單模板的例子里,方程應(yīng)該設(shè)置為:
glStencilFunc(GL_EQUAL, 1, 0xFF)
它會告訴OpenGL,無論何時,一個片段模板值等于(GL_EQUAL)引用值1,片段就能通過測試被繪制了,否則就會被丟棄。
但是glStencilFunc只描述了OpenGL對模板緩沖做什么,而不是描述我們?nèi)绾胃戮彌_。這就需要glStencilOp登場了。
void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)函數(shù)包含三個選項,我們可以指定每個選項的動作:
sfail: 如果模板測試失敗將采取的動作。
dpfail: 如果模板測試通過,但是深度測試失敗時采取的動作。
dppass: 如果深度測試和模板測試都通過,將采取的動作。
每個選項都可以使用下列任何一個動作。
| 操作 | 描述 |
|---|---|
| GL_KEEP | 保持現(xiàn)有的模板值 |
| GL_ZERO | 將模板值置為0 |
| GL_REPLACE | 將模板值設(shè)置為用glStencilFunc函數(shù)設(shè)置的ref值 |
| GL_INCR | 如果模板值不是最大值就將模板值+1 |
| GL_INCR_WRAP與GL_INCR | 一樣將模板值+1,如果模板值已經(jīng)是最大值則設(shè)為0 |
| GL_DECR | 如果模板值不是最小值就將模板值-1 |
| GL_DECR_WRAP與GL_DECR | 一樣將模板值-1,如果模板值已經(jīng)是最小值則設(shè)為最大值 |
| GL_INVERT | Bitwise inverts the current stencil buffer value. |
glStencilOp函數(shù)默認設(shè)置為 (GL_KEEP, GL_KEEP, GL_KEEP) ,所以任何測試的任何結(jié)果,模板緩沖都會保留它的值。默認行為不會更新模板緩沖,所以如果你想寫入模板緩沖的話,你必須像任意選項指定至少一個不同的動作。
使用glStencilFunc和glStencilOp,我們就可以指定在什么時候以及我們打算怎么樣去更新模板緩沖了,我們也可以指定何時讓測試通過或不通過。什么時候片段會被拋棄。
物體輪廓(object outlining)
看了前面的部分你未必能理解模板測試是如何工作的,所以我們會展示一個用模板測試實現(xiàn)的一個特別的和有用的功能,叫做物體輪廓(object outlining)。

物體輪廓就像它的名字所描述的那樣,它能夠給每個(或一個)物體創(chuàng)建一個有顏色的邊。在策略游戲中當(dāng)你打算選擇一個單位的時候它特別有用。給物體加上輪廓的步驟如下:
- 在繪制物體前,把模板方程設(shè)置為GL_ALWAYS,用1更新物體將被渲染的片段。
- 渲染物體,寫入模板緩沖。
- 關(guān)閉模板寫入和深度測試。
- 每個物體放大一點點。
- 使用一個不同的片段著色器用來輸出一個純顏色。
- 再次繪制物體,但只是當(dāng)它們的片段的模板值不為1時才進行。
- 開啟模板寫入和深度測試。
這個過程將每個物體的片段模板緩沖設(shè)置為1,當(dāng)我們繪制邊框的時候,我們基本上繪制的是放大版本的物體的通過測試的地方,放大的版本繪制后物體就會有一個邊。我們基本會使用模板緩沖丟棄所有的不是原來物體的片段的放大的版本內(nèi)容。
我們先來創(chuàng)建一個非常基本的片段著色器,它輸出一個邊框顏色。我們簡單地設(shè)置一個固定的顏色值,把這個著色器命名為shaderSingleColor:
void main()
{
outColor = vec4(0.04, 0.28, 0.26, 1.0);
}
我們只打算給兩個箱子加上邊框,所以我們不會對地面做什么。這樣我們要先繪制地面(繪制地板時確保關(guān)閉模板緩沖的寫入),然后再繪制兩個箱子(同時寫入模板緩沖),接著我們繪制放大的箱子(同時丟棄前面已經(jīng)繪制的箱子的那部分片段)。
我們先開啟模板測試,設(shè)置模板、深度測試通過或失敗時采取動作:
glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
如果任何測試失敗我們都什么也不做,我們簡單地保持深度緩沖中當(dāng)前所儲存著的值。如果模板測試和深度測試都成功了,我們就將儲存著的模板值替換為1,我們要用glStencilFunc來做這件事。
我們清空模板緩沖為0,為箱子的所有繪制的片段的模板緩沖更新為1:
glStencilFunc(GL_ALWAYS, 1, 0xFF); //所有片段都要寫入模板緩沖
glStencilMask(0xFF); // 設(shè)置模板緩沖為可寫狀態(tài)
normalShader.Use();
DrawTwoContainers();
使用GL_ALWAYS模板測試函數(shù),我們確保箱子的每個片段用模板值1更新模板緩沖。因為片段總會通過模板測試,在我們繪制它們的地方,模板緩沖用引用值(ref)更新。
現(xiàn)在箱子繪制之處,模板緩沖更新為1了,我們將要繪制放大的箱子,但是這次關(guān)閉模板緩沖的寫入:
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // 禁止修改模板緩沖
glDisable(GL_DEPTH_TEST);
shaderSingleColor.Use();
DrawTwoScaledUpContainers();
我們把模板方程設(shè)置為GL_NOTEQUAL,它保證我們只箱子上不等于1的部分,這樣只繪制前面繪制的箱子外圍的那部分。注意,我們也要關(guān)閉深度測試,這樣放大的的箱子也就是邊框才不會被地面覆蓋。做完之后還要保證再次開啟深度緩沖。
場景中的物體邊框的繪制方法最后看起來像這樣:
glEnable (GL_DEPTH_TEST);
glStencilOp (GL_KEEP, GL_KEEP, GL_REPLACE);
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glStencilMask (0x00); // 繪制地板時確保關(guān)閉模板緩沖的寫入
normalShader.Use ();
DrawFloor ()
glStencilFunc (GL_ALWAYS, 1, 0xFF);
glStencilMask (0xFF);
DrawTwoContainers ();
glStencilFunc (GL_NOTEQUAL, 1, 0xFF);
glStencilMask (0x00);
glDisable (GL_DEPTH_TEST);
shaderSingleColor.Use ();
DrawTwoScaledUpContainers ();
glStencilMask (0xFF);
glEnable (GL_DEPTH_TEST);
這個邊框的算法的結(jié)果在深度測試教程的那個場景中,看起來像這樣:

在這里查看源碼和著色器,看看完整的物體邊框算法是怎樣的。
項目代碼在這。
你可以看到兩個箱子邊框重合通常正是我們希望得到的(想想策略游戲中,我們打算選擇10個單位;我們通常會希望把邊界合并)。如果你想要讓每個物體都有自己的邊界那么你需要為每個物體清空模板緩沖,創(chuàng)造性地使用深度緩沖。
你目前看到的物體邊框算法在一些游戲中顯示備選物體(想象策略游戲)非常常用,這樣的算法可以在一個模型類中輕易實現(xiàn)。你可以簡單地在模型類設(shè)置一個布爾類型的標(biāo)識來決定是否繪制邊框。如果你想要更多的創(chuàng)造性,你可以使用后處理(post-processing)過濾比如高斯模糊來使邊框看起來更自然。
除了物體邊框以外,模板測試還有很多其他的應(yīng)用目的,比如在后視鏡中繪制紋理,這樣它會很好的適合鏡子的形狀,比如使用一種叫做shadow volumes的模板緩沖技術(shù)渲染實時陰影。模板緩沖在我們的已擴展的OpenGL工具箱中給我們提供了另一種好用工具。