From d89d8571c7e686ed5a3abd21cd612d6d496f0df4 Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Sun, 17 Oct 2021 23:20:05 +0200 Subject: [PATCH] added StaticSpriteBatch --- CHANGELOG.md | 1 + MLEM/Extensions/SpriteBatchExtensions.cs | 17 ++ MLEM/Misc/StaticSpriteBatch.cs | 347 +++++++++++++++++++++++ MLEM/Textures/TextureRegion.cs | 35 +++ 4 files changed, 400 insertions(+) create mode 100644 MLEM/Misc/StaticSpriteBatch.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index fd52627..5da4032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Jump to version: Additions - Added a strikethrough formatting code - Added GenericFont SplitStringSeparate which differentiates between existing newline characters and splits due to maximum width +- Added StaticSpriteBatch class ### MLEM.Ui Additions diff --git a/MLEM/Extensions/SpriteBatchExtensions.cs b/MLEM/Extensions/SpriteBatchExtensions.cs index bf6b67a..854dd45 100644 --- a/MLEM/Extensions/SpriteBatchExtensions.cs +++ b/MLEM/Extensions/SpriteBatchExtensions.cs @@ -127,6 +127,23 @@ namespace MLEM.Extensions { batch.Draw(texture, destinationRectangle, null, color); } + /// + public static void Add(this StaticSpriteBatch batch, Texture2D texture, RectangleF destinationRectangle, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth) { + var source = sourceRectangle ?? new Rectangle(0, 0, texture.Width, texture.Height); + var scale = new Vector2(1F / source.Width, 1F / source.Height) * destinationRectangle.Size; + batch.Add(texture, destinationRectangle.Location, sourceRectangle, color, rotation, origin, scale, effects, layerDepth); + } + + /// + public static void Add(this StaticSpriteBatch batch, Texture2D texture, RectangleF destinationRectangle, Rectangle? sourceRectangle, Color color) { + batch.Add(texture, destinationRectangle, sourceRectangle, color, 0, Vector2.Zero, SpriteEffects.None, 0); + } + + /// + public static void Draw(this StaticSpriteBatch batch, Texture2D texture, RectangleF destinationRectangle, Color color) { + batch.Add(texture, destinationRectangle, null, color); + } + private static void AutoDispose(SpriteBatch batch, Texture2D texture) { batch.Disposing += (sender, ars) => { if (texture != null) { diff --git a/MLEM/Misc/StaticSpriteBatch.cs b/MLEM/Misc/StaticSpriteBatch.cs new file mode 100644 index 0000000..f3a4db0 --- /dev/null +++ b/MLEM/Misc/StaticSpriteBatch.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MLEM.Extensions; + +namespace MLEM.Misc { + /// + /// A static sprite batch is a variation of that keeps all batched items in memory, allowing for them to be drawn multiple times. + /// To add items to a static sprite batch, use to clear currently batched items, to begin batching, Add and its various overloads to add batch items and to end batching. + /// To draw the batched items, call . + /// + public class StaticSpriteBatch : IDisposable { + + /// + /// The amount of vertices that are currently batched + /// + public int Vertices => this.vertices.Count; + + private readonly GraphicsDevice graphicsDevice; + private readonly SpriteEffect spriteEffect; + + private readonly List vertexArrays = new List(); + private readonly List vertices = new List(); + private short[] indices; + private Texture2D texture; + private bool batching; + private bool batchChanged; + + /// + /// 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. + /// Note that, if was not called, items that are batched will be appended to the existing batch. + /// + /// Thrown if this batch is currently batching already + public void BeginBatch() { + if (this.batching) + throw new InvalidOperationException("Already batching"); + 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 any batch items, we don't have to recalculate anything + if (!this.batchChanged) + return; + this.batchChanged = false; + + // this maximum is limited by indices being a short + const int maxBatchItems = short.MaxValue / 6; + + // ensure we have enough vertex arrays + var arraysRequired = (this.vertices.Count / (maxBatchItems * 4F)).Ceil(); + while (this.vertexArrays.Count < arraysRequired) + this.vertexArrays.Add(new VertexPositionColorTexture[maxBatchItems * 4]); + + // fill vertex arrays + var arrayIndex = 0; + var totalIndex = 0; + while (totalIndex < this.vertices.Count) { + var array = this.vertexArrays[arrayIndex]; + var now = Math.Min(this.vertices.Count - totalIndex, array.Length); + for (var i = 0; i < now; i++) + array[i] = this.vertices[totalIndex + i]; + totalIndex += now; + arrayIndex++; + } + + // ensure we have enough indices + var maxItems = Math.Min(this.vertices.Count / 4, maxBatchItems); + // each item has 2 triangles which each have 3 indices + if (this.indices == null || this.indices.Length < 6 * maxItems) { + this.indices = 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) + this.indices[index++] = (short) (item * 4 + 0); + this.indices[index++] = (short) (item * 4 + 1); + this.indices[index++] = (short) (item * 4 + 2); + // bottom right triangle (1 -> 3 -> 2) + this.indices[index++] = (short) (item * 4 + 1); + this.indices[index++] = (short) (item * 4 + 3); + this.indices[index++] = (short) (item * 4 + 2); + } + } + } + + /// + /// Clears the batch, removing all currently batched vertices. + /// After this operation, will return 0. + /// + /// Thrown if this batch is currently batching + public void ClearBatch() { + if (this.batching) + throw new InvalidOperationException("Cannot clear while batching"); + this.vertices.Clear(); + this.texture = null; + this.batchChanged = true; + } + + /// + /// 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.spriteEffect.TransformMatrix = transformMatrix; + this.spriteEffect.CurrentTechnique.Passes[0].Apply(); + + var totalIndex = 0; + foreach (var array in this.vertexArrays) { + var tris = Math.Min(this.vertices.Count - totalIndex, array.Length) / 4 * 2; + if (effect != null) { + foreach (var pass in effect.CurrentTechnique.Passes) { + pass.Apply(); + this.graphicsDevice.Textures[0] = this.texture; + this.graphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, array, 0, array.Length, this.indices, 0, tris, VertexPositionColorTexture.VertexDeclaration); + } + } else { + this.graphicsDevice.Textures[0] = this.texture; + this.graphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, array, 0, array.Length, this.indices, 0, tris, VertexPositionColorTexture.VertexDeclaration); + } + totalIndex += array.Length; + } + } + + /// + /// 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. + public void 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) { + this.Add(texture, position - origin, size, color, texTl, texBr, layerDepth); + } else { + 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. + public void Add(Texture2D texture, Vector2 position, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) { + 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. + public void 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); + + if (rotation == 0) { + this.Add(texture, destinationRectangle.Location.ToVector2() - origin, destinationRectangle.Size.ToVector2(), color, texTl, texBr, layerDepth); + } else { + this.Add(texture, destinationRectangle.Location.ToVector2(), -origin, destinationRectangle.Size.ToVector2(), (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. + public void Add(Texture2D texture, Vector2 position, Rectangle? sourceRectangle, Color color) { + 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. + public void Add(Texture2D texture, Rectangle destinationRectangle, Rectangle? sourceRectangle, Color color) { + 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. + public void Add(Texture2D texture, Vector2 position, Color color) { + 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. + public void Add(Texture2D texture, Rectangle destinationRectangle, Color color) { + this.Add(texture, destinationRectangle, null, color); + } + + /// + public void Dispose() { + this.spriteEffect.Dispose(); + GC.SuppressFinalize(this); + } + + private void Add(Texture2D texture, Vector2 pos, Vector2 offset, Vector2 size, float sin, float cos, Color color, Vector2 texTl, Vector2 texBr, float depth) { + this.Add(texture, + 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) + 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 void Add(Texture2D texture, Vector2 pos, Vector2 size, Color color, Vector2 texTl, Vector2 texBr, float depth) { + this.Add(texture, + 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 void Add(Texture2D texture, VertexPositionColorTexture tl, VertexPositionColorTexture tr, VertexPositionColorTexture bl, VertexPositionColorTexture br) { + if (!this.batching) + throw new InvalidOperationException("Not batching"); + if (this.texture != null && this.texture != texture) + throw new ArgumentException("Cannot use multiple textures in one batch"); + this.texture = texture; + this.vertices.Add(tl); + this.vertices.Add(tr); + this.vertices.Add(bl); + this.vertices.Add(br); + this.batchChanged = true; + } + + } +} \ No newline at end of file diff --git a/MLEM/Textures/TextureRegion.cs b/MLEM/Textures/TextureRegion.cs index 2f69183..16d1488 100644 --- a/MLEM/Textures/TextureRegion.cs +++ b/MLEM/Textures/TextureRegion.cs @@ -171,5 +171,40 @@ namespace MLEM.Textures { batch.Draw(texture, destinationRectangle, color, 0, Vector2.Zero, SpriteEffects.None, 0); } + /// + public static void Add(this StaticSpriteBatch batch, TextureRegion texture, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) { + batch.Add(texture.Texture, position, texture.Area, color, rotation, origin + texture.PivotPixels, scale, effects, layerDepth); + } + + /// + public static void Add(this StaticSpriteBatch batch, TextureRegion texture, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) { + batch.Add(texture, position, color, rotation, origin, new Vector2(scale), effects, layerDepth); + } + + /// + public static void Add(this StaticSpriteBatch batch, TextureRegion texture, Rectangle destinationRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth) { + batch.Add(texture.Texture, destinationRectangle, texture.Area, color, rotation, origin + texture.PivotPixels, effects, layerDepth); + } + + /// + public static void Add(this StaticSpriteBatch batch, TextureRegion texture, RectangleF destinationRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth) { + batch.Add(texture.Texture, destinationRectangle, texture.Area, color, rotation, origin + texture.PivotPixels, effects, layerDepth); + } + + /// + public static void Add(this StaticSpriteBatch batch, TextureRegion texture, Vector2 position, Color color) { + batch.Add(texture, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0); + } + + /// + public static void Add(this StaticSpriteBatch batch, TextureRegion texture, Rectangle destinationRectangle, Color color) { + batch.Add(texture, destinationRectangle, color, 0, Vector2.Zero, SpriteEffects.None, 0); + } + + /// + public static void Add(this StaticSpriteBatch batch, TextureRegion texture, RectangleF destinationRectangle, Color color) { + batch.Add(texture, destinationRectangle, color, 0, Vector2.Zero, SpriteEffects.None, 0); + } + } } \ No newline at end of file