進(jìn)階!Cocos Creator 中使用模板測試實(shí)現(xiàn)遮罩效果
在之前的章節(jié)里,我們知道的都是平面上的渲染,直接往屏幕上畫東西就可以了,繪制的內(nèi)容都比較單一。但在 3D 游戲里,我們需要考慮的東西則會多很多。比如在人群中,視野方向的人物模型很多,每一個(gè)人物模型都需要繪制,如何讓距離相機(jī)進(jìn)的物體不被離得遠(yuǎn)的遮擋?再比如,街道兩邊有很多帶有玻璃窗的商店,如何通過玻璃窗看到里面的景象?
要實(shí)現(xiàn)這里的功能,就需要涉及到渲染流程的最后一個(gè)階段:alpha 測試與混合。在這個(gè)階段里 GPU 主要的工作是逐片元操作,將它們的顏色以某種形式合并,得到最終在屏幕上顯示的像素顏色。主要涉及的工作有兩個(gè):對片元進(jìn)行測試并進(jìn)行合并。測試步驟決定了片元最終會不會被顯示出來。在 WebGL 里主要的測試有裁剪測試、透明度測試、模板測試以及深度測試,這幾個(gè)測試都是高度可配置的。其中,考量到裁剪測試沒有模板測試來得更加靈活,因此本次就不涉及裁剪測試內(nèi)容。整個(gè)測試流程如下圖:

從圖中可以看出,片元著色器輸出的顏色緩沖并不是最終屏幕上呈現(xiàn)的顏色緩沖,還必須經(jīng)過模板、深度和混合測試影響后才能得到最終用來輸出的顏色緩沖。本章重點(diǎn)介紹模板測試和深度測試,混合測試將在下一章中為大家介紹。
>注意:由于這部分內(nèi)容是補(bǔ)充知識,重點(diǎn)在于了解一下這部分的概念以及在 Cocos Creator 3.x 中的應(yīng)用即可。
模板測試(Stencil Test)
Stencil 的本質(zhì)是鏤空,通過這樣的板子就可以方便的畫出某個(gè)特定的形狀。模板測試的核心是持有一個(gè)模板緩沖,每個(gè)像素/片段都有一個(gè)模板值,通常每個(gè)模板值是 8 位(用掩碼表示),也就是可以有 256 種不同的值,這樣就可以通過設(shè)置我們想要的模板值來丟棄或保留這個(gè)片段。一個(gè)簡單模板測試?yán)尤缦拢?/p>

圖片摘自 OpenGL
通常,用戶在啟用模板緩沖的時(shí)候,會將整個(gè)模板緩沖中的所有片段的模板值設(shè)置為 0,丟棄所有片段。然后再設(shè)置特定區(qū)域的模板值(大于 0)以及比較函數(shù)。GPU 會讀取用戶設(shè)置的模板值,然后將該值與模板緩沖中該位置的模板值按比較函數(shù)進(jìn)行比較,最終決定是保留還是舍棄該片段,形成鏤空,也就是遮罩效果。在模板測試中,有兩個(gè)很重要的方法是 “stencilFunc” 和 “stencilOp”,前者用來控制 stencil 的測試方式,得出測試結(jié)果,后者根據(jù)結(jié)果決定要如何處理緩沖中的數(shù)據(jù)。
`void stencilFunc(GLenum func, GLint ref, GLuint mask)` :
func:指定模板測試比較函數(shù)。默認(rèn)是 Always。

ref:用來做模板測試的參考值。
mask:指定操作掩碼。在測試時(shí)會先將 ref 與 mask 進(jìn)行與運(yùn)算,再將 ref 與模板緩沖(stencil buffer)中的值進(jìn)行與運(yùn)算,最后根據(jù)比較函數(shù)得出結(jié)果。
單純看這些描述可能還不是很理解這里的意思,在這舉個(gè)例子:
gl.stencilFunc(gl.GEQUAL,?1,?0xff);?//?此處,mask 采用 16 進(jìn)制的原因是因?yàn)閿?shù)據(jù)在計(jì)算機(jī)中的表示最終都是以二進(jìn)制形式存在,但二進(jìn)制寫起來太長了,因此可以采用 16 進(jìn)制或者 8 進(jìn)制解決。進(jìn)制越大,數(shù)的表達(dá)長度也就越短。
這個(gè)配置的意思是將值 1&0xff 與 stencil buffer&0xff 進(jìn)行比較,判斷是否滿足 GEQUAL 的條件,滿足則測試通過,否則測試不通過。因此,我們在這里只是想單純的讓 ref 和 stencil buffer 進(jìn)行比較,mask 就不能成為干擾因素,因此設(shè)置為 0xff(11111111),讓它每一位都為 1,“與”計(jì)算都會保持原值。如果想禁用模板,則可以設(shè)置 0x00 的值,這樣模板緩沖中的值都是 0。
模板測試通過或者不通過后要對模板緩沖進(jìn)行什么操作,就需要用到 `void stencilOp(GLenum fail, GLenum zfail, GLenum zpass);`
fail:指定當(dāng)前模板測試不通過時(shí)的行為。默認(rèn)為 KEEP。

zfail:指定當(dāng)前模板測試通過但深度測試未通過時(shí)的行為。允許和默認(rèn)的值同 fail。
zpass:指定當(dāng)前模板測試通過且深度測試也通過時(shí)的行為,或者當(dāng)模板測試通過且沒有開啟深度測試時(shí)的行為。允許和默認(rèn)的值同 fail。
這里的內(nèi)容就比較好理解,通常,我們會對測試失敗時(shí)采用保持當(dāng)前值的方式,測試通過時(shí)用設(shè)置值替換模板緩沖值。
gl.stencilOp(gl.KEEP,?gl.KEEP,?gl.REPLACE);
模板測試默認(rèn)是處于禁用狀態(tài),使用時(shí)需要手動開啟。
//?默認(rèn)情況下模板測試處于禁用狀態(tài),需要手動啟用模板測試
gl.enable(gl.STENCIL_TEST);
//?同樣需要在每次迭代之前清除模板緩沖
gl.clear(gl.COLOR_BUFFER_BIT?|?gl.STENCIL_BUFFER_BIT);
這里順帶提醒一下,如果想自己嘗試在 WebGL 上寫模板測試的同學(xué),遇到模板測試沒有生效的情況,可以檢查一下,在請求上下文的時(shí)候是否有要求包含一個(gè)模板緩沖區(qū)。
const?gl?=?canvas.getContext("webgl",?{?stencil:?true?});
深度測試(Depth test)
深度測試是 3D 游戲里不可或缺的重要環(huán)節(jié),可以幫助實(shí)現(xiàn) 3D 渲染上物體的遮擋效果,如果沒有深度測試,可能會出現(xiàn)前后物體的渲染錯(cuò)亂或者閃爍的現(xiàn)象。
深度測試的核心跟模板測試類似,也是持有一個(gè)深度緩沖,深度緩沖就像顏色緩沖(Color Buffer)(最終生成的像素顏色值的存儲緩沖,最終設(shè)備上呈現(xiàn)的像素顏色就是從這里讀取)一樣存儲了每個(gè)片段的深度值,以 16、24 或者 32 位 float 的形式存儲,在大多數(shù)系統(tǒng)中默認(rèn)精度是 24。當(dāng)開啟深度測試的時(shí)候,會將當(dāng)前渲染的每一個(gè)片段的深度值與深度緩沖的內(nèi)容進(jìn)行對比測試。如果測試通過,深度緩沖則會更新新的深度值,如果測試失敗,片段則會被丟棄。
深度緩沖是在片段著色器運(yùn)行之后(也在模板測試之后),在屏幕空間中運(yùn)行的。屏幕空間坐標(biāo)與 "gl.viewport" 設(shè)置有關(guān),WebGL 會直接使用 GLSL 的內(nèi)建變量 "gl_FragCoord" 從片段著色器直接訪問。“gl_FragCoord” 的 x 和 y 分量代表了片段的屏幕空間坐標(biāo)。同時(shí),它也包含了一個(gè) z 分量,這個(gè)是用來存儲真正的深度值,最終用它來和深度緩沖中的內(nèi)容進(jìn)行對比。
深度緩沖也有一個(gè)重要的函數(shù) “void depthFunc(GLenum func)” 用來設(shè)置深度比較函數(shù),比較的參數(shù)跟模板緩沖的比較函數(shù)所使用參數(shù)一樣,默認(rèn)參數(shù)為 LESS。深度測試默認(rèn)也是禁用的,同樣需要手動開啟。
const?gl?=?canvas.getContext("webgl",?{?stencil:?true,?depth:?true?});
gl.clear(gl.COLOR_BUFFER_BIT?|?gl.STENCIL_BUFFER_BIT?|?gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
當(dāng)深度測試通過之后,會將當(dāng)前片段的 z 值存入深度緩沖。當(dāng)前片段的 z(深度)值是介于 0.0 到 1.0 直接的值。從觀察者角度看到場景中物體的 z 值,這個(gè)值是投影矩陣作用后又經(jīng)過標(biāo)準(zhǔn)設(shè)備坐標(biāo)變換,最終再轉(zhuǎn)換到 0.0 到 1.0 之間的值。
在 3.x 中的應(yīng)用
根據(jù)上面的內(nèi)容相信大家應(yīng)該基本了解了模板測試和深度測試的原理,接下來,我們試試在 ?Cocos Creator 如何應(yīng)用這部分。Cocos Creator 底層默認(rèn)對模板/深度等做了初始化處理,實(shí)現(xiàn)的模塊是在引擎源碼中的 webgl1/webgl2-device.ts 模塊,由于此處我測試使用的是 WebGL1 的后端,因此,我在 Creator 版本安裝目錄下找到 resources->3d->engine->cocos->core->webgl->webgl-device.ts 模塊,可以看到如下初始化內(nèi)容:

注:雖然 API 部分有輕微差異,這是因?yàn)?WebGL 提供了不止一種方法設(shè)置,但概念基本相同。
這里羅列出的默認(rèn)配置,主要針對 3D 對象配置,由于 2D 對象大多數(shù)包含透明像素,因此底層 2D 管線沒有處理深度部分,這樣就不需要進(jìn)行深度測試,可以在之前像 builtin-sprite 這類 2D Effect 上看到針對深度測試部分都采用了手動關(guān)閉的形式。因此,在接下來的深度模板測試實(shí)踐中,選用的是 3D 對象。嘗試在場景里擺放上 2 個(gè)模型,來測試一下深度相關(guān)。人物模型為 O1,場景模型為 O2。模型與相機(jī)的擺放位置如下:

