骨骼动画初体验
Pin Young Lv9

前言:

运营需求的一个重要核心功能在于页面内指定内容可配置,比如:头图、不同情况的提示文案等都可以支持根据运营同学的配置随时更改,提供灵活性。
但是这种配置的方式也出现了遇到难题的情况, 图片中包含复杂特效的情况往往会选择直接用视觉同学导出的gif动图, 问题在于页面中涉及的动图量大加载成本大大提高的问题。
为了尝试既维持灵活可配 & 减少加载成本,加下来介绍下骨骼动画的方案。

H5动画/游戏 - 伴侣们

  1. Three.js
    Three.js 是被广泛了解的开源项目,他是基于 JS 的 3D 库,我们可以依赖他完成炫酷的3D展示效果。

  2. Phaser
    桌面与移动端的 HTML5 游戏框架, 他提供了足够多的功能,支持我们完成H5游戏。

  3. Pixi.js
    依赖于canvas的WebGL渲染器,官网中他对自己的定位就是渲染“引擎”,提供的 API 功能支持上, 不如 Phaser 等丰富,但是他在渲染部分做的很出众。同时 PIXI 也是 Phaser的 渲染内核;

  4. 小注释
    引擎:实现特定的功能,属于底层,和硬件驱动打交道
    框架:为了快速完成需求提供的功能集合,是对一系列功能的一层封装
    如果要说他们俩的关联点 => 引擎是为框架而服务的

PIXI 简单介绍

PIXI 主打支持硬件 GPU 渲染的 WebGL API,依赖他你可在不了解 WebGL 的 API 或者处理浏览器兼容就可以创建丰富交互式图形的跨平台应用的渲染器(当然,更深入的了解和更好的使用也需要你对 WebGL 的基础)。之所以称他为跨平台应用是因为他可以自动识别浏览器是否支持 WebGL 否则降级使用了 canvas2D 进行视图渲染。

三种渲染模式

1
2
3
4
5
6
7
8
9
10
11
12
// 自动识别当前环境是否支持
renderer = PIXI.autoDetectRenderer(
256, 256,
{ antialias: false, transparent: false, resolution: 1 }
);

// 强制使用canvas
renderer = new PIXI.CanvasRenderer(256, 256);

// 强制使用 WebGL
renderer = new PIXI.WebGLRenderer(256, 256);

PIXI 简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 创建canvas元素
const app = new PIXI.Application();
// 可插入到DOM中
document.body.append(app.view);
// 加载需要的精灵
PIXI.loader
.add('animals', '../images/animals.png')
.load((loader, resource) => {
// 资源加载后处理事件
// 创建图片
const animals = new PIXI.Sprite(resource.animals.texture);

// 设置图片的位置
animals.x = app.renderer.width / 2;
animals.y = app.renderer.height / 2;

// 设置旋转中卫为图案中央
animals.anchor.x = 0.5;
animals.anchor.y = 0.5;

// 添加到舞台
app.stage.addChild(animals);

// 监听帧更新 以下为进行旋转
app.ticker.add(() => {
animals.rotation += 0.01;
})
});

更多PIXI示例代码

PIXI 的渲染压力测试结果

同样环境条件下,对5千张,1万,2万张图片进行循环渲染,进行帧率数据的比较;如图可见,PIXI 作为渲染器的表现是很优秀的。

PIXI-SPINE 骨骼动画

帧动画 & 骨骼动画 区别

  • 帧动画:帧动画是对角色的特定姿势的快照, 计算的依照点在于设备的帧率,所以他的流畅性和平滑度效果会取决于用户的设备出现不同

  • 骨骼动画:把角色的身体各部分进行拆分,绑定到一根根互相作用,互相连接的“骨头”上,控制其中某一个骨骼的位置、旋转、放大、缩小… 带动其关联的部分随之移动和变化,达到想要的动画效果。

骨骼优化的优势

  • 更少的美术资源: 一块块小部件的结合拼凑成每帧画面的不同效果,不再需要每一帧完成图片进行切换;

  • 体积小:用 JSON 文件代替图片资源进行控制,大大节省了资源大小

  • 流畅性:JSON 配置文件的设置节点是时间帧,骨骼动画是根据差值计算出中间帧,保证动画保持的更流畅;

  • 附件:这是一个集合的概念,使得对特定某个区域的切换控制,提供了方便;

  • 混合动画:一个 JSON 文件可同时这是多个动画,这些动画可混合使用,同时进行多个动画;

  • 程序可控:动画播放的整体速度、旋转,或者根据用户的行为去触发再去控制动画的播放等 都是方便可控的。

骨骼动画解剖

骨骼动画的配置文件由以上几部分构成

  • Bone: 骨头 是骨骼动画的基础,用来计算位置,每个骨头会有自己的位移缩放和旋转属性,骨头也可以有自己的子节点,最终形成树形结构;

  • Slot: 插槽 是骨头上的挂载点,Slot 是用来标记特殊的骨头位置,也可以说重要的骨头节点就是 Slot, 插槽是用来控制描画的;

  • 附件:是挂在插槽上的内容,可以是图片、人物外表等的表示;

  • 皮肤:皮肤是一套附件的结合,皮肤的切换给动画带来了更多的灵活性;

  • 动画:是根据时间轴控制的骨头状态的列表,每个动画都会有自己特属的 name, 利用此属性可指定特定的动画名称;

播放骨骼动画流程

骨骼动画配置文件分析完, 接下来就可以开始引用啦~

直接上代码是不是应该更容易理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建 canvas 并添加到 DOM
const app = new PIXI.Application(750, 1334);
document.body.appendChild(app.view);

// 加载 JSON 文件,绑定加载完之后的方法
PIXI.loader
.add('loading', './loading.json')
.load(onAssetsLoaded);

function onAssetsLoaded (loader, res) {
// 设置精灵 并且 设置他的位置
const loading = new PIXI.spine.Spine(res.loading.spineData);
loading.x = app.screen.width / 2;
loading.y = app.screen.height;
// 添加到舞台中
app.stage.addChild(loading);
// 开始循环播放 play 的动画
loading.state.setAnimation(0, 'play', true);
}

使用体验/感想

  1. 图片超时处理: PIXI 的图片加载是使用 new Image(), 再触发 img.onload 的时候才会执行事件,如果我们没有显性处理 err 的情况, 就会一直处在等待状态不继续;

  2. 资源拆分: 应根据实际情景,进行资源拆分和合并。不同场景用不同的配置文件进行区分,根据所需情况处理预加载 ;同一场景下不同动画文件可进行配置文件的合并, 用动画名称进行区分触发。
    引入 JSON 文件,pixi-spine 会读取文件之后转化为 js 对象等待被调用,因此对可按需再加载的部分进行拆分,有利于减少js的工作量及占用的内存,也能提高访问的初始速度

  3. 独立到 DOM: 不管是用 WebGL 还是 canvas 渲染,都是依赖于 canvas 作为画布,因此我们也可以灵活利用 DOM,将不变的背景部分抽离出画布, 独立到 节点中进行控制;

  4. GPU 部分: texture 是 GPU 运算中非常实用也常用的数据结构,他可以存储图片数据; z在使用 WebGL进行渲染时,纹理图占用的是 GPU 内存,在确定这些纹理不在被使用时,我们可以手动执行 PIXI 的 dispose 方法主动释放纹理,保证当前占用的 GPU 中不包含多余纹理;

最后

几乎100%复原动效同学的设计稿并且以尽可能高效的完成,最大限度减少和动效同学确认并调整动效效果的方面,个人认为骨骼动画的前景很乐观;结合我们的配置平台,也相信能快速更灵活的进行更新替换;后续会更深入的了解实现的更多细节和原理部分,尝试更多切入实际问题的实际改良方案,争取做到更好。欢迎大家踊跃提出疑问和建议,更多的尝试和心得会持续进行更新。