一. 簡介
動(dòng)畫在2D游戲里用得十分廣泛, 根據(jù)這些動(dòng)畫的特點(diǎn),我們可以大概歸為3類
1. 粒子動(dòng)畫
這種動(dòng)畫是由幾百甚至上千個(gè)粒子構(gòu)成, 所有粒子都共享一個(gè)紋理, 這些粒子都是從一個(gè)發(fā)射器發(fā)出, 加以一定的隨機(jī)因素, 在不同發(fā)射速度和重力等外力作用下,每個(gè)粒子呈現(xiàn)不一樣的運(yùn)動(dòng)狀態(tài), 大量粒子可以組合成各種各樣不一樣的效果, 比如煙花, 火焰. 粒子動(dòng)畫的實(shí)現(xiàn)一般都會(huì)使用批次渲染和對(duì)象池來保證性能.
2. 骨骼動(dòng)畫
這種動(dòng)畫通常用于表現(xiàn)有多個(gè)動(dòng)作的角色, 它通常是由骨骼(bone)和綁定在骨骼上的蒙皮(skin/mesh)構(gòu)成.
動(dòng)畫師通常在spine(2d)或者3dmax等工具里面對(duì)骨骼動(dòng)作進(jìn)行設(shè)計(jì), 同時(shí)對(duì)蒙皮進(jìn)行編輯.
3. 特效動(dòng)畫
特效動(dòng)畫不需要或者難以使用骨骼進(jìn)行表達(dá), 比如一個(gè)刀光效果或者一閃一閃的星星, 我們可以使用最原始的實(shí)現(xiàn)方式, 對(duì)動(dòng)畫的每一幀都畫一張圖片, 依次連續(xù)展示這些圖片就可以達(dá)到動(dòng)畫效果.
但是這種方法實(shí)現(xiàn)的動(dòng)畫過于浪費(fèi)空間和內(nèi)存. 其中有非常多的特效我們可以通過關(guān)鍵幀動(dòng)畫的方式來實(shí)現(xiàn), 常使用Flash工具進(jìn)行關(guān)鍵幀動(dòng)畫的設(shè)計(jì).
本文中下面只討論關(guān)鍵幀動(dòng)畫的實(shí)現(xiàn).
二. 關(guān)鍵幀動(dòng)畫介紹
1. 動(dòng)畫舉例
我們先來看下面這樣一個(gè)動(dòng)畫:
動(dòng)畫設(shè)計(jì)師進(jìn)行編輯的時(shí)候, 是這樣的:
設(shè)計(jì)師把動(dòng)畫分成了4層, 每一層里帶有黑色小點(diǎn)的就是關(guān)鍵幀,
- 底座: 這一層就只有一個(gè)關(guān)鍵幀, 放入了一個(gè)靜態(tài)的底座圖片
- 鐵錘: 這一層就放了一個(gè)鐵錘, 鐵錘在每個(gè)關(guān)鍵幀里都具備不同的位置和角度, 在動(dòng)畫播放過程中, 在2個(gè)關(guān)鍵幀之間的鐵錘的位置和角度, 自動(dòng)進(jìn)行插值運(yùn)算, 這個(gè)地方一般使用線性插值, 也可以使用更復(fù)雜的貝塞爾曲線插值.
- 火花2: 前面幾幀是空白的,到后面鐵錘敲打在底座上時(shí), 會(huì)在后面幾幀產(chǎn)生火花, 由于這幾幀火花使用的都是不同的圖片, 而且間隔最多1-2幀,所以這個(gè)地方不需要進(jìn)行插值運(yùn)算
- 火花1: 同火花2
2. 關(guān)鍵幀動(dòng)畫的好處
從上面的一個(gè)動(dòng)畫分析,我們可以看到關(guān)鍵幀動(dòng)畫的好處:
- 節(jié)省了資源
- 動(dòng)畫分層設(shè)計(jì), 邏輯清晰
3. 關(guān)鍵幀動(dòng)畫的適用范圍
我們可以看到 2個(gè)關(guān)鍵幀之間, 元件可以對(duì)下面的幾種屬性進(jìn)行插值計(jì)算從而實(shí)現(xiàn)動(dòng)畫的平滑過渡:
- 位置(x,y)
- 旋轉(zhuǎn)和傾斜(rotation/skew)
- 縮放(scale)
- 透明度(alpha)
- 顏色(color-rgb)
如果我們要做的動(dòng)畫不在上面說的這幾種范圍內(nèi)(比如對(duì)元件進(jìn)行Z軸翻轉(zhuǎn)), 那么就不適合使用關(guān)鍵幀動(dòng)畫.
三. 播放機(jī)制的實(shí)現(xiàn)
1. 特效結(jié)構(gòu)圖
從flash編輯器里的動(dòng)畫分層圖, 我們可以直接腦補(bǔ)出以下這張結(jié)構(gòu)圖:
2. 播放步驟
1) 創(chuàng)建4層空的容器層
2) 一幀一幀往后解析, 對(duì)于每一個(gè)容器層
- 容器層當(dāng)前為空時(shí), 如果遇到關(guān)鍵幀則創(chuàng)建該關(guān)鍵幀對(duì)應(yīng)圖片放入
- 容器層當(dāng)前不為空, 預(yù)先判斷下一個(gè)關(guān)鍵幀內(nèi)容,
- 如果下一個(gè)關(guān)鍵幀是對(duì)本幀圖片進(jìn)行了屬性修改(5種屬性), 那么根據(jù)當(dāng)前幀位置進(jìn)行插值計(jì)算, 修改本幀圖片的屬性
- 如果下一個(gè)關(guān)鍵幀是只是更換成另外一張圖片,那么本幀保持不變直到播放到下一個(gè)關(guān)鍵幀時(shí)替換圖片
- 如果當(dāng)前幀遇到空白幀, 則刪除容器里的所有內(nèi)容
3. cocos2d-x的實(shí)現(xiàn)
對(duì)于容器層我們不需要?jiǎng)?chuàng)建實(shí)際的顯示節(jié)點(diǎn), 我們可以畫出一個(gè)特效動(dòng)畫在某一瞬間的顯示樹結(jié)構(gòu):
四. 性能優(yōu)化
1. 使用紋理集 textureAtlas
我們可以把以上例子中使用到的散圖, 整合到一張大圖上(sprite sheet), 減少多次的io讀文件, 讓動(dòng)畫播放更加流暢, 也為下一步的批次渲染優(yōu)化打下基礎(chǔ).
2. 盡可能的批次渲染
我們知道在opengl進(jìn)行繪圖的時(shí)候, 如果我們幾個(gè)圖形都有一樣的顯示狀態(tài)( 紋理, shader及其uniform參數(shù), blend方式), 那么我們通過一次draw就可以同時(shí)畫出這幾個(gè)圖形.
在cocos2d-x v3.x版本里, 底層會(huì)自動(dòng)做判斷合并多次draw為一次批次渲染, 而在v2.x里, 我們需要自己實(shí)現(xiàn), 一個(gè)小成本的做法就是, 當(dāng)判斷可以批次渲染的時(shí)候, 在原本Node.addChild(sprite)的地方, 給改成 batchNode.addChild(sprite)即可.
渲染樹結(jié)構(gòu)如下:
3. 合理使用對(duì)象池
如果特效是長時(shí)間的不斷的循環(huán)播放, 那么我們在remove元件的時(shí)候, 最好不要馬上銷毀, 可以把它放入一個(gè)對(duì)象池里, 需要使用的時(shí)候,重新初始化元件拿出來使用就可以了.
五. 功能擴(kuò)展: 嵌套子特效
為了節(jié)省資源, 動(dòng)畫設(shè)計(jì)師可能會(huì)在某一層里放入以前做過的另外一個(gè)特效, 我們可以簡單的調(diào)整代碼就可以做到嵌套播放, 播放時(shí)的一個(gè)渲染樹結(jié)構(gòu)如下:
由于子特效很可能使用跟父特效不一樣的紋理, 如果我們?nèi)耘f想使用批次渲染,
我們有2種做法:
- A. 動(dòng)態(tài)合并特效紋理: 除非我們有太多的draw call需要合并, 不然動(dòng)態(tài)合并紋理的開銷明顯不合算
- B. 盡可能把使用相同紋理的相鄰層(個(gè)數(shù)超過1個(gè)才有合并的意義)進(jìn)行批次渲染
我們選擇B做法,那么調(diào)整過后, 渲染樹結(jié)構(gòu)如下:
六. 功能擴(kuò)展: 動(dòng)態(tài)子元件
考慮如下場景:
動(dòng)畫師設(shè)計(jì)了個(gè)抽卡動(dòng)畫特效, 他在設(shè)計(jì)的時(shí)候, 卡牌是畫死的, 但是我們在游戲里使用這個(gè)特效的時(shí)候, 需要這個(gè)卡牌可以動(dòng)態(tài)替換成我們要的卡牌.
要做到這個(gè)功能也不麻煩, 需要:
- 在導(dǎo)出特效的時(shí)候, 需要剔除掉這個(gè)畫死的卡牌圖, 免得浪費(fèi)資源, 同時(shí)對(duì)這個(gè)資源做一個(gè)標(biāo)記, 表示這個(gè)元件需要外部創(chuàng)建
- 在實(shí)現(xiàn)播放特效的代碼里, 在創(chuàng)建元件的地方(通常我們會(huì)使用工廠模式來實(shí)現(xiàn)), 發(fā)現(xiàn)某元件是需要外部創(chuàng)建的, 那么調(diào)用之前埋入的外部創(chuàng)建器進(jìn)行元件生成
七. 功能擴(kuò)展: 遮罩實(shí)現(xiàn)
考慮以下的動(dòng)畫效果:
動(dòng)畫分2層, 下面一層是背景圖層, 上面一層是一個(gè)圓形遮罩, 圓形遮罩會(huì)做一個(gè)從左到右的移動(dòng), 而只有在圓形覆蓋下的背景區(qū)域才會(huì)顯示出來.
opengl渲染管線里, 在fragment shader之后, 寫入frame buffer之前, 可以進(jìn)行stencil test, 它可以剔除不要的像素, 依據(jù)是stencil buffer里對(duì)應(yīng)的取值(1或者0).
對(duì)應(yīng)這個(gè)功能, cocos2d-x里有一個(gè)clippingNode類, 我們可以設(shè)置它的模板(stencil), 那么它里面的子節(jié)點(diǎn), 只有stencil覆蓋范圍內(nèi), 才會(huì)被渲染出來, 這就可以實(shí)現(xiàn)我們的遮罩功能了.
加入ClippingNode之后我們的渲染樹如下:
到這里為止我們已經(jīng)基本了解動(dòng)畫特效的實(shí)現(xiàn)原理. 下面我們會(huì)對(duì)上面提到的5種屬性做更深入的介紹.
八. 屬性詳細(xì)介紹
我們知道,在opengl的渲染, 本質(zhì)上就是定義頂點(diǎn)的位置, 顏色, 紋理坐標(biāo), 然后進(jìn)行繪制. 這里面頂點(diǎn)的位置(x,y) 是最基本的一個(gè)元素.
在我們實(shí)際的應(yīng)用里, 圖形可以縮放, 旋轉(zhuǎn), 傾斜, 移動(dòng),這些操作,本質(zhì)上是對(duì)頂點(diǎn)進(jìn)行位置的調(diào)整, 從數(shù)學(xué)上, 我們都可以歸結(jié)為對(duì)頂點(diǎn)(x,y) 做矩陣乘法.
1. 縮放(scale)
縮放是最簡單的, 就是把頂點(diǎn)的(x,y) 變成
x = x * scaleX
y = y * scaleY
這樣的一種坐標(biāo)變換, 我們可以用矩陣乘法來表達(dá):
$ \begin{bmatrix}scaleX & 0 \\0 & scaleY\end{bmatrix}\begin{bmatrix}x \\ y \end{bmatrix} = \begin{bmatrix}x*scaleX \\ y*scaleY \end{bmatrix} $
所以, 縮放操作對(duì)應(yīng)的變換矩陣為: $ \begin{bmatrix}scaleX & 0 \\0 & scaleY\end{bmatrix} $
2. 旋轉(zhuǎn)和傾斜(rotation/skew)
旋轉(zhuǎn)和傾斜本身是2種不同的變換, 這里放在這里一起講,是因?yàn)閒lash編輯器里的skew其實(shí)是rotation的變種, 它不同于傳統(tǒng)意義上的skew
1). rotation 旋轉(zhuǎn)
旋轉(zhuǎn)比較好理解, 就是點(diǎn)(x,y) 圍繞某點(diǎn)(通常是原點(diǎn)) 進(jìn)行旋轉(zhuǎn), 關(guān)于點(diǎn)旋轉(zhuǎn)有個(gè)數(shù)學(xué)公式(逆時(shí)針旋轉(zhuǎn)A角度):
x = cosA * x - sinA * y
y = sinA * x + cosA * y
所以我們很容易得出旋轉(zhuǎn)操作的變換矩陣為: $\begin{bmatrix}cosA & -sinA \\ sinA & cosA\end{bmatrix}$
2). 傳統(tǒng)意義的skew 傾斜
這里我們先看一下傳統(tǒng)意義上的skew的含義:
圖里黃色虛線框是skew之前的圖形,它原本是一個(gè)長方形,
- 在x方向上進(jìn)行A角度的skew, 就相當(dāng)長方形的4個(gè)頂點(diǎn)保持y坐標(biāo)不變, 而x = x + y*tanA
- 在y方向上進(jìn)行A角度的skew, 就相當(dāng)長方形的4個(gè)頂點(diǎn)保持x坐標(biāo)不變, 而y = y + x*tanA
所以我們很容易得出傾斜操作的變換矩陣為: $\begin{bmatrix}1 & tan(skewX) \\ tan(skewY) & 1\end{bmatrix}$
3). flash編輯器里的skew
經(jīng)過一些測試, 我們可以發(fā)現(xiàn)flash里的skew其實(shí)是 rotation的變種.
我們可以推算出它的變換矩陣為: $\begin{bmatrix}cos(skewY) & -sin(skewX) \\ sin(skewY) & cos(skewX)\end{bmatrix}$
也就是當(dāng)skewX = skewY = A的時(shí)候, 你會(huì)發(fā)現(xiàn)它就等于旋轉(zhuǎn)操作
3. 移動(dòng) (dx,dy)
1) 坐標(biāo)系的方向問題
flash編輯器里, y軸正方向是垂直向下的, 而opengl系的引擎比如cocos2d-x, 它的y軸是向上的, 這就需要flash里導(dǎo)出動(dòng)畫信息的時(shí)候, 如果是給cocos2d-x的引擎使用,需要把y坐標(biāo)做一個(gè)翻轉(zhuǎn).
2) 錨點(diǎn)問題
我們看一下上面動(dòng)畫例子里的底座在第一幀的信息:
動(dòng)畫師在編輯元件的時(shí)候, 一般都會(huì)把元件的錨點(diǎn)定在圖片內(nèi)的某個(gè)位置, 這樣方便后面做旋轉(zhuǎn)縮放,flash里元件的(x,y), 其實(shí)就是元件錨點(diǎn)的(x,y).
所以如果在cocos2d-x里, 把圖片創(chuàng)建出來成Sprite之后, 默認(rèn)錨點(diǎn)是在圖片正中間, 需要重新設(shè)置圖片的錨點(diǎn)為當(dāng)時(shí)flash里元件的錨點(diǎn)位置, 這樣才能保證(x,y)是正確的.
3) 變換矩陣
我們知道移動(dòng)之后,
y = y + dy
x = x + dx
我們會(huì)發(fā)現(xiàn)我們無法用之前的2維矩陣來表示這種變換, 我們需要把我們之前的2維矩陣擴(kuò)展成3維:
$$
\begin{bmatrix}1 & 0 & dx \\ 0 & 1 & dy \\ 0 & 0 & 1 \end{bmatrix}\begin{bmatrix}x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix}x + dx \\ y + dy \\ 1 \end{bmatrix} $$
同理, 為了統(tǒng)一, 我們回頭把我們上面提到的縮放,旋轉(zhuǎn),傾斜的2維矩陣也擴(kuò)展成3維的, 只需要右下角填充1, 其他位置填充0即可.
到這里為止, 縮放, 旋轉(zhuǎn),傾斜, 平移, 這些操作我們稱之為 仿射變換(Affine Transformation)
4. 多個(gè)矩陣相乘
我們對(duì)一個(gè)頂點(diǎn)同時(shí)做縮放, 旋轉(zhuǎn), 等多個(gè)操作, 其實(shí)相當(dāng)于就是對(duì)這個(gè)頂點(diǎn)做幾次矩陣乘法, 因?yàn)橐话銇碚f圖形會(huì)有多個(gè)頂點(diǎn), 對(duì)每個(gè)頂點(diǎn)都做這個(gè)計(jì)算會(huì)比較浪費(fèi), 我們可以先把這些矩陣先提前相乘, 最后把結(jié)果再統(tǒng)一跟所有頂點(diǎn)做一次矩陣乘法即可.
同時(shí)需要注意, cocos2d-x等引擎都會(huì)有渲染樹的概念, 也就是圖形可以是另外一個(gè)圖形的子節(jié)點(diǎn), 那么在實(shí)際渲染子圖形的時(shí)候, 需要依次取得到根節(jié)點(diǎn)的所有變換矩陣, 全部進(jìn)行相乘, 才可以正確的渲染.
5. 透明度(alpha)
1) alpha用在什么地方
我們可以對(duì)顯示對(duì)象設(shè)置alpha值, 顯示對(duì)象的紋理本身每個(gè)坐標(biāo)對(duì)應(yīng)的像素也有自己的alpha值, 最終2個(gè)alpha值會(huì)相乘得到這個(gè)顯示對(duì)象某點(diǎn)坐標(biāo)的最終alpha值.
alpha值它可以用在alpha test, 或者一些shader中作為某種特殊輸入, 更多的, 它可以用于繪制最后一步上: 寫入frame buffer.
這一步我們叫做blend, blend公式是可以選擇的,
大多數(shù)情況下, 寫入公式為:
src為本圖形某像素點(diǎn)的顏色
dest為目標(biāo)像素點(diǎn)的原來顏色
新顏色 = src.rgb * src.alpha + dest.rgb*(1-src.alpha )
我們可以調(diào)整blend方法來修改這個(gè)默認(rèn)公式.
2) pre-multiply alpha
我們看看上面這個(gè)公式里src.rgb * src.alpha, 我們可以把這個(gè)計(jì)算優(yōu)化掉, 讓圖片在導(dǎo)出的時(shí)候, 就讓它每個(gè)像素的rgb 都乘以alpha(這個(gè)過程叫alpha premultiply), 這樣雖然導(dǎo)出來的圖片會(huì)有點(diǎn)怪, 但是在游戲渲染的時(shí)候, 我們可以告訴它我們這個(gè)紋理已經(jīng)是預(yù)先乘過alpha了, 你在blend的時(shí)候就可以少做一些乘法運(yùn)算了.
通過這個(gè)辦法我們可以提高一些渲染性能.
premultiple alpha還有一些其他的好處, 比如可以統(tǒng)一blend模式方便batch渲染, 顏色線性插值計(jì)算時(shí)不會(huì)因?yàn)閍lpha的差異過大導(dǎo)致出現(xiàn)奇怪的邊緣等問題.
6. 顏色(color)
我們看一下flash編輯器里的顏色編輯:
顏色效果這里, 有2個(gè)部分, 一個(gè)是RGB分量, 一個(gè)是RGB的絕對(duì)值疊加,
RGB分量部分可以通過給頂點(diǎn)設(shè)置顏色(setColor) 實(shí)現(xiàn),
而RGB絕對(duì)值疊加部分會(huì)有點(diǎn)麻煩, 一般我們需要通過寫一個(gè)fragment shader來進(jìn)行顏色分量的疊加, 而且還需要考慮父節(jié)點(diǎn)的顏色疊加效果 跟 子節(jié)點(diǎn)的顏色疊加效果需要做一個(gè)加法.