這家公司太牛逼了,雖然這次不是重新造輪子!動畫蒙版

01 目的

本文的目的是介紹如何在場景(可能含有多個spine動畫)上實現動畫蒙版(也就是遮罩mask會動會變形), 根據實現方式的不一樣, 會有如下的效果:

圖片
圖片

02 收穫

  1. 瞭解到什麼是模板測試
  2. 瞭解到矩陣變換
  3. 瞭解 Cocos Creator 基礎的渲染流
  4. 瞭解 Cocos Creator 裏如何寫一個 shader 並傳遞 uinform 參數
  5. 本文涉及到的素材和代碼:

一個spine動畫(從spine官方示例中取得) 一個圓形蒙版圖片(ps裏隨便畫一個)

1

本文中涉及到的所有代碼都可以在這裏(https://github.com/laomoi/ccc-test-mask)找到

cocos creator 工程在2.3.1下測試通過。

03 蒙版測試實現動畫蒙版

3.1 什麼是模板測試(stencil test)

在opengl渲染管線中, 當片元着色器處理完着色之後, 着色結果在實際寫入顏色緩衝之前, 可以進行模板測試從而可以丟棄一些片元的着色結果。

而gpu是如何知道改丟棄那些片元呢? 主要取決於模板緩衝中的值。

模板緩衝區跟顏色緩衝區類似, 它也是一塊畫布, 我們假設它的分辨率跟屏幕分辨率一樣,只是它每個像素的精度是8位。

當我們開啓了模板測試之後, 某個座標(x,y)的片元顏色在實際寫入顏色緩衝之前, gpu會從模板緩衝區同樣的座標(x,y)取到該點的模板值進行後繼判斷:

當這個值符合我們的設定規則時(比如:該模板值>0時不丟棄片元), 則讓片元生效, 否則丟棄該片元。

所以我們可以把模板緩衝區理解爲一個篩沙子的竹籃子, 值爲0的地方我們可以認爲是鏤空的

當開啓了模板測試之後,後繼所有的圖元繪製都需要在竹籃子裏篩一遍, 直到關閉模板測試爲止。

要實現我們上文中的圖形效果, 我們可以腦補一下模板緩衝中每一幀的變化:

2

通過這樣的模板緩衝變化, 就可以不斷的讓每幀多顯示一些動畫內容, 從而實現我們要的效果。

(想更全面理解模板測試的話, 可以看LearnOpengl這篇文章

https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/02%20Stencil%20testing/

3.2 如何在每一幀里正確的填充模板緩衝區來實現我們的目的。

我們只需要按照下面的步驟來通知webgl/opengl, 即可往模板緩衝區裏寫入我們要的形狀:

  1. 先清空模板緩衝區, 也就是所有像素的模板值都置爲0
  2. 開啓模板緩衝區寫入
  3. 按照正常的圖元繪製方式畫圓形遮罩圖
  4. 圓形遮罩圖的片元着色器中開啓alpha test, 比如我們設定一個alpha閾值0.1, 如果片元的alpha度大於該閾值, 就把1寫入模板緩衝區, 否則丟棄該片元(相當於該位置的模板緩衝值保留原0的取值)

我們只需要每一幀都這麼執行這幾步,只是每一幀裏不斷把圓形遮罩圖的y座標不斷的往上增加即可實現我們要的效果。

所幸的是我們不需要自己操作webgl/opengl的api來做這幾步, cocos creator的 cc.Mask組件已經幫我們做了這幾件事:

3

我們只需要 把我們的圓形遮罩圖(shadow.png)拖入 mask 組件的 sprite frame 即可。

不過接下來我們遇到的問題是, cc.Mask 目前的設計是比較死板的, 它的模板測試只能針對它下面的子節點,  在我們這個例子中,節點是這樣的關係:

4

我們希望 hero-mask 這個節點的y座標每幀往上增加一些,這樣模板緩衝裏的圓形才能往上升, 但是我們又希望保持 spine 節點(hero-pro)在屏幕上的座標不動,但是目前cc.Mask的設計導致了我們無法同時做到這2點, 因爲移動 hero-mask 的 y 座標會導致下面的 spine 節點也一起移動。

看上去我們只能祭出魔改引擎這招大殺器了, 在打開引擎底層的 cc.Mask 代碼閱讀之前, 我們需要先補習一下 Cocos Creator v2.x 之後的渲染流機制。

Cocos Creator v2.x 的渲染流(render-flow)本質上其實跟以前版本並沒有太大的區別,只是使用了一個單獨的數字renderFlag來記錄單個節點的所有dirty狀態

圖片

這個renderFlag的每一位(bit)記錄了一個dirtyFLag, 如圖所示。

比如說第3位設置爲1, 則表示節點的本地座標或者縮放等局部屬性發生了變化, 在繪製這個節點的時候,需要調用 updateLocalMatrix() 來進行更新, 本質上其實也是if判斷。

每個節點繪製的時候,是按照上圖表示的順序, 從上到下進行判斷, 本文中僅關注以下幾個dirtyFlag的變化:

  • LOCAL_TRANSFORM(本地座標等屬性)
  • WORLD_TRANSFORM(世界座標等屬性)
  • UPDATE_RENDER_DATA(頂點數據屬性)
  • RENDER(渲染本身,以及其他操作比如開啓模板測試)
  • CHILDREN(遍歷子節點)
  • POST_RENDER(完成所有子節點遍歷後的渲染函數,比如用於關閉模板測試)

我們忽略掉其他不關心的 dirtyFlag, 用一個比較簡單的僞代碼函數來描述整套渲染的流程(跟實際代碼並不完全一致, 只是可以大體描述):

6

(想深入學習的可以閱讀 Cocos 源碼中的render-flow.js)

上圖紅框中標記的代碼是我們目前最關心的2行代碼, 在 updateRenderData() 中會生成頂點數據, 在fillBuffers()會把這些頂點數據寫入頂點緩衝區.

我們這裏不考慮修改 fillBuffers(),我們只需要魔改 cc.Mask.updateRenderData() 函數的實現,讓(圓形遮罩圖片)的頂點數據每幀向上移動即可, 只要沒有修改節點本身的本地矩陣和世界矩陣,  也就不會影響下面的2個 spine子節點。

3.3 第1種魔改方法

修改assembler.updateWorldVetex()

打開引擎的 mask-assembler.js 和 2d/simple.js,assembler-2d.js 可以看到如下代碼

7
圖片

hero-mask 這個節點在繪製本身的時候, 從 updateRenderData() 最終會調用到 updateWorldVertex() 函數, 這個函數的內容是我們需要魔改的重點。

首先我們需要先看懂 updateWorldVertex() 裏面的這些代碼究竟在做什麼事情。

這裏先簡單補習一下矩陣變換的意義。

在圖形渲染中, 我們通常使用一個 4x4的矩陣來表示點的仿射變換(縮放, 旋轉, 斜切, 移動), 雖然在 2D 世界裏其實我們用一個 3x3 的矩陣也夠用了, 不過爲了兼容3D世界的座標系, 所以我們統一使用的是4x4。重新看一下我們的節點樹結構:

8

我們使用M1矩陣來表示 節點 hero-mask 節點的本地變換矩陣, M2來表示場景根節點的世界變換矩陣, 那麼 hero-mask 的世界變換矩陣就是 M3 = M2 x M1,有了這個M3之後, 我們就可以很方便的把 hero-mask 的任意一個本地座標換算到世界座標:

頂點的世界座標 =  M3 x (hero-mask頂點的本地座標)

那麼這個矩陣和座標的乘法具體是怎麼計算的呢, 如下圖所示:

10

回頭再來看 updateWorldVertex() 裏面的代碼, this._local是圓形遮罩4個頂點的本地座標,

com.node._worldMatrix 就是我們上面提到的M3, 也就是 hero-mask 的世界變換矩陣,

我們把M3 x 頂點本地座標 就會得到4個頂點的世界座標, 然後存入 renderData的vDatas 數組中。

我們現在魔改的方法就是,我們需要在 M3 x 頂點本地座標之前,先對頂點本地座標做一個臨時變換,讓頂點的本地y座標往上增加之後,再去跟 M3 相乘即可。

在這個例子中我們可以簡單的修改 local.y += distance 來達到目的,不過如果以後我們想對圓形遮罩想做一些更復雜的變換,比如縮放旋轉之類的,那麼就得用一個矩陣變換來做了:

11

更詳細的細節可以看我上面提供的源碼。

這種修改JS層的 updateWorldVertex 方法在原生平臺下並不是總是生效,如果 hero-mask 的父節點每幀都在移動或者變形導致 hero-mask 每幀都會觸發原生層重新計算頂點的世界座標。

*因爲在native層的渲染跟js層並不完全一致,* 比如 maskAssembler 在原生層是在 fillBuffers() 裏會判斷 world transform 是否 dirty, 從而去重新計算頂點的世界座標。

這樣即使在 js 層 updateRenderData() 的時候你修改了頂點的世界座標數據, 但是原生層的 fillBuffers() 是在後面執行的,會導致這個修改失效。

3.4 第2種魔改方法

把 mask節點拆成2個節點

根據引擎大佬提供的另外一種思路,我實現了第2種魔改方法。

我們通過上述描述應該知道了整個繪製是這樣的流程:

  1. hero-mask 節點打開模板測試開關
  2. 打開模板緩衝寫入,繪製圓形遮罩到模板緩衝上, 關閉模板緩衝寫入
  3. 繪製 hero-mask 節點下的2個 spine 子節點
  4. hero-mask 節點關閉模板測試開關

我們把整個顯示結構改成下圖所示:

12

begin-mask 和 end-mask 節點都掛載了自定義的 mask 組件:

13

只是一個開啓了 fillBuffers() 用來開啓模板測試, 另外一個開啓了 postBuffers() 用來關閉模板測試。

同樣的我們在代碼裏讓 beginMask 這個節點做一個向上運動動畫即可, 因爲這次 spine 節點不再是 beginMask 節點的子節點,

所以 beginMask 的移動不會影響到 spine 節點。

整個顯示效果跟第一個魔改方法一樣。

注意這種修改方法,在原生層要生效的話需要修改 C++ 代碼, 因爲原生層的 fillBuffers 和 PostFillBuffers()是否調用跟JS層無關。

在上面提供的源碼裏我同時也修改了 C++ 代碼裏的 MaskAssembler.cpp 代碼,增加了 setBeginMask() 和 setEndMask()方法, 有興趣可以看一下。

04 自定義材質實現動畫蒙版

上面使用模板測試的方法來實現遮罩, 它的優點是實現簡單, 而且支持多層遮罩嵌套(最多8層), 缺點是遮罩邊緣比較生硬。

如果大家使用過photoshop等美術工具裏面的圖層蒙版, 就會發現這些美術工具裏的蒙版是支持邊緣羽化的, 我猜測他們實際上就是用了一張帶透明度的蒙版紋理,覆蓋在原圖層上面。

原圖層某像素的顏色.alpha *= 蒙版紋理在在該像素點上的alpha

通過這種方式就可以實現類似片元剔除和羽化的功能。

實現原理非常簡單, 缺點是它需要修改需要被遮罩的所有子節點的片元着色器, 也就是修改他們的材質, 在新的片元着色器中傳入這張遮罩圖的紋理, 進行紋理採樣之後 再乘上原來片元的顏色即可。

也就是對片元着色器做如下修改即可:

14

紅色框是我們在片元着色器中新加的代碼,texture2 是那張半透明的白色遮罩圖的紋理, mask_uv 是一個 uv 座標,是一個 vec2 的結構,表示對應在渲染該片元時,應該去白色遮罩圖的哪個座標上進行採樣從而得到疊加的 alpha 度。

(關於什麼是uv座標這裏不闡述)

如何在片元着色器中得到一個正確的 mask_uv 是我們接下來要重點解決的問題,如下圖所示:

15

當我們在渲染這個spine節點的頭髮上這個藍色點時,如果我們能知道這個藍色點的世界座標,那我們就可以反推計算這個點在這個圓形白色遮罩圖上的本地座標, 得到這個本地座標之後, 再進行座標映射,就可以得到這個座標對應的mask_uv值。

我們在上面的模板測試中提到, 一個本地座標 localPt 乘以一個世界變換矩陣M就可以得到世界座標 worldPt , 同理我們可以知道, 把一個世界座標 worldPt 乘以這個M的逆矩陣就可以得到對應的 localPt, 也就是:

worldPt = M x localPt
localPt = M的逆矩陣 x worldPt

(計算一個矩陣的逆矩陣, Cocos Creator已經給我們提供了方法:mat4.invert())

接下來要做一個本地座標到 uv 座標的映射變換,因爲我們得到的是圖片內部的本地座標(x,y), 圖片的中心點座標是(0,0), 最終我們要得到uv座標範圍是[0,1]的值, 已知圖片的寬度w, 高度h:

16

很容易可以算出來:

u = (x + w/2) / w = x/w + 0.5 v = (y + h/2) /h = y/h + 0.5

我們可以把這個紋理座標的映射放入頂點着色器裏去算, 但是這樣需要把 w 和 h 當作 uniform 傳入頂點着色器,所以我們乾脆再對原來計算好的逆矩陣把紋理座標映射的計算疊加上去, 整個代碼如下:

17

頂點着色器裏代碼如下:

18

有幾點需要注意:

  1. 我們在傳遞一個矩陣數據給片元着色器時,需要把這個4x4的矩陣 轉換爲一個float32的數組;
  2. 圓形白色遮罩這張紋理,在編輯器裏必須關掉Packable的屬性,如果這個紋理被動態合入了大紋理集,那uv座標就沒法正確計算;
  3. cocos creator的默認頂點着色器裏有一個CC_USE_MODEL的宏, 當定義了這個宏的時候,意味着頂點數據裏的頂點數據並不是世界座標, 我們需要額外再乘上一個 cc_matWorld 才能得到我們要的世界座標;
  4. spine的着色器跟上圖代碼中貼出來的略有不同, 有興趣的可以看下上面我提供的源碼。

到這裏爲止我們已經提供了2種不同方式實現動畫的動態蒙版, 在實際項目開發時, 我們大部分情況並不會用代碼的方式來控制這個圓形遮罩的運動, 而是讓美術在編輯器裏把整套動畫都做好.

當然現在這套編輯器目前僅是公司內使用, 等以後或許有機會把編輯器放出來給大家試用。

在後面的時間裏,我們會陸續給大家分享一些我們項目中用到的一些魔改技巧或者圖形效果之類的, 希望可以給cocos社區貢獻一份綿薄之力。



樂府互娛 是9102年成立的的一家非常年輕的遊戲公司, 位於上海目前遊戲新秀扎堆的漕河涇開發區, 有興趣進一步瞭解的可以點開我們的官網:http://www.lovengame.com


本文分享自微信公衆號 - Creator星球遊戲開發社區(creator-star)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章