從 《Cocos Shader 系列:基礎(chǔ)入門(五)》中有關(guān)于模板深度緩沖的寫法就可以看出每一個(gè) pass 都可以對模板/深度緩沖進(jìn)行配置。大致的寫法如下:
CCEffect?%{
??techniques:
??-?name:?opaque
????passes:
????-?vert:?...
??????frag:?...
??????properties:?...
??????depthStencilState:
??????????deprhTest:?false
??????...
}%
這樣的寫法通常是為了設(shè)置 pass 初始化時(shí)的數(shù)據(jù),如果需要修改,Cocos Creator 3.x 也針對 Effect 寫法進(jìn)行了封裝。在屬性檢查器面板上每一個(gè)材質(zhì)的 pass 下都可以看到 PipelineStates 屬性,可以很方便的進(jìn)行可視化配置。

可配置參數(shù)以及說明如下:

接著,對人物模型 O1 修改一些配置:
材質(zhì)關(guān)閉 depthTest,并應(yīng)用。直接在編輯器上可以觀察到由于沒有進(jìn)行深度測試,所以 O1 的繪制內(nèi)容被地面覆蓋,同時(shí)自身身上的裝飾物渲染順序也出現(xiàn)錯(cuò)亂。

材質(zhì)開啟 depthTest,調(diào)整 depthFunc 為 GREATER,并應(yīng)用。此時(shí)可以發(fā)現(xiàn),無論你怎么找,模型都無法被看見。正常來說被背景模型因?yàn)樯疃葴y試函數(shù)使用還是 LESS,所以會覆蓋深度緩沖內(nèi)容,但是不至于全部都能覆蓋得到,那么覆蓋不到的部分也不可能完全看不見人物模型的。這主要是因?yàn)槲覀兠繋紩宄疃染彌_,清除的深度緩沖區(qū)默認(rèn)值為 1.0,表示最大的深度值。因此,人物模型再怎么遠(yuǎn)都不可能比最大值來的遠(yuǎn)。
將所有修改還原,接下來,進(jìn)行模板測試。模板測試需要有兩個(gè)基礎(chǔ)操作,一個(gè)是將所有的片段清空,一個(gè)是設(shè)定特定區(qū)域的模板值。然后,需要繪制的物體只需要選擇在特定的模板值上繪制即可。我這里準(zhǔn)備實(shí)現(xiàn)一個(gè)只顯示人物模型上半身的效果。準(zhǔn)備兩個(gè)面片(quad),每個(gè)面片都有自己的材質(zhì)(Effect 用默認(rèn)的 builtin-standard),A 面片離相機(jī)最近,做人物消失效果;B 面片只有人物上半身大小,位于人物上半身位置,相比于 A 離相機(jī)第二近,做人物只顯示區(qū)域設(shè)置;最后人物在這兩個(gè)面片后面。擺放位置如下:

