在做这个项目之前,我也和很多人的想法一样觉得:H5做动画性能不行,只能完成简单动画,可是事实并非如此。所以借此篇分享振奋下想在H5下做酷炫游戏的人心。

体验游戏请长按二维码识别:

好吧,知道你懒。不想扫码的可以看下面的视频:

初识入门

什么是骨骼动画,本篇先简单做下科普,其他大家自行百度哦。

比帧动画:它比帧动画大幅节省了资源空间,也比帧动画对手机性能有更高的要求,WebGL下能达到最佳的展示效果。

编辑器选择:业界比较主流的骨骼动画编辑器有SPINE和DragonBones(龙骨,egret白鹭公司在维护)。我们联系到CP使用的SPINE编辑器比较多,而又需要同样一个资源文件,三端公用(iOS,安卓,HTML5)。SPINE的运行库选择更多,所以我们选用了SPINE编辑器,虽然都是骨骼动画,但是他们的动画原理是略有差别的。

运行库选择:SPINE官网上有各种语言运行库的推荐,单js有10余种运行库,到底选哪个游戏引擎,工欲善其事必先利其器,选择很重要。选择恐惧症的话还是在科学的量化的方式选择更适合我们项目的游戏引擎。主要有性能,是否支持webgl,库体积,论坛活跃度,API健全度等5个维度综合分析后,选择了PIXI引擎。附件会附上对比表格。PIXI的使用上比较便利,官网上有丰富的例子让你熟悉它的使用,这里不做详述。

深入了解骨骼动画原理

在新接手这个项目之前,公司km上还鲜有骨骼动画H5的实践,听了IEG T4专家david分享的游戏开发经验,虽然没有提到H5上的经验,但是他建议的熟读游戏引擎源码很是受用,熟读代码既能让你实现功能的时候更得心应手(一下就找到最优方案,而不是不停返工),也能在性能优化时有的放矢,所以就这样,一手api,一手源码开始了空间宠物的开发。

先让我们解开骨骼动画神秘的面纱吧~纯手画给大家奉上我对于骨骼动画的理解,这里仅仅是SPINE编辑器下的原理,不同的编辑器有略微的不同,这里不多余说明。如果当时有人给我这张图,能节省我1天工作量,呜呜~

图解:

1、骨骼动画中的人物是由有骨骼,插槽,附件部分组成,这三部分都是1对N的关系:

  1. 骨骼是一个树状结构,有个明显的好处就是,如果动画时要设置位移,只要设置根节点的位移,整个任务都能一起位移。

  2. 附件其实人物外表的展示,主要有三种类型:图片,蒙皮,权重蒙皮。图片好理解,就好像是一张贴花贴着,很僵硬,就像下面图一的那个长枪,图二就是蒙皮,可以定义形变,让整个长枪动得更加自然流畅

图一

图二

为什么蒙皮能自由形变呢?因为它有顶点,边缘,三角区域这三个概念,能对图片某个区域变形,这中特性在webgl是原生支持的,但canvas2d下是引擎自己写的,这个就是说骨骼动画对性能要求更高就是因为这个。为什么这三个概念能自由变形呢?看下面的图解。

只要移动了那个顶点就能拉长鼻子

2、骨骼动画中的动画,是基于时间轴的,定义某个时间点显示那个附件,骨骼的位移和旋转等等,如我手画的下图所示:

3、引擎播放骨骼动画流程。

  1. 引入SPINE编辑器导出的json文件

  2. 引擎自动引入同名的atlas和png文件

  3. 解析json和atlas文件,生成spine对象

  4. 加入到容器里面

  5. 定时器渲染,播放动作

4、其他,主要的就上面3点了,其他的自行了解咯。

空间宠物关键技术实现

1、实时换装

换装功能跟附件息息相关,图片类型的附件引擎自带换装方式,但是蒙皮类型的附件却没有!

最简单的方式,hack引擎从atlas读取出来的附件信息,修改它的texture指向换装之后texture(webgl渲染用的纹理)。再new 一遍 Spine对象,这样虽然能实现需求,但是画面会有闪动,体验不好

所以在熟读了一遍PIXI代码之后,找到了更优雅的方式,pixi有一个Texture.fromCanvas的接口,可以把一个canvas作为一个纹理绘制,所以:把canvas代替png绘制,如果有换装,就用canvas的clearRect擦除和drawImage覆盖以前位置的图片(需要注意的是旋转这个参数)。bingo,换装功能完成,换装时就不再闪动了,而且图片类型的附件还是使用引擎自带换装方式,更快!流程大致如下:

