jjzjj

Unity动画文件(AnimationClip)精简容量的研究

阿赵3D 2024-01-13 原文

一、研究动画精简的原因

最近做一个3D卡牌项目,角色非常的多,每个角色的动作也比较多。
项目打包之后,发现包体容量比较大,然后分析了AssetBundle资源,发现里面的动画文件非常的大,有些角色一个动画文件打包AssetBundle之后居然有1M多,这样一个角色单纯是动画文件就占了超过10M了。
由于这次的美术团队是新合作的,一开始也没有留意动画的制作方式是否有问题,既然发现存在异常了,于是就对美术资源进行分析。
经过一番尝试之后,成果还是很明显的,在保持原有动画效果的基础上,能把原来1m多的的AssetBundle文件减小到不到200k。

二、动画文件容量分析

1、动画文件的导入问题

一般复杂的角色动画,都不会是在Unity里面k出来的,而是从比如3DsMax之类的软件制作,然后导出fbx文件到Unity里面使用,在导入的时候,Unity有针对动画的压缩选项,Anim.Compression:

具体有3种不同的选项:
(1).Off
(2)Keyframe Reduction
(3)Optimal
具体来研究一下这三个选项对产生的动画文件的区别在哪里:
(1).Off:
顾名思义,就是完全不压缩。但实际上,他还有另外的一个作用,就是烘焙关键帧。
举个例子:
我们有这么一段动画,正常的情况下,关键帧的分布情况是这样的。

现在我们把动画压缩的选项改为Off

现在再看关键帧,就变成这样了。
在选择了Off之后,引擎对于动画的每一个节点的每一帧,都做了计算并生成了关键帧,这样做的好处是,动画会变得非常的准确,不再因为动画的曲线过渡,或者打忘记打前后关键帧而导致各种动画的滑步、抖动、动画切换某些节点异常等问题。
不过说句我个人的看法。如果动画真的出现了各种异常,本身就是k动画的过程中的疏忽或者错漏导致的,正常的解决办法应该是去修改原始动画文件,而不是靠烘焙关键帧这种操作来避免错误。
从肉眼可以看出当选择了Off之后,由于关键帧大量增多,所以动画文件也会变得巨大无比。
(2)Keyframe Reduction
选择这种方式,从单词直译,就是减少关键帧。这种情况下,Unity会保持动画在导出Fbx时的关键帧信息,并做一定量的关键帧优化。选择这个选项后,动画关键帧看起来是不会有什么区别的。

(3)Optimal
从单词直译,这种方式叫做最优的,最佳的。从命名上可以看出,Unity是非常推荐这种压缩方式的。实际上,从关键帧的分布看,Optimal和KeyframeReduction是一样的。从Unity的官方文档看,Optimal是在KeyframeReduction的基础上,进一步选择最优的动画曲线,最终减少内存的占用。
Optimal实际上进行的操作,应该是进一步的去掉重复和多余的关键帧,以最少关键帧作为曲线过渡的形式做插值计算。

单纯从Unity的Api描述,还有实际看Animation时间轴的关键帧分别,我们就可以知道,从容量来说,肯定是Off>KeyframeReduction>Optimal的。所以按道理来说,这里应该是选择Optimal作为动画的压缩方式。
在我的这个项目里面,我发现很多动画选择了Off,我问了一下动画师,原来是因为有些角色的动画从3DsMax里面导入到Unity,发现播放时角色的脚出现了滑步,而原始动作的脚应该是固定的。所以动画师选择了把动画压缩设置Off的方式,想用大量的关键帧去解决这个问题。
这里我想说一个纯美术人员和技术人员之间的明显分歧,篇幅有点长,如果不敢兴趣可以直接跳过到下一小节。
从动画师的角度看,最终动画的确不会滑步了,感觉问题已经解决了,所以也不会去再找技术去解决这个问题。但从技术的角度看,虽然看起来的动画表现好像是没问题的,但实际上产生了容量巨大的动画文件,占用的包体容量和内存也会大幅度的升高。
我一直觉得,美术团队一定要有可靠的技术美术(TA)作为支撑,不能靠纯美术人员凭经验来想办法解决问题。因为所谓的经验,一般就是,以前某个项目是怎样怎样做没问题。但实际的情况是,不同的项目,使用资源的方式和遇到的重点问题可能都不一样,如果在遇到问题的时候,没有从学术性上的理论支撑,而单凭经验来解决问题,其实有很大的运气成分,可能真的解决了问题,也可能根本没有解决,甚至产生了新的问题。
有人可能会说,项目很紧急,来不及思考这么多,就先随便动手解决了。但如果连产生问题的根本原因都不知道的情况下,贸然动手往往是浪费更多人力成本而没有达到目的。花时间做实际的研究分析,虽然前期可能会花更多时间,但实际动手的过程会很快,往往是写几行代码批处理一下就行了,并且问题解决得彻底。我个人习惯是比较喜欢每件事情都搞清楚原因和原理,才去动手做的。
当然了,如果某些团队没有合格的技术人员作为支撑,也的确没有查找问题的能力,到最后就只能碰运气了。
回到我的项目里,遇到了精简关键帧之后出现动画异常的问题时,我认为正确的解决办法是动画师通过分析关键帧过渡出错的地方,然后在正确的位置加关键帧来解决。但这种方式需要动画师有比较高的水平和经验,我这边的美术团队暂时没有这个能力。所以只能我来想办法通过技术手段来解决问题了。

