Stencil Test的應用總結

0. 前言

一直以來,對Stencil的Operation知其然而不知其因此然,不太明白提供這些Operation更新Stencil有什麼用。而GPU的Stencil更新機制實際上是根據應用的需求才這麼設計的,理解好Stencil的應用狀況,才能理解好Stencil Test的更新機制。所以,本文將對其主要的應用作下梳理,加強對Stencil Test的認知。html

1. Stencil Test簡介

在OpenGL/Direct3D的流水線中,Stencil Test被納入Pixel Shader以後的Output Merger Stage,其處理單位是像素(若是MSAA打開,則是Sample)。Stencil Test的有兩個要點:node

  • Stencil值的測試,用於剔除像素
  • Stencil值的更新,用於產生實現特定效果的Stencil值

Stencil值的測試很簡單——從Stencil Buffer 裏讀出該像素的Stencil值(8bit的UINT)與參考值比較,知足比較條件則pass最終畫出(假設能經過Depth Test或其餘剔除),不然fail直接剔除。比較函數以及參考值都是經過API設定,例如OpenGL的glStencilFunc(GLenum func, GLint ref, GLuint mask)函數。與Depth Test的比較函數相似,Stencil Test的比較函數包括NEVER, LESS, LEQUAL, GREATER, GEQUAL, EQUAL, NOTEQUAL和ALWAYS。 算法

經過Stencil值的測試咱們能夠限制渲染的區域,好比下面的例子把渲染區域限制爲Stencil值等於1的區域。 windows

clipboard.png
圖1 給定中間圖片中的Stencil值,將比較條件設爲EQUAL,參考值設爲1時,左側圖片的color經過Stencil Test後。 ide

咱們看到,只要Stencil Buffer裏存儲了指望的Stencil值,咱們就能夠經過Stencil Test剔除像素來畫出指望的區域,正如Stencil自己的含義(模板)。而事實上問題重點常在於如何構造出指望的Stencil值,除了少數應用使用特定已知的模板外,大部分是在渲染過程當中產生須要的模板,這就是要講的第二個要點——Stencil值的更新,它是實現各類效果的關鍵。
在OpenGL中,寫Stencil Buffer的開啓與否是經過函數glStencilMask(GLuint mask)設置的,這個函數的參數mask對應Stencil值的各個bit是否容許寫入,當mask設爲0表示徹底關閉寫Stencil Buffer。在開啓寫Stencil Buffer的狀況下,不管像素是否被Stencil Test或Depth Test剔除,GPU都會執行Stencil值的更新。更新方式是跟Stencil Test和Depth Test的測試結果緊密聯繫的,OpenGL/D3D把測試結果分爲三種狀況:函數

  • sfail: Stencil Test fail
  • dpfail: Stencil Test pass但Depth Test fail
  • dppass: Stencil Test pass且Depth Test pass

經過API glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)能夠分別爲這三種測試結果指定更新該像素Stencil數值的方式,可選的方式包括測試

Action Description
GL_KEEP 當前的Stencil值保持不變
GL_ZERO 將Stencil值更新爲0.
GL_REPLACE 將Stencil值替換爲參考值
GL_INCR 若當前Stencil值小於最大值,則加1
GL_INCR_WRAP Stencil值加1,若超過最大值則wrap爲0
GL_DECR 若當前Stencil值大於最小值,則減1
GL_DECR_WRAP Stencil值減1,若小於0則wrap爲最大值
GL_INVERT 按位反轉當前Stencil值

GPU在執行Stencil Test和Depth Test(沒有Enable Depth Test的話將一直pass),按照測試結果(sfail,dpfail,dppass)對應的方式算出新的Stencil值,若有發生變化則寫回Stencil Buffer裏。
正是有上面的多種更新方式,以及Depth Test和Stencil Test的緊密聯繫使得Stencil Test能經過多個pass實現多種效果。網站

2. Stencil Test的應用

從上面能夠看出,Stencil應用的過程大概是這樣:ui

  • 開啓寫Stencil Buffer
  • 渲染物體,更新Stencil Buffer的內容
  • 關閉寫Stencil Buffer
  • 渲染(其餘)物體,經過Stencil Buffer的內容把部分像素剔除掉。

咱們看下不一樣的更新機制如何實現特定需求的。spa

2.1 輪廓

