流体效果相信大家都不陌生,一种常用的实现方式是基于metaballs,可以参考这里和这里获取metaballs的shader代码。
如果要渲染有一定体积的流体,需要实时渲染几十上百甚至上千个metaball,此时渲染的效率就需要考虑进来。
论坛里已经有不少相关的实现,本文将对两种不同的实现进行学习分析并提出优化方案。
优化方案只针对运行效率,本文不讨论metaballs渲染的视觉效果优化。
方案1:box2d + shader
实现原理
通过box2d产生一批粒子, 将N个粒子的坐标通过uniform变量一次性传入shader
。
shader的片元着色器中,累积计算每个粒子对当前片元产生的“势能”(势能和距离相关),“势能”大于阈值时输出颜色,否则透明。
示例代码(shader)
uniform ARGS{
// ...
// ts中从box2d直接获取每个粒子的坐标,进行简单处理后传入shader
vec4 metaballs[500];
};
void main () {
// ...
float v = 0.0; // 所有metaball对当前片元的影响将累加到变量v中
for(int i = 0; i < 500; i++){
vec4 mb = metaballs[i];
// ...
// ss是第i个metaball 对当前片元影响的“势能”, (cx, cy)为metaball坐标,r为半径
float ss = r * r / (cx * cx + cy * cy);
// 累加势能
v += ss;
}
// 势能超过阈值则渲染为水流颜色,否则渲染为透明
// ... 略
}
分析
片元着色器程序需要遍历所有metaball,500个metaball的情况下需要执行上千条指令。每一帧每个片元都需要执行这么长的程序,并且往往这种渲染方式需要全屏幕渲染,GPU压力将非常大。
方案2:PhysicsManager
实现原理
每个粒子是一个cc.Node,并挂上 物理碰撞组件
。
每个粒子用一张圆形渐变图渲染到内存纹理,纹理的半透明部分将叠加,相当于metaball里的势能叠加效果。然后用一个简单的shader按阈值处理颜色。
示例代码
略
分析
cc自带的碰撞检测的性能相对于box2d的粒子组碰撞检测效率要差一些,前者算法时间复杂度是O(N^2),后者在渲染同半径的粒子组时可以优化为O(NlogN)。在metaball数量较多的情况下差异会显现出来。
另外由于使用cc.Node包装了粒子,对引擎带来一定的overhead,如render-flow遍历时需要逐粒子做RenderData更新(相对于碰撞检测来说这部分可以忽略)。
方案3:box2d + assembler
实现原理
- 跟方案1一样,使用box2d产生粒子组。
- 在assembler里获取所有粒子坐标,批量组装成
RenderData
。
针对每个粒子生成一个四边形,附带它在世界坐标里的原心位置,同时省略了uv和color属性。
可以学习方案2对每个粒子使用圆形纹理图,本方案为了简单起见直接在shader里画圆。
顶点格式如下:
var vfmtPosCenter = new gfx.VertexFormat([
{ name: gfx.ATTR_POSITION, type: gfx.ATTR_TYPE_FLOAT32, num: 2 }, // 粒子顶点(1个粒子有3个或4个顶点)
{ name: "a_center", type: gfx.ATTR_TYPE_FLOAT32, num: 2 } // 原粒子中心(每个顶点相同数据)
]);
示例代码
见Demo内的 SceneMetaBalls
分析
避开了方案1的GPU瓶颈和方案2的CPU碰撞计算瓶颈。
缺点是代码量相对较高,在自定义assembler内需要处理box2d坐标空间到屏幕空间的换算。
性能对比
测试环境:华为P9手机,chrome访问,开发模式
测试数据均来自cc自带调试面板数据的目测。
数据解释
WebGL的数据不知道为啥一直是0,以上没有记录。
方案1的Game Logic很低,但是帧率较低,结合不难推断出是GPU压力影响了整体帧率;
方案2的Game Logic偏高,主要是物理碰撞检测导致的CPU压力;
进一步对方案3进行profile,可以发现,最耗时的仍然是粒子碰撞检测部分,其中最耗时的部分是box2d里寻找粒子之间的连接点,见下图。
经实际运行统计,在1000个粒子的场景下将产生7000+个碰撞点,函数内部循环次数加到14000+。这是一帧的运算量,所以内部循环是热点代码。
利用临时变量合并一些公共表达式
可以实现少量性能提升,不过需要改引擎内部。Demo地址
https://github.com/caogtaa/CCBatchingTricks
参考
https://forum.cocos.org/t/topic/92305
https://forum.cocos.org/t/shader/92906
https://forum.cocos.org/t/happy-glass/72468/46
https://docs.google.com/presentation/d/1fEAb4-lSyqxlVGNPog3G1LZ7UgtvxfRAwR0dwd19G4g/htmlpresent
20200819更新
iOS微信小游戏测试数据
方案3 Demo在iPhone SE2上实测,1000个粒子流动过程中为60fps,粒子积压在底部时为5fps。
直接在Safari浏览器里面跑可以达到60fps。
分析
由于iOS微信小游戏无法开启jit,在计算密集型场景下效率非常差。
在流体里面物理计算量和粒子之间的连接点数成正比。
在水流动过程中连接点较少,当粒子全部落到底部趋于静止时每个粒子周围都有多个连接点,计算量最大。
期待物理大佬能提供一些解决思路。