2、anim文件的分离

先说一个在Unity使用资源的习惯问题。
一般导入3D模型到Unity里面,都是使用fbx格式的模型。在Unity里面,我们可以直接看到Fbx文件展开之后,里面可能有网格模型(Mesh),有动画片段,也可能有贴图,有导入时Unity自动生成的默认材质球等等。
我们是可以直接用这些Fbx里面的资源的一部分,比如只有Mesh,只用动画。但在打包的时候,Unity会默认引用了整个Fbx文件,把我们没有用到的一些资源也作为引用给打包进去了。如果我们不想引用多余的Fbx文件部分,只是想要使用某些东西,我们可以把这部分的内容复制出来。
比如我这里有一个测试技能动画的fbx文件,我可以在展开fbx之后,把里面的skill动画选中,然后按Ctrl+d快捷键,把skill动画片段复制出来。

由于接下来要分析三种不同压缩设置对文件容量的实际影响,所以我分别设置了Off、KeyframeReduction和Optimal三种情况,并用Ctrl+d把文件复制出来:

可以看到,原始的fbx文件只有2m左右,Off模式的anim文件竟然达到了差不多22m,而KeyframeReduction和Optimal小一点,也达到了8m多。
看着这个容量,可能大家会说,原来fbx文件这么小,Ctrl+d之后反而变大了,是不是有问题?其实,我们使用引擎资源要习惯不要单纯看文件本身的内存大小。这个就好比使用贴图,不管你用jpg、png把图片压缩得多小,但实际上进入了Unity之后,它还是会还原成RGBA32,实际容量是在引擎里面再次调整的。
这里也是一样的,fbx文件小的原因是他使用二进制格式来存储并压缩。而我们Ctrl+d出来的anim文件,是以纯文本的方式记录数据的,anim文件容量虽然大,但实际上在Unity真正使用的时候,两者是一样的。
复制出来anim之后,我们已经把动画文件和fbx脱离关系了,而且由于它是纯文本,所以我们可以进行各种直接的编辑。

随便打开一个anim文件看看,里面很容易看懂,就是记录了这段动画的各种曲线,每条曲线里面有各个关键帧的数据

3、不同的导入设置对分离anim文件的具体影响

由于刚才说了,anim文件本身的容量并不能准确表达最终动画的容量,所以我们先把3段动画打包成AssetBundle,然后对比一下容量

这里可以很直观的看出,Off的动画容量是惨不忍睹的,占了1.4m,KeyframeReduction的容量也达到了674k,而Optimal的是262k。
如果从这样的角度看,只要设置成Optimal,是不是可以很轻松的解决了动画容量的问题呢?答案并没有这么简单。因为策划设置成Off的原因,是有些动画出现了滑步问题。
我们这里对比一下容量,只是为了搞清楚3种不同的动画压缩设置,实际上产生了什么差异而已。我们可以通过对比3个anim文件,看看他们的容量为什么差别这么大。

4、影响文件容量的因素

先看skill_Off和skill_KeyframeReduction这两个anim文件的对比

Off的容量很大,是因为记录了非常多的关键帧信息,包括了各种位移旋转缩放的曲线、编辑器用的曲线,都是每个物体节点每隔0.03333秒都会记录一个关键帧,这部分数据比KeyframeReduction大很多倍,所以直接导致了文件容量也大很多。其他部分两个文件是一样的。
再来看skill_KeyframeReduction和skill_Optimal这两个anim文件的对比。这两个文件容量是几乎一样的

通过对比,发现两个文件基本上是一样的,只有m_UseHighQualityCurve这个选项。在KeyframeReduction里面这个值是1,在Optimal里面这个值是0。
我个人的猜测是,当这个值开启的时候,Unity会尽量保留原有的动画曲线数据,当关闭时,Unity会对动画曲线做精简,去掉Unity认为没必要的曲线数据,选择最优的曲线数据保留。由于anim文件其他部分是一样的,所以这个设置的生效阶段,应该是在动画真正使用的时候才会触发,在编译AssetBundle的时候,也会触发。

三、对动画文件进行精简尝试

基于以上的分析我们可以对anim文件做一下的处理

1、去掉重复的帧数

