using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
#if FNA
using MLEM.Extensions;
using System.IO;
#endif
namespace MLEM.Graphics {
///
/// A static sprite batch is a variation of that keeps all batched items in a , allowing for them to be drawn multiple times.
/// To add items to a static sprite batch, use to begin batching, to clear currently batched items, Add and its various overloads to add batch items, to remove them again, and to end batching.
/// To draw the batched items, call .
///
public class StaticSpriteBatch : IDisposable {
// this maximum is limited by indices being a short
private const int MaxBatchItems = short.MaxValue / 6;
private static readonly VertexPositionColorTexture[] Data = new VertexPositionColorTexture[StaticSpriteBatch.MaxBatchItems * 4];
///
/// The amount of vertices that are currently batched.
///
public int Vertices => this.currentItems.Count * 4;
///
/// The amount of vertex buffers that this static sprite batch has.
/// To see the amount of buffers that are actually in use, see .
///
public int Buffers => this.vertexBuffers.Count;
///
/// The amount of textures that this static sprite batch is currently using.
///
public int Textures => this.textures.Distinct().Count();
///
/// The amount of vertex buffers that are currently filled in this static sprite batch.
/// To see the amount of buffers that are available, see .
///
public int FilledBuffers { get; private set; }
private readonly GraphicsDevice graphicsDevice;
private readonly SpriteEffect spriteEffect;
private readonly List vertexBuffers = new List();
private readonly List textures = new List();
private readonly ISet- currentItems = new HashSet
- ();
private readonly List
- sortedItems = new List
- ();
private IndexBuffer indices;
private bool batching;
private bool batchChanged;
private SpriteSortMode sortMode;
private Comparer
- comparer;
///
/// Creates a new static sprite batch with the given
///
/// The graphics device to use for rendering
public StaticSpriteBatch(GraphicsDevice graphicsDevice) {
this.graphicsDevice = graphicsDevice;
this.spriteEffect = new SpriteEffect(graphicsDevice);
}
///
/// Begins batching.
/// Call this method before calling Add or any of its overloads.
///
/// The drawing order for sprite drawing. by default, since it is the best in terms of rendering performance. Note that is not supported.
/// Thrown if this batch is currently batching already
/// Thrown if the is , which is not supported.
public void BeginBatch(SpriteSortMode sortMode = SpriteSortMode.Texture) {
if (this.batching)
throw new InvalidOperationException("Already batching");
if (sortMode == SpriteSortMode.Immediate)
throw new ArgumentOutOfRangeException(nameof(sortMode), "Cannot use sprite sort mode Immediate for static batching");
// update comparer and re-sort our list if our sort mode changed
if (this.sortMode != sortMode) {
this.sortMode = sortMode;
this.comparer = Comparer
- .Create(StaticSpriteBatch.CreateComparison(sortMode));
this.sortedItems.Sort(this.comparer);
}
this.batching = true;
}
///
/// Ends batching.
/// Call this method after calling Add or any of its overloads the desired number of times to add batched items.
///
/// Thrown if this method is called before was called.
public void EndBatch() {
if (!this.batching)
throw new InvalidOperationException("Not batching");
this.batching = false;
// if we didn't add or remove any batch items, we don't have to recalculate anything
if (!this.batchChanged)
return;
this.batchChanged = false;
this.FilledBuffers = 0;
this.textures.Clear();
// fill vertex buffers
var dataIndex = 0;
Texture2D texture = null;
// we use RemoveAll to iterate safely while being able to remove
this.sortedItems.RemoveAll(i => {
// we remove items in here to avoid having to search for them when removing them in Remove
if (i.Removed)
return true;
// if the texture changes, we also have to start a new buffer!
if (dataIndex > 0 && (i.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) {
this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
dataIndex = 0;
}
StaticSpriteBatch.Data[dataIndex++] = i.TopLeft;
StaticSpriteBatch.Data[dataIndex++] = i.TopRight;
StaticSpriteBatch.Data[dataIndex++] = i.BottomLeft;
StaticSpriteBatch.Data[dataIndex++] = i.BottomRight;
texture = i.Texture;
return false;
});
if (dataIndex > 0)
this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data);
// ensure we have enough indices
var maxItems = Math.Min(this.currentItems.Count, StaticSpriteBatch.MaxBatchItems);
// each item has 2 triangles which each have 3 indices
if (this.indices == null || this.indices.IndexCount < 6 * maxItems) {
var newIndices = new short[6 * maxItems];
var index = 0;
for (var item = 0; item < maxItems; item++) {
// a square is made up of two triangles
// 0--1
// | /|
// |/ |
// 2--3
// top left triangle (0 -> 1 -> 2)
newIndices[index++] = (short) (item * 4 + 0);
newIndices[index++] = (short) (item * 4 + 1);
newIndices[index++] = (short) (item * 4 + 2);
// bottom right triangle (1 -> 3 -> 2)
newIndices[index++] = (short) (item * 4 + 1);
newIndices[index++] = (short) (item * 4 + 3);
newIndices[index++] = (short) (item * 4 + 2);
}
this.indices?.Dispose();
this.indices = new IndexBuffer(this.graphicsDevice, IndexElementSize.SixteenBits, newIndices.Length, BufferUsage.WriteOnly);
this.indices.SetData(newIndices);
}
// sanity check, this shouldn't be able to happen
if (this.currentItems.Count != this.sortedItems.Count)
throw new InvalidOperationException("Current and sorted items have mismatched sizes");
}
///
/// Draws this batch's content onto the 's current render target (or the back buffer) with the given settings.
/// Note that this method should not be called while a regular is currently active.
///
/// State of the blending. Uses if null.
/// State of the sampler. Uses if null.
/// State of the depth-stencil buffer. Uses if null.
/// State of the rasterization. Uses if null.
/// A custom to override the default sprite effect. Uses default sprite effect if null.
/// An optional matrix used to transform the sprite geometry. Uses if null.
/// Thrown if this batch is currently batching
public void Draw(BlendState blendState = null, SamplerState samplerState = null, DepthStencilState depthStencilState = null, RasterizerState rasterizerState = null, Effect effect = null, Matrix? transformMatrix = null) {
if (this.batching)
throw new InvalidOperationException("Cannot draw the batch while batching");
this.graphicsDevice.BlendState = blendState ?? BlendState.AlphaBlend;
this.graphicsDevice.SamplerStates[0] = samplerState ?? SamplerState.LinearClamp;
this.graphicsDevice.DepthStencilState = depthStencilState ?? DepthStencilState.None;
this.graphicsDevice.RasterizerState = rasterizerState ?? RasterizerState.CullCounterClockwise;
this.graphicsDevice.Indices = this.indices;
this.spriteEffect.TransformMatrix = transformMatrix;
this.spriteEffect.CurrentTechnique.Passes[0].Apply();
var totalIndex = 0;
for (var i = 0; i < this.FilledBuffers; i++) {
var buffer = this.vertexBuffers[i];
var texture = this.textures[i];
var verts = Math.Min(this.currentItems.Count * 4 - totalIndex, buffer.VertexCount);
this.graphicsDevice.SetVertexBuffer(buffer);
if (effect != null) {
foreach (var pass in effect.CurrentTechnique.Passes) {
pass.Apply();
this.graphicsDevice.Textures[0] = texture;
this.DrawPrimitives(verts);
}
} else {
this.graphicsDevice.Textures[0] = texture;
this.DrawPrimitives(verts);
}
totalIndex += buffer.VertexCount;
}
}
///
/// Adds an item to this batch.
/// Note that this batch needs to currently be batching, meaning has to have been called previously.
///
/// A texture.
/// The drawing location on screen.
/// An optional region on the texture which will be rendered. If null - draws full texture.
/// A color mask.
/// A rotation of this sprite.
/// Center of the rotation. 0,0 by default.
/// A scaling of this sprite.
/// Modificators for drawing. Can be combined.
/// A depth of the layer of this sprite.
/// The that was created from the added data
public Item Add(Texture2D texture, Vector2 position, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
origin *= scale;
Vector2 size, texTl, texBr;
if (sourceRectangle.HasValue) {
var src = sourceRectangle.Value;
size.X = src.Width * scale.X;
size.Y = src.Height * scale.Y;
texTl.X = src.X * (1F / texture.Width);
texTl.Y = src.Y * (1F / texture.Height);
texBr.X = (src.X + src.Width) * (1F / texture.Width);
texBr.Y = (src.Y + src.Height) * (1F / texture.Height);
} else {
size.X = texture.Width * scale.X;
size.Y = texture.Height * scale.Y;
texTl = Vector2.Zero;
texBr = Vector2.One;
}
if ((effects & SpriteEffects.FlipVertically) != 0)
(texBr.Y, texTl.Y) = (texTl.Y, texBr.Y);
if ((effects & SpriteEffects.FlipHorizontally) != 0)
(texBr.X, texTl.X) = (texTl.X, texBr.X);
if (rotation == 0) {
return this.Add(texture, position - origin, size, color, texTl, texBr, layerDepth);
} else {
return this.Add(texture, position, -origin, size, (float) Math.Sin(rotation), (float) Math.Cos(rotation), color, texTl, texBr, layerDepth);
}
}
///
/// Adds an item to this batch.
/// Note that this batch needs to currently be batching, meaning has to have been called previously.
///
/// A texture.
/// The drawing location on screen.
/// An optional region on the texture which will be rendered. If null - draws full texture.
/// A color mask.
/// A rotation of this sprite.
/// Center of the rotation. 0,0 by default.
/// A scaling of this sprite.
/// Modificators for drawing. Can be combined.
/// A depth of the layer of this sprite.
/// The that was created from the added data
public Item Add(Texture2D texture, Vector2 position, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
return this.Add(texture, position, sourceRectangle, color, rotation, origin, new Vector2(scale), effects, layerDepth);
}
///
/// Adds an item to this batch.
/// Note that this batch needs to currently be batching, meaning has to have been called previously.
///
/// A texture.
/// The drawing bounds on screen.
/// An optional region on the texture which will be rendered. If null - draws full texture.
/// A color mask.
/// A rotation of this sprite.
/// Center of the rotation. 0,0 by default.
/// Modificators for drawing. Can be combined.
/// A depth of the layer of this sprite.
/// The that was created from the added data
public Item Add(Texture2D texture, Rectangle destinationRectangle, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth) {
Vector2 texTl, texBr;
if (sourceRectangle.HasValue) {
var src = sourceRectangle.Value;
texTl.X = src.X * (1F / texture.Width);
texTl.Y = src.Y * (1F / texture.Height);
texBr.X = (src.X + src.Width) * (1F / texture.Width);
texBr.Y = (src.Y + src.Height) * (1F / texture.Height);
origin.X = origin.X * destinationRectangle.Width * (src.Width != 0 ? src.Width : 1F / texture.Width);
origin.Y = origin.Y * destinationRectangle.Height * (src.Height != 0 ? src.Height : 1F / texture.Height);
} else {
texTl = Vector2.Zero;
texBr = Vector2.One;
origin.X = origin.X * destinationRectangle.Width * (1F / texture.Width);
origin.Y = origin.Y * destinationRectangle.Height * (1F / texture.Height);
}
if ((effects & SpriteEffects.FlipVertically) != 0)
(texBr.Y, texTl.Y) = (texTl.Y, texBr.Y);
if ((effects & SpriteEffects.FlipHorizontally) != 0)
(texBr.X, texTl.X) = (texTl.X, texBr.X);
var destSize = new Vector2(destinationRectangle.Width, destinationRectangle.Height);
if (rotation == 0) {
return this.Add(texture, destinationRectangle.Location.ToVector2() - origin, destSize, color, texTl, texBr, layerDepth);
} else {
return this.Add(texture, destinationRectangle.Location.ToVector2(), -origin, destSize, (float) Math.Sin(rotation), (float) Math.Cos(rotation), color, texTl, texBr, layerDepth);
}
}
///
/// Adds an item to this batch.
/// Note that this batch needs to currently be batching, meaning has to have been called previously.
///
/// A texture.
/// The drawing location on screen.
/// An optional region on the texture which will be rendered. If null - draws full texture.
/// A color mask.
/// The that was created from the added data
public Item Add(Texture2D texture, Vector2 position, Rectangle? sourceRectangle, Color color) {
return this.Add(texture, position, sourceRectangle, color, 0, Vector2.Zero, 1, SpriteEffects.None, 0);
}
///
/// Adds an item to this batch.
/// Note that this batch needs to currently be batching, meaning has to have been called previously.
///
/// A texture.
/// The drawing bounds on screen.
/// An optional region on the texture which will be rendered. If null - draws full texture.
/// A color mask.
/// The that was created from the added data
public Item Add(Texture2D texture, Rectangle destinationRectangle, Rectangle? sourceRectangle, Color color) {
return this.Add(texture, destinationRectangle, sourceRectangle, color, 0, Vector2.Zero, SpriteEffects.None, 0);
}
///
/// Adds an item to this batch.
/// Note that this batch needs to currently be batching, meaning has to have been called previously.
///
/// A texture.
/// The drawing location on screen.
/// A color mask.
/// The that was created from the added data
public Item Add(Texture2D texture, Vector2 position, Color color) {
return this.Add(texture, position, null, color);
}
///
/// Adds an item to this batch.
/// Note that this batch needs to currently be batching, meaning has to have been called previously.
///
/// A texture.
/// The drawing bounds on screen.
/// A color mask.
/// The that was created from the added data
public Item Add(Texture2D texture, Rectangle destinationRectangle, Color color) {
return this.Add(texture, destinationRectangle, null, color);
}
///
/// Removes the given item from this batch.
/// Note that this batch needs to currently be batching, meaning has to have been called previously.
///
/// The item to remove
/// Whether the item was successfully removed
/// Thrown if this method is called before was called
public bool Remove(Item item) {
if (!this.batching)
throw new InvalidOperationException("Not batching");
if (!item.Removed && this.currentItems.Remove(item)) {
this.batchChanged = true;
// this item will only actually get removed from sortedItems in EndBatch for performance
item.Removed = true;
return true;
}
return false;
}
///
/// Clears the batch, removing all currently batched vertices.
/// After this operation, will return 0.
///
/// Thrown if this method is called before was called
public void ClearBatch() {
if (!this.batching)
throw new InvalidOperationException("Not batching");
this.currentItems.Clear();
this.sortedItems.Clear();
this.textures.Clear();
this.FilledBuffers = 0;
this.batchChanged = true;
}
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
public void Dispose() {
this.spriteEffect.Dispose();
this.indices?.Dispose();
foreach (var buffer in this.vertexBuffers)
buffer.Dispose();
GC.SuppressFinalize(this);
}
private Item Add(Texture2D texture, Vector2 pos, Vector2 offset, Vector2 size, float sin, float cos, Color color, Vector2 texTl, Vector2 texBr, float depth) {
return this.Add(texture, depth,
new VertexPositionColorTexture(new Vector3(
pos.X + offset.X * cos - offset.Y * sin,
pos.Y + offset.X * sin + offset.Y * cos, depth),
color, texTl),
new VertexPositionColorTexture(new Vector3(
pos.X + (offset.X + size.X) * cos - offset.Y * sin,
pos.Y + (offset.X + size.X) * sin + offset.Y * cos, depth),
color, new Vector2(texBr.X, texTl.Y)),
new VertexPositionColorTexture(new Vector3(
pos.X + offset.X * cos - (offset.Y + size.Y) * sin,
pos.Y + offset.X * sin + (offset.Y + size.Y) * cos, depth),
color, new Vector2(texTl.X, texBr.Y)),
new VertexPositionColorTexture(new Vector3(
pos.X + (offset.X + size.X) * cos - (offset.Y + size.Y) * sin,
pos.Y + (offset.X + size.X) * sin + (offset.Y + size.Y) * cos, depth),
color, texBr));
}
private Item Add(Texture2D texture, Vector2 pos, Vector2 size, Color color, Vector2 texTl, Vector2 texBr, float depth) {
return this.Add(texture, depth,
new VertexPositionColorTexture(
new Vector3(pos, depth),
color, texTl),
new VertexPositionColorTexture(
new Vector3(pos.X + size.X, pos.Y, depth),
color, new Vector2(texBr.X, texTl.Y)),
new VertexPositionColorTexture(
new Vector3(pos.X, pos.Y + size.Y, depth),
color, new Vector2(texTl.X, texBr.Y)),
new VertexPositionColorTexture(
new Vector3(pos.X + size.X, pos.Y + size.Y, depth),
color, texBr));
}
private Item Add(Texture2D texture, float depth, VertexPositionColorTexture tl, VertexPositionColorTexture tr, VertexPositionColorTexture bl, VertexPositionColorTexture br) {
if (!this.batching)
throw new InvalidOperationException("Not batching");
var item = new Item(texture, depth, tl, tr, bl, br);
this.currentItems.Add(item);
// add item in a sorted fashion
var pos = this.sortedItems.BinarySearch(item, this.comparer);
this.sortedItems.Insert(pos >= 0 ? pos : ~pos, item);
this.batchChanged = true;
return item;
}
private void FillBuffer(int index, Texture2D texture, VertexPositionColorTexture[] data) {
if (this.vertexBuffers.Count <= index)
this.vertexBuffers.Add(new DynamicVertexBuffer(this.graphicsDevice, VertexPositionColorTexture.VertexDeclaration, StaticSpriteBatch.MaxBatchItems * 4, BufferUsage.WriteOnly));
this.vertexBuffers[index].SetData(data, 0, data.Length, SetDataOptions.Discard);
this.textures.Insert(index, texture);
}
private void DrawPrimitives(int vertices) {
#if FNA
this.graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices, 0, vertices / 4 * 2);
#else
this.graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices / 4 * 2);
#endif
}
private static Comparison
- CreateComparison(SpriteSortMode sortMode) {
switch (sortMode) {
case SpriteSortMode.Texture:
return (i1, i2) => i1.TextureHash.CompareTo(i2.TextureHash);
case SpriteSortMode.BackToFront:
return (i1, i2) => i2.Depth.CompareTo(i1.Depth);
case SpriteSortMode.FrontToBack:
return (i1, i2) => i1.Depth.CompareTo(i2.Depth);
default:
return (i1, i2) => 0;
}
}
///
/// A struct that represents an item added to a using Add or any of its overloads.
/// An item returned after adding can be removed using .
///
public class Item {
internal readonly Texture2D Texture;
internal readonly int TextureHash;
internal readonly float Depth;
internal readonly VertexPositionColorTexture TopLeft;
internal readonly VertexPositionColorTexture TopRight;
internal readonly VertexPositionColorTexture BottomLeft;
internal readonly VertexPositionColorTexture BottomRight;
internal bool Removed;
internal Item(Texture2D texture, float depth, VertexPositionColorTexture topLeft, VertexPositionColorTexture topRight, VertexPositionColorTexture bottomLeft, VertexPositionColorTexture bottomRight) {
this.Texture = texture;
this.TextureHash = texture.GetHashCode();
this.Depth = depth;
this.TopLeft = topLeft;
this.TopRight = topRight;
this.BottomLeft = bottomLeft;
this.BottomRight = bottomRight;
}
}
#if FNA
private class SpriteEffect : Effect {
private EffectParameter matrixParam;
private Viewport lastViewport;
private Matrix projection;
public Matrix? TransformMatrix { get; set; }
public SpriteEffect(GraphicsDevice device) : base(device, SpriteEffect.LoadEffectCode()) {
this.CacheEffectParameters();
}
private SpriteEffect(SpriteEffect cloneSource) : base(cloneSource) {
this.CacheEffectParameters();
}
public override Effect Clone() {
return new SpriteEffect(this);
}
private void CacheEffectParameters() {
this.matrixParam = this.Parameters["MatrixTransform"];
}
protected override void OnApply() {
var vp = this.GraphicsDevice.Viewport;
if (vp.Width != this.lastViewport.Width || vp.Height != this.lastViewport.Height) {
Matrix.CreateOrthographicOffCenter(0, vp.Width, vp.Height, 0, 0, -1, out this.projection);
this.projection.M41 += -0.5f * this.projection.M11;
this.projection.M42 += -0.5f * this.projection.M22;
this.lastViewport = vp;
}
if (this.TransformMatrix.HasValue) {
this.matrixParam.SetValue(this.TransformMatrix.GetValueOrDefault() * this.projection);
} else {
this.matrixParam.SetValue(this.projection);
}
}
private static byte[] LoadEffectCode() {
using (var stream = typeof(Effect).Assembly.GetManifestResourceStream("Microsoft.Xna.Framework.Graphics.Effect.Resources.SpriteEffect.fxb")) {
using (var memory = new MemoryStream()) {
stream.CopyTo(memory);
return memory.ToArray();
}
}
}
}
#endif
}
}