【内存优化】
首先要意识到运行时和编辑时的区别,当运行时和编辑时所需的数据相差不大时,我们用同一套数据结构即可,当两者差异较多或者数据量很大时,需要有各自的数据结构,这意味着在打包或构建时需要将编辑时数据转为运行时数据。
(所以Unity中的AnimationClip Curve数据不提供给非editor情况下使用)
(内存优化把握三个核心方向:一是内存中只存在当前或最近需要的资源及数据;二是需要的资源及数据在内存中仅存在一份;三是简化资源和数据的结构,运行时和编辑时的数据区分是方向1)
由前文可知,动画至少有30个骨骼节点,每个至少有9条Curve曲线,每个Curve曲线有60个KeyFrame,按照编辑时的Keyframe的结构,1s的动画至少需要30*9*60*8*4/1024 =506.25kb的内存
高品质的动作游戏的骨骼节点会更多,1s内的动画数据占用的内存会超过506.25kb。
一般而言,内存中存在的所有动画数据时长加起来超过30分钟很正常,那么占用内存至少为506.25*60*30/1024 = 889.89MB
这么大的内存在移动端是不可接受的。
根据计算公式,我们可以从多个方面优化内存。
优化关键帧结构
在运行时我们只需要知道曲线函数即可,编辑时点的数据可直接转为函数参数,从前文可知函数,可以有:
- 水平线(即常量),点的值
- 直线,y= ax+b ,两个值
- 三次多项式,y= ax^3 + bx^2 + cx + d 四个值
- 二次贝塞尔曲线 B(t) = (1-t)^2 * P0 + 2t(1-t) * P1 + t^2 * P2,展开后为y= ax^2 + bx + c,三个值
- 三次贝塞尔曲线 B(t) = (1 − t)^3 *P0 + 3t (1 − t)^2 *P1 + 3t^2 (1 − t)*P2 + t3* P3,展开后为y= ax^3 + bx^2 + cx + d 四个值
- 三次埃尔米特曲线,展开后为y= ax^3 + bx^2 + cx + d 四个值
可以看到,他们都是同样的形式,不必再区分具体的类型,只要有四个参数值即可。
那么运行时关键帧的数据结构A为:
///Struct KeyFrame{
/// float a
/// float b
/// float c
/// float d
///}
按照这个优化,内存将降低为原来的一半,为444.95MB。
接下来还按照核心方向3来优化:
uint类型有32位,分成4份,每份有8位,我们用高位标记其实小数点后几位,剩余7位表示参数值,可表示的最大精度为0.0000001,这个精度基本够了
按照这个方式运行时关键帧结构B为:uint keyFrame
内存将降低为原来的1/8,为:111.24MB(下文以这个为准)
优化曲线结构
曲线核心数据是关键帧数据的组合,优化要依靠不同关键帧之前的关联(类似数据压缩中上下文联系)来简化数据(核心方向3),有以下关联:
1.如果曲线中所有关键帧都是常量,我们用一个数据就可以,在关键帧数据结构A中,内存占用降低为原来的1/(60*4);在关键帧数据结构B中,内存占用降低为原来的1/60。例如:盆骨节点的部分曲线基本都是常量
2.如果曲线中有部分连续的常量,我们可以需要标识出从第几帧到第几帧是常量,这是不用一个数组表示了,需要用一个类封装,这也是划算的
3.如果曲线中所有关键帧都是直线,在关键帧数据结构A中,可以去掉c、d,内存占用降低为原来的1/2;在关键帧数据结构B中,可以用short类型,内存占用降低为原来的1/2
以上优化都属于核心方向3中的方法1:转换数据结构,其特点是不损失数据精度
在允许精度损失的情况下,我们有了方法2:朝着符合转换数据结构的情况精简数据
针对关联1:我们可以设置一个阈值N,凡是变化在阈值内的数据,都认为是常量
在游戏行业,动画数据生成的方式一般有三种:1.美术在3D软件中手K 2.动捕 3.AI生成。其中动捕或AI生成会产生较多抖动细节,都可以去掉以生成更多的常量。
针对关联2:我们需要在曲线的所有位置都检测是否存在常量,一些斜率很小的直线可以简化为常量
针对关联3:有些其他曲线完全很小,可以近似简化为直线,以尝试获得更多的直线
(有些算法常识,你应该可以知道,上述所有阈值,都可以做自适应。
理论上,自适应是针对不同情况的,对当前曲线有自适应值,对不同情况下生成的近似曲线也要有不同的自适应值;对不同的肢体,例如手部、腿部、面部的自适应值不同,越靠近根骨骼,所需精度越高,允许的误差阈值越小。
实际上,为了简化,可能都是一样的自适应值。)
允许有精度损失时,还有其他关联:
4.连续多个关键帧数据可以通过一条曲线拟合,实际上这种情况是很常见的,只要不是突兀的变化,时间是在1/60s这样小的时间尺度下,多个连续帧是按照同一规律渐变的,可以用同一条曲线拟合。
注意,我们并不是一次性拟合整条曲线,而是对曲线分段拟合
综合以上关联,优化后的曲线结构中,关键帧数据不再是数组,而是一个封装关键帧数据的类或结构体。在Unity中,表现为IntPtr m_Ptr。
一般来说,常量占到曲线的60%-80%,直线占比为20%~30%,其他各类曲线占比20~30%,假设30%关键帧数据可以做拟合。
做保守估计,常量取60%,直线取20%,其他各类曲线取20%,优化后的内存为:111.24*60%/60 + 111.24*20%/2 + 111.24*20%*(60 - 60*30%+1 )/60 = 28.181MB
优化关键帧数量
有些动作变化简单,例如walk、run等,实际上不需要60帧,2D动漫也常用做这样的减帧处理。(核心方向3的方法3:减少数据量)
因为大部分动作都是平滑的,我们可以预测下一帧的数据,从而减少帧数。曲线拟合也可以看作是减帧,与预测不同的是,曲线拟合有精度要求,无论动画正放还是倒放都无影响,且不需要前置数据。
而预测首先的有几帧的前置数据,由于是从前到后预测,动画不能倒放,预测最好是完全准确的,在阈值内有偏差也可,超过阈值,记录一个delta即可校正。
假设通过上述方式,可以将平均帧数降低为40帧,那么优化后的内存为:28.181 * 40 / 60 = 18.787MB
优化曲线数量
上述的优化都是基于数据本身做优化,并没有考虑到数据所在的场景。在动作中,节点位置、朝向、缩放的三条曲线之前是有关联的,我们完全可以将其做进一步的封装。
例如:xz可能共同绕着y做同样规律的变化,或者xy绕着z做同样规律的变化,考虑人体结构和肢体动作,这是很常见的,减少30%-50%的数据是可能的。例如Unity内部的QuaternionCurve,Vector3Curve
对于缩放,一般情况下角色没有缩放,可以直接去掉缩放曲线。
假设去掉Scale曲线,并做30%优化,那么优化后的内存为:18.787 * (6/9)*70% = 8.767MB
优化节点数量
角色骨骼节点数量在项目之初就确定好的,不会随意更改,这里要结合核心方向1来做优化。
更多的骨骼节点是为了走更精致、品质更高的动作,在不在视野内、或距离视野很远的角色而言,精致的动作也看不到,角色不需要或仅需要很少的动画,也相当于一些节点的数据直接可以省略掉,不用在内存中存在。
假设只能优化10%的节点点,那么优化后内存为:8.767 * 90% = 7.908MB
综上,内存可以从889.89MB优化到7.908MB,至少可以优化88%
压缩优化
按照数据压缩中的方法,将动画数据做压缩(注意选择高性能的压缩方法),使用时再解压数据。
加载优化
主要是按照方向1别把不需要的动画数据加载到内存中。
unity中的优化设置
Unity针对AnimationClip提供三种压缩格式:
1.Off——不做压缩处理,动画中每一帧都生成关键帧
2.Keyframe Reduction——Stream格式存储,使用关键帧缩减算法(简单来讲,就是对去除关键帧前后的曲线进行比较,如果对应的曲线值的差小于容错值/误差宽容度,则去掉关键帧)
3.Optimal——Unity会使用启发式算法,从而决定使用Keyframe Reduction算法进行压缩(Stream格式存储),或者使用Dense格式压缩存储动画曲
Stream格式可以认为是带有曲线的;Dense格式是存储所有关键帧数据,用线性插值,可以看作是直线的,内存占用比Stream少
Inspector上可以看到不同类型曲线占据的大小
Rotation/Position/Scale Error是压缩时的阈值
可以对不同的动画资源选择合适的压缩格式和阈值
包体优化
一般来说,游戏内所有的动画数据时长加起来有6个小时比较正常,内存优化的某些方式,也会减少包体大小。
同时,压缩优化是必须的,在内存中的动画数据可以是非压缩的,在硬盘上的动画数据一般是压缩后的,读取文件时要先解压。
【性能优化】
这里的优化仅针对AnimationClip,由于解压、采样、计算都由引擎内部做处理,在不改源码的情况下,可以做的不多。
加载优化
在合适的时机做加载,以减少峰值卡顿等
缓存友好
让数据的排布更符合读取的顺序,减少CPU cache miss的情况,例如由三条曲线,四个关键帧的数据,一般都是这样的:
/// 曲线1:关键帧1 关键帧2 关键帧3 关键帧4
/// 曲线2:关键帧1 关键帧2 关键帧3 关键帧4
/// 曲线3:关键帧1 关键帧2 关键帧3 关键帧4
/// 曲线4:关键帧1 关键帧2 关键帧3 关键帧4
改成:
/// 关键帧1:曲线1 曲线2 曲线3 曲线4
/// 关键帧2:曲线1 曲线2 曲线3 曲线4
/// 关键帧3:曲线1 曲线2 曲线3 曲线4
/// 关键帧4:曲线1 曲线2 曲线3 曲线4
由于各类压缩和优化内存的方式,缓存友好的方式改动难度很大
Job计算
多个不同的AnimationClip的数据的采样、计算可以放在Job中进行
采样降频
动画数据采样是指,输入一个时间,从曲线中得到一个值,每秒要进行很多次这样的采样。一般来说,会有预设的固定值,或者按照Update的频率采样。
但我们可以结合业务实际去调整采样的频率,类似常见物体LOD的概念,我们可以根据角色和相机的距离来设置动画的采样频率。
在Unity中,如果项目使用了Playable做动作系统,可以使用Playable.Evaluate来手动更新,降低频率
【加载优化】
加载关乎到内存和性能,这里拿出来单独说,从加载系统的交互看,动画资源和其他资源没什么区别,以下加载优化的方式适用其他任何资源的加载优化
内存上
按需加载
动态卸载
性能上
提前加载:有些基础性的动画资源是必然会存在的,可以在切场景时提前加载到内存中
设置加载优先级:动画资源的加载优先级设置高些,让加载系统优先加载动画
异步加载:动画资源从同步加载改成异步加载,且需要加上时间限制,固定多少帧内必须加载完成
分帧请求加载:一次性需要加载的动画资源较多,分成多帧去请求
预测加载:可以在逻辑上预测哪些动画资源可能在接下来需要加载,提前做加载,以避免集中加载导致卡顿