Off的anim产生的文件大,是因为它记录的帧很多,但实际上,大部分的帧都是重复无用的,所以其实可以通过一定的方法,把重复的帧去掉。
我的处理方式是,在anim文件里面进行字符串处理,找到同一个物体的Curve段,然后每个Curve段找到不同的time的段,判断下一个time的各种参数是否和上一个time完全一样,如果完全一样,就把中间的time段全部去掉,只保留一头一尾的time的关键帧信息。

2、修改HighQualityCurve项

匹配m_UseHighQualityCurve: 1字符串,如果存在,就替换成m_UseHighQualityCurve: 0。

3、精简浮点数位数

由于Unity默认生成的anim文件里面的各种浮点数都是保留了8位小数,但实际上太精确的数值作用并不是很大,我认为保留3-4位小数就基本上能满足动画表现了。所以可以通过匹配字符串,找到浮点数,并把小数位保留成4位。
这些操作,我都实现了,先说一下结果:
1、手动去掉帧数的操作,在KeyframeReduction和Optimal模式下,并没有很大的效果,这应该是因为本身这两种模式就会精简关键帧,所以一些很明显的重复的关键帧,本身就会被去掉,所以自己手动去掉的关键帧的意义不是很大。
而且由于有曲线数据的存在,有些看着像是重复的关键帧,实际上是不能直接去掉的,比如原来中间有一帧被删掉了,前后的帧的曲线应该相应调整数值才能达到原有的动画效果。如果直接去掉,动画的效果就可能被改变了。
所以这个操作被我放弃了。
2、手动修改UseHighQualityCurve,是很有效果的,等于是在anim文件脱离了Fbx后,还能再进行一次曲线优化的设置。
3、精简浮点数。这个操作也是很有效果,我猜测的是,在关掉UseHighQualityCurve后,Unity会优化曲线,而精简了浮点数之后,把一些原本就非常接近的关键帧,让Unity认为是可以进行优化的曲线,并进行了合并。
于是,我对之前3个anim文件,进行了字符串修改,得到了下面的结果,后面加x的就是处理过的文件

从文本对比看,就是在原有的基础上改了曲线模式和小数点位数

发现文件并没有小很多,我们再打包AssetBundle看看:

可以看到精简之后,不论原来设置了哪种动画压缩方式,包括off,最终也变成了100多k的容量了。
我们把生成的动画播放一下,发现用Off模式生成出来的anim文件经过精简之后,并没有出现滑步的问题。
最后,我们把6个文件通过AssetBundle加载到实际运行里面,看看运行的内存占用情况:

可以看出,没有带x的,符合我们的预期,是Off>KeyframeReduction>Optimal,
然后带x的,就算是Off_x也比不带x的Optimal要占用得小。在精简之后,KeyframeReduction和Optimal占用的内存是一样的。

到这里,试验成功,可以些批量工具把整个项目的anim文件全部处理一下,就完事了。

四、结论

1、关于精简的操作

通过AssetBundle文件的容量、播放的效果、占用内存的大小这3方面的对比,可以看出,如果没有特殊情况,动画压缩选择Optimal,并且精简浮点小数位数,是最优的选择。
如果出现脚步滑动等状况,可以选择Off,并且关闭HighQualityCurve,精简浮点小数,也能得到较小的AssetBundle容量和内存占用。
本来我是想写工具,在anim导入的时候,自动修改压缩选项并精简浮点数。不过由于有些情况还是需要使用Off选项,所以后来并没有这么做,而是做了根据文件夹寻找anim文件并批量修改的工具来处理。

2、关于字符串处理的一些经验

我这里没有直接给出精简anim文件的工具代码,因为这些毕竟是公司项目在使用的工具。如果有这方面疑问,可以私下联系我进行交流。
我可以提供一些我做替换时遇到的问题,各位可以自己注意一下:
一开始,我是使用正则匹配来查找和替换文本的,这样代码写起来很简单。但在实际的运行中,发现有些anim文件实在很大,纯文本可能达到上百万行。这样巨大的文本,用正则匹配速度有点慢,不过也还勉强能接受。但到了替换这一步,如果直接操作巨大的字符串,会发现不论是用String自带的Replace方法,还是正则的Replace方法,都会非常非常的慢,甚至会卡死。
后来我改了实现的方式,改为了把巨大的字符串通过’\n’来分割为多行,然后每一行单独的处理查找和替换工作,最后,使用string.Join方法,把多行数据用’\n’重新拼接起来,这样的速度就非常的快,上百万行的文件,2-3秒就能处理完。
在匹配小数的时候,还要留意有没有小数是用科学计数法来表达的,也要把这种情况给匹配出来。

有关Unity动画文件(AnimationClip)精简容量的研究的更多相关文章

  1. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

    我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  8. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  9. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  10. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

随机推荐