給物體添加輪廓的思路很簡單——把同一個物體畫兩遍,其中第一遍正常地渲染物體,第二遍將原物體作微小拉伸(比原來多出輪廓),並讓Pixel Shader輸出輪廓顏色。同時要使第一遍所畫的像素位置上在第二遍渲染中不會再被畫出新的像素,即須要使用一種剔除方法,使第二次渲染時只保留兩次渲染物體的非重疊部分。
一開始咱們可能會想到用Depth Test——第一次渲染時打開Depth Write,在第二遍渲染時在Vertex Shader給構成網格的每一個頂點設一個足夠大的深度值,這樣第二次渲染時重疊部分會在GPU的Depth Test中由於遮擋而被剔除。然而,當場景裏存在其餘背景物體時,輪廓也會被遮擋住。所以,Depth Test並非過濾像素區域的好方法,而這樣的需求場景,原本就是Stencil Test的舞臺。

利用Stencil Test畫輪廓的大概步驟是這樣的:
1)將sfail, dfail, dpass的更新方式分別設爲KEEP,KEEP,REPLACE

glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

2)關閉寫Stencil Buffer,按正常方式渲染背景。

glStencilMask(0x00);
    //draw the background
    ...

3)開啓寫Stencil Buffer,比較函數爲ALWAYS,Stencil Test參考值設爲1。渲染物體,這樣渲染後物體每一個像素的Stencil值將等於1

glStencilFunc(GL_ALWAYS, 1, 0xFF);
    glStencilMask(0xFF);
    //draw the object
    ...

4)關閉寫Stencil Buffer,比較函數設爲NOTEQUAL,關閉Depth Test。將物體作微小拉伸並渲染物體,Pixel Shader輸出輪廓顏色

glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
    glStencilMask(0x00);
    glDisable(GL_DEPTH_TEST);
    //draw the scaled object
    ...

clipboard.png
圖 2 輪廓渲染

這個方法的思想很簡單:第一次渲染物體後,最終全部畫出的像素對應的Stencil值均爲1,而第二次渲染時只畫出Stencil值不等於1的輪廓,從而實現了指望的效果。圖2是用learnopengl教程在Stencil這一章中畫出的例子,我的以爲這個網站的教程很適合初學OpenGL,裏面對第三方庫怎樣build和使用有詳細的解釋,而且從最基本的例子開始展開按部就班,最重要的是每一個例子都有代碼可參考。

2.2 Dissolve

在Graphics或Video領域,Dissolve用於描述一種過渡效果——一張圖片漸漸地褪去,在同時另外一張圖片替換原來的圖片。Dissolve可以使用Stencil Buffer實現,在一開始將Stencil Buffer清零,經過設置不一樣的比較函數,使第一張圖片所有畫出,而第二張圖片所有不畫。接着逐幀改變Stencil Buffer,逐漸增長1的個數,並以一樣的方式畫兩張圖片,直到最後Stencil Buffer全爲1,只畫出了第二張圖片的全部像素。

clipboard.png

實現Dissolve的其中一幀的過程大概是
1) 開啓Stencil,並將stencil比較函數設爲GL_NEVER,參考值設爲1,將sfail的更新方式設爲GL_REPLACE,

glStencilFunc(GL_NEVER, 1, 1)
    glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP)

2)經過畫幾何體或glDrawPixels函數往Stencil Buffer裏寫入特定的Dissolve樣式,因爲Stencil Test一直fail,全部這些像素不會被畫出
3)關閉寫Stencil Buffer,將比較函數設爲GL_EQUAL,參考值設爲0,並畫第一張圖片,這樣只有模板上爲0值的地方纔畫出這張圖片的像素

glStencilFuncGL_EQUAL, 0, 1(GL_EQUAL, 0, 1).
    //draw the 1st image
    ...

4)改變比較的參考值爲1,並畫第二張圖片

glStencilFuncGL_EQUAL, 1, 1(GL_EQUAL, 1, 1).
    //draw the 2nd image
    ...

2.3 Shadow Volume

以上的更新機制比較簡單,這裏咱們繼續看一個相對較複雜的應用——Shadow Volume,Shadow Volume最先是Frank Crow於1977年提出的一種爲3D場景添加陰影的算法,後來也有其餘研究者獨立地提出一些變種算法。The Theory of Stencil Shadow Volumes給出了Shadow Volume的詳細介紹。
Shadow Volume算法旨在光柵化的渲染中,確認出所渲染物體上那些受遮擋影響未能被光源照到像素,生成一個模板,而後剔除對應的像素不作lighting,從而實現陰影效果。該算法的第一步是構造一個Shadow Volume(這裏不是指算法名字了,而是一個圖3那樣的Volume),其基本步驟是

  • 以光源爲視點,找出遮擋物的全部輪廓邊(那些同時被正面三角形和反面三角形包含的邊)
  • 將輪廓邊上的每一點向光源與其連線的方向延伸,全部邊構成的多邊形造成一個立體(即Shadow Volume,圖3的陰影部分)的四周表面。
  • 另外可能要加上Front Cap或Back Cop,從而造成封閉的Shadow Volume。加何種Cap因不一樣算法而異。

