关于 批处理(合批)

前言

众所周知,在电脑硬盘上进行文件移动操作,对于单独一个1G的文件 和 1024个1M的文件,前者通常会比后者更快完成。OpenGL渲染批处理的概念,可以考虑成与之类似的一种处理。

# 一. 什么是批处理?

设想下面两种情况:

# 1.一帧中,执行1000次以下步骤:

  1. 创建一个四边形的6个顶点(两个三角形),索引,颜色,并绑定;
  2. 绑定纹理;
  3. 使用shader;
  4. glDrawElements();

# 2.一帧中,执行1次以下步骤:

  1. 创建一个1000个四边形的6000个顶点,索引,颜色,并绑定;
  2. 绑定纹理;
  3. 使用shader;
  4. glDrawElements();

两种做法最终都在一帧中完成了1000个四边形,6000个顶点的绘制。但B方法的2,3,4步仅仅只执行了一次,而A的2,3,4各执行了1000次。

TIP

其实方法1,就是我们所谓的“批处理” —— 把多个顶点放在“同一批”去绘制。


# 二. 图集为什么能批处理?

通过上面的分析,我们知道,要让多个绘制合并到一个Drawcall, 要满足以下几个前提条件:

  1. 多个顶点同时绑定;
  2. 这批顶点使用同一个(或同一组)纹理数据;
  3. 这批顶点使用相同的shader及其他相同的绘制参数。

考虑条件2,我们要保证绘制列表里的元素都使用同一份纹理数据 ,而图集就是让我们达到这一点的手段。

# 那怎么让不同内容的Sprite使用图集上的不同部分呢?

我们准备提交给GL的顶点数据中,不仅包括每个顶点的位置,索引,颜色,同时也会将一个“UV坐标”提交给GL,它指某个顶点在纹理中对应的像素位置。引擎会在shader中根据UV坐标,来确定渲染出来的像素对应图集中的哪一部分。

比如cocosCreator的默认shader,builtin-2d-sprite.effect:

  void main () {
    vec4 o = vec4(1, 1, 1, 1);
 
    #if USE_TEXTURE
    o *= texture(texture, v_uv0); //根据UV坐标确定像素值
      #if CC_USE_ALPHA_ATLAS_TEXTURE
      o.a *= texture2D(texture, v_uv0 + vec2(0, 0.5)).r;
      #endif
    #endif
 
    o *= v_color;
 
    ALPHA_TEST(o);
 
    gl_FragColor = o;
  }

因为UV坐标是和其他顶点信息一起提交的,它只在每次绑定时被改变(被赋值),所以虽然渲染不同Sprite使用了不同的UV值,但不会造成drawCalls的增加。 合图就是用这种方式实现了不同内容的多个Sprite合批。

由此可知,我们对顶点分别设置不同的颜色,也不会造成drawCalls增加。比如在合批的前提下,进行 nodeA.color = cc.Color("#ff0000")这样的操作是不会导致drawCalls增加的。因为colorCreator引擎将色值作为顶点数据一起绑定。

# 三. 什么情况会打断合批?

我们已经知道如何利用图集实现批处理,但仅仅采用图集并不能保证多个视图元素drawCalls次数为1。OPENGL的绘制有几个步骤,但凡其中任一步骤发生变化,都会“打断合批”,导致drawCalls次数增加。

# 下面是一些常见的必定会打断合批的应用场景:

  • Sprite 使用了不同的贴图文件(散图);
  • 用了 cc.Mask 遮罩(使用了不同的模板测试策略);
  • 多个 Sprite 运用了不同的混合模式 cc.Blend ;
  • 多个 Sprite 运用了不同的 shader ;
  • 多个 Sprite 运用了相同的 shader, 但使用了不同的 uniform 值;
  • 渲染队列中有文本框“插队”,而这个文本框中的字符贴图并没有使用图集中的素材;

# 下面的这些改变不会打断合批(在已经保证合批的前提下):

  • node.color, node.opacity的变化;
  • 纹理拉伸策略的不同,SIMLE, SLICED, TILED;
  • Sprite SizeMode的不同,CUSTOM,TRIMMED;
  • 顶点动画(简单的比如位移,scale,skew,rotate, 或者一些复杂的顶点动画效果,比如Spine的FFD(挤压,弯曲,变形)) ;