什么是骨骼动画,本篇先简单做下科普,其他大家自行百度哦
比帧动画:它比帧动画大幅节省了资源空间,也比帧动画对手机性能有更高的要求,webgl下能达到最佳的展示效果
编辑器选择:业界比较主流的骨骼动画编辑器有SPINE和DragonBones(龙骨,egret白鹭公司在维护)。我们联系到cp使用的SPINE编辑器比较多,而又需要同样一个资源文件,三端公用(ios,安卓,H5)SPINE的运行库选择更多,所以我们选用了SPINE编辑器,虽然都是骨骼动画,但是他们的动画原理是略有差别的。
先让我们解开骨骼动画神秘的面纱吧~纯手画给大家奉上我对于骨骼动画的理解,这里仅仅是SPINE编辑器下的原理,不同的编辑器有略微的不同,这里不多余说明。如果当时有人给我这张图,能节省我1天工作量,呜呜~
图解:
(a)骨骼是一个树状结构,有个明显的好处就是,如果动画时要设置位移,只要设置根节点的位移,整个任务都能一起位移。
(b)附件其实人物外表的展示,主要有三种类型:图片,蒙皮,权重蒙皮。图片好理解,就好像是一张贴花贴着,很僵硬,就像下面左边的那个长枪,右边就是蒙皮,可以定义形变,让整个长枪动得更加自然流畅
左图右图
为什么蒙皮能自由形变呢?因为它有顶点,边缘,三角区域这三个概念,能对图片某个区域变形,这中特性在webgl是原生支持的,但canvas2d下是引擎自己写的,这个就是说骨骼动画对性能要求更高就是因为这个。为什么这三个概念能自由变形呢?看下面的图解
只要移动了那个顶点就能拉长鼻子
(1)引入SPINE编辑器导出的json文件
(2)引擎自动引入同名的atlas和png文件
(3)解析json和atlas文件,生成spine对象
(4)加入到容器里面
(5)定时器渲染,播放动作
最简单的方式,hack引擎从atlas读取出来的附件信息,修改它的texture指向换装之后texture(webgl渲染用的纹理)。再new一遍Spine对象,这样虽然能实现需求,但是画面会有闪动,体验不好
所以在熟读了一遍PIXI代码之后,找到了更优雅的方式,pixi有一个Texture.fromCanvas的接口,可以把一个canvas作为一个纹理绘制,所以:把canvas代替png绘制,如果有换装,就用canvas的clearRect擦除和drawImage覆盖以前位置的图片(需要注意的是旋转这个参数)。bingo,换装功能完成,换装时就不再闪动了,而且图片类型的附件还是使用引擎自带换装方式,更快!流程大致如下:
经验教训:实现代码之前务必熟读引擎源码,理解原理流程,打通任督二脉,找到最佳实现方法。
实现原理:
animation.state['setAnimationByName'|'addAnimationByName'](track,act_name,loop,delay);可以基于轨道track(int)来做动作的叠加
亮点:
(a)实现播放动作有限次
(b)维持一个播放队列
问题1:webgl截图空白
答:
(a)WebGL获取上下文时,有一个关键参数:preserveDrawingBuffer,默认为false,表示在绘图完成后不保留绘图缓冲区。如果设置成true,会影响性能(b)在定时器里面,同步截图,DONE!
问题2:毛玻璃效果怎么实现?需要重新引入库来解决吗?
答:否,pixi引擎自带支持哦,Container对象底下有个filters参数可实现毛玻璃效果
方案1:单次播放法,满帧截取的情况下,ios能达到要求,但是安卓上不同机型,不同机器现状都可能截出不同效果的gif,差的时候只能2,3张,效果差而且不稳定
返回:由Math.floor(t*fps)生成的index截图组成的数组;最终的截图数组长度>=fps*动画总时长就停止截图最终的效果也是很赞的
动画的平均长度为1.8s,两端的截图耗时IOS:2025msAndroid:4535ms
第一次用PIXI,第一次用PIXI-SPINE。作为一个要上线承载上亿用户的产品,开发过程,遇到了一些引擎水土不服的地方,主要有:
定位:是因为同一个插槽下面有多种类型的附件
if(type===spine.AttachmentType.region){if(slot.currentMesh){slot.currentMesh.visible=false;slot.currentMesh=undefined;slot.currentMeshName=undefined;}....}
if(type===spine.AttachmentType.skinnedmesh||type===spine.AttachmentType.mesh||type===spine.AttachmentType.linkedmesh){if(slot.currentSprite){slot.currentSprite.visible=false;slot.currentSprite=undefined;slot.currentSpriteName=undefined;}....}最近看了下PIXI-SPINE的最新版本这个bug已经fix了。
定位:有mesh且在某些机器上才能重现,比如nexus5。这些机器并没有明显的共性。把webgl渲染强行切成canvas2d的,显示就正常了,说明还是webgl下mesh某种特性兼容性的问题
解决:通过google搜索,和自己debug。发现是因为一个参数引起的,如果设置成true,出问题的机器上就正常了
PIXI.glCore.VertexArrayObject.FORCE_NATIVE=true;这句话的意思,如果是false的话,就用每种浏览器对于创建VAO(顶点数组对象)的扩展(之前有提到过,mesh是有顶点,三角形区域这样的组成)
if(!VertexArrayObject.FORCE_NATIVE){this.nativeVaoExtension=gl.getExtension('OES_vertex_array_object')||gl.getExtension('MOZ_OES_vertex_array_object')||gl.getExtension('WEBKIT_OES_vertex_array_object');}
功能开发好不容易完了,兼容性也OK了,但是性能却挺糟糕,crash,发热,进入游戏慢,性能bug单狂轰滥炸,静下心来各个击破,最终项目各个性能指标达标,在外网稳定运行。
工欲善其事必先利其器,定位性能问题,要通过工具去分析哪里是性能瓶颈,才能有的放矢,虽然通过chrome的性能分析工具已经能发现大部分问题,但是ios和安卓上因为实现的差异是不是存在其他的问题,也需要测试一下才放心。这里搜罗了一下常用的性能分析工具,供大家参考
原因:页面内存占用过高是主要因素,什么资源最占用内存呢,通过chromeprofiles面板分析便知
54%的内存都消耗在动作的timeline数据上了。用排除法分析了一下一个动作的json占用的内存数
每个动作原始数据+解析成数组总共占用390KB。每个用户每次用的动作有限,并不需要把完整的动作数据加载出来,所以解决crash可以通过一下方法
(a)首次只加载模型json以及必须的2个动作数据,其余的按需加载,解析完塞到宠物的动作数据里面(这里需要改写源码暴露读取动作数据的接口)
效果:内存减少:49M减小到18M,减少了80%(chrome上测试)
解决:降级策略看起来这里唯一行得通的解决方案了,没有渲染就没有发热。所以就要把渲染用在刀口上。损失非关键体验,安卓停掉默认动作停止渲染,需要时再打开渲染,改成隔3秒播放一次动作让用户感觉也是在一直动
效果:有效解决手机发热的问题
PIXI是支持自动识别浏览器是否支持webgl来选择是canvas2d还是webgl来渲染动画,先科普下webgl的市场占用率吧
ios:ios7.1以上都是支持WebGL的
android:安装了tbs的机器支持WebGL
因为tbs是热更新的,新安装的APP没有tbs,就不支持WebGL,而又是我们推广的关键时期。所以canvas2d下的渲染也需要做好它的优化。
解决:canvas2d下的帧耗时的优化可以通过将canvas2d中不变的背景独立到dom上,不放在canvas里面渲染
效果:优化了40%左右。举证材料都会在附件里面
解决:
(a)不采用一般游戏进入先loading资源,采用背景和宠物初步加载的方式
(b)动作JSON按需加载
表现:安卓上黑屏,ios上因为用了wkwebview会重新loading
定位:在排行榜频繁切换好友的时候会必现,排行榜的设计模式是每个好友都是独立的,每次都销毁上一个宠物,再添加下一个宠物,这是面向对象编程基本的思路。
可是这样切换几次后就会异常,直觉告诉我是跟WebGL或者内存有关。这里先科普下浏览器的内存占用分几块:
jsheap和Dom内存的占用通过chrome的profiles和timelines面板可以看出来,但是其他的内存占用可以在哪里看呢?通过chrome的更多工具->资源管理器即可!
有了这个强大的工具,问题定位将不是问题,其实主要是两个问题
(a)js内存上涨,切换12个好友js内存上涨了18M,定位下来增长的内存是附件mesh等等信息
(b)GPU内存只升不降,GPU内存就是WebGL占用的内存,如果有独立显卡的话他的内存占用是和浏览器内存独立的,但是手机端没有独显,可能会跟浏览器有共享内存,这个按机型而异。但是GPU内存增长太多绝不是好事,它可能会影响浏览器申请更多的GPU,造成页面黑屏,也会导致浏览器占用内存过多,被原生app杀掉或者重启。所以这里就是频繁申请释放GPU内存造成的内存泄漏
解决:既然已经发了问题,解决方法就应运而出啦
(a)同一种宠物模型对象复用,切换下一个好友的宠物就是换装,解决js内存过度增长!
效果:切换12个好友内存增长了5M而已
(b)换装纹理复用,不销毁。因为换装纹理是一个canvas,完全可以擦除之后再回收再利用。解决了GPU的内存泄漏
效果:
以前的方案,5次切换好友之后,GPU内存直接飙升了90M166M到255M
复用纹理后,5次切换好友,GPU只上涨了30M,第二次切换后并没有上涨166M到199M
(c)从排行榜切换回来之后,销毁好友的宠物模型。这个是PIXI官方提供的销毁接口。
pet.stage.destroy(true)销毁后,GPU内存恢复到最初状态170M左右
经过上述三次改造后,内存就维护在一个比较稳定的状态,黑屏和reload的问题就修复好了
1.做游戏,需要熟悉引擎源码以及WebGL。越熟悉越好
2.项目需要的内存越多,能够运行它的终端就越少,所以一定要想方设法的定位内存占用大头,逐个击破
3.可以使用降级策略,降级策略要能因机制宜,切忌大刀阔斧。根据机器的剩余内存和GPU核数或者当前的FPS值来做降级策略