经验教训:实现代码之前务必熟读引擎源码,理解原理流程,打通任督二脉,找到最佳实现方法。

2、组合动作编辑  用户对于宠物不同部分的动作自由组合,形成特色动作

实现原理:

animation.state['setAnimationByName'|'addAnimationByName'](track, act_name, loop, delay);

可以基于轨道track(int)来做动作的叠加

亮点:

  1. 实现播放动作有限次

  2. 维持一个播放队列

3、快照分享

问题1:WebGL截图空白

答:

  1. WebGL获取上下文时,有一个关键参数:preserveDrawingBuffer,默认为false,表示在绘图完成后不保留绘图缓冲区。如果设置成true,会影响性能

  2. 在定时器里面,同步截图,DONE!

问题2:毛玻璃效果怎么实现?需要重新引入库来解决吗?

答:否,pixi引擎自带支持哦,Container对象底下有个filters参数可实现毛玻璃效果

4、分享gif,因为我们本来是骨骼动画,如果要分享出去必须截图再合成gif。

方案1:单次播放法,满帧截取的情况下,ios能达到要求,但是安卓上不同机型,不同机器现状都可能截出不同效果的gif,差的时候只能2,3张,效果差而且不稳定

方案2:多次播放截图法,我们通常用的定时器计数的方式比较多,但是浏览器每轮播放状态无法做到一致,所以计数截图法不可取,所以就要用到计时的方式,所幸requestAnimationFrame里面回调会传过来一个time,引擎会转化当前动作已经播放了多长时间t,我们可以通过t来截取。

返回:由Math.floor(t*fps)生成的index截图组成的数组;最终的截图数组长度 >= fps * 动画总时长就停止截图  最终的效果也是很赞的。

这种截图方式损失的时间,那会不会截图的时间过长呢,我针对截图时间做了下统计:

动画的平均长度为1.8s,在两个移动平台上的截图耗时:

iOS: 2025ms

Android: 4535ms

这个分享页面需要用户填写心情之类的,4s以内的截图耗时可以接受。

引擎优化

第一次用PIXI,第一次用PIXI-SPINE。作为一个要上线承载上亿用户的产品,开发过程,遇到了一些引擎水土不服的地方,主要有:

1、播放动画的时候展示错乱,脸部五官漂移

定位:是因为同一个插槽下面有多种类型的附件。

解决:看源码时,发现引擎在定时器更新遍历插槽时间轴的时候,在region切换到mesh类型的时候或者mesh切到region的时候,引擎没有隐藏之前的附件。所以就会产生漂移。之后如果还要展示之前的附件只需要重新设置可见性即可。修改代码:

 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;
           }
....
}

2、画面闪动,无法正常显示动画

定位:把webgl渲染强行切成canvas2d的,显示就正常了,说明还是webgl下某种特性兼容性的问题。

解决:把PIXI升级到v4.0.2解决。至于具体原因这里不做具体说明。

3、增加图片超时逻辑

解决:PIXI加载图片的逻辑就是先声明一个img,img.onload之后再触发loaded事件,业务再去处理依赖于这个图片的逻辑,没有超时逻辑,会一直等待。页面就是不可点击的状态,对于一个成熟的产品必须有很高的可用性,所以必须有完善的错误兼容的逻辑!就在源码里加上了超时逻辑,业务能正常执行。

性能调优

功能开发好不容易完了,兼容性也OK了,但是性能却挺糟糕,crash,发热,进入游戏慢,性能bug单狂轰滥炸,静下心来各个击破,最终项目各个性能指标达标,在外网稳定运行。

工欲善其事必先利其器,定位性能问题,要通过工具去分析哪里是性能瓶颈,才能有的放矢,虽然通过chrome的性能分析工具已经能发现大部分问题,但是ios和安卓上因为实现的差异是不是存在其他的问题,也需要测试一下才放心。这里搜罗了一下常用的性能分析工具,供大家参考。

1、Crash

原因:页面内存占用过高是主要因素,什么资源最占用内存呢,通过Chrome Profiles面板分析便知。

54%的内存都消耗在动作的Timeline数据上了。用排除法分析了一下一个动作的json占用的内存数。

每个动作原始数据+解析成数组总共占用390kB。每个用户每次用的动作有限,并不需要把完整的动作数据加载出来,所以解决Crash可以通过一下方法

a. 首次只加载模型json以及必须的2个动作数据,其余的按需加载,解析完塞到宠物的动作数据里面(这里需要改写源码暴露读取动作数据的接口)

效果:内存减少:49M减小到18M,减少了80%(Chrome上测试)。

b. iOS上启用wkWebView,大幅提高Webview的稳定性。

2、发热

