Microsoft Developer Network >
Forums Home
>
Archived Forums Forums
>
XNA Framework
>
Write your own SpriteBatch 101 (long:code included)
Write your own SpriteBatch 101 (long:code included)
- People keep asking "why doesn't SpriteBatch do X?". I keep saying "write your own". Well I decided to just write one for you, so people can see how it's done.
The SpriteBatch class I wrote performs about 50% faster than the XNA SpriteBatch when all Draw calls are using the same texture. (Best case scenario, but you SHOULD put all your sprites in one or few textures for performance anyways.)
On my card, I get about 188 fps with 1000 sprites with MySpriteBatch, and about 124 fps with 1000 Sprites using xna SpriteBatch.
MySpriteBatch also supports, or could easily support:
Using your own effect file. (2 parameters, viewProjection, and diffuseTexture)
Using 3d vertices if you want. (just add the Draw method for it)
World, View, Projection matrix state.
Here is the MySpriteBatch code:
//////////////////////////////////////////////////////////////////////////////////////////////////
// BEGIN MySpriteBatch.cs
//////////////////////////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
namespace SpriteBatchTest
{
public class MySpriteBatch
{
VertexPositionColorTexture[] vertices;
short[] indices;
int vertexCount = 0;
int indexCount = 0;
Texture2D texture;
VertexDeclaration declaration;
GraphicsDevice device;
// these should really be properties
public Matrix World;
public Matrix View;
public Matrix Projection;
public Effect Effect;
public MySpriteBatch(GraphicsDevice device)
{
this.device = device;
this.vertices = new VertexPositionColorTexture[256];
this.indices = new short[vertices.Length * 3 / 2];
}
public void ResetMatrices(int width, int height)
{
this.World = Matrix.Identity;
this.View = new Matrix(
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, -1.0f, 0.0f, 0.0f,
0.0f, 0.0f, -1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f);
this.Projection = Matrix.CreateOrthographicOffCenter(
0, width, -height, 0, 0, 1);
}
public void Draw(Texture2D texture, Rectangle srcRectangle, Rectangle dstRectangle, Color color)
{
// if the texture changes, we flush all queued sprites.
if (this.texture != null && this.texture != texture)
this.Flush();
this.texture = texture;
// ensure space for my vertices and indices.
this.EnsureSpace(6, 4);
// add the new indices
indices[indexCount++] = (short)(vertexCount + 0);
indices[indexCount++] = (short)(vertexCount + 1);
indices[indexCount++] = (short)(vertexCount + 3);
indices[indexCount++] = (short)(vertexCount + 1);
indices[indexCount++] = (short)(vertexCount + 2);
indices[indexCount++] = (short)(vertexCount + 3);
// add the new vertices
vertices[vertexCount++] = new VertexPositionColorTexture(
new Vector3(dstRectangle.Left, dstRectangle.Top, 0)
, color, GetUV(srcRectangle.Left, srcRectangle.Top));
vertices[vertexCount++] = new VertexPositionColorTexture(
new Vector3(dstRectangle.Right, dstRectangle.Top, 0)
, color, GetUV(srcRectangle.Right, srcRectangle.Top));
vertices[vertexCount++] = new VertexPositionColorTexture(
new Vector3(dstRectangle.Right, dstRectangle.Bottom, 0)
, color, GetUV(srcRectangle.Right, srcRectangle.Bottom));
vertices[vertexCount++] = new VertexPositionColorTexture(
new Vector3(dstRectangle.Left, dstRectangle.Bottom, 0)
, color, GetUV(srcRectangle.Left, srcRectangle.Bottom));
// we premultiply all vertices times the world matrix.
// the world matrix changes alot and we don't want to have to flush
// every time it changes.
Matrix world = this.World;
for (int i = vertexCount - 4; i < vertexCount; i++)
Vector3.Transform(ref vertices
.Position, ref world, out vertices
.Position);
}
Vector2 GetUV(float x, float y)
{
return new Vector2(x / (float)texture.Width, y / (float)texture.Height);
}
void EnsureSpace(int indexSpace, int vertexSpace)
{
if (indexCount + indexSpace >= indices.Length)
Array.Resize(ref indices, Math.Max(indexCount + indexSpace, indices.Length * 2));
if (vertexCount + vertexSpace >= vertices.Length)
Array.Resize(ref vertices, Math.Max(vertexCount + vertexSpace, vertices.Length * 2));
}
public void Flush()
{
if (this.vertexCount > 0)
{
if (this.declaration == null || this.declaration.IsDisposed)
this.declaration = new VertexDeclaration(device, VertexPositionColorTexture.VertexElements);
device.VertexDeclaration = this.declaration;
Effect effect = this.Effect;
// set the only parameter this effect takes.
effect.Parameters["viewProjection"].SetValue(this.View * this.Projection);
effect.Parameters["diffuseTexture"].SetValue(this.texture);
EffectTechnique technique = effect.CurrentTechnique;
effect.Begin();
EffectPassCollection passes = technique.Passes;
for (int i = 0; i < passes.Count; i++)
{
EffectPass pass = passes
;
pass.Begin();
device.DrawUserIndexedPrimitives<VertexPositionColorTexture>(
PrimitiveType.TriangleList, this.vertices, 0, this.vertexCount,
this.indices, 0, this.indexCount / 3);
pass.End();
}
effect.End();
this.vertexCount = 0;
this.indexCount = 0;
}
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// END MySpriteBatch.cs
//////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////
// BEGIN Game1.cs
//////////////////////////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
namespace SpriteBatchTest
{
/// <summary>
/// This is the main type for your game
/// </summary>
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
ContentManager content;
SpriteBatch batch;
MySpriteBatch batch2;
Texture2D texture;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
graphics.SynchronizeWithVerticalRetrace = false;
content = new ContentManager(Services);
}
/// <summary>
/// Load your graphics content. If loadAllContent is true, you should
/// load content from both ResourceManagementMode pools. Otherwise, just
/// load ResourceManagementMode.Manual content.
/// </summary>
/// <param name="loadAllContent">Which type of content to load.</param>
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
{
// TODO: Load any ResourceManagementMode.Automatic content
batch = new SpriteBatch(this.graphics.GraphicsDevice);
texture = content.Load<Texture2D>("earth");
batch2 = new MySpriteBatch(this.graphics.GraphicsDevice);
batch2.Effect = content.Load<Effect>("SpriteEffect");
}
// TODO: Load any ResourceManagementMode.Manual content
}
/// <summary>
/// Unload your graphics content. If unloadAllContent is true, you should
/// unload content from both ResourceManagementMode pools. Otherwise, just
/// unload ResourceManagementMode.Manual content. Manual content will get
/// Disposed by the GraphicsDevice during a Reset.
/// </summary>
/// <param name="unloadAllContent">Which type of content to unload.</param>
protected override void UnloadGraphicsContent(bool unloadAllContent)
{
if (unloadAllContent == true)
{
content.Unload();
}
}
double startTime = 0;
int frameCount = 0;
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime)
{
// write out the fps occasionally.
frameCount++;
if ((frameCount % 100) == 0)
{
double time = gameTime.TotalRealTime.TotalSeconds;
double fps = frameCount / (time - startTime);
Console.WriteLine((int)fps);
startTime = time;
frameCount = 0;
}
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
Random random = new Random();
int clientWidth = this.Window.ClientBounds.Width;
int clientHeight = this.Window.ClientBounds.Height;
int width = 32;
int height = 16;
if (true)
{
// XNA SpriteBatch
batch.Begin();
for (int i = 0; i < 1000; i++)
{
int x = random.Next(0, clientWidth - width);
int y = random.Next(0, clientHeight - height);
batch.Draw(texture, new Rectangle(x, y, width, height), Color.White);
}
batch.End();
}
else
{
// Custom SpriteBatch
batch2.ResetMatrices(this.Window.ClientBounds.Width, this.Window.ClientBounds.Height);
for (int i = 0; i < 1000; i++)
{
int x = random.Next(0, clientWidth - width);
int y = random.Next(0, clientHeight - height);
batch2.Draw(texture, new Rectangle(0, 0, texture.Width, texture.Height), new Rectangle(x, y, width, height), Color.White);
}
batch2.Flush();
}
base.Draw(gameTime);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// END Game1.cs
//////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////
// BEGIN SpriteEffect.fx
//////////////////////////////////////////////////////////////////////////////////////////////////
//Input variables
float4x4 viewProjection : ViewProjection;
//------------------------------------
texture diffuseTexture : Diffuse <string ResourceName = "default_color.dds";>;
sampler TextureSampler = sampler_state
{
texture = <diffuseTexture>;
AddressU = CLAMP;
AddressV = CLAMP;
AddressW = CLAMP;
MIPFILTER = POINT;
MINFILTER = POINT;
MAGFILTER = POINT;
};
struct Vertex
{
float4 Position: POSITION;
float4 Color: COLOR;
float2 TexCoord : TEXCOORD0;
};
Vertex VS_Identity(Vertex v)
{
Vertex result;
// 2D vertices are already pre-multiplied times the world matrix.
result.Position = mul(v.Position, viewProjection);
result.Color = v.Color;
result.TexCoord = v.TexCoord;
return result;
}
//-----------------------------------
float4 PS_Textured(Vertex v): COLOR
{
float4 diffuseTexture = tex2D( TextureSampler, v.TexCoord);
return v.Color * diffuseTexture;
}
technique Identity
{
pass p0
{
VertexShader = compile vs_1_1 VS_Identity();
PixelShader = compile ps_1_1 PS_Textured();
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// END SpriteEffect.fx
//////////////////////////////////////////////////////////////////////////////////////////////////
All Replies
- Totally cool - thanks for the effort - Is this going to appear on one of the community sites as well?
- Ya, if you want to copy it and post it.
Forgot to mention. Once you have you own SpriteBatch class, you can extend it to use different colors in all four corners, and anything else you want very easily.
If you're wondering why this one performs better, it's because xna SpriteBatch performs a bunch of transformations into intermediate internal data formats that wastes time. The intermediate forms are related to sprite sorting operations. When using the XNA SpriteBatch with SpriteSortMode.Immediate(the fastest mode), it's behaviour is basically the same as mine (flush on texture change) but it's performance still suffers from the intermediate calculations. Kris,
you should probably fire up a CodePlex project for this, in case other people want to contribute/enhance.
- I am planning to write something similar to this but not exactly. I am thinking of using DrawIndexedPrimitives, so I would have to use a VertexBuffer/IndexBuffer, which means I have to copy from my own internal vertices and indices, through SetData<>() call. Also, I plan to batched it up according to material (shader, texture combo) before I draw it.
So basically, when a game object gets a draw call, it sends a sprite element (contains info about material, localToWorld, etc) to be added to my SpriteBatch class. The SpriteBatch class sorts it according to the material as the element is added. Finally, the SpriteBatch draws all the elements, and only changes the material as it goes to the next batch of sprites.
What do you think? Any tips suggestions? - Don't use a VertexBuffer for dynamic vertices. Use the DrawIndexedUserPrimitives calls. They are faster than loading your own VertexBuffers. Batching up by Effect, then Texture is fine if you have the need. All of my 3d rendering is batched in the same way. For 2d graphics, I just batch by texture because I generally only use one effect for 2d rendering.
- Ah alright. I was under the impression that Draw*UserPrimitives calls were slower. Alright thanks.
