Open johanzhu opened 2 months ago
Spine 的裁减需要大量的 CPU 计算 这块主要计算在哪里,包围盒么?Mask 的话,性能不一定会有帮助
包围盒是否可以估算,而不是精确计算,可以把大部分在屏幕外面的裁剪掉就行
我记得 Spine 里面的更新 Update 函数是无论是否被裁剪都会被执行?是放在 onUpdate 里的
Spine 的裁减需要大量的 CPU 计算 这块主要计算在哪里,包围盒么?Mask 的话,性能不一定会有帮助
这里指的是 Spine 自身的遮罩裁减哈~
我记得 Spine 里面的更新 Update 函数是无论是否被裁剪都会被执行?是放在 onUpdate 里的
是的。这里需要做处理,RFC里给出了方案,有个参数判断一下就好啦
包围盒是否可以估算,而不是精确计算,可以把大部分在屏幕外面的裁剪掉就行
有道理,我思考一下能否估算。因为计算包围盒会影响每帧性能。
背景
当前的 Spine 运行时在代码结构和性能方面仍有改进空间,需要进一步优化。
在 1.3 里程碑中,我们集中优化了一波内存占用;在本次里程碑中,会针对运行时性能进一步进行的优化。
目标:
本 RFC 包含对于代码结构和代码细节的一些调整,以优化运行时的性能。这些优化点来自于对竞品的调研,包括 spine 官方的: unity 运行时(主要), ue 运行时,threejs 运行时,webgl 运行时以及 pixi 运行时。
index 更新优化
简述:Spine 动画在播放时,顶点的序号一般不会发生修改,更新的是顶点的位置。优化 index 更新机制能够减少相关计算,并减少和 GPU 的交互从而提升性能。
方案:为了优化 index 的更新,增加一个对象存储渲染的基本信息(比如顶点的总数,子网格顶点数,待渲染的附件等)。每帧对比这些信息,满足条件的情况下跳过 index 的更新以优化性能。
优劣:该方案虽然优化了 index 更新但是增加了内存,以及部分 CPU 开销。
代码方案:
index 更新优化涉及到修改核心的 buildPrimitive 方法的结构。下面方案的图示参考:
绿色是逻辑有更新的部分,核心的改动只有两条:
双缓冲优化
简述:对于 Spine 这种需要高频更新 buffer 并渲染的场景,双缓冲能有效提高帧率的稳定性,减少GPU 的空闲等待时间,提升了硬件资源的利用率。
方案:在渲染器内维护两个 vertexBuffer 和 indexBuffer,用于实现双缓冲。每一帧,都会绑定下一个 buffer 用于渲染,当前 buffer 则用于计算得到当前帧的数据。
优劣:优势上面已经提到了,双缓冲的劣势也是显而易见的:增加输入延迟,占用更多内存是否能够带来切实提升需要在端上进行测试。
代码方案:
参考 Unity 的实现,Unity 内部维护了一个 MeshRenderBuffer 对象,并在内部管理双缓冲对象,通过一个DoubleBuffered 类以及两个 unity 的 Mesh 来实现缓冲的 swap。
https://github.com/EsotericSoftware/spine-runtimes/blob/4.2/spine-unity/Assets/Spine/Runtime/spine-unity/Mesh%20Generation/MeshRendererBuffers.cs
https://github.com/EsotericSoftware/spine-runtimes/blob/4.2/spine-unity/Assets/Spine/Runtime/spine-unity/Mesh%20Generation/DoubleBuffered.cs
新增一个类(暂定 RenderBuffer)来管理 Primitive 和 Buffer。Buffer 的修改以及交换,都交给 RenderBuffer 类处理。
裁减优化
方案:裁减后,暂停动画的更新
代码方案:isCulled 如果为 true,停止动画的更新
方案:根据 Clip 附件的形状绘制一个 Mask 添加到场景中用于遮罩 spine 动画。
优劣:CPU 裁减 在大面积裁减,尤其是裁减了复杂 Mesh 附件的情况下,不但会增加 CPU 计算开销,而且会增加非常多顶点数量。使用遮罩能够有效减少这部分开销。但是,使用遮罩会打断合批,在 Spine 动画数量非常多的情况下,会增加大量 drawcall。
⚠️ 目前 2d 还不支持 Graphic,除非自己定制一个 spine 专用的遮罩类(非常 hack),故先不做该优化。
条件优化
简述:大多数的 Spine 动画只有一个 submesh。在这一前提下,Spine 动画在渲染循环中,可以跳过对于 Submesh 分割的逻辑判断(对比 Texture, BlendMode ),节省性能。
方案:增加 SingleSubMesh 开关,开关开启后,每帧跳过 subMesh 判断逻辑,以优化性能。
简述:大多数的 Spine 动画并不会上裁减。可以针对这种情况,可以优化每帧顶点的赋值计算,优化计算性能。
方案:遍历当前状态下的 skeleton 对象,若不存在裁减附件即无裁减时,跳过裁减判断,同时优化 buffer 的赋值计算逻辑。
代码方案:
存在裁减时:需要添加一个中间变量记录顶点数据,然后二次遍历进行赋值
优化方案:得到顶点数据后,直接按照顺序赋值至顶点数据中
MeshAttachment 同理
当存在裁减时,由于裁剪后的多边形和三角形可能是不规则的,需要根据裁剪后的形状重新生成顶点和三角形索引。因此,索引的生成同样需要一个中间变量,并二次遍历赋值。
当无裁减时,索引可以直接通过固定的规则生成(region 每 4 个顶点生成 2 个三角形, mesh 附件直接用 attachmentTriangles),无需引入新的中间变量,可根据按照顺序依次赋值。
Region 按照顶点顺序赋值:
Mesh 需要遍历其索引数据赋值:
其他优化
把 Math.max 和 Math.min 替换成 > < 比较
性能参考:https://stackoverflow.com/questions/1232345/speed-and-style-of-math-max-vs-ternary-operator-in-javascript
PS: 目前的包围盒的计算会在以下时机更新
buffer 的 setData 修改为 Discard 模式
由引擎合批管线负责处理,runtime 层不进行额外处理