進階!Cocos Creator 中使用模板測試實現遮罩效果

放空發表於2021-11-10
在之前的章節裡,我們知道的都是平面上的渲染,直接往螢幕上畫東西就可以了,繪製的內容都比較單一。但在 3D 遊戲裡,我們需要考慮的東西則會多很多。比如在人群中,視野方向的人物模型很多,每一個人物模型都需要繪製,如何讓距離相機進的物體不被離得遠的遮擋?再比如,街道兩邊有很多帶有玻璃窗的商店,如何通過玻璃窗看到裡面的景象?

要實現這裡的功能,就需要涉及到渲染流程的最後一個階段:alpha 測試與混合。在這個階段裡 GPU 主要的工作是逐片元操作,將它們的顏色以某種形式合併,得到最終在螢幕上顯示的畫素顏色。主要涉及的工作有兩個:對片元進行測試並進行合併。測試步驟決定了片元最終會不會被顯示出來。在 WebGL 裡主要的測試有裁剪測試、透明度測試、模板測試以及深度測試,這幾個測試都是高度可配置的。其中,考量到裁剪測試沒有模板測試來得更加靈活,因此本次就不涉及裁剪測試內容。整個測試流程如下圖:

進階!Cocos Creator 中使用模板測試實現遮罩效果

從圖中可以看出,片元著色器輸出的顏色緩衝並不是最終螢幕上呈現的顏色緩衝,還必須經過模板、深度和混合測試影響後才能得到最終用來輸出的顏色緩衝。本章重點介紹模板測試和深度測試,混合測試將在下一章中為大家介紹。

>注意:由於這部分內容是補充知識,重點在於瞭解一下這部分的概念以及在 Cocos Creator 3.x 中的應用即可。

模板測試(Stencil Test)

Stencil 的本質是鏤空,通過這樣的板子就可以方便的畫出某個特定的形狀。模板測試的核心是持有一個模板緩衝,每個畫素/片段都有一個模板值,通常每個模板值是 8 位(用掩碼錶示),也就是可以有 256 種不同的值,這樣就可以通過設定我們想要的模板值來丟棄或保留這個片段。一個簡單模板測試例子如下:

進階!Cocos Creator 中使用模板測試實現遮罩效果
圖片摘自 OpenGL

通常,使用者在啟用模板緩衝的時候,會將整個模板緩衝中的所有片段的模板值設定為 0,丟棄所有片段。然後再設定特定區域的模板值(大於 0)以及比較函式。GPU 會讀取使用者設定的模板值,然後將該值與模板緩衝中該位置的模板值按比較函式進行比較,最終決定是保留還是捨棄該片段,形成鏤空,也就是遮罩效果。在模板測試中,有兩個很重要的方法是 “stencilFunc” 和 “stencilOp”,前者用來控制 stencil 的測試方式,得出測試結果,後者根據結果決定要如何處理緩衝中的資料。

`void stencilFunc(GLenum func, GLint ref, GLuint mask)` :

func:指定模板測試比較函式。預設是 Always。

進階!Cocos Creator 中使用模板測試實現遮罩效果

ref:用來做模板測試的參考值。

mask:指定操作掩碼。在測試時會先將 ref 與 mask 進行與運算,再將 ref 與模板緩衝(stencil buffer)中的值進行與運算,最後根據比較函式得出結果。

單純看這些描述可能還不是很理解這裡的意思,在這舉個例子:

gl.stencilFunc(gl.GEQUAL, 1, 0xff); // 此處,mask 採用 16 進位制的原因是因為資料在計算機中的表示最終都是以二進位制形式存在,但二進位制寫起來太長了,因此可以採用 16 進位制或者 8 進位制解決。進位制越大,數的表達長度也就越短。

這個配置的意思是將值 1&0xff 與 stencil buffer&0xff 進行比較,判斷是否滿足 GEQUAL 的條件,滿足則測試通過,否則測試不通過。因此,我們在這裡只是想單純的讓 ref 和 stencil buffer 進行比較,mask 就不能成為干擾因素,因此設定為 0xff(11111111),讓它每一位都為 1,“與”計算都會保持原值。如果想禁用模板,則可以設定 0x00 的值,這樣模板緩衝中的值都是 0。

模板測試通過或者不通過後要對模板緩衝進行什麼操作,就需要用到 `void stencilOp(GLenum fail, GLenum zfail, GLenum zpass);`

fail:指定當前模板測試不通過時的行為。預設為 KEEP。

進階!Cocos Creator 中使用模板測試實現遮罩效果

zfail:指定當前模板測試通過但深度測試未通過時的行為。允許和預設的值同 fail。

zpass:指定當前模板測試通過且深度測試也通過時的行為,或者當模板測試通過且沒有開啟深度測試時的行為。允許和預設的值同 fail。

這裡的內容就比較好理解,通常,我們會對測試失敗時採用保持當前值的方式,測試通過時用設定值替換模板緩衝值。

gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);

模板測試預設是處於禁用狀態,使用時需要手動開啟。

// 預設情況下模板測試處於禁用狀態,需要手動啟用模板測試

gl.enable(gl.STENCIL_TEST);

// 同樣需要在每次迭代之前清除模板緩衝

gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);

這裡順帶提醒一下,如果想自己嘗試在 WebGL 上寫模板測試的同學,遇到模板測試沒有生效的情況,可以檢查一下,在請求上下文的時候是否有要求包含一個模板緩衝區。

const gl = canvas.getContext("webgl", { stencil: true });

深度測試(Depth test)

深度測試是 3D 遊戲裡不可或缺的重要環節,可以幫助實現 3D 渲染上物體的遮擋效果,如果沒有深度測試,可能會出現前後物體的渲染錯亂或者閃爍的現象。

深度測試的核心跟模板測試類似,也是持有一個深度緩衝,深度緩衝就像顏色緩衝(Color Buffer)(最終生成的畫素顏色值的儲存緩衝,最終裝置上呈現的畫素顏色就是從這裡讀取)一樣儲存了每個片段的深度值,以 16、24 或者 32 位 float 的形式儲存,在大多數系統中預設精度是 24。當開啟深度測試的時候,會將當前渲染的每一個片段的深度值與深度緩衝的內容進行對比測試。如果測試通過,深度緩衝則會更新新的深度值,如果測試失敗,片段則會被丟棄。

深度緩衝是在片段著色器執行之後(也在模板測試之後),在螢幕空間中執行的。螢幕空間座標與 "gl.viewport" 設定有關,WebGL 會直接使用 GLSL 的內建變數 "gl_FragCoord" 從片段著色器直接訪問。“gl_FragCoord” 的 x 和 y 分量代表了片段的螢幕空間座標。同時,它也包含了一個 z 分量,這個是用來儲存真正的深度值,最終用它來和深度緩衝中的內容進行對比。

深度緩衝也有一個重要的函式 “void depthFunc(GLenum func)” 用來設定深度比較函式,比較的引數跟模板緩衝的比較函式所使用引數一樣,預設引數為 LESS。深度測試預設也是禁用的,同樣需要手動開啟。

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);

當深度測試通過之後,會將當前片段的 z 值存入深度緩衝。當前片段的 z(深度)值是介於 0.0 到 1.0 直接的值。從觀察者角度看到場景中物體的 z 值,這個值是投影矩陣作用後又經過標準裝置座標變換,最終再轉換到 0.0 到 1.0 之間的值。

在 3.x 中的應用

根據上面的內容相信大家應該基本瞭解了模板測試和深度測試的原理,接下來,我們試試在  Cocos Creator 如何應用這部分。Cocos Creator 底層預設對模板/深度等做了初始化處理,實現的模組是在引擎原始碼中的 webgl1/webgl2-device.ts 模組,由於此處我測試使用的是 WebGL1 的後端,因此,我在 Creator 版本安裝目錄下找到 resources->3d->engine->cocos->core->webgl->webgl-device.ts 模組,可以看到如下初始化內容:

進階!Cocos Creator 中使用模板測試實現遮罩效果
注:雖然 API 部分有輕微差異,這是因為 WebGL 提供了不止一種方法設定,但概念基本相同。

這裡羅列出的預設配置,主要針對 3D 物件配置,由於 2D 物件大多數包含透明畫素,因此底層 2D 管線沒有處理深度部分,這樣就不需要進行深度測試,可以在之前像 builtin-sprite 這類 2D Effect 上看到針對深度測試部分都採用了手動關閉的形式。因此,在接下來的深度模板測試實踐中,選用的是 3D 物件。嘗試在場景裡擺放上 2 個模型,來測試一下深度相關。人物模型為 O1,場景模型為 O2。模型與相機的擺放位置如下:

進階!Cocos Creator 中使用模板測試實現遮罩效果

《Cocos Shader 系列:基礎入門(五)》中有關於模板深度緩衝的寫法就可以看出每一個 pass 都可以對模板/深度緩衝進行配置。大致的寫法如下:

CCEffect %{

techniques:

- name: opaque

passes:

- vert: ...

frag: ...

properties: ...

depthStencilState:

deprhTest: false

...

}%

這樣的寫法通常是為了設定 pass 初始化時的資料,如果需要修改,Cocos Creator 3.x 也針對 Effect 寫法進行了封裝。在屬性檢查器皮膚上每一個材質的 pass 下都可以看到 PipelineStates 屬性,可以很方便的進行視覺化配置。

進階!Cocos Creator 中使用模板測試實現遮罩效果

可配置引數以及說明如下:

進階!Cocos Creator 中使用模板測試實現遮罩效果

接著,對人物模型 O1 修改一些配置:

材質關閉 depthTest,並應用。直接在編輯器上可以觀察到由於沒有進行深度測試,所以 O1 的繪製內容被地面覆蓋,同時自身身上的裝飾物渲染順序也出現錯亂。

進階!Cocos Creator 中使用模板測試實現遮罩效果

材質開啟 depthTest,調整 depthFunc 為 GREATER,並應用。此時可以發現,無論你怎麼找,模型都無法被看見。正常來說被背景模型因為深度測試函式使用還是 LESS,所以會覆蓋深度緩衝內容,但是不至於全部都能覆蓋得到,那麼覆蓋不到的部分也不可能完全看不見人物模型的。這主要是因為我們每幀都會清除深度緩衝,清除的深度緩衝區預設值為 1.0,表示最大的深度值。因此,人物模型再怎麼遠都不可能比最大值來的遠。

將所有修改還原,接下來,進行模板測試。模板測試需要有兩個基礎操作,一個是將所有的片段清空,一個是設定特定區域的模板值。然後,需要繪製的物體只需要選擇在特定的模板值上繪製即可。我這裡準備實現一個只顯示人物模型上半身的效果。準備兩個面片(quad),每個面片都有自己的材質(Effect 用預設的 builtin-standard),A 面片離相機最近,做人物消失效果;B 面片只有人物上半身大小,位於人物上半身位置,相比於 A 離相機第二近,做人物只顯示區域設定;最後人物在這兩個面片後面。擺放位置如下:

進階!Cocos Creator 中使用模板測試實現遮罩效果

接著,做如下操作:

將材質 A(面片 A 的材質)的正反面 stencilTest 都開啟,有關正反面是什麼會在下一章中說到。將 stencilFunc 設定為 NEVER,stencillFailOp 設定為 ZERO,並點選應用。此處的配置讓模板測試永不通過,執行 fail 函式,將所有模型繪製區域的模板緩衝都設定為 0。

將材質 B(面片 B 的材質)的正反面 stencilTest 都開啟,stencilFunc 設定為 NEVER,stencillFailOp 設定為 REPLACE,stencilRef 設定為 1,並點選應用。此處的配置讓模板測試永不通過,執行 fail 函式,將所有模型繪製區域的模板緩衝都設定為 ref。

將人物材質的正反面 stencilTest 都開啟,stencilFunc 設定為 EQUAL,stencilRef 設定為 1,stencilReadMask 和 stencilWriteMask 設定值為 ref,stencillFailOp、stencilZFailOp 和 stencilPassOp 設定為 KEEP,並點選應用。此處的配置只有 ref 值和模板測試值相等的情況下才能通過模板測試,測試通過後用 stencilRef&stencilWriteMask 替換模板緩衝中的相對應片段。

最後得出的效果如下:

進階!Cocos Creator 中使用模板測試實現遮罩效果

只有模型的上半身顯示了出來,大家可以嘗試著從不同角度來觀察效果。

有關模板測試和深度測試的內容就到這為止,感興趣的同學可以去增加更多不同的組合實現特別的效果。在下一個章節我們將來認識一下混合測試(BlendState)和麵剔除(CullMode)。

內容參考:

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
原文:https://mp.weixin.qq.com/s/NGWdIZrasvXqtBI75rw9xg


相關文章