1
\$\begingroup\$

I started experimenting with OpenGL ES 2.0, and I'm trying to figure out a way to optimize/batch sprite drawing, where sprites only have the texture in common, but differ in the following properties:

  • Position
  • Size
  • Rotation
  • Texture coordinates
  • Color
  • Hue

I tried two different approaches, which are "good enough" for most games, but I'm still curious on how I could optimize drawing sprites further. All the code is available here on GitHub, but I'll explain my approaches below.

(Both approaches use 4 vertices + 6 indices to draw a quad.)

  1. One drawcall per sprite.

    Position and texture coordinates are passed as a vec4 vertex attribute. The MVP matrix (that handles size and rotation) is passed as a mat4 uniform. Color is passed as a vec4 uniform and hue is passed as a float uniform.

    Since pretty much everything is being passed as an uniform, I have to set all uniforms and call glDrawElements once per sprite.

    I can draw 50000 sprites at 60FPS on my desktop (i7 2400k, GTX 970).

  2. One drawcall per N sprites.

    The second idea I had and tried was dividing sprites in batches of N. In this approach, everything is passed as a vertex attribute.

     attribute mat4 a_projection_view_model;
     attribute vec4 a_pos_tex_coords;
     attribute vec4 a_color;
     attribute float a_hue;
    

    From my tests it seems slightly faster than the previous approach, but it isn't a big improvement. Also, passing the same mat4 and the same vec4 (a_color) four times seems weird and unnecessarily expensive.

    I couldn't figure out a different solution since every sprite has its own MVP matrix, color an hue.

How is sprite batching (when the texture is the same) usually done? Are my approaches "correct" in theory, or am I doing something which is considered bad practice?

How can I optimize my sprite drawing?

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

The typical situation is that all sprites are rendered with the same view/projection matrices (same "virtual camera").

So the way I see it, all your sprites need just one draw call - just generate all the vertices on the CPU:

  • Multiply vertex positions of each sprite with the model matrix
  • Pass view*projection as MVP matrix
  • Calculate final color for the sprite on CPU, write it to each vertex (so color becomes a vertex attribute)

P.S.

I couldn't figure out a different solution since every sprite has its own MVP matrix, color an hue.

Why would you possibly need one MVP matrix for each sprite and color+hue (instead of just color)?

\$\endgroup\$
5
  • \$\begingroup\$ Derp, I was multiplying MVP with position in the shader... but I can do it on the CPU and avoid sending the matrix to the shader. Hue needs to be applied per-fragment, so I'm pretty sure I need to pass it to the shader. Color is there for opacity and tint, and gets applied before hue. Should I multiply the model matrix with viewprojection on the CPU? Or pass viewprojection as an uniform? \$\endgroup\$ Commented Oct 14, 2015 at 15:47
  • 1
    \$\begingroup\$ @VittorioRomeo Passing view*projection as an uniform avoids the need to multiply more matrices than necessary per sprite. With sprites there usually is no such thing as a "model matrix" anyway - pretransformed direction vectors can be used to build sprites turned towards screen. Why do you have a model matrix for sprites? \$\endgroup\$
    – snake5
    Commented Oct 14, 2015 at 17:18
  • \$\begingroup\$ Thanks for your help. This is what I have so far - the code should be pretty readable. I will pass view * projection as an uniform. Regarding the "pretransformed direction vectors" that should be used in place of the model matrix: I'm inexperienced with graphics programming and never heard of those until now. Couldn't find anything by briefly searching on Google. Can you point me in the right direction with an article/example? \$\endgroup\$ Commented Oct 14, 2015 at 17:23
  • 1
    \$\begingroup\$ @VittorioRomeo From looking at the code, it seems that pretransformed direction vectors (which is simply transforming vec3(1,0,0),vec3(0,1,0) by inverse of view matrix rotation part) aren't going to be of much use. Still, the draw_sprite code is inefficient and a few cross products could solve that. It's very hard to dig up articles or examples but I do suggest reading about 3D vector cross product, normalization and matrix transform identities if you haven't already, it should help in optimizing these things. This could help too: nehe.gamedev.net/article/billboarding_how_to/18011 \$\endgroup\$
    – snake5
    Commented Oct 14, 2015 at 18:15
  • \$\begingroup\$ Update: new code. On my machine, this code allows me to have 100000 (100k) individual sprites at 75+ FPS. I'm satisfied... for now. I also computed the matrix multiplication using Mathics, but there was no benefit in doing that. Either glm uses expression templates, or the compilers I've tried (both g++ and clang++) optimize the multiplication away. Cool stuff! \$\endgroup\$ Commented Oct 14, 2015 at 20:52
1
\$\begingroup\$

You should probably avoid the first method. One draw call for just four vertexes is suboptimal. It might work on a bulky desktop PC, but will certainly be very slow on a mobile device.

One solution that I have been using as of late and like it, is to generate the 2D vertexes on the CPU-side and pass them to the shader. Each vertex is comprised of whatever shading attributes you may need (tex-coords, color, position, whatnot), then my Vertex Shader is basically just a pass-through. I also convert the 2D vertexes to OpenGL clip space, since I'm already calculating the final vertex position in the CPU, this is a cheap operation, so my typical 2D sprite drawing Vertex Shader looks something like this:

in vec3 inPosition;
in vec2 inTexCoords;
in vec4 inColor;

out vec2 vTexCoords;
out vec4 vColor;

void main()
{
    gl_Position = vec4(inPosition, 1.0);
    vTexCoords  = inTexCoords;
    vColor      = inColor;
}

That avoids the need of supplying different matrices via uniforms, which facilitates batching. My only batching criteria is the texture applied to the sprites, so I only need one draw call per texture. This can be further optimized if you use texture altases to pack small textures into a larger one.

This is similar to the approach proposed by @snake5's answer.

\$\endgroup\$
2
  • \$\begingroup\$ How do you solve transparency? Problem with batching per texture is you cannot maintain correct back-to-front order, can you? \$\endgroup\$
    – wondra
    Commented Oct 16, 2015 at 11:01
  • \$\begingroup\$ @wondra, good question, I forgot about this. Yes, with batching per texture draw order will no longer necessarily match the order of function calls in the code. There are two solutions for this that I know of: You can sort the elements before drawing, keeping an extra integer with depth/layer index in your Sprite structure for sorting predicate. Don't need to send that to the shader, of course. Another approach I have only recently tried and haven't tested much, but seems interesting, is to use a 3D position for each sprite, them draw with depth enabled, letting the GPU do the sorting for you. \$\endgroup\$
    – glampert
    Commented Oct 16, 2015 at 16:48

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .