为什么Sprite Batcher更快?

时间:2011-07-13 10:16:13

标签: opengl-es

我正在阅读Beginning Android Games(Mario Zechner)。

在阅读使用OpenGL ES 1.0的2D游戏时,作者介绍了 SpriteBatcher 的概念,它采用每个精灵来渲染坐标和角度。然后SpriteBatcher计算精灵矩形的最终坐标并将其放入一个大缓冲区。

渲染方法中,SpriteBatcher设置所有精灵的状态一次(纹理,混合,顶点缓冲区,纹理坐标缓冲区)。所有精灵都使用相同的纹理但不是相同的纹理坐标。

这种行为的好处是:

  • 渲染管道不会停止,因为渲染所有精灵时没有状态更改。
  • OpenGL调用较少。 (=减少JNI开销)

但我看到一个主要的劣势:

  • 为了旋转,CPU必须计算正弦和余弦,并为每个精灵执行16次乘法运算。据我所知,计算正弦和余弦是非常昂贵和缓慢的。

但SpriteBatcher方法比使用大量glRotate / glTranslate逐个渲染精灵要快得多。

最后我的问题:

  • 为什么它更快? OpenGL状态变化真的那么贵吗?
  • GPU针对向量乘法和旋转进行了优化,而CPU则不是。为什么不重要?
  • 是否会在桌面上使用SpriteBatcher并使用专用的GFX卡?
  • 是否存在SpriteBatcher效率低下的问题?

2 个答案:

答案 0 :(得分:6)

  

但我看到一个主要的劣势:

     
      
  • 为了旋转,CPU必须计算正弦和余弦,并为每个精灵执行16次乘法运算。据我所知,计算正弦和余弦是非常昂贵和缓慢的。
  •   

实际上 sin cos 非常快,在现代架构中,如果管道之前没有停止,它们需要1个时钟周期才能执行。但是,如果每个精灵单独旋转并使用普通的平截头体透视投影,则此代码的作者不知道他的线性代数。

如果有人回忆起,整个任务可以简化很多,模型视图矩阵将线性局部/世界坐标图映射到眼睛空间。旋转位于左上3×3子矩阵中,该列形成局部基矢量。通过获取该子矩阵的逆矩阵,您可以精确地给出您需要的精灵基础,将平面映射到眼睛空间。在仅应用旋转(和缩放,可能)的情况下,左上角3×3的倒数是转置;因此,通过使用左上3×3行作为精灵基础,您可以在不进行任何三角函数的情况下获得该效果:

/* populates the currently bound VBO with sprite geometry */
void populate_sprites_VBO(std::vector<vec3> sprite_positions)
{
    GLfloat mv[16];
    GLfloat sprite_left[3];
    GLfloat sprite_up[3];

    glGetMatrixf(GL_MODELVIEW_MATRIX, mv);

    for(int i=0; i<3; i++) {
        sprite_left[i] = mv[i*4];
        sprite_up[i]   = mv[i*4 + 4];
    }

    std::vector<GLfloat> sprite_geom;
    for(std::vector<vec3>::iterator sprite=sprite_positions.begin(), end=sprite_positions.end();
        sprite != end;
        sprite++ ){
           sprite_geom.append(sprite->x + (-sprite_left[0] - sprite_up[0])*sprite->scale);
           sprite_geom.append(sprite->y + (-sprite_left[1] - sprite_up[1])*sprite->scale);
           sprite_geom.append(sprite->z + (-sprite_left[2] - sprite_up[2])*sprite->scale);

           sprite_geom.append(sprite->x + ( sprite_left[0] - sprite_up[0])*sprite->scale);
           sprite_geom.append(sprite->y + ( sprite_left[1] - sprite_up[1])*sprite->scale);
           sprite_geom.append(sprite->z + ( sprite_left[2] - sprite_up[2])*sprite->scale);


           sprite_geom.append(sprite->x + ( sprite_left[0] + sprite_up[0])*sprite->scale);
           sprite_geom.append(sprite->y + ( sprite_left[1] + sprite_up[1])*sprite->scale);
           sprite_geom.append(sprite->z + ( sprite_left[2] + sprite_up[2])*sprite->scale);


           sprite_geom.append(sprite->x + (-sprite_left[0] + sprite_up[0])*sprite->scale);
           sprite_geom.append(sprite->y + (-sprite_left[1] + sprite_up[1])*sprite->scale);
           sprite_geom.append(sprite->z + (-sprite_left[2] + sprite_up[2])*sprite->scale);
    }
    glBufferData(GL_ARRAY_BUFFER, 
                 sprite_positions.size() * sizeof(sprite_positions[0]), &sprite_positions[0], 
                 GL_DRAW_STREAM);
}    

如果着色器可用,则可以使用几何着色器或顶点着色器,而不是在每个帧上重建CPU上的精灵数据。几何着色器将采用位置,比例,纹理等矢量并发出四边形。使用顶点着色器,您将发送大量[-1,1]四边形,其中每个顶点将携带它所属的精灵的中心位置作为附加的vec3属性。


  

最后我的问题:

     
      
  • 为什么它更快? OpenGL状态变化真的那么贵吗?
  •   

某些状态变化非常昂贵,您将尽可能避免这些变化。切换纹理非常昂贵,切换着色器的成本相当昂贵。

  
      
  • GPU针对向量乘法和旋转进行了优化,而CPU则不是。为什么不重要?
  •   

这不是GPU和CPU之间的区别。 GPU与CPU不同的是,它对并行的大块记录执行相同的操作序列(帧缓冲区的每个像素都被渲染)。另一方面,CPU一次运行程序一条记录。

但CPU也可以进行矢量操作,即使不比GPU更好。特别是在精度问题上,CPU仍然比GPU更受欢迎。 MMX,SSE和3DNow!是矢量数学指令集。

  
      
  • 是否会在桌面上使用SpriteBatcher并使用专用的GFX卡?
  •   

可能不是这种形式,因为今天可以使用几何和顶点着色器,为其他东西释放CPU。但更重要的是,这可以节省CPU和GPU之间的带宽。带宽是更严格的瓶颈,处理能力现在不是头号问题(当然,处理能力从来没有足够的处理能力)。

  
      
  • 是否存在SpriteBatcher效率低下的问题?
  •   

是的,即CPU→GPU传输瓶颈。今天,人们使用几何着色器和实例来完成这种事情,非常快。

答案 1 :(得分:0)

我不知道SpriteBatcher,但看看你提供的信息是我的想法:

  • 它更快,因为它使用更少的状态更改,更重要的是,减少绘制调用。移动平台对每帧的绘制电话号码有特别严格的限制。
  • 这无关紧要,因为他们可能正在使用CPU进行轮换。我个人认为没有理由不使用GPU,这会更快,并使带宽负载无效。
  • 我认为考虑到第1点,它仍然是一个很好的优化。
  • 我可以介意两种极端情况:当精灵太少或复合纹理(包含所有旋转的精灵)变得太大时(移动设备的尺寸限制较小)。