QQ空間萌寵之舞:HTML5骨骼動畫實踐
4月16日QCon前端工程實踐專場會議上,騰訊QQ空間營收方向負責人、web前端工程師李振文以”QQ空間萌寵之舞——HTML5骨骼動畫實踐”為主題,用去年上線的QQ空間萌寵作為實踐案例,為大家分享了骨骼動畫的實踐技術點、骨骼動畫實踐中遇到的問題以及最終的解決方案,***是根據總結的經驗得出得性能優化方案。
李振文:我是來自QQ空間的李振文,很高興在QCon上跟大家交流,QQ空間去年上線了一個寵物,我們使用了骨骼動畫實現這個游戲。從零基礎最終到這個游戲上線,我們開發實踐過程當中遇到很多的問題。開發實踐中遇到的問題和一些思路想和大家交流一下。
這是我今天介紹的一個大綱,首先給大家介紹一下骨骼動畫的基礎知識點。然后是項目中幾個工程技術實踐,以及遇到的問題和解決方案,***講性能優化上面的幾個實踐點。
先介紹一下骨骼動畫的基礎知識點,我拿幀動畫和骨骼動畫的素材做了一個對比。屏幕上幀動畫實現不停的切換每一張圖片實現動畫播放的。這樣如果我們要去實現多套動作的話,就需要準備很多套圖,靈活性很差,素材體積也大。但骨骼動畫是將素材拆解成一個個零件,圖片上人拆解成頭、身子、盾、矛,然后用這一套素材拼成不同的動作,跑步、跳躍等都可以實現。骨骼動畫設計上的做法跟我們代碼模塊化游戲相似。
這里列出了骨骼動畫的特點。
***,資源體積更小,骨骼動畫保存骨骼相關的動畫數據,不需要把每一幀圖片都保存下來,所以需要的資源很小,小的圖片就可以實現。
第二,多角色可以共用同一套數據,我們實現一套骨骼之后,只需要把骨骼上面圖片素材更換或者修改就可以實現不同的角色。
第三,動作可以自由組合,比如一個角色它可以這樣搖頭,搖頭同時把手抬起來,把腳起來,可以自由組合。
第四,骨骼動畫有網格的功能,可以實現蒙皮、自由變換,動畫更加逼真。
第五,骨骼動畫對處理器要求性能更高,骨骼動畫運動的時候需要大量的計算,因此性能要求更高,這也是我們實踐當中遇到的***挑戰。
這是一個組裝好的骨骼動畫,圖片中將骨骼顯示出來的,每一條灰色線就是一根骨骼,骨骼上面的小黑點是關節,每個都可以旋轉,骨骼動畫運動只需要動一個骨骼,與之相連的骨骼就可以一起運動。這張圖藍色線條的部分,是手的上臂骨骼,移動這個上臂骨骼,子骨骼,前臂和手掌也跟著一起運動。這樣就非常美妙了,我們實現聳肩只需要關注肩膀的運動,不需要關注手臂和手掌的運動。
這是我們整理出來的骨骼動畫的組成結構,這個組成結構對應的動畫編輯器是SPINE。包括有骨骼、插槽、附件和動畫。骨骼是核心,是豎狀結構,插槽附著到骨骼上面,插槽上面可以放很多附件,附件包括圖片和網格,圖片是我們肉眼可以看到的,圖片素材頭、腳、身子、腿這些。網格可以用來實現自由變形和蒙皮效果,讓動畫更加逼真。動畫控制每一幀骨骼位移和旋轉位置。
這是一個骨架的樹狀結構,這個骨架下面有軀干、左腿、右腿和盆骨。而軀干下面又有左臂、右臂和脖子。可以看到骨骼上面是附著有圖片的,這些圖片其實就是附著在骨骼的插槽上面,紅色圓圈部分就是插槽。插槽是SPINE編輯器的一個概念,為了讓骨骼不影響繪制順序而創造的,例如,如果我的軀干部分有衣服和肚子,髖骨有褲子,繪制順序應該是肚子在最下面,褲子在中間,衣服在最上面,如果沒有插槽這個概念,那么就要建立三個骨骼,軀干要拆成兩根骨骼,這樣不合理,但是有了插槽就只需要兩根骨骼,可以在軀干這一根骨骼上面建立兩個插槽分別放衣服和肚子。
介紹完骨骼我們介紹一下網格,圖片中藍色的線條就是網格,一條一條的線非常形象。剛才講網格實現蒙皮和自由變形。為什么可以實現自由變形?因為有頂點、邊緣、三角區域三個概念。頂點就是藍色的點。三角區域就是三個點組成的三角區域。邊緣就是整個大框架的邊緣。有了這三個概念,只要我們移動***可以將三角區域紋理進行變形,圖片就變形了。***張圖我們拖動藍色的頂點,就可以拉長鼻子。蒙皮效果就是能夠讓骨骼運動影響網格的方法,進而影響圖片素材的運動,運動效果顯得更逼真。我們來看一個演示效果:
左邊動畫沒有使用蒙皮,右邊動畫使用了蒙皮,左邊耳朵和長矛是僵硬的,右邊耳朵和長茅抖動顯得更逼真,沒有蒙皮效果的骨骼動畫設計是不完整的。
做骨骼動畫,***步選擇對應的編輯工具,這影響后面動畫實現和運行庫的選擇。現在市面上主要有兩款,***個是SPINE,國外付費軟件,運行庫和功能都很多,而且普及程度高,大部分設計公司都用的這個,因為考慮后續設計資源的緣故,我們選擇了SPINE。另一款是龍骨,我們國產軟件,免費,運行庫和功能相對較少,但是作者在不斷完善中,功能也在向spine靠齊,而且關鍵的是可以直接與作者溝通。這里大家在學習的時候建議可以先用龍骨上手。后續再根據游戲的需求來選擇用哪個軟件。
SPINE支持的運行庫非常多,官網上列了很多,我們這里挑了幾個來做對比,主要從性能、是否支持webgl、文件大小、文檔還有活躍度方面來做選擇。因為幾個底層庫實現都相同,性能上沒什么太大的差別;cocos2d的骨骼動畫組件由于沒人維護,直接放棄了;最終我們結合項目特色,選擇了PIXI這款輕量的引擎。它的體積、文檔還有活躍度上都非常不錯,當然PIXI也有他的缺點,PIXI是一個較底層的游戲引擎,很多功能需要使用插件或者自己去實現,因為我們要做的游戲主要是以播放動畫為主,再附帶一些界面交互,所以選擇了PIXI,這里大家在實際項目中可以列出自己關注的維度來進行對比挑選適合項目的引擎。
介紹完了骨骼動畫的一些基礎知識點,來看下我們QQ空間做的這款寵物游戲,這是一個養成類的游戲,主要功能包括喂食、偷糖果、換裝、組合動作。還可以和你聊天,講笑話,玩成語接龍,在空間主頁也會顯示這只寵物。大家可以下載***版的QQ空間手機版來體驗這款游戲。
接下來介紹項目實踐中幾個技術點實現。我們寵物游戲核心功能之一就是換裝扮,可以自由替換帽子、衣服、褲子、背部掛件,換裝扮對骨骼動畫來講就是換附件,附件有普通的圖片類型,還有蒙皮類型,普通圖片PIXI支持了,但是蒙皮類型的沒有,要實現蒙皮類附件的替換,最開始想到的方式,hack引擎從圖集讀取出來的附件信息,修改它的紋理指向換裝之后紋理。再構造一個新的SPINE對象,這樣雖然能實現需求,但是畫面會有閃動。
為了實現更完善的方案,我們熟讀了一遍PIXI代碼,找到了更好的方式,pixi有一個使用canvas做紋理的接口,所以:把canvas代替png圖片繪制,如果有換裝,就覆蓋canvas以前位置的圖片。實現換裝有兩部分,***部分是在頁面打開初始化的時候,需要給寵物穿上現有的裝扮,首先將原始圖集轉成canvas,再將裝扮圖片插入canvas指定的位置,這個位置是在SPINE導出的配置文件里定義好的,然后再通過PIXI的接口把canvas轉成紋理,接著用這個新的紋理替換掉附件的舊紋理,再渲染就行了。第二部分是在裝扮商城里換裝,只需要覆蓋原來衣服在canvas上的位置,在刷新下紋理就可以實現無縫換裝了,代碼里是簡單示例,***步清理canvas上的舊衣服圖片,第二步把新的衣服圖片畫到canvas上面,***更新畫布。經過這兩部分,就能***地實現換裝功能了。
第二個功能點分享GIF圖,游戲分享出去有GIF圖動起來轉化效果更好。我們實現GIF主流程有三個大步驟。***步H5截取每一幀圖片,然后傳給客戶端組裝成GIF圖同時壓縮,***上傳到后臺發布分享。合成GIF圖的也可以用純JS方案或者后臺方案,純JS方案需要引入第三方庫,而由于GIF圖太大我們要進行壓縮,壓縮就涉及到很多像素對比運算,效率很低,需要10秒左右。后臺方案,如果我們把圖片截好之后傳給后臺,這中間網絡傳輸耗時就會很大,所以我們選擇用客戶端幫助我們合并GIF圖的方案。
我們來看下H5截圖的邏輯,主要是從canvas中將每一幀的圖片截取出來,因為我們頁面上會播放這個動畫給用戶看,為了節省資源,決定利用這個已有播放動畫的canvas。最簡單的,播放動畫的時候截取每一幀的圖片,但是實現之后發現一些低端機型截出來的圖片不完整,播放起來就像演示一樣很卡頓,原因是這些低端機性能很差,導致截圖的時候會漏掉幀數。所以我們采取了多輪截圖方式,通過FPS和動畫時長計算出需要截取的圖片數量,將截取的圖片按照計算出來的key值進行保存,這樣就能保證截取動畫的完整性。FPS我們在游戲中會有一份緩存,沒有的話會再跑一遍計算FPS。然后通過FPS和動畫時長可以計算出需要截取的張數,然后在動畫播放過程中進行截圖,截取的圖片會通過key值進行保存,判斷截取的圖片數量達到預期后就結束截圖,通知客戶端進行合并。
這是通過新的方式截取出來的gif圖,效果不錯。這個方案的優勢在于利用了頁面中已有播放動畫的canvas來實現,減少了額外的資源。在設計方案的同時考慮性能和業務場景,來制定更合適的方案。
接下來向大家分享一下開發過程當中遇到的問題點。***個問題在調試素材的時候我們發現有些動作會展示錯位,它的肚子和衣服都會飄到天上去,我們發現這個問題發生時有共性,只有蒙皮類型的動畫才會出現這樣的問題。為什么會這樣?我們定位源碼,閱讀他們的源碼,找到了觸發原因,同一個插槽上面有多個附件就會觸發這個bug,PIXI引擎在定時器更新遍歷插槽時間軸的時候,在region切換到mesh類型的時候或者mesh切到region的時候,引擎沒有隱藏之前的附件,所以就會產生漂移。通過修改源碼解決了這個bug,***版的PIXI已經修復了這個bug。
第二個問題發現部分機型播放蒙皮類動畫會有閃爍的問題,當時離發布只有一天了,時間非常緊急。我們一方面查源碼定位這個問題,一方面查是不是素材導致了這個bug。在定位問題時,我們發現用官方的示例代碼在特定機器上也能重現,確定了是引擎自身問題,我們當時嘗試切換PIXI到舊版本,發現在某個低版本這個問題沒有,可以肯定是新版本PIXI某個代碼改動導致了這個bug,當時時間緊迫,所以臨時用了舊版本PIXI解決問題。但仍要搞清楚新版本為什么有這個問題,我們對比新舊代碼,看到了是這行代碼的緣故,這行代碼到底做了什么事情呢?這個變量是控制是否使用系統的VAOS插件,只要用了系統的VAOS插件就會有問題。VAOS插件可以避免循環渲染中不必要的開銷,也會改進我們做循環處理時的代碼寫法。但是它對性能的提升不大的,基本上可以忽略不計,主要作用可以讓我們處理循環渲染的時候做一些寫法上面寫起來更加優雅,代碼上PIXI給我們做好了封裝,我們全局關閉就可以了。
我們小結一下剛剛講得這些內容,兩個功能點實現,實時換裝和技術點截圖。蒙皮錯位和閃爍bug,一方面我們熟讀源碼,第二是與作者交流。熟讀源碼讓我們更快定位到問題的癥結點,與作者交流可以幫助我們找到***辦法解決問題。同時我們解決問題的時候,同時朝著不同的方向去努力的。如果當時因為一些特殊的原因使用了臨時方案的話,之后要找到問題的本質,用***方案解決它,避免今后某個時間點背負這些技術債。
接下來交流我們在性能優化上面做的幾件事情。當時主要從CPU、GPU、內存三個指標上測量性能,剛開發完時我們CPU有40%,GPU占67%,內存增量有100兆,這個數據光看起來就很差,實際測試對用戶影響導致手機耗電、發熱、卡頓和崩潰。
先來看下為什么CPU和GPU占用會這么高?看圖片中的人物,我將所有的骨骼和網格都顯示出來了,骨骼動畫每一幀都需要控制這些點的運動,做了很多矩陣運算,而且開啟了gpu加速,所以只要運動,cpu和gpu的占用就會飆升,越精細的動作,它控制的骨骼和網格數量就越大,所以消耗也會越大
我們做了三個處理,***、減少每一幀運動的骨骼及網格數量,根據實際情況權衡的,減少太多動作就會不精美。第二、我們通過減少不必要的運動頻率,例如把待機動作改成隔幾秒動一次,減少持續性的消耗。第三APP切換到后臺時停止動畫,防止切換到后臺動畫一直播放導致APP崩潰。
做了這幾點以后,我們待機時CPU占用到12%,GPU占到34%,基本上跟客戶端的占用差不多了。
第二個性能優化,減少內存占用。內存的占用主要來自紋理圖片、JS對象的占用,于是我們做了幾個測試,***個測試是將兩倍尺寸的紋理圖換成單倍圖,但是實際測試優化效果并不大,和預期不符,經過查證,原因是我們這個測試是用chrome的profile來測試的,profile只會檢測js對象和DOM占用的內存,不會檢測GPU內存,而紋理圖占用的是GPU內存,所以這里檢測不出來,這也是我們從普通web開發轉游戲開發后的一個誤區,沒有關注到GPU內存的占用,后面會講到gpu內存的優化。這里先看第二個測試,將動作數據進行精簡,對比發現精簡后的內存占用大大減少,于是我們對動作數據進行了拆分。要實現動作數據拆分,并且按需加載,只在用到某個動作的時候才加載這個動作數據,我們暴露了PIXI的這個私有接口,這樣就能直接添加新的動作數據進去。動作數據為什么占這么的內存?我們寵物游戲的特色,用戶可以通過組合不同的動作來生成新的動作,所以它的動作數據很多。最開始我們完整的動作數據有70個,文件大小有3兆,轉化成JS對象以后占用的內存更多,拆分后的初始動作從70多個減少到4個,文件大小從3M減到200K,拆分之后,JS占用的內存從49M優化到了18M,而且因為配置文件體積也變小了,頁面加載速度也提升了30%。
第三個性能問題,游戲里面有個排行榜,可以切換到好友寵物去偷糖果,測試中不停地切換好友,內存會不停地往上漲,表現出來的問題就是webview進程會崩潰。我們的好友數量一般都有上百個,多的有上千個,所以這個問題很明顯。
瀏覽器內存分為這幾塊,JS、DOM、GPU內存、編譯后的Code等。JS和DOM的內存占用可以通過chrome的Timeline和Profiles來分析,其他的則可以通過chrome的任務管理器來看。開發調試時主要用chrome工具,上線前測試的時候需要在APP里面看實際環境的數據,就要借助客戶端相關的測試工具。IOS的話用xcode,安卓是用的我們公司內部自研的一個工具。經過測試,切換12個好友后,JS內存增長了18M,GPU內存增長了90M,非常的嚴重。
***部分JS的內存占用通過定位下來主要是附件、網格數據的增長。如果新建一個模型對象的話會有很多重復的附件數據,因此針對同一種寵物,我們復用他的模型對象,切換好友就相當于只是換裝扮。切換之后的SPINE對象以及初始紋理都能夠復用。JS的內存增量從18M優化到了5M。
我們再來看GPU內存,由于前面復用了模型,模型的紋理也能復用,因此可以減少GPU內存占用。在確定這些紋理不再使用后,可以手動執行PIXI的dispose方法主動釋放紋理,GPU內存占用從90兆優化到了30兆,效果很明顯。***我們從排行榜切換出來,銷毀好友的寵物數據,回收內存。
我們總結一下這次的分享內容,性能優化上面針對CPU、GPU內存主要有三個方面,***減少待機動畫的頻率。將待機動畫這種一直在播放的動畫減少頻率減少持續性壓力。第二點動作、素材文件做成按需加載,一方面減少內存的占用,第二方面可以提高訪問的速度。第三紋理和模型盡可能的復用,減少內存占用。另外使用臨時方案或者我們迫不得已使用臨時方案,也需要深究***方案,避免某個時點背負這些技術債務。閱讀源碼、與原作者交流能夠更好幫助我們。發布標準的制定,使用參照物對比。我們***測試階段的時候有找一些參照物的,比如說我們之前開發過普通的游戲,我們可以將這些普通游戲CPU、GPU占用,以及內存增量做一個對比。非常感謝大家,我的分享就到這里。