clipboard.png
圖3 遮擋物在光源的延伸方向上造成的Shadow Volume

在構造Shadow Volume完成後,渲染過程大概以下:

  • 按無光照渲染整個場景,即全部物體都出於陰影中
  • 對於每一個光源,執行如下步驟:

    1. 渲染構造好的Volume,利用深度信息構造出一個模板,使出於光照中的像素在模板上有不一樣的Stencil值
    2. 按有光照渲染整個場景,利用步驟1構造的模板區分陰影區域,使用額外的Blending把渲染結果添加到已有場景中

按照構造模板方法分類,Shadow Volume算法可分爲兩類

  • Depth pass
  • Depth fail

Depth pass和Depth fail分別在dppass和dpfail兩種測試結果更新Stencil值。Wiki裏還提到Exclusive-or的方法,這種方法也是在Depth pass時更新Stencil值,但它只採用了1bit的Stencil值,更新方式爲INVERT,所以並不適用於有多個Shadow Volume重疊的狀況。下面着重看戲這兩種方法對於Stencil Buffer的使用,對二者的優缺點暫不作討論。

2.3.1 Depth pass

Depth pass的思路是分兩次分別渲染Shadow Volume的正面和反面,並用Stencil值記錄位於物體前方的次數。若是正面和反面的次數相等,那麼該位置出於光照中。若是正面的次數比反面多,那麼該位置出於陰影中。由於Stencil值是在經過depth測試時更新的,因此這種方法較Depth pass。Depth pass構造應用模板的步驟爲:

  • 關閉寫Depth Buffer和Color Buffer,設置back-face culling,將dppass的更新方式設爲GL_INCR.
  • 渲染Shadow Volume,因爲Culling,只畫了Shadow Volume的正面.
  • 設置front-face culling,將dppass的更新方式設爲GL_DECR
  • 渲染Shadow Volume,因爲Culling.只畫了Shadow Volume的反面

clipboard.png
圖4 Depth pass Shadow volume

如圖4,箭頭末端的數字分別對應每一個位置通過以上步驟後最終在Stencil Buffer裏的數值,能夠看到,出於陰影中的位置最終爲1,由於它出於Shadow Volume的正面和反面之間,正面未被物體遮住depth pass以後Stencil值增1,而反面被物體遮住depth測試失敗未能將Stencil值減1。當一個位置與眼睛的連線未闖過Shadow Volume(從左到右的第1條連線)或者穿過正反面(第2和第4條連線),那麼意味着該位置在光照中。

2.3.2 Depth fail

另外一種方法Depth fail經過在dpfail時更新Stencil值來構造模板,Depth fail的步驟爲:

  • 關閉寫Depth Buffer和Color Buffer,設置front-face culling,將dpfail的更新方式設爲GL_INCR.
  • 渲染Shadow Volume,因爲Culling,只畫了Shadow Volume的正面.
  • 設置back-face culling,將dpfail的更新方式設爲GL_DECR
  • 渲染Shadow Volume,因爲Culling.只畫了Shadow Volume的反面

Depth fail實際上是depth pass的一個「翻轉版本」——depth pass算出正面和反面在物體前方的次數,而depth fail則算反面和正面在物體後方的次數。這種差別致使了二者在實際應用中有各自的優點和不足,這些超出本文範圍,就不深刻了。這裏是一個提供代碼的depth pass例子:Shadow Volume

2.3.3 Two-Sided Stencil

以上Shadow Volume的正反面是分兩次渲染的,這無疑增長了Vertex Shader的帶寬。事實上能夠利用Two-Sided Stencil功能,對於OpenGL可經過下面兩個函數分別爲Front和Back設置不一樣的更新方式,那麼整個Shadow Volume實際上只須要畫一次,同時畫正面和背面,由GPU根據三角形的Face去選擇更新Stencil值的方式。

void glStencilFuncSeparate(GLenum face​, GLenum func​, GLint ref​, GLuint mask​);
    void glStencilOpSeparate(GLenum face​, GLenum sfail​, GLenum dpfail​, GLenum dppass​);

2.3.4 總結

Shadow Volume算法是將Stencil Buffer的數值當作計數器來使用,用於統計物體每一個位置的正面和反面的數量,以之判斷物體與Shadow Volume的關係。本質上,Stencil Buffer使用來記錄物體與Shadow Volume兩個面的遮擋關係,這也解釋了Stencil值的更新爲何要跟Depth Test的結果綁定在一塊兒。

2.4 其餘

除了上述提到的應用外,Wiki中提到的Stencil Test其餘應用還有Decaling,portal rendering,Reflections,intersection highlighting等,留待慢慢消化。

相關文章
相關標籤/搜索