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 highly optimized 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.itemAmount * 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 SortedDictionary items = new SortedDictionary(); private SpriteSortMode sortMode = SpriteSortMode.Texture; private IndexBuffer indices; private bool batching; private bool batchChanged; private int itemAmount; /// /// 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. When is passed, the last used sort mode will be used again. The initial sort mode is . 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 = null) { 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"); // if the sort mode changed (which should be very rare in practice), we have to re-sort our list if (sortMode != null && this.sortMode != sortMode) { this.sortMode = sortMode.Value; if (this.items.Count > 0) { var tempItems = this.items.Values.SelectMany(s => s.Items).ToArray(); this.items.Clear(); foreach (var item in tempItems) this.AddItemToSet(item); this.batchChanged = true; } } 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; foreach (var itemSet in this.items.Values) { foreach (var item in itemSet.Items) { // if the texture changes, we also have to start a new buffer! if (dataIndex > 0 && (item.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) { this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data); dataIndex = 0; } StaticSpriteBatch.Data[dataIndex++] = item.TopLeft; StaticSpriteBatch.Data[dataIndex++] = item.TopRight; StaticSpriteBatch.Data[dataIndex++] = item.BottomLeft; StaticSpriteBatch.Data[dataIndex++] = item.BottomRight; texture = item.Texture; } } if (dataIndex > 0) this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data); // ensure we have enough indices var maxItems = Math.Min(this.itemAmount, 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); } } /// /// 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.itemAmount * 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); } /// /// Adds an item to this batch. /// Note that this batch needs to currently be batching, meaning has to have been called previously. /// /// The item to add. /// The added , for chaining. public Item Add(Item item) { if (!this.batching) throw new InvalidOperationException("Not batching"); this.AddItemToSet(item); this.itemAmount++; this.batchChanged = true; return item; } /// /// 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"); var key = item.GetSortKey(this.sortMode); if (this.items.TryGetValue(key, out var itemSet) && itemSet.Remove(item)) { if (itemSet.IsEmpty) this.items.Remove(key); this.itemAmount--; this.batchChanged = 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.items.Clear(); this.textures.Clear(); this.FilledBuffers = 0; this.itemAmount = 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(); } 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) { return this.Add(new Item(texture, depth, tl, tr, bl, br)); } 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 void AddItemToSet(Item item) { var sortKey = item.GetSortKey(this.sortMode); if (!this.items.TryGetValue(sortKey, out var itemSet)) { itemSet = new ItemSet(); this.items.Add(sortKey, itemSet); } itemSet.Add(item); } /// /// 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 float Depth; internal readonly VertexPositionColorTexture TopLeft; internal readonly VertexPositionColorTexture TopRight; internal readonly VertexPositionColorTexture BottomLeft; internal readonly VertexPositionColorTexture BottomRight; internal Item(Texture2D texture, float depth, VertexPositionColorTexture topLeft, VertexPositionColorTexture topRight, VertexPositionColorTexture bottomLeft, VertexPositionColorTexture bottomRight) { this.Texture = texture; this.Depth = depth; this.TopLeft = topLeft; this.TopRight = topRight; this.BottomLeft = bottomLeft; this.BottomRight = bottomRight; } internal float GetSortKey(SpriteSortMode sortMode) { switch (sortMode) { case SpriteSortMode.Texture: return this.Texture.GetHashCode(); case SpriteSortMode.BackToFront: return -this.Depth; case SpriteSortMode.FrontToBack: return this.Depth; default: return 0; } } } private class ItemSet { public IEnumerable Items { get { if (this.items != null) return this.items; if (this.single != null) return Enumerable.Repeat(this.single, 1); return Enumerable.Empty(); } } public bool IsEmpty => this.items == null && this.single == null; private HashSet items; private Item single; public void Add(Item item) { if (this.items != null) { this.items.Add(item); } else if (this.single != null) { this.items = new HashSet(); this.items.Add(this.single); this.items.Add(item); this.single = null; } else { this.single = item; } } public bool Remove(Item item) { if (this.items != null && this.items.Remove(item)) { if (this.items.Count <= 1) { this.single = this.items.Single(); this.items = null; } return true; } else if (this.single == item) { this.single = null; return true; } else { return false; } } } #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 } }