37
\$\begingroup\$

I'm working on a small tile/sprite-based PC game with a team of people, and we're running into performance issues. The last time I used OpenGL was around 2004, so I've been teaching myself how to use the core profile, and I'm finding myself a little confused.

I need to draw in the neighborhood of 250-750 48x48 tiles to the screen every frame, as well as maybe around 50 sprites. The tiles only change when a new level is loaded, and the sprites are changing all the time. Some of the tiles are made up of four 24x24 pieces, and most (but not all) of the sprites are the same size as the tiles. A lot of the tiles and sprites use alpha blending.

Right now I'm doing all of this in immediate mode, which I know is a bad idea. All the same, when one of our team members tries to run it, he gets very bad frame rates (~20-30 fps), and it's much worse when there are more tiles, especially when a lot of those tiles are the kind that are cut into pieces. This all makes me think that the problem is the number of draw calls being made.

I've thought of a few possible solutions to this, but I wanted to run them by some people who know what they're talking about so I don't waste my time on something stupid:

TILES:

  1. When a level is loaded, draw all the tiles once into a frame buffer attached to a big honking texture, and just draw a big rectangle with that texture on it every frame.
  2. Put all the tiles into a static vertex buffer when the level is loaded, and draw them that way. I don't know if there's a way to draw objects with different textures with a single call to glDrawElements, or if this is even something I'd want to do. Maybe just put all the tiles into a big giant texture and use funny texture coordinates in the VBO?

SPRITES:

  1. Draw each sprite with a separate call to glDrawElements. This seems to involve a lot of texture switching, which I'm told is bad. Are texture arrays maybe useful here?
  2. Use a dynamic VBO somehow. Same texture question as number 2 above.
  3. Point sprites? This is probably silly.

Are any of these ideas sensible? Is there a good implementation somewhere I could look over?

\$\endgroup\$
2
  • \$\begingroup\$ If tiles are not moving nor changing and they look the same way whole level, you should use first idea - frame buffer. It will be most efficient. \$\endgroup\$
    – zacharmarz
    Commented Jun 9, 2012 at 22:51
  • \$\begingroup\$ Try using a texture atlas so you don't have to switch textures, but keeping everything else the same. Now how's their framerate? \$\endgroup\$ Commented Dec 2, 2016 at 10:24

5 Answers 5

27
\$\begingroup\$

The fastest way to render the tiles is packing the vertex data into a static VBO with indices (as glDrawElements indicates). Writing it into another image is totally unnecessary and will only require a lot more memory. Texture switching is VERY costly, so you will probably want to pack all tiles into a so called Texture Atlas and give each triangle in the VBO the right texture coordinates. Based on this, it should not be a problem to render 1000, even 100000 tiles, depending on your hardware.

The only difference between Tile rendering and Sprite rendering is probably that sprites are dynamic. So for the best, yet easy accomplishable, performance you can just put the coordinates for the sprite vertices into a stream draw VBO each frame and draw with glDrawElements. Also pack all textures in a Texture Atlas. If your sprites rarely move, you could also try to make a dynamic VBO and update it when a sprite moves, but that's total overkill here, as you only want to render some sprites.

You can look at a small prototype I made in C++ with OpenGL: Particulate

I render about 10000 point sprites I guess, with an average fps of 400 on a usual machine (Quad Core @ 2.66GHz). It is CPU capped, that means that the graphics card could render even more. Note that I don't use Texture Atlases here, since I only have a single texture for the particles. The particles are rendered with GL_POINTS and the shaders compute the actual quad size then, but I think there is also a Quad Renderer.

Oh, and yes, unless you have a square and use shaders for texture mapping, GL_POINTS is quite silly. ;)

