动画控制器 Animator Controllers
引入
为了更好地理解本节的数据,我想指出以下两点:
- 所有测试都没有包括任何状态行为(State Behaviour)。如果一个控制器的任何动画层上有一个或多个状态行为,所有这些控制器的运行时间会增加50%。无论使用哪种状态行为,这种增加都是存在的。由于默认的Action & Gesture层中包含状态行为,这种额外的开销可能会出现在每个使用的Avatar上。由于这种开销总是存在的,我在测试中选择不包含它,因为我的测试目的是进行比较。然而,如果你查看原始数据,可能需要在结果上增加50%的开销,以获得实际的帧时间。
- Animator Controller的性能不会线性缩放。我将在后文详细讨论,但需要注意的是,两个拥有100个动画层的Avatar并不会比一个拥有200个动画层的头像更慢。优化Animator Controller是有益的,因为减少帧时间是有好处的,但如果你发现一些异常数据(例如,按线性缩放计算,40个头像每个有50个动画层应该需要68毫秒,但实际上只需6.4毫秒),这表明可能有问题。
我将首先介绍单个Controller的帧时间性能,然后讨论在使用多个Controller时性能的变化。
参考基准:两个状态切换
我们的参考基准将采用简单的双状态切换作为参考。
默认的切换对比图。图中显示了两个动画,每个动画包含两个帧,数值相同。已启用“写入默认”。
在每次测试动画层数时,层数与帧时间的关系图呈二次曲线。这意味着,随着动画层数的增加,添加额外动画层的帧时间开销会显著增加。然而,由于二次曲线的性质不是很强,因此在动画层数较少时,可以用线性图来近似。对于基本的状态切换,每增加一个动画层的额外开销约为0.01毫秒。这将作为我们比较的基准。
基本切换在未被主动切换时的帧时间与动画层数关系图。
如果我们在进行层动画时重新运行相同的基准测试,将得到如下图表。
基本切换在主动切换时的帧时间与动画层数关系图。
该图表显示,对于不断进行切换的情况,其成本比不切换时高出大约20-30%。例如,面部或眼部追踪情况即是如此。这种20-30%的额外开销在我的所有测试中都表现出一致性(如AnyState、AnyState自我过渡、多个Animator等),仅在直接使用Blend Trees时有所不同,这取决于具体的设置。
任意状态(AnyState) & 混合树(Direct Blend Trees)
AnyState
无论AnyState切换的数量多少,AnyState的性能与非AnyState切换相似。这表明,转换检查的数量对帧时间的影响并不显著,这一点也在其他测试中得到了验证。
唯一的例外是启用了“可以过渡到自身”选项的AnyState切换,这会比未启用该选项的情况多出20%的开销,即使是在持续切换的比较中也是如此。
Direct Blend Trees
当首次发现动画层的巨大开销时,很多人曾希望直接Blend Trees能够作为一种魔法般的解决方案,大幅度降低帧时间。我的测试结果表明,虽然它们并不能完全消除帧时间,但确实在减少帧时间方面表现出色。
我发现了两种比普通动画层切换更高效的设置:一种较为简单,但略显迟缓;另一种设置较为复杂,但性能稍好。
对于较慢的设置,我使用了一个直接Blend Tree,包含多个1D Blend Trees作为子项。所有子项的权重均为1,而1D Blend Tree的混合值则根据切换进行调整。
Direct Blend Tree在未进行频繁切换时的帧时间
Direct Blend Tree在频繁切换时的帧时间
从这些结果来看,这种设置能将我们的帧时间减少约四分之三,这在性能上有显著的提升。尤其在大量切换的情况下,这种方法能够显著提升性能。
要了解如何创建这样的Blend Tree,可以参考以下文章:使用Direct Blend Trees组合动画层。
另一个稍微更快的办法是,需要两个层。第一个层是一个动画,控制所有切换的默认值;第二个层是一个直接Blend Tree,使用切换参数来控制每个切换的状态,每个切换配有一个动画。
例如,如果我有一双鞋子需要切换,并且默认状态下它们是开启的,那么在第一个层的大动画中,我会将IsActive值设置为true,而在第二个层的大Direct Blend Tree中,我会设置一个动画,将IsActive值设置为false。混合参数是我想用的参数。当参数为0时,动画权重为0,默认层将生效;当参数为1时,动画权重为1,将覆盖默认层。
带有默认动画层的Direct Blend Tree在未进行持续切换时的帧时间
带有默认动画层的Direct Blend Tree在持续切换时的帧时间
从这些结果来看,相比简单的双状态切换,这种方法将帧时间减少了约五分之四,相比1D Blend Tree设置在主动情况下减少了50%。这是一项显著的改进,但其工作原理可能不够直观,设置也可能更加复杂。
如果你需要频繁切换(例如,在面部追踪或经常使用语音参数时),这种方法可能会有所帮助。但如果情况允许,我个人建议继续使用双状态设置。
杂项数据
以下是一些与切换不直接相关但可能对某些人有用的杂项数据:
- 每层的状态数量和过渡数量对性能的影响似乎不大(这可能解释了为什么AnyState的性能较好)。
- 唯一的例外是当单个状态有大量布尔过渡(例如1000个过渡需要1毫秒)时。浮点数和整数似乎不受此额外开销的影响。
- 在使用人体骨架、非人体骨架或不使用Avatar的情况下,与两状态切换设置相比,每层的帧时间减少约50%。
- 遮罩对帧时间的影响似乎微乎其微。
- 使用子状态机对帧时间的影响也似乎很小。
- 为了增加易读性而嵌套Blend Trees对帧时间的影响几乎可以忽略。
- 对于Direct Blend Trees,禁用“写入默认”(WD off)对帧时间几乎没有影响。
- 对于动画层,禁用“写入默认”(WD off)似乎会使帧时间增加约50%。
- 本地Avatar上的参数每1000个大约需要1.5毫秒,但这个开销不适用于远程Avatar。
由于面部追踪占用了许多层,我对比了普通的VRCFT控制器和由Razgriz的VRCFTGenerator生成的FX控制器(该生成器将层打包成Blend Trees)。
尽管图表仅显示为直线,我还是将数据提供出来(基于37个面部追踪参数):
- VRCFT控制器:每个控制器0.1733毫秒
- VRCFTGenerator:每个控制器0.066毫秒,比非Blend Tree版本减少了62%
多控制器
使用多个控制器的性能不会按线性比例增长(也就是说,两个每个有100层的控制器比起一个有200层的控制器会有显著更少的延迟)。
实际的关系难以精确描述,但可以通过下面的图示来帮助理解。每条线代表一个恒定的帧时间。例如,5个控制器每个有580层的开销与15个控制器每个有300层的开销是一样的。
从这个图表中,我得出两个主要结论:
- 大型控制器相比于多个小型控制器会使用更多帧时间。如果你有很多动画层,优化尤其重要。(一个100层的控制器的帧时间开销相当于10个30层的控制器。)
- 即使有很多控制器,如果将它们的层数都减半,总帧时间仍然能减少50%。因此,如果每个人都优化他们的层数和动画层,将会提高整体性能。
这种关系似乎适用于所有控制器类型和层配置。