uSpringBone弹簧骨骼插件——Unity2019兼容版
一、简介
uSpringBone是一款骨骼动画插件,用于实现一些如随着角色动作变化,头发甩动、衣服飘动等效果。
uSpringBone在原UnityChanSpringBone插件基础上使用了Unity的面向数据技术栈( DOTS : ECS + JobSystem + Burst Compile)对插件中的大量数学运算进行了性能优化。但只实现了原先插件部分的功能,只有球型碰撞器,没有Editor辅助工具。
原作者在2018年后停止了对该项目的维护,2018年的DOTS刚问世尚未成熟,后续经历了大幅度的改版,故原有项目在Unity2019版本上无法顺利运行。
本方案是基于Unity2019.4.14f修改的适配版本。
二、接入流程
- PackageManager中添加DOTS相关的库(外网环境直接在PackageManager中下载即可)
- 将UnityPackage文件夹中的库拷到
C:\Users\Administrator(用户名)\AppData\Local\Unity\cache\packages\packages.unity.com - 打开Assets同级目录Packages下的manifest.json文件
- 复制下方3个package名及版本到“dependencies”中
- 将UnityPackage文件夹中的库拷到
{
"dependencies": {
"com.unity.mathematics": "1.2.1",
"com.unity.entities": "0.11.2-preview.1",
"com.unity.burst": "1.3.9",
},
...
}
- 导入uSpringBone2019.unitypackage
ps :这里是模拟PackageManager下载完毕,从本地缓存中读取的流程,目的是让Unity能够自动正确处理好各个package间的依赖关系
三、使用说明
- 给需要动画的模型预设最外层添加SpringBoneChain组件
- 按需给子物体添加SpringBoneComponent和SphereColliderComponent
以上图模型为例,给角色驱干添加了SphereColliderComponent,头发上添加了SpringBoneComponent,用来模拟头发随身体动作甩动的效果。
四、Demo工程
- 开发环境
- Unity2019.4.14f
- Package Manager 中需要额外添加的package
- Burst 1.3.9
- Entities 0.11.2-preview.1
- Mathematics 1.2.1
五、源码分析
uSpringBone 插件的核心脚本主要有四个
- SphereColliderComponent
球型碰撞体组件,主要包含碰撞球体的半径信息 - SpringBoneComponent
骨骼节点组件,主要包含骨骼的Transform信息 - SpringBoneChain
挂载在需要进行骨骼动画的模型上,收集子节点下的所有球型碰撞体组件和骨骼节点的信息,汇总进行物理效果的模拟运算,计算得到骨骼节点的位置和旋转信息 - SpringBoneJobScheduler
维护场景中所有激活SpringBoneChain组件的List,在Update和LaterUpdate中控制所有SpringBoneChain组件的运算方法
部分伪代码
- SpringBoneJobScheduler主逻辑执行流程
循环逻辑主要由三部分组成:初始化计算所需参数 -> JobSystem多线程计算 -> 同步计算结果到游戏物体上
private void Update()
{
//chains 场景中所有激活SpringBoneChain组件的列表
//新建JobSystem任务
foreach (var chain in chains)
chain.ScheduleCalculateJob();
//将上步骤新建的任务添加到执行队列
JobHandle.ScheduleBatchedJobs();
}
private unsafe void LateUpdate()
{
//获取JobSystem任务计算结果
foreach (var chain in chains)
chain.CompleteCalculateJob();
//同步计算结果到每个骨骼的Transform
foreach (var chain in chains)
chain.UpdateBoneData();
//更新骨骼根节点、碰撞体的数据 准备下一帧计算的输入数据
foreach (var chain in chains)
chain.UpdateParentData();
}
- 单个JobSystem的计算过程伪代码如下
双层for循环,包含大量的数学运算,骨骼和碰撞器的数量会明显影响计算的次数
for(该角色下的所有骨骼组件)
{
for(该角色下的所有球型碰撞器)
{
//大量的运算 模拟弹簧的物理效果
}
//大量的运算 计算每根骨骼的位置和旋转
}
六、性能评估
-
测试场景
- 延用uSpringBone 2018版本的测试场景
- 单个模型包含38个骨骼节点和11个球型碰撞体
- 场景中同屏共35个模型,共计1330骨骼节点和385个球型碰撞体
-
测试设备
- Android : 小米Note3(中低端设备)骁龙660/4G内存
-
测试结果
步骤 | 平均耗时(大概值) | 耗时原因分析 |
---|---|---|
初始化计算所需参数 | 1.1ms | 从每个碰撞体上获取当前位置信息 |
计算骨骼位置 | 0.08ms | 经Burst编译优化再由JobSystem多线程执行,耗时非常小 |
计算结果同步 | 2.8ms | 使用计算结果刷新每个骨骼的位置信息 |
可见在JobSystem和Burst的加持下,中间大量计算的耗时占比十分小,性能瓶颈主要集中在从游戏物体中获取位置数据及修改游戏物体的位置上。
上图中可见计算任务经由JobSystem自动分配到了各个子线程上并行执行。
七、源码修改说明
主要对源码进行了以下修改
- 废弃变量 & 接口名替换
- Position -> Transform
- Component后缀修改 Wrapper -> Proxy
- DOTS使用方式修改
- 背景
2018版本的ECS可以通过任意Enity实例的EnityManager调用GetComponentFromEnity方法获取到某种Components的索引器(可使用Enity作为下标,像数组一样访问该实体的数据) - 原先的设计
2018版本通过上述GetComponentFromEnity接口能够非常便利的查询修改Enity的数据,原作者的设计上其实并没有按照ECS的思想在System中进行逻辑运算,推测只是为了方便通过GetComponentFromEnity接口修改数据而使用了ECS - 新版本的问题
但在后续版本DOTS的迭代中,官方删除了EnityManager中的GetComponentFromEnity接口,推测可能是由于该接口并不符合ECS的设计理念,现在该方法只能在System中调用,原来的实现方式必须要进行修改。 有两种可行的修改思路,一是继续使用ECS,将原来插件的逻辑改到System中执行,二是弃用ECS的部分。二种方法分析优劣如下:
- 背景
修改方式 | 优势 | 劣势 |
---|---|---|
逻辑处理改到System中 | 能够使用Unity提供的GameObject到Enity高性能转化工作流,效率上限极高,耗时有望压缩到1ms内 | 改动较大,面向数据的设计思路刚接触,需要较长时间理解、摸索 |
去除原插件中的ECS部分 | 改动较小,面向对象的设计熟悉,更容易实现 | 传统Mono修改transform的方式性能开销较大 |
最后由于项目组需求紧、更注重插件的稳定性,且第二种方式性能上也能达到项目组的预期,于是目前选择了第二种方式完成了修改。
八、总结
本次方案主要是对DOTS的学习和应用。DOTS目前还在发展阶段,尚未成熟,且面向数据意味着项目现有的成熟框架大部分需要从头设计,开发人员上手也有一定的学习成本,短期内应该无法达到大规模商用的条件。
但在这次的实践中,也可以看到DOTS在性能方面的表现确实是十分优异的,经过ECS(提高缓存命中、轻量化组件)+ JobSystem(多线程并行)+ Burst (编译器层面的优化),时间复杂度O(n2)的复杂算法同帧几十次调用的情况下,在低端移动设备上能跑出0.0x毫秒级别的耗时数据,DOTS在性能优化方面的上限还是非常高的。
可以保持对DOTS技术的关注,同时在一些重运算、低耦合的插件或模块上尝试应用,为将来一些对性能要求严苛的开发场景做知识储备。