\$\endgroup\$
7
  • \$\begingroup\$ The sprites change their positions and which texture they're using, and most of them do this every frame. Also, sprites and being created and destroyed very often. Are these things that a stream draw VBO can handle? \$\endgroup\$
    – Nic
    Commented Jun 9, 2012 at 23:51
  • 2
    \$\begingroup\$ Stream draw basically means: "Send this data to the graphics card and discard it after drawing". So you have to send the data again each frame and that means it does not matter how many sprites you render, what position they have, what texture coordinates or which color. But sending all data at once and let the GPU process it is A LOT faster than immediate mode, of course. \$\endgroup\$
    – Marco
    Commented Jun 9, 2012 at 23:54
  • \$\begingroup\$ This all makes sense. Is it worth it to use an index buffer thingy for this? The only vertices that'll be repeated are two corners from every rectangle, right? (My understanding is that indices are the difference between glDrawElements and glDrawArrays. Is that right?) \$\endgroup\$
    – Nic
    Commented Jun 10, 2012 at 22:01
  • 1
    \$\begingroup\$ Without indices you can not use GL_TRIANGLES, which is usually bad, since this drawing method is the one with guranteed best performance. Also, GL_QUADS implementation is deprecated in OpenGL 3.0 (source: stackoverflow.com/questions/6644099/…). Triangles are the native mesh of any graphics card. So, you "use" 2*6 byte more to save 2 vertex shader executions and vertex_size*2 byte. So, you can generally say it is ALWAYS better. \$\endgroup\$
    – Marco
    Commented Jun 11, 2012 at 12:46
  • 2
    \$\begingroup\$ The link to Particulate is dead... Could you provide a new one please? \$\endgroup\$
    – SWdV
    Commented Aug 2, 2018 at 16:44
4
\$\begingroup\$

Even with this number of draw calls you shouldn't be seeing that kind of performance drop - immediate mode may be slow but it's not that slow (for reference, even dear-old Quake can manage several thousand immediate-mode calls per frame without falling down so badly).

I suspect that there is something more interesting going on here. The first thing you need to do is invest some time in profiling your program, otherwise you stand a huge risk of rearchitecting based on an assumption that may result in zero performance gain. So run it through even something as basic as GLIntercept and see where your time is going. Based on the results of that you'll be able to tackle the problem with some real info about what your primary bottleneck(s) is/are.

\$\endgroup\$
3
  • \$\begingroup\$ I have done some profiling, though it's awkward because the performance problems aren't happening on the same machine as the development. I'm a little skeptical that the problem is elsewhere because the problems definitely increase with the number of tiles, and the tiles literally do nothing except be drawn. \$\endgroup\$
    – Nic
    Commented Jun 11, 2012 at 3:27
  • \$\begingroup\$ How about state changes then? Are you grouping your opaque tiles by state? \$\endgroup\$ Commented Jun 11, 2012 at 9:11
  • \$\begingroup\$ That's a possibility. This does definitely deserve more attention on my part. \$\endgroup\$
    – Nic
    Commented Jun 11, 2012 at 16:01
3
\$\begingroup\$

Okay, since my last answer kinda got out of hands here is a new one wich is maybe more useful.


About 2D-Performance

First some general advice: 2D isn't demanding for current hardware, even largely unoptimized code will work. That doesn't mean you should Intermediate Mode though, atleast make sure you don't change states when unnecessary (for example don't bind a new texture with glBindTexture when the same texture is already bound, a if check on the CPU is tons faster than a glBindTexture-call) and not to use something so totally wrong and stupid as glVertex (even glDrawArrays will be way faster, and isn't any more difficult to use, it's not very "modern" though). With those two very simple rules the frame time should be atleast down to 10ms (100 fps). Now to get even more speed the next logical step is batching, e.g. bundling as many draw calls into one, for this you should consider implementing texture atlases, so you can minimize the amount of texture binds and thus increase the amount of rectangles you can draw with one call to a large amount. If you now aren't down to about 2ms (500fps) you are doing something wrong :)


Tile maps

Implementing the drawing code for tile maps is finding the balance between flexibility and speed. You can use static VBOs but that won't work with animated tiles or you can just generate the vertex data each frame and apply the rules I explained above, thats very flexible but by far not as fast.

In my previous answer I had introduced a different model in which the fragment shader takes care of the whole texturing, but it was pointed out that it requires a dependent texture lookup and thus might not be as fast as the other methods. (The idea is basically that you upload just the tile-indicies and in the fragment shader you calculate the texture coordinates, meaning that you can draw the whole map with just one rectangle)


Sprites

