不适合采用 System 的一种情况
ECS 框架中,System 通常是对某种 Component 的集中处理。大部分情况下,一次性对大量对象中的简单的数据结构做简单的处理,比每次对一个对象的复杂数据结构(通常是简单数据结构的组合)做复杂处理,依次处理所有的对象要更好。
凡事总有例外。我们最近就发现有一类情况不适合这样做。那就是骨骼动画的骨骼计算。
骨骼计算会分为若干步骤,有骨骼数据的展开,骨骼姿态的混合,IK 计算等等。一开始,我们遵循着 ECS 的设计原则,把这些步骤分到不同 System 种。例如并非所有的对象都需要做 IK 计算,这样 IK System 就只用遍历一个子集;同样,也并非所有的动画都需要做多个姿态的混合,等等。
但我们在实现时却发现了很大的问题。
骨骼相关运算是需要在一个临时短生命期的 Buffer 上进行的。骨骼本身的持久化数据结构占内存不大,但是如果需要做中间运算,就需要把紧凑的数据展开到一个中间 Buffer 中,有关混合、IK 这些运算都是基于这个中间 Buffer 的,而当这些运算完毕,到了蒙皮阶段,中间 Buffer 又没有存在的必要了。
如果遵循 ECS 的原则,我们把这些步骤分离,每个步骤都遍历所有的对象各做一次,就会生成大量的中间 Buffer ,徒耗内存。其实本质上,一个线程只需要一个中间 Buffer 就够了。因为对一个对象做完处理,这个 Buffer 就不再使用,可以被下一组运算复用。
所以,我们近期做的调整就是把这一系列关于骨骼的算法全部集中在一个 System 中处理。
下面再谈谈 Lua 封装代码的处理。
如果整个过程是在 C/C++ 层面完成,那么,这个中间 Buffer 完全可以开在 stack 上。等一系列操作完成后,就会自动回收。但是将 C 函数封装为 Lua 函数后, C stackframe 无法保留任何私有数据。如果你把中间 Buffer 封装成一个 userdata 扔回 Lua ,就面临被滥用的可能。一旦滥用,对 gc 带来额外负担。
我们的做法是把它封装成 userdata 后,隐藏在 C api 的 upvalue 里面,应用层无法直接访问。这一系列针对骨骼数据的 API 就好像 opengl 的 api 那样,有个内在的状态维持在 vm 中。每组调用都从一个显式的调用开始:对于骨骼运算,就是将骨骼数据展开到临时 Buffer 中。
相比昂贵的骨骼运算操作步骤,一次 Lua 到 C 的调用开销算不得什么,所以我们没有将它封装成 [数学库] (https://blog.codingnow.com/2019/03/improved_math3d.html) 的样子(允许单次数学库调用进行足够多的数学运算),还是保留成一个个独立的 Lua API 调用。
我认为以后会有很多类似的情况不适合分 System 分步处理,值得注意。
总结一下,当针对单个对象进行的一系列操作,需要的中间临时资源相对昂贵时,不应该拆分成多步 System 。昂贵的资源可能是内存,也可能是显卡资源。
Comments
Posted by: m | (10) September 21, 2023 10:15 PM
Posted by: 止水轻扬 | (9) April 16, 2020 07:47 PM
Posted by: Mutoo | (8) February 22, 2020 06:50 PM
Posted by: maninmatrix | (7) February 2, 2020 08:12 PM
Posted by: Cloud | (6) January 20, 2020 05:31 PM
Posted by: ruihong | (5) January 20, 2020 04:17 PM
Posted by: mayao11 | (4) January 20, 2020 12:33 PM
Posted by: Cloud | (3) January 20, 2020 11:41 AM
Posted by: ruihong | (2) January 20, 2020 10:19 AM
Posted by: 林冲 | (1) January 20, 2020 09:27 AM