接著,做如下操作:
將材質(zhì) A(面片 A 的材質(zhì))的正反面 stencilTest 都開啟,有關(guān)正反面是什么會在下一章中說到。將 stencilFunc 設(shè)置為 NEVER,stencillFailOp 設(shè)置為 ZERO,并點(diǎn)擊應(yīng)用。此處的配置讓模板測試永不通過,執(zhí)行 fail 函數(shù),將所有模型繪制區(qū)域的模板緩沖都設(shè)置為 0。
將材質(zhì) B(面片 B 的材質(zhì))的正反面 stencilTest 都開啟,stencilFunc 設(shè)置為 NEVER,stencillFailOp 設(shè)置為 REPLACE,stencilRef 設(shè)置為 1,并點(diǎn)擊應(yīng)用。此處的配置讓模板測試永不通過,執(zhí)行 fail 函數(shù),將所有模型繪制區(qū)域的模板緩沖都設(shè)置為 ref。
將人物材質(zhì)的正反面 stencilTest 都開啟,stencilFunc 設(shè)置為 EQUAL,stencilRef 設(shè)置為 1,stencilReadMask 和 stencilWriteMask 設(shè)置值為 ref,stencillFailOp、stencilZFailOp 和 stencilPassOp 設(shè)置為 KEEP,并點(diǎn)擊應(yīng)用。此處的配置只有 ref 值和模板測試值相等的情況下才能通過模板測試,測試通過后用 stencilRef&stencilWriteMask 替換模板緩沖中的相對應(yīng)片段。
最后得出的效果如下:

只有模型的上半身顯示了出來,大家可以嘗試著從不同角度來觀察效果。
有關(guān)模板測試和深度測試的內(nèi)容就到這為止,感興趣的同學(xué)可以去增加更多不同的組合實(shí)現(xiàn)特別的效果。在下一個(gè)章節(jié)我們將來認(rèn)識一下混合測試(BlendState)和面剔除(CullMode)。
內(nèi)容參考:
1. 模板測試:
https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/02%20Stencil%20testing/
2. 深度測試:
https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/01%20Depth%20testing/
Cocos Shader 基礎(chǔ)入門系列
···更新中···
該系列教程視頻版
已更新至 EP09
歡迎點(diǎn)擊【閱讀原文】
前往 B 站觀看交流 ↓
B 站關(guān)注「Cocos 引擎官方」