原因:发热跟CPU和GPU占用息息相关,WebView作为一个比较高层的应用,对CPU和GPU的占用是要比原生App的占用高很多,这应该也是HTML 5游戏发展的瓶颈所在。减少资源大小,将Canvas里面固定的图片独立出来等等措施都收效甚微。只要WebGL在requestAnimationFrame里面不停渲染就会发热严重。

解决:降级策略看起来这里唯一行得通的解决方案了,没有渲染就没有发热。所以就要把渲染用在刀口上。损失非关键体验,安卓停掉默认动作停止渲染,需要时再打开渲染,改成隔3秒播放一次动作让用户感觉也是在一直动。

效果:有效解决手机发热的问题。

3、帧耗时-FPS

PIXI是支持自动识别浏览器是否支持WebGL来选择是Canvas2D,还是WebGL来渲染动画,先科普下WebGL的市场占用率吧。

iOS:iOS 7.1以上都是支持WebGL的。

Android:安装了TBS的机器支持WebGL。

注:TBS是腾讯QQ浏览器研发的Android平台WebView内核,目前广泛用于手机QQ、微信、QQ空间中。

因为 TBS是热更新的,新安装的APP没有TBS,就不支持WebGL,而又是我们推广的关键时期。所以Canvas2D下的渲染也需要做好它的优化。

解决:Canvas2D下的帧耗时的优化可以通过将Canvas2D中不变的背景独立到DOM上,不放在Canvas里面渲染

效果:优化了40%左右。

4、游戏启动速度优化

解决:

a. 不采用一般游戏进入先loading资源,采用背景和宠物初步加载的方式。

效果:背景可见时间减少4s 8.4->4.2s

b. 动作JSON按需加载

效果:宠物可交互的时间减少了2.8s  8.4->5.6s

5、WebGL内存泄漏

表现:安卓上黑屏,iOS上因为用了wkWebView会重新loading。

定位:在排行榜频繁切换好友的时候会必现,排行榜的设计模式是每个好友都是独立的,每次都销毁上一个宠物,再添加下一个宠物,这是面向对象编程基本的思路。

可是这样切换几次后就会异常,直觉告诉我是跟WebGL或者内存有关。这里先科普下浏览器的内存占用分几块:

js heap和Dom内存的占用通过chrome的profiles和timelines面板可以看出来,但是其他的内存占用可以在哪里看呢?通过chrome的更多工具 -> 资源管理器即可!

有了这个强大的工具,问题定位将不是问题,其实主要是两个问题:

  1. js内存上涨,切换12个好友js内存上涨了18M,定位下来增长的内存是附件mesh等等信息。

  2. GPU内存只升不降,GPU内存就是WebGL占用的内存,如果有独立显卡的话他的内存占用是和浏览器内存独立的,但是手机端没有独显,可能会跟浏览器有共享内存,这个按机型而异。但是GPU内存增长太多绝不是好事,它可能会影响浏览器申请更多的GPU,造成页面黑屏,也会导致浏览器占用内存过多,被原生app杀掉或者重启。所以这里就是频繁申请释放GPU内存造成的内存泄漏。

解决:既然已经发了问题,解决方法就应运而出啦

a. 同一种宠物模型对象复用,切换下一个好友的宠物就是换装,解决js内存过度增长!

效果:切换12个好友内存增长了5M而已

b. 换装纹理复用,不销毁。因为换装纹理是一个canvas,完全可以擦除之后再回收再利用。解决了GPU的内存泄漏。

效果:

以前的方案,5次切换好友之后,GPU内存直接飙升了90MB,从166MB到255MB。

复用纹理后,5次切换好友,GPU只上涨了30MB,第二次切换后并没有上涨。从166M到199M。

c. 从排行榜切换回来之后,销毁好友的宠物模型。这个是PIXI官方提供的销毁接口。

pet.stage.destroy(true)

销毁后,GPU内存恢复到最初状态170MB左右。

经过上述三次改造后,内存就维护在一个比较稳定的状态,黑屏和reload的问题就修复好了。

总结

  1. 做游戏,需要熟悉引擎源码以及WebGL。越熟悉越好。

  2. 项目需要的内存越多,能够运行它的终端就越少,所以一定要想方设法的定位内存占用大头,逐个击破。

  3. 可以使用降级策略,降级策略要能因机制宜,切忌大刀阔斧。根据机器的剩余内存和GPU核数或者当前的FPS值来做降级策略。

