SyntaxHighlighter

Thursday, 3 February 2011

2D Rendering with SpriteBatch: My approach

I'll be posting an update (along with hopefully a video of the first prototype) of my progress on Sphero at the weekend, but today I wanted to take a second to share my approach to 2D rendering using SpriteBatch.
SpriteBatch itself if an optimisation feature of XNA, grouping and/or sorting sprites to reduce the number of draw calls or textures swaps that need to take place to render the scene. However, the performance benefit from using a SpriteBatch can vary depending on how its used.

The approach I took was to try and figure out what the optimal way of using sprite batch was, and then try and work the rest of my engine around that. The following is the approach I came up with.

***HEALTH WARNING***

This method has not been tested on an Xbox, so I can give no guarentees about it's performance.

At the centre of the set-up are a class, a struct, and 2 dictionarys.

The key to it all is the RenderManager class. However, I won't say too much about this class just yet.

I'm going to try and go through these in a logical order. First of all, we have a dictionary which contains all of the textures we use in the game, with their names as keys (you could use unique ID number if you prefer, I

just find strings easier to work with and remember). All textures are loaded through the RenderManager class, which loads the texture from the content pipeline, and stores it in the Texture dictionary with its name as its key.

The next piece of the puzzle is the RenderObject2D struct. A better name might have been the 'sprite' class, as this is essentially what it is. It holds all of the information needed for the RenderManager to draw the sprite that it describes. This includes a string representing the texture of the sprite.

The penultimate item that makes up this system is the Layer Dictionary.

This is not quite as it sounds. This is not a Dictionary, but rather a Dictionary. The int refers to the order of the layers, the string is used by the game code to tell the RenderManager which layer to add the new sprite to.

Finally we get to the RenderManager.

The RenderManager does 3 main jobs. It loads and stores textures (as we've already seen). It keeps track of all of the sprites to be drawn this frame, and it does the actual drawing.

Essentially the way all of this fits together is this:

Each frame, the game code calls RenderManager.AddToScene() in place of its normal draw calls. This (heavily overloaded) method takes all of the parameters you would need in a SpriteBatch.Draw() call, with two big differences. The first is that instead of a reference to a texture, it passes the name of the texture, and the second is that instead of a depth parameter, it passes the name of a layer.

Inside the RenderManager, every time AddToScene() is called, a RenderObject2D struct is created and filled out with all of the parameters barring the layer name.

Next, the RenderObject2D Struct is stored inside what is effectivey a 3 dimensional array (List>> scene). This is indexed by layer and texture.

In plain english, the scene is a List of layers. Each layer is a list of sub-layers grouped by texture. Each sub-layer is a list of RenderObject2Ds.

After all other components have had 'draw' called on them, RenderManager's draw() method is called. The RenderManager then starts a spriteBatch in 'immediate' mode, and then loops through the scene, starting with the backmost layer, looping through each Texture sub-layer to ensure the minimum number of texture swaps.

Essentially this means the sprites never need sorting, because they were filed away in the right place when they were added to the RenderManager's scene.

There are some optimisations around the Lists to ensure that they dont alloate memory each frame from resizing, and it could be made more efficient by drastically reducing the size of a RenderObject2D by using a numeric identifier for textures, but that should give you a flavour of how I've approached dealing with SpriteBatch.

Now the question is, have I got this completely wrong? Is there a glaringly obvious reason not to go down this route? What are the pros and cons?

I'd love to hear your thoughts!

3 comments:

  1. This sounds very similar to how I'm doing things, with a few little differences. My RenderObject2D (which I call SpriteInfo) has a Sort field on it, of type uint. When I call my version of AddToScene, I bit pack integer values representing the effect and texture (which are just handles to the real resources), and the layer into specific bit ranges of the Sort field. Then I just store the SpriteInfo at the next free location in an array of SpriteInfos. When I've finished building up a scene, I sort this array using an awesome tail-recursive quicksort that I borrowed from the quake 3 source and ported, and then just loop through, calling spriteBatch.Draw() in deferred mode, which I believe offers the least overhead if you're already sorted. The benefits of doing things this way is that I'm not taking a performance hit using Dictionary and List to store the SpriteInfos, and the quicksort is fast enough to sort tens of thousands of sprites per frame and still leave enough milliseconds for other fun stuff, like actually drawing them. Could be overkill, but it's working pretty well so far:) And this *has* been tested on the xbox.

    ReplyDelete
  2. Thanks for your feedback. You're probably right that there must be sorting algorithms (such as the one you've mentioned) that are faster than List/ Dictionary lookups, something to look into if I ever run into performance problems.

    I do want to say thanks though for mentioning deferred sort mode. When I out together this Rendering system I was using xna 3.1, when Immediate mode did texture batching if the sprite used the same texture as the previous sprite. Based on Shawn's blog this is no longer the case and as you say deferred is now faster!

    ReplyDelete
  3. Hi!!! great article! is the a download link to the code?
    THANKS!

    ReplyDelete