Unity AnimationClip详解(2)——动画数据的优化

【内存优化】

首先要意识到运行时和编辑时的区别,当运行时和编辑时所需的数据相差不大时,我们用同一套数据结构即可,当两者差异较多或者数据量很大时,需要有各自的数据结构,这意味着在打包或构建时需要将编辑时数据转为运行时数据。

(所以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来手动更新,降低频率

【加载优化】

加载关乎到内存和性能,这里拿出来单独说,从加载系统的交互看,动画资源和其他资源没什么区别,以下加载优化的方式适用其他任何资源的加载优化

内存上

按需加载

动态卸载

性能上

提前加载:有些基础性的动画资源是必然会存在的,可以在切场景时提前加载到内存中

设置加载优先级:动画资源的加载优先级设置高些,让加载系统优先加载动画

异步加载:动画资源从同步加载改成异步加载,且需要加上时间限制,固定多少帧内必须加载完成

分帧请求加载:一次性需要加载的动画资源较多,分成多帧去请求

预测加载:可以在逻辑上预测哪些动画资源可能在接下来需要加载,提前做加载,以避免集中加载导致卡顿

相关推荐
新知图书10 小时前
MySQL 9从入门到性能优化-创建触发器
数据库·mysql·性能优化
大数据AI人工智能培训专家培训讲师叶梓10 小时前
基于模型内部的检索增强型生成答案归属方法:MIRAGE
人工智能·自然语言处理·性能优化·大模型·微调·调优·检索增强型生成
Feng.Lee1 天前
性能测试中的操作系统参数优化
开发语言·性能优化
ellis19701 天前
Unity性能优化3【内存基础篇】
unity·性能优化
小远yyds2 天前
鸿蒙中常见的性能优化
前端·华为·性能优化·harmonyos·1024程序员节
Juicedata2 天前
vivo 轩辕文件系统:AI 计算平台存储性能优化实践
人工智能·性能优化
逐·風2 天前
Unity 游戏性能优化实践:内存管理与帧率提升技巧
游戏·unity·性能优化
小杨4042 天前
并发编程基础五(线程相关方法1)
java·后端·性能优化
EterNity_TiMe_2 天前
【Linux网络】Linux网络基础入门:初识网络,理解网络协议
linux·运维·服务器·网络·学习·性能优化
程序猿进阶3 天前
MySQL 数据出海之数据同步方案
java·数据库·后端·mysql·面试·性能优化·系统架构