webGL动画的更多相关文章

  1. WebGL入门教程(三)-webgl动画

    前面文章: WebGL入门教程(一)-初识webgl WebGL入门教程(二)-webgl绘制三角形 WebGL动画有移动.旋转和缩放,我们将移动.旋转和缩放图形,然后将其绘制到屏幕上,称为变换(tr ...

  2. CSS3、SVG、Canvas、WebGL动画精选整理

    一.CSS3动画 名称 用途 链接 阴影波纹特效 1.元素hover效果 2.突出表现效果 http://www.jq22.com/code80 横板导航菜单动画 导航菜单 http://www.jq ...

  3. webgl动画小测试

    // MultiPoint.js (c) 2012 matsuda // Vertex shader program var VSHADER_SOURCE = 'attribute vec4 a_Po ...

  4. 八大疯狂的HTML5 Canvas及WebGL动画效果——8 CRAZY ANIMATIONS WITH WEBGL AND HTML5 CANVAS【收藏】

    HTML5, WebGL and Javascript have changed the way animation used to be. Past few years, we can only a ...

  5. WebGL入门教程(五)-webgl纹理

    前面文章: WebGL入门教程(一)-初识webgl WebGL入门教程(二)-webgl绘制三角形 WebGL入门教程(三)-webgl动画 WebGL入门教程(四)-webgl颜色 这里就需要用到 ...

  6. WebGL入门教程(四)-webgl颜色

    前面文章: WebGL入门教程(一)-初识webgl WebGL入门教程(二)-webgl绘制三角形 WebGL入门教程(三)-webgl动画 颜色效果图: 操作步骤: 1.创建HTML5 canva ...

  7. 更好的逐帧动画函数 — requestAnimationFrame 简介

    本文将会简单讲讲 requestAnimationFrame 函数的用法,与 setTimeout/setInterval 的区别和联系,以及当标签页隐藏时 requestAnimationFrame ...

  8. requestAnimationFrame 持续动画效果

    1. requestAnimationFrame 概述 requestAnimationFrame 是浏览器用于定时循环操作的一个API, 类似于setTimeout, 主要用途是按帧对网页进行重绘. ...

  9. JavaScript动画基础:canvas绘制简单动画

    动画是将静止的画面变为动态的艺术.实现由静止到动态,主要是靠人眼的视觉残留效应.视觉残留也叫视觉暂留现象,物体在快速运动时, 当人眼所看到的影像消失后,人眼仍能继续保留其影像0.1~0.4秒左右的图像 ...

随机推荐

  1. 【POJ】3259 Wormholes

    题目链接:http://poj.org/problem?id=3259 题意:n个农场,m条双向路径,w条单向路径(虫洞).单向虫洞路径是负值.农夫想知道自己能不能看到自己(X). 题解:其实刚开始没 ...

  2. 从虚拟地址,到物理地址(开PAE)

    学了好久好久,但是好久好久都没有用过,今天突然要用,都快忘了怎么玩了, 这里记录一下吧. 如何检测PAE r cr4 第5位如果是1,则开了PAE,否则没开 切入目标进程 查找一个自己关注的字符串s ...

  3. capserjs-prototype(中)

    evaluateOrDie() 具体样式: evaluateOrDie(Function fn[, String message, int status]) Evaluates an expressi ...

  4. 几个实用的js函数

    在阅读JavaScript DOM编程艺术这本书时看到了一些比较实用的代码. //加载多个window.onload事件 function addLoadEvent(func) { var oldon ...

  5. IDEA 打开Run Dashboard 分组启动

    一,项目文件夹中,找到 .idea-->workspace.xml 添加: <component name="RunDashboard"> <option ...

  6. c_数据结构_哈希表

    #include <stdio.h> #include <stdlib.h> #include <string.h> #define ERROR 0 #define ...

  7. 使用Element的upload上传组件,不使用action属性上传

    1.需要实现的效果如下图,在点击提交的时候再提交file数据,和其他数据统一上传,我把file转换成了base64的格式,可以再上传之前显示缩略图 2.代码分析 action属性值为"#&q ...

  8. 线性dp,后缀处理——cf1016C好题

    绝对是好题 #include<bits/stdc++.h> using namespace std; #define maxn 300005 #define ll long long ll ...

  9. hadoop高可用HA的配置

    zk3 zk4 zk5 配置hadoop的HA大概可以分为以下几步: 配置zookpeer(namenode之间的通信要靠zk来实现) 配置hadoop的 hadoop-env.sh hdfs-sit ...

  10. socket2里面,有些函数带WSA开头,有些不带。请问有何区别?

    WSASocket可以使用WinSock特有功能,比如重叠IO,用dwflags指定.    WSA的A是指api,用于区别spi,因为在spi中还有wspsocket,wspaccept等... 在 ...