This week in the XNA 4.0 workshop we're covering 2D graphics using the SpriteBatch class. For those that used XNA 3.1 or earlier, and especially those that have never used DirectX, the new arguments to SpriteBatch.Begin can be a bit off-putting.
When using the SpriteBatch class there are really just four functions that'll you'll use most of the time. These are
- Draw()
- DrawString()
- Begin()
- End()
The first two are fairly obvious. Any time you want to draw a sprite or a string you call the corresponding function. The latter two need some explanation.
XNA is built on DirectX which is a hardware accelerated API for manipulating 3D primitives. This means that everything we do with XNA is really in 3D using triangles. Drawing sprites is no exception. Every time you call Draw or DrawString it's really just queuing a texture to be drawn on a 3D quad rendered in pre-transformed screen space.
Because XNA/DirectX is really 3D, the API has to do some initial setup with the Graphics Pipeline to make sure things are drawn with the appearance of 2D. In XNA 3.1 and prior this was done with shader code that was called from SpriteBatch during the drawing process. When that happened depended on which sort mode you were using. But ultimately your textures, combined with specific device states, would be drawn to the screen in a series of pre-transformed quads.
In XNA 3.1 you had a little bit of control over this process, but not much. When you called Begin you could pass it a number of arguments depending on which overload you chose. The most complicated overload looked like this:
Begin(SpriteBlendMode, SpriteSortMode, SaveStateMode, Matrix)
In the above overload, SpriteBlendMode, SpriteSortMode, and SaveStateMode were all just simple enumerations.
SpriteBlendMode was used to determine how sprites you drew were combined with data already in the target buffer. The options were either SpriteBlendMode.Additive, SpriteBlendMode.AlphaBlend, or SpriteBlendMode.None.
SaveStateMode was used to determine whether or not device states would be reset after calling Begin/End on the SpriteBatch class. The options were SaveStateMode.None and SaveStateMode.SaveState. The default was None, and would often cause confusion for people when switching back and forth between drawing with SpriteBatch and without in the same draw pass. Suddenly, people's 3D models would draw without respect for the depth buffer and all of their alpha blending states would be reset.
SpriteSortMode was used to determine in what order your sprites were drawn. The possible values were: BackToFront, Deferred, FrontToBack, Immediate, Texture.
In deferred mode all of your draw calls were deferred, and nothing was actually flushed to the hardware until End was called. Once End was called, the device had all of its render states set and all of your sprites were drawn to the target surface in the same order as they were "drawn". Of course, if you drew your sprites in something other than a painter's algorithm, or if you didn't intelligently split up your sprites by texture, it could result in undesirable layering or poorer performance, especially if you were rendering sprite-based particles.
The BackToFront and FrontToBack sort modes were designed to handle the painter's algorithm. That is, by passing a z-value in with your Draw calls you had more control over the order in which sprites were drawn. In these modes, it first sorted all your sprites by the z-value, ascending or descending, and then drew them. So it didn't matter the order in which you called Draw.
The Texture mode is/was for performance. If you drew from several different texture sheets within the same Begin/End pair, each switch from one texture to another would cause a flush and the device had to make a unique DrawPrimitive DirectX call. This is bad m'kay. By using the Texture mode it first sorted all of your sprites by texture, and then called them with as few DrawPrimitive calls as possible. Note, if all your sprites are/were from the same (or relatively few) texture sheet(s), then Texture sorting is useless, and you're better off using BackToFront or just Deferred.
The Immediate mode is interesting. With Immediate sort mode the device state was initialized as soon as you called Begin, and then each time you called Draw the quad was immediately pushed to the target surface. This is bad performance, but gave you control over your sprite rendering in a way none of the previous sort modes did. This is because you could call Begin, then manually make changes to the GraphicsDevice, such as changing blending values, filtering modes, depth buffer flags, etc... and THEN draw your quads. The problem was, now all of those quads were being drawn individually.
Enter XNA 4.0. With XNA 4.0, Microsoft acknowledged a need to give greater control over sprite drawing, while still maintaining performance. So the SpriteBatch.Begin method has a different set of arguments. The corresponding overload now looks like this:
Begin(SpriteSortMode, BlendState, SamplerState, DepthStencilState,
RasterizerState, Effect, Matrix)
Notice that gone are most of the enumerations. While there is still an enumeration for SpriteSortMode, BlendState, SamplerState, DepthStencilState, RasterizerState, and even Effect are all fully realized objects. This allows you to set properties on the device at the time you call Begin, and then have those states be set for the duration of the Begin/End pair. So you can set certain properties, set your Sort mode to Defferred, and gain the benefit of both custom settings and batched rendering.
If you're like most people, when you see that set of object parameters your first instinct is to go..."uh, how do I set all those." Fortunately for us, Microsoft provided static fields on each of those classes which grants access to instances of the most common scenarios you'll need when rendering with SpriteBatch.
- For BlendState, there are static fields for Additive, AlphaBlend, NonPremultiplied, and Opaque Blend States.
- For SamplerState there are several static fields, but you'll most often use the default, which is LinearClamp.
- For DepthStencilState there are Default, DepthRead, and None. Interestingly enough, Default is pre-configured with the default states you'll want set if you want to use the stencil buffer in your 2D rendering. In truth, None is actually the "default" setting.
- And last, RasterizerState includes CullClockwise, CullCounterClockwise, and CullNone. The default is CullCounterClockwise.
You'll also notice from the above prototype the addition of the Effect parameter. This allows you to pass in your own custom effect, so you could even do your 2D drawing in 3D if you wanted to. It helps to have something to start from though if you're going to provide your own custom effect. That's why Microsoft has released the Source Code to the SpriteEffect, which can be downloaded from the AppHub.
If at the end of the day you just want to draw in 2D and don't need any of the options you can just call the simpler version of the Begin method, or use the above but pass in null for as many arguments as you like. Since those are objects and not enumerations, it's fine to just let the SpriteBatch class determine what the most appropriate paramter is.
So you see, with the transition from XNA 3.1 to XNA 4.0 Microsoft has given users a lot more control over how they do 2D rendering with the SpriteBatch class. By messing with the different state objects you can make your sprite blend with the surface in new and unique ways, have it draw to the stencil buffer, ignore (or not) the depth buffer, or even have sprites wrap within the target rectangle. Because you can also provide your own effect it's now also possible to do things such as having your sprites cast shadows, or react in unique ways to custom lighting, all without sacrificing performance.