Sprites require a lot flexibility, making it very hard to optimize, aside from those discussed in the "About 2D-Performance" section. And unless you want ten thousands of sprites at the screen at the same time it's probably not worth the effort.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ And even if you have ten thousands of sprites, modern hardware should run it at a decent speed :) \$\endgroup\$
    – Marco
    Commented Jun 11, 2012 at 19:57
  • \$\begingroup\$ @API-Beast wait what? how do you calculate Texture UV's in the fragment shader? Arent you supposed to send the UV's to the fragment shader? \$\endgroup\$
    – HgMerk
    Commented Jul 4, 2015 at 1:09
0
\$\begingroup\$

If all-else fails...

Set-up a flip-flop drawing method. Only update every other sprite at a time. Though, even with VisualBasic6 and simple bit-blit methods, you can actively draw thousands of sprites per frame. Perhaps you should look into those methods, as your direct method of just drawing sprites seems to be failing. (Sounds more like you are using a "rendering method", but trying to use it like a "gaming method". Rendering is about clarity, not speed.)

Chances are, you are constantly redrawing the whole screen, over and over. Instead of just redrawing only the changed areas. That is a LOT of overhead. The concept is simple, yet not easy to understand.

Use a buffer for the virgin static background. This is never rendered itself, unless there is no sprites on the screen. This is constantly used to "revert" where a sprite was drawn, to undraw the sprite in the next call. You also need a buffer to "draw on", which is not the screen. You draw there, then, once all drawn, you flip that onto the screen, once. That should be one screen-call per all your sprites. (As opposed to drawing each sprite on the screen, one at a time, or attempting to do it all at once, which will make your alpha blending fail.) Writing to memory is fast, and does not require screen-time to "draw". Each draw-call will wait for a return signal, before it attempts to draw again. (Not a v-sync, an actual hardware tick, which is a lot slower than the wait-time that RAM has.)

I imagine that is part of the reason you only see this issue on one computer. Or, it is falling back to software rendering of ALPHA-BLEND, which all cards do not support. Do you check to see if that feature is hardware supported, before you attempt to use it? Do you have a fallback (non-alpha-blend mode), if they do not have it? Obviously, you don't have code which limits (number of things blended), as I assume that would degrade your game content. (Unlike if these were just particle-effects, which are all alpha-blended, and thus, why programmers limit them, as they are highly taxing on most systems, even with hardware support.)

Lastly, I would suggest to limit what you are alpha-blending, to only things which need it. If everything needs it... You have no choice but to demand your users have better hardware requirements, or to degrade the game for the desired performance.

\$\endgroup\$
-1
\$\begingroup\$

Create a sprite sheet for objects and a tile set for terrain like you would in other 2D game, there's no need to switch textures.

Rendering tiles can be a pain because each triangle pair needs their own texture coordinates. There's a solution to this problem however, it's called instanced rendering.

As long as you can sort your data in a way so that, for example, you can have a list of grass tiles and their positions, you can render every grass tile with a single draw call, all you have to do is provide an array of model to world matrices for each tile. Sorting your data this way shouldn't be an issue with even the simplest scene graph.

\$\endgroup\$
5
  • \$\begingroup\$ -1: Instancing is a worse idea than Mr. Beast's pure-shader solution. Instancing works best for performance when rendering objects of moderate complexity (~100 triangles or so). Each triangle tile needing texture coordinates isn't a problem. You just create a mesh with a bunch of loose quads that happen to form a tilemap. \$\endgroup\$ Commented Jun 10, 2012 at 21:27
  • 1
    \$\begingroup\$ @NicolBolas alright, i'm gonna leave the answer for the sake of learning \$\endgroup\$
    – dreta
    Commented Jun 10, 2012 at 21:48
  • 1
    \$\begingroup\$ For clarity, Nicol Bolas, what is your suggestion for how to deal with all this? Marco's stream draw thing? Is there somewhere I can see an implementation of this? \$\endgroup\$
    – Nic
    Commented Jun 10, 2012 at 21:59
  • \$\begingroup\$ @Nic: Streaming to buffer objects is not particularly complex code. But really, if you're only talking about 50 spites, that's nothing. Odds are good that it's your terrain drawing that was causing the performance issue, so switching to static buffers for that would probably be good enough. \$\endgroup\$ Commented Jun 11, 2012 at 3:15
  • \$\begingroup\$ Actually, if instancing worked as we might think it should, it would be the best solution - but since it doesn't, baking all the instances into a single static vbo is the way to go. \$\endgroup\$ Commented Jun 11, 2012 at 6:48

You must log in to answer this question.

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