diff --git a/Blarg.GameFramework/Blarg.GameFramework.csproj b/Blarg.GameFramework/Blarg.GameFramework.csproj
index 2d145fc..ed9c3f6 100644
--- a/Blarg.GameFramework/Blarg.GameFramework.csproj
+++ b/Blarg.GameFramework/Blarg.GameFramework.csproj
@@ -131,6 +131,7 @@
+
diff --git a/Blarg.GameFramework/Graphics/BillboardSpriteBatch.cs b/Blarg.GameFramework/Graphics/BillboardSpriteBatch.cs
new file mode 100644
index 0000000..fc88f67
--- /dev/null
+++ b/Blarg.GameFramework/Graphics/BillboardSpriteBatch.cs
@@ -0,0 +1,518 @@
+using System;
+using System.Text;
+
+namespace Blarg.GameFramework.Graphics
+{
+ public enum BillboardSpriteType
+ {
+ Spherical,
+ Cylindrical,
+ ScreenAligned,
+ ScreenAndAxisAligned
+ }
+
+ public class BillboardSpriteBatch
+ {
+ static StringBuilder _buffer = new StringBuilder(8192);
+
+ const int DefaultSpriteCount = 128;
+ const int ResizeSpriteIncrement = 16;
+ const int VerticesPerSprite = 4;
+ const int IndicesPerSprite = 6;
+
+ VertexBuffer _vertices;
+ IndexBuffer _indices;
+ Texture[] _textures;
+ RenderState _defaultRenderState;
+ RenderState _providedRenderState;
+ BlendState _defaultBlendState;
+ BlendState _providedBlendState;
+ Vector3 _cameraPosition;
+ Vector3 _cameraForward;
+ SpriteShader _shader;
+ int _currentSpritePointer;
+ bool _hasBegunRendering;
+ Color _defaultSpriteColor = Color.White;
+
+ // since it's not valid C# to use 'ref' with a static readonly field...
+ Vector3 _zeroVector = Vector3.Zero;
+ Vector3 _yAxis = Vector3.YAxis;
+ Vector3 _up = Vector3.Up;
+
+ public GraphicsDevice GraphicsDevice { get; private set; }
+
+ public BillboardSpriteBatch(GraphicsDevice graphicsDevice)
+ {
+ if (graphicsDevice == null)
+ throw new ArgumentNullException("graphicsDevice");
+
+ GraphicsDevice = graphicsDevice;
+
+ int numSprites = DefaultSpriteCount;
+
+ _currentSpritePointer = 0;
+
+ _vertices = new VertexBuffer(GraphicsDevice, VertexAttributeDeclarations.TextureColorPosition3D, (numSprites * VerticesPerSprite), BufferObjectUsage.Stream);
+ _indices = new IndexBuffer(GraphicsDevice, (numSprites * IndicesPerSprite), BufferObjectUsage.Stream);
+ _textures = new Texture[numSprites];
+
+ FillSpriteIndicesFor(0, numSprites - 1);
+
+ _defaultRenderState = RenderState.Default;
+ _providedRenderState = null;
+
+ _defaultBlendState = BlendState.AlphaBlend;
+ _providedBlendState = null;
+
+ _hasBegunRendering = false;
+ }
+
+ #region Begin/End
+
+ public void Begin(SpriteShader shader = null)
+ {
+ InternalBegin(null, null, shader);
+ }
+
+ public void Begin(RenderState renderState, SpriteShader shader = null)
+ {
+ InternalBegin(renderState, null, shader);
+ }
+
+ public void Begin(BlendState blendState, SpriteShader shader = null)
+ {
+ InternalBegin(null, blendState, shader);
+ }
+
+ public void Begin(RenderState renderState, BlendState blendState, SpriteShader shader = null)
+ {
+ InternalBegin(renderState, blendState, shader);
+ }
+
+ public void End()
+ {
+ if (!_hasBegunRendering)
+ throw new InvalidOperationException();
+
+ // don't do anything if nothing is to be rendered!
+ if (_currentSpritePointer == 0)
+ {
+ _hasBegunRendering = false;
+ return;
+ }
+
+ if (_providedRenderState != null)
+ _providedRenderState.Apply();
+ else
+ _defaultRenderState.Apply();
+
+ if (_providedBlendState != null)
+ _providedBlendState.Apply();
+ else
+ _defaultBlendState.Apply();
+
+ GraphicsDevice.BindShader(_shader);
+ _shader.SetModelViewMatrix(GraphicsDevice.ViewContext.ModelViewMatrix);
+ _shader.SetProjectionMatrix(GraphicsDevice.ViewContext.ProjectionMatrix);
+ RenderQueue();
+ GraphicsDevice.UnbindShader();
+
+ _hasBegunRendering = false;
+ }
+
+ private void InternalBegin(RenderState renderState, BlendState blendState, SpriteShader shader)
+ {
+ if (_hasBegunRendering)
+ throw new InvalidOperationException();
+
+ _cameraPosition = GraphicsDevice.ViewContext.Camera.Position;
+ _cameraForward = GraphicsDevice.ViewContext.Camera.Forward;
+
+ if (shader == null)
+ _shader = GraphicsDevice.Sprite3DShader;
+ else
+ {
+ if (!shader.IsReadyForUse)
+ throw new InvalidOperationException("Shader not usable for rendering.");
+ _shader = shader;
+ }
+
+ if (renderState != null)
+ _providedRenderState = renderState;
+ else
+ _providedRenderState = null;
+
+ if (blendState != null)
+ _providedBlendState = blendState;
+ else
+ _providedBlendState = null;
+
+ _currentSpritePointer = 0;
+ _hasBegunRendering = true;
+ _vertices.MoveToStart();
+ _indices.MoveToStart();
+ }
+
+ private void RenderQueue()
+ {
+ GraphicsDevice.BindVertexBuffer(_vertices);
+ GraphicsDevice.BindIndexBuffer(_indices);
+
+ int firstSpriteIndex = 0;
+ int lastSpriteIndex = 0;
+
+ for (int i = 0; i < _currentSpritePointer; ++i)
+ {
+ if (_textures[lastSpriteIndex] != _textures[i])
+ {
+ // if the next texture is different then the last range's
+ // texture, then we need to render the last range now
+ RenderQueueRange(firstSpriteIndex, lastSpriteIndex);
+
+ // switch to the new range with this new texture
+ firstSpriteIndex = i;
+ }
+
+ lastSpriteIndex = i;
+ }
+
+ // we'll have one last range to render at this point (the loop would have
+ // ended before it was caught by the checks inside the loop)
+ RenderQueueRange(firstSpriteIndex, lastSpriteIndex);
+
+ // clear out the texture's array so it's not holding references
+ // to stuff that might need to be collected by the GC
+ // (e.g. if we render a lot of sprites one frame, and then the next
+ // bunch of frames we don't render as much, the end of the array
+ // will still hold old references to Texture objects used previously)
+ for (int i = 0; i < _textures.Length; ++i)
+ _textures[i] = null;
+
+ GraphicsDevice.UnbindIndexBuffer();
+ GraphicsDevice.UnbindVertexBuffer();
+ }
+
+ private void RenderQueueRange(int firstSpriteIndex, int lastSpriteIndex)
+ {
+ int startVertexIndex = firstSpriteIndex * IndicesPerSprite;
+ int lastVertexIndex = (lastSpriteIndex + 1) * IndicesPerSprite; // render up to and including the last sprite
+
+ // take the texture from anywhere in this range (doesn't matter where, it should all be the same texture)
+ Texture spriteTexture = _textures[firstSpriteIndex];
+ bool hasAlphaOnly = spriteTexture.Format == TextureFormat.Alpha ? true : false;
+
+ GraphicsDevice.BindTexture(spriteTexture);
+ _shader.SetTextureHasAlphaOnly(hasAlphaOnly);
+
+ GraphicsDevice.RenderTriangles(startVertexIndex, (lastVertexIndex - startVertexIndex) / 3);
+ }
+
+ #endregion
+
+ #region Render: Sprites
+
+ public void Render(Texture texture, float x, float y, float z, float width, float height, BillboardSpriteType type)
+ {
+ Render(texture, x, y, z, width, height, type, ref _defaultSpriteColor);
+ }
+
+ public void Render(Texture texture, float x, float y, float z, float width, float height, BillboardSpriteType type, ref Color color)
+ {
+ var position = new Vector3(x, y, z);
+ AddSprite(type, texture, ref position, width, height, 0, 0, texture.Width, texture.Height, ref color);
+ }
+
+ public void Render(Texture texture, ref Vector3 position, float width, float height, BillboardSpriteType type)
+ {
+ Render(texture, ref position, width, height, type, ref _defaultSpriteColor);
+ }
+
+ public void Render(Texture texture, ref Vector3 position, float width, float height, BillboardSpriteType type, ref Color color)
+ {
+ AddSprite(type, texture, ref position, width, height, 0, 0, texture.Width, texture.Height, ref color);
+ }
+
+ public void Render(TextureAtlas atlas, int index, float x, float y, float z, float width, float height, BillboardSpriteType type)
+ {
+ Render(atlas, index, x, y, z, width, height, type, ref _defaultSpriteColor);
+ }
+
+ public void Render(TextureAtlas atlas, int index, float x, float y, float z, float width, float height, BillboardSpriteType type, ref Color color)
+ {
+ RectF texCoords;
+ atlas.GetTileTexCoords(index, out texCoords);
+
+ var position = new Vector3(x, y, z);
+ AddSprite(type, atlas.Texture, ref position, width, height, texCoords.Left, texCoords.Top, texCoords.Right, texCoords.Bottom, ref color);
+ }
+
+ public void Render(TextureAtlas atlas, int index, ref Vector3 position, float width, float height, BillboardSpriteType type)
+ {
+ Render(atlas, index, ref position, width, height, type, ref _defaultSpriteColor);
+ }
+
+ public void Render(TextureAtlas atlas, int index, ref Vector3 position, float width, float height, BillboardSpriteType type, ref Color color)
+ {
+ RectF texCoords;
+ atlas.GetTileTexCoords(index, out texCoords);
+
+ AddSprite(type, atlas.Texture, ref position, width, height, texCoords.Left, texCoords.Top, texCoords.Right, texCoords.Bottom, ref color);
+ }
+
+ #endregion
+
+ #region Render: Fonts
+
+ public void Render(SpriteFont font, float x, float y, float z, BillboardSpriteType type, ref Color color, float pixelScale, string text)
+ {
+ var position = new Vector3(x, y, z);
+ Render(font, ref position, type, ref color, pixelScale, text);
+ }
+
+ public void Render(SpriteFont font, ref Vector3 position, BillboardSpriteType type, ref Color color, float pixelScale, string text)
+ {
+ int textWidth;
+ int textHeight;
+ font.MeasureString(out textWidth, out textHeight, text);
+
+ // the x,y,z coordinate specified is used as the position to center the
+ // text billboard around. we start drawing the text at the top-left of this
+ float startX = -(float)((textWidth / 2) * pixelScale);
+ float startY = -(float)((textHeight / 2) * pixelScale);
+
+ float drawX = startX;
+ float drawY = startY;
+ float lineHeight = (float)(font.LetterHeight * pixelScale);
+
+ Matrix4x4 transform;
+ GetTransformFor(type, ref position, out transform);
+
+ RectF texCoords;
+ Rect dimensions;
+ var drawCoordinates = new Vector3();
+
+ for (int i = 0; i < text.Length; ++i)
+ {
+ char c = text[i];
+ if (c == '\n')
+ {
+ // new line
+ drawX = startX;
+ drawY += lineHeight;
+ }
+ else
+ {
+ font.GetCharTexCoords(c, out texCoords);
+ font.GetCharDimensions(c, out dimensions);
+
+ float glyphWidth = (float)(dimensions.Width * pixelScale);
+ float glyphHeight = (float)(dimensions.Height * pixelScale);
+
+ drawCoordinates.X = -drawX;
+ drawCoordinates.Y = -drawY;
+
+ AddSprite(
+ type,
+ ref transform,
+ font.Texture,
+ ref drawCoordinates,
+ glyphWidth, glyphHeight,
+ texCoords.Left, texCoords.Top, texCoords.Right, texCoords.Bottom,
+ ref color
+ );
+
+ drawX += glyphWidth;
+ }
+ }
+ }
+
+ public void Printf(SpriteFont font, float x, float y, float z, BillboardSpriteType type, ref Color color, float pixelScale, string format, params object[] args)
+ {
+ _buffer.Clear();
+ _buffer.AppendFormat(format, args);
+
+ var position = new Vector3(x, y, z);
+ Render(font, ref position, type, ref color, pixelScale, _buffer.ToString());
+ }
+
+ public void Printf(SpriteFont font, ref Vector3 position, BillboardSpriteType type, ref Color color, float pixelScale, string format, params object[] args)
+ {
+ _buffer.Clear();
+ _buffer.AppendFormat(format, args);
+
+ Render(font, ref position, type, ref color, pixelScale, _buffer.ToString());
+ }
+
+ #endregion
+
+ #region Internal Sprite Addition / Management
+
+ private void AddSprite(BillboardSpriteType type, Texture texture, ref Vector3 position, float width, float height, int sourceLeft, int sourceTop, int sourceRight, int sourceBottom, ref Color color)
+ {
+ if (!_hasBegunRendering)
+ throw new InvalidOperationException();
+
+ Matrix4x4 transform;
+ GetTransformFor(type, ref position, out transform);
+
+ // zero vector used as offset since the transform will translate the billboard
+ // to the specified position
+ AddSprite(type, ref transform, texture, ref _zeroVector, width, height, sourceLeft, sourceTop, sourceRight, sourceBottom, ref color);
+ }
+
+ private void AddSprite(BillboardSpriteType type, Texture texture, ref Vector3 position, float width, float height, float texCoordLeft, float texCoordTop, float texCoordRight, float texCoordBottom, ref Color color)
+ {
+ if (!_hasBegunRendering)
+ throw new InvalidOperationException();
+
+ Matrix4x4 transform;
+ GetTransformFor(type, ref position, out transform);
+
+ // zero vector used as offset since the transform will translate the billboard
+ // to the specified position
+ AddSprite(type, ref transform, texture, ref _zeroVector, width, height, texCoordLeft, texCoordTop, texCoordRight, texCoordBottom, ref color);
+ }
+
+ private void AddSprite(BillboardSpriteType type, ref Matrix4x4 transform, Texture texture, ref Vector3 offset, float width, float height, int sourceLeft, int sourceTop, int sourceRight, int sourceBottom, ref Color color)
+ {
+ if (!_hasBegunRendering)
+ throw new InvalidOperationException();
+
+ int sourceWidth = sourceRight - sourceLeft;
+ if (sourceWidth < 1)
+ throw new InvalidOperationException("Zero-length width");
+
+ int sourceHeight = sourceBottom - sourceTop;
+ if (sourceHeight < 1)
+ throw new InvalidOperationException("Zero-length height.");
+
+ float texLeft = sourceLeft / (float)sourceWidth;
+ float texTop = sourceTop / (float)sourceHeight;
+ float texRight = sourceRight / (float)sourceWidth;
+ float texBottom = sourceBottom / (float)sourceHeight;
+
+ if (GetRemainingSpriteSpaces() < 1)
+ AddMoreSpriteSpace(ResizeSpriteIncrement);
+
+ SetSpriteInfo(_currentSpritePointer, type, ref transform, texture, ref offset, width, height, texLeft, texTop, texRight, texBottom, ref color);
+ ++_currentSpritePointer;
+ }
+
+ private void AddSprite(BillboardSpriteType type, ref Matrix4x4 transform, Texture texture, ref Vector3 offset, float width, float height, float texCoordLeft, float texCoordTop, float texCoordRight, float texCoordBottom, ref Color color)
+ {
+ if (!_hasBegunRendering)
+ throw new InvalidOperationException();
+ }
+
+ private void SetSpriteInfo(int spriteIndex, BillboardSpriteType type, ref Matrix4x4 transform, Texture texture, ref Vector3 offset, float width, float height, float texCoordLeft, float texCoordTop, float texCoordRight, float texCoordBottom, ref Color color)
+ {
+ int baseVertexIndex = spriteIndex * VerticesPerSprite;
+
+ float halfWidth = width / 2.0f;
+ float halfHeight = height / 2.0f;
+
+ // TODO: come back to this and re-figure out why I needed to reverse this like so...
+ float left = halfWidth;
+ float top = -halfHeight;
+ float right = -halfWidth;
+ float bottom = halfHeight;
+
+ // TODO: I'm unsure if all of this is better, or if putting the transformation matrix
+ // in the VBO as an extra vertex attribute to do the transform in the
+ // shader would be better
+ // transforming 4 vertices on the CPU vs copying 4 matrices into a VBO...
+
+ Vector3 v1 = new Vector3(left + offset.X, top + offset.Y, 0.0f + offset.Z);
+ Vector3 v2 = new Vector3(right + offset.X, top + offset.Y, 0.0f + offset.Z);
+ Vector3 v3 = new Vector3(right + offset.X, bottom + offset.Y, 0.0f + offset.Z);
+ Vector3 v4 = new Vector3(left + offset.X, bottom + offset.Y, 0.0f + offset.Z);
+
+ Matrix4x4.Transform(ref transform, ref v1, out v1);
+ Matrix4x4.Transform(ref transform, ref v2, out v2);
+ Matrix4x4.Transform(ref transform, ref v3, out v3);
+ Matrix4x4.Transform(ref transform, ref v4, out v4);
+
+ //
+
+ _vertices.SetPosition3D(baseVertexIndex + 0, ref v1);
+ _vertices.SetPosition3D(baseVertexIndex + 1, ref v2);
+ _vertices.SetPosition3D(baseVertexIndex + 2, ref v3);
+ _vertices.SetPosition3D(baseVertexIndex + 3, ref v4);
+
+ _vertices.SetTexCoord(baseVertexIndex + 0, texCoordLeft, texCoordBottom);
+ _vertices.SetTexCoord(baseVertexIndex + 1, texCoordRight, texCoordBottom);
+ _vertices.SetTexCoord(baseVertexIndex + 2, texCoordRight, texCoordTop);
+ _vertices.SetTexCoord(baseVertexIndex + 3, texCoordLeft, texCoordTop);
+
+ _vertices.SetColor(baseVertexIndex + 0, ref color);
+ _vertices.SetColor(baseVertexIndex + 1, ref color);
+ _vertices.SetColor(baseVertexIndex + 2, ref color);
+ _vertices.SetColor(baseVertexIndex + 3, ref color);
+
+ _textures[spriteIndex] = texture;
+ }
+
+ private void GetTransformFor(BillboardSpriteType type, ref Vector3 position, out Matrix4x4 transform)
+ {
+ switch (type)
+ {
+ case BillboardSpriteType.Spherical:
+ Matrix4x4.CreateBillboard(ref position, ref _cameraPosition, ref _up, ref _cameraForward, out transform);
+ break;
+ case BillboardSpriteType.Cylindrical:
+ Matrix4x4.CreateCylindricalBillboard(ref position, ref _cameraPosition, ref _cameraForward, ref _yAxis, out transform);
+ break;
+ case BillboardSpriteType.ScreenAligned:
+ Matrix4x4.CreateScreenAlignedBillboard(ref position, ref _up, ref _cameraForward, out transform);
+ break;
+ case BillboardSpriteType.ScreenAndAxisAligned:
+ Matrix4x4.CreateScreenAndAxisAlignedBillboard(ref position, ref _cameraForward, ref _yAxis, out transform);
+ break;
+ default:
+ throw new NotImplementedException();
+ }
+ }
+
+ private int GetRemainingSpriteSpaces()
+ {
+ int currentMaxSprites = _vertices.NumElements / VerticesPerSprite;
+ return currentMaxSprites - _currentSpritePointer;
+ }
+
+ private void AddMoreSpriteSpace(int numSprites)
+ {
+ int numVerticesToAdd = numSprites * VerticesPerSprite;
+ int numIndicesToAdd = numSprites * IndicesPerSprite;
+ int newTextureArraySize = _textures.Length + numSprites;
+
+ int oldSpriteCount = _vertices.NumElements / VerticesPerSprite;
+
+ _vertices.Extend(numVerticesToAdd);
+ _indices.Extend(numIndicesToAdd);
+ Array.Resize(ref _textures, newTextureArraySize);
+
+ int newSpriteCount = _vertices.NumElements / VerticesPerSprite;
+
+ FillSpriteIndicesFor(oldSpriteCount - 1, newSpriteCount - 1);
+ }
+
+ private void FillSpriteIndicesFor(int firstSprite, int lastSprite)
+ {
+ for (int i = firstSprite; i <= lastSprite; ++i)
+ {
+ int indicesStart = i * IndicesPerSprite;
+ int verticesStart = i * VerticesPerSprite;
+
+ _indices.Set(indicesStart + 0, (ushort)(verticesStart + 0));
+ _indices.Set(indicesStart + 1, (ushort)(verticesStart + 1));
+ _indices.Set(indicesStart + 2, (ushort)(verticesStart + 2));
+ _indices.Set(indicesStart + 3, (ushort)(verticesStart + 0));
+ _indices.Set(indicesStart + 4, (ushort)(verticesStart + 2));
+ _indices.Set(indicesStart + 5, (ushort)(verticesStart + 3));
+ }
+ }
+
+ #endregion
+ }
+}
+