Skip to content

Instantly share code, notes, and snippets.

@boki
Created April 18, 2011 11:35
Show Gist options
  • Save boki/925164 to your computer and use it in GitHub Desktop.
Save boki/925164 to your computer and use it in GitHub Desktop.
A simple SpriteBatch for XNA in Silverlight 5
namespace Botomata.Xna
{
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
/// <summary>
/// The glyph batch class provides methods and properties to render batches
/// of glyphs.
/// </summary>
public class SpriteBatch : IDisposable
{
/// <summary>
/// Describes a custom vertex format structure that contains position,
/// color and one set of texture coordinates.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
struct SpriteVertex
{
/// <summary>
/// An array of three vertex elements describing the position,
/// texture coordinate and color of this vertex.
/// </summary>
public static readonly VertexElement[] VertexElements = new VertexElement[]
{
new VertexElement(0, VertexElementFormat.Vector4, VertexElementUsage.Position, 0),
new VertexElement(16, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0),
new VertexElement(24, VertexElementFormat.Color, VertexElementUsage.Color, 0)
};
/// <summary>
/// The vertex position.
/// </summary>
public Vector4 Position;
/// <summary>
/// The vertex texture coordinates.
/// </summary>
public Vector2 TextureCoordinate;
/// <summary>
/// The vertex color.
/// </summary>
public Color Color;
/// <summary>
/// Initializes a new instance of the GlyphVertex structure.
/// </summary>
/// <param name="position">Position of the vertex.</param>
/// <param name="textureCoordinate">Texture coordinate of the vertex.</param>
/// <param name="color">Color of the vertex.</param>
public SpriteVertex(Vector4 position, Vector2 textureCoordinate, Color color)
{
Position = position;
TextureCoordinate = textureCoordinate;
Color = color;
}
/// <summary>
/// Gets the size of the GlyphVertex structure.
/// </summary>
public static int SizeInBytes
{
get { return 7 * 4; }
}
/// <summary>
/// Compares two objects to determine whether they are the same.
/// </summary>
/// <param name="vertex1">Object to the left of the equality operator.</param>
/// <param name="vertex2">Object to the right of the equality operator.</param>
/// <returns>true if the objects are the same; false otherwise.</returns>
public static bool operator ==(SpriteVertex vertex1, SpriteVertex vertex2)
{
return vertex1.Position == vertex2.Position &&
vertex1.TextureCoordinate == vertex2.TextureCoordinate &&
vertex1.Color == vertex2.Color;
}
/// <summary>
/// Compares two objects to determine whether they are different.
/// </summary>
/// <param name="vertex1">Object to the left of the inequality operator.</param>
/// <param name="vertex2">Object to the right of the inequality operator.</param>
/// <returns>true if the objects are different; false otherwise.</returns>
public static bool operator !=(SpriteVertex vertex1, SpriteVertex vertex2)
{
return vertex1.Position != vertex2.Position ||
vertex1.TextureCoordinate != vertex2.TextureCoordinate ||
vertex1.Color != vertex2.Color;
}
/// <summary>
/// Returns a value that indicates whether the current instance is
/// equal to a specified object.
/// </summary>
/// <param name="obj">The Object to compare with the current GlyphVertex.</param>
/// <returns>true if the objects are the same; false otherwise.</returns>
public override bool Equals(Object obj)
{
return obj != null && obj.GetType() == typeof(SpriteVertex) && this == (SpriteVertex)obj;
}
/// <summary>
/// Gets the hash code for this instance.
/// </summary>
/// <returns>Hash code for this object.</returns>
public override int GetHashCode()
{
return base.GetHashCode();
}
/// <summary>
/// Retrieves a string representation of this object.
/// </summary>
/// <returns>String representation of this object.</returns>
public override String ToString()
{
Object[] args = new object[] { Position, Color, TextureCoordinate };
return String.Format(CultureInfo.CurrentCulture, "{{Position:{0} Color:{1} TextureCoordinate:{3}}}", args);
}
}
/// <summary>
/// The size of the vertex buffer.
/// </summary>
const int VertexBufferSize = 2048;
/// <summary>
/// The size of the index buffer.
/// </summary>
const int IndexBufferSize = 6 * VertexBufferSize;
/// <summary>
/// The graphics device where glyphs will be drawn.
/// </summary>
GraphicsDevice graphicsDevice;
/// <summary>
/// The size of the drawing surface.
/// </summary>
Vector4 viewportSize;
/// <summary>
/// A value indicating whether Begin was called.
/// </summary>
bool inBeginEnd;
/// <summary>
/// The currently queued glyph vertices.
/// </summary>
SpriteVertex[] spriteQueue;
/// <summary>
/// The number of vertices in the glyphQueue.
/// </summary>
int spriteQueueCount;
/// <summary>
/// The fonts glyph texture used to render the glyphs.
/// </summary>
Texture2D spriteTexture;
/// <summary>
/// The vertex buffer used to render the glyphs.
/// </summary>
DynamicVertexBuffer vertexBuffer;
/// <summary>
/// The vertex declaration used to render the glyphs.
/// </summary>
VertexDeclaration vertexDeclaration;
/// <summary>
/// The index buffer used to render the glyphs.
/// </summary>
DynamicIndexBuffer indexBuffer;
/// <summary>
/// The vertex shader used to render the glyphs.
/// </summary>
VertexShader vertexShader;
/// <summary>
/// The pixel shader used to render the glyphs.
/// </summary>
PixelShader pixelShader;
/// <summary>
/// The index into the vertex buffer at which to render from next.
/// </summary>
int vertexBufferPosition;
/// <summary>
/// A value that indicates whether the object is disposed.
/// </summary>
bool isDisposed;
/// <summary>
/// Initializes a new instance of the GlyphBatch class.
/// </summary>
/// <param name="graphicsDevice">The graphics device where glyphs will be drawn.</param>
/// <param name="width">The width of the drawing surface.</param>
/// <param name="height">The height of the drawing surface.</param>
public SpriteBatch(GraphicsDevice graphicsDevice, int width, int height)
{
if (graphicsDevice == null)
{
throw new ArgumentNullException("graphicsDevice", "Graphics device cannot be null");
}
this.graphicsDevice = graphicsDevice;
this.viewportSize = new Vector4(width, height, 0, 0);
this.spriteQueue = new SpriteVertex[VertexBufferSize];
ConstructPlatformData();
}
/// <summary>
/// Occurs when Dispose is called or when this object is finalized and
/// collected by the garbage collector of the Microsoft .NET common
/// language runtime.
/// </summary>
public event EventHandler Disposing;
/// <summary>
/// Gets a value indicating whether the object is disposed.
/// </summary>
public bool IsDisposed
{
get { return isDisposed; }
}
/// <summary>
/// Prepares the graphics device for drawing sprites.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Begin has been called before calling End after the last call to
/// Begin. Begin cannot be called again until End has been successfully
/// called.
/// </exception>
public void Begin(GraphicsDevice graphicsDevice, Texture2D texture)
{
if (inBeginEnd == true)
{
throw new InvalidOperationException("End must be called before Begin");
}
inBeginEnd = true;
this.graphicsDevice = graphicsDevice;
spriteTexture = texture;
}
/// <summary>
/// Flushes the sprite batch.
/// </summary>
/// <exception cref="InvalidOperationException">
/// End was called, but Begin has not yet been called. You must call
/// Begin successfully before you can call End.
/// </exception>
public void End()
{
if (inBeginEnd == false)
{
throw new InvalidOperationException("Begin must be called before End");
}
if (spriteQueueCount > 0)
{
RenderBatch();
spriteTexture = null;
}
inBeginEnd = false;
}
/// <summary>
/// Adds a glyph to the batch of glyphs to be rendered, specifying the
/// screen position, glyph source rectangle and color.
/// </summary>
/// <param name="position">The location, in screen coordinates, where the sprite will be drawn.</param>
/// <param name="source">The glyph source rectangle in the fonts texture.</param>
/// <param name="color">The color to render the glyph.</param>
/// <exception cref="InvalidOperationException">
/// Draw was called, but Begin has not yet been called. You
/// must call Begin successfully before you can call Draw.
/// </exception>
public void Draw(Vector2 position, Rectangle source, Color color)
{
if (inBeginEnd == false)
{
throw new InvalidOperationException("Begin must be called before Draw");
}
Vector2 pos = position;
AppendGlyphVertex(pos, new Vector2(source.Left, source.Top), color);
pos.X += source.Width;
AppendGlyphVertex(pos, new Vector2(source.Right, source.Top), color);
pos.Y += source.Height;
AppendGlyphVertex(pos, new Vector2(source.Right, source.Bottom), color);
pos.X -= source.Width;
AppendGlyphVertex(pos, new Vector2(source.Left, source.Bottom), color);
}
public void Draw(Rectangle position, Rectangle source, Color color)
{
if (inBeginEnd == false)
{
throw new InvalidOperationException("Begin must be called before Draw");
}
Vector2 pos = new Vector2(position.X, position.Y);
AppendGlyphVertex(pos, new Vector2(source.Left, source.Top), color);
pos.X += position.Width;
AppendGlyphVertex(pos, new Vector2(source.Right, source.Top), color);
pos.Y += position.Height;
AppendGlyphVertex(pos, new Vector2(source.Right, source.Bottom), color);
pos.X -= position.Width;
AppendGlyphVertex(pos, new Vector2(source.Left, source.Bottom), color);
}
/// <summary>
/// Performs application-defined tasks associated with freeing,
/// releasing, or resetting unmanaged resources.
/// </summary>
/// <filterpriority>2</filterpriority>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Performs application-defined tasks associated with freeing,
/// releasing, or resetting unmanaged resources.
/// </summary>
/// <param name="disposing">A value indicating whether this method was called from Dispose.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing == true && isDisposed == false)
{
if (Disposing != null)
{
Disposing(this, EventArgs.Empty);
}
vertexDeclaration = null;
vertexShader = null;
pixelShader = null;
vertexBuffer = null;
indexBuffer = null;
isDisposed = true;
}
}
/// <summary>
/// Creates the triangle list indices for the internal buffers.
/// </summary>
/// <returns>The generated indices.</returns>
static short[] CreateIndexData()
{
short[] indeces = new short[IndexBufferSize];
for (int i = 0; i < VertexBufferSize; i++)
{
indeces[(i * 6) + 0] = (short)((i * 4) + 0);
indeces[(i * 6) + 1] = (short)((i * 4) + 1);
indeces[(i * 6) + 2] = (short)((i * 4) + 2);
indeces[(i * 6) + 3] = (short)((i * 4) + 0);
indeces[(i * 6) + 4] = (short)((i * 4) + 2);
indeces[(i * 6) + 5] = (short)((i * 4) + 3);
}
return indeces;
}
/// <summary>
/// Renders the glyphs queued in this batch.
/// </summary>
void RenderBatch()
{
graphicsDevice.SetVertexShader(vertexShader);
graphicsDevice.SetPixelShader(pixelShader);
graphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
graphicsDevice.DepthStencilState = DepthStencilState.None;
graphicsDevice.BlendState = BlendState.AlphaBlend;
graphicsDevice.Textures[0] = spriteTexture;
graphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
graphicsDevice.SetVertexBuffer(vertexBuffer);
graphicsDevice.Indices = indexBuffer;
graphicsDevice.SetVertexShaderConstantFloat4(0, ref viewportSize);
var textureSize = new Vector4()
{
X = spriteTexture.Width,
Y = spriteTexture.Height
};
graphicsDevice.SetVertexShaderConstantFloat4(1, ref textureSize);
int count = spriteQueueCount;
int offset = 0;
while (count > 0)
{
SetDataOptions noOverwrite = SetDataOptions.NoOverwrite;
int drawCount = count;
if (drawCount > (VertexBufferSize - vertexBufferPosition))
{
drawCount = VertexBufferSize - vertexBufferPosition;
if (drawCount < VertexBufferSize / 8)
{
vertexBufferPosition = 0;
noOverwrite = SetDataOptions.Discard;
drawCount = count;
if (drawCount > VertexBufferSize)
{
drawCount = VertexBufferSize;
}
}
}
int vertexStride = SpriteVertex.SizeInBytes;
int offsetInBytes = (vertexBufferPosition * vertexStride);
vertexBuffer.SetData<SpriteVertex>(offsetInBytes, spriteQueue, offset, drawCount, vertexStride, noOverwrite);
// 2 triangles = 4 vertices, 6 indeces
int minVertexIndex = vertexBufferPosition;
int numVertices = drawCount;
int startIndex = vertexBufferPosition / 2 * 3;
int primitiveCount = drawCount / 2;
graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, minVertexIndex, numVertices, startIndex, primitiveCount);
vertexBufferPosition += drawCount;
offset += drawCount;
count -= drawCount;
}
spriteQueueCount = 0;
}
/// <summary>
///
/// </summary>
void ConstructPlatformData()
{
var streamResourceInfo = ContentManager.GetStreamResourceInfo("Content/Shaders/SpriteBatch.vs");
vertexShader = VertexShader.FromStream(graphicsDevice, streamResourceInfo.Stream);
streamResourceInfo = ContentManager.GetStreamResourceInfo("Content/Shaders/SpriteBatch.ps");
pixelShader = PixelShader.FromStream(graphicsDevice, streamResourceInfo.Stream);
vertexDeclaration = new VertexDeclaration(SpriteVertex.VertexElements);
AllocateBuffers();
}
/// <summary>
/// Allocates the internal vertex and index buffers.
/// </summary>
void AllocateBuffers()
{
if (vertexBuffer == null)
{
vertexBuffer = new DynamicVertexBuffer(graphicsDevice, vertexDeclaration, VertexBufferSize, BufferUsage.WriteOnly);
vertexBufferPosition = 0;
}
if (indexBuffer == null)
{
indexBuffer = new DynamicIndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, IndexBufferSize, BufferUsage.WriteOnly);
indexBuffer.SetData<short>(0, CreateIndexData(), 0, IndexBufferSize);
}
}
/// <summary>
/// Appends a glyph vertex onto the current batch.
/// </summary>
/// <param name="position">The location, in screen coordinates, of the vertex to append.</param>
/// <param name="textureCoordinate">The texture coordinates of the vertex to append.</param>
/// <param name="color">The color of the vertex to append.</param>
void AppendGlyphVertex(Vector2 position, Vector2 textureCoordinate, Color color)
{
if (spriteQueueCount >= spriteQueue.Length)
{
Array.Resize<SpriteVertex>(ref spriteQueue, spriteQueue.Length * 2);
}
spriteQueue[spriteQueueCount].Position.X = position.X;
spriteQueue[spriteQueueCount].Position.Y = position.Y;
spriteQueue[spriteQueueCount].Position.Z = 0;
spriteQueue[spriteQueueCount].Position.W = 1;
spriteQueue[spriteQueueCount].TextureCoordinate = textureCoordinate;
spriteQueue[spriteQueueCount].Color = color;
spriteQueueCount++;
}
}
}
// Copyright (C) 2011, Bjoern Graf <bjoern.graf@gmx.net>
// All rights reserved.
//
// This software is licensed as described in the file license.txt, which
// you should have received as part of this distribution. The terms
// are also available at http://www.codeplex.com/Bnoerj/Project/License.aspx.
float4 ViewportSize;
float4 TextureSize;
texture SpriteTexture : register(t0);
sampler spriteSampler : register(s0);
struct VS_INPUT
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
float4 Color : COLOR0;
};
struct VS_OUTPUT
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
float4 Color : COLOR0;
};
VS_OUTPUT SpriteVS(VS_INPUT In)
{
VS_OUTPUT Out;
Out.Position.xy = In.Position.xy - 0.5;
Out.Position.xy = Out.Position.xy / ViewportSize.xy;
Out.Position.xy = Out.Position.xy * float2(2, -2) + float2(-1, 1);
Out.Position.zw = In.Position.zw;
Out.TexCoord.xy = In.TexCoord.xy / TextureSize.xy;
Out.Color = In.Color;
return Out;
}
float4 SpritePS(VS_OUTPUT In) : COLOR0
{
float4 color = tex2D(spriteSampler, In.TexCoord);
color = color * In.Color;
return color;
}
technique SpriteBatch
{
pass SinglePass
{
VertexShader = compile vs_2_0 SpriteVS();
PixelShader = compile ps_2_0 SpritePS();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment