using System; using System.Text; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MonoGame.Extended.BitmapFonts; using MonoGame.Extended.Graphics.Effects; using MonoGame.Extended.Graphics.Geometry; namespace MonoGame.Extended.Graphics { /// /// A general purpose for two-dimensional geometry that change /// frequently between frames such as sprites and shapes. /// /// /// /// For drawing user interfaces, consider using instead because it supports scissor rectangles. /// public sealed class Batcher2D : Batcher { internal const int DefaultMaximumVerticesCount = 8192; internal const int DefaultMaximumIndicesCount = 12288; private readonly VertexBuffer _vertexBuffer; private readonly IndexBuffer _indexBuffer; private readonly VertexPositionColorTexture[] _vertices; private int _vertexCount; private readonly ushort[] _indices; private int _indexCount; private readonly ushort[] _sortedIndices; private readonly GeometryBuilder2D _geometryBuilder; /// /// Initializes a new instance of the class. /// /// The graphics device. /// /// The maximum number of vertices that can be enqueued before a /// is required. The default value is 8192. /// /// /// The maximum number of indices that can be enqueued before a /// is required. The default value is 12288. /// /// /// The maximum number of structs that can be enqueued before a /// is required. The default value is 2048. /// /// . /// /// is less than or equal /// 0, or is less than or equal to 0, or, /// is less than or equal to 0. /// public Batcher2D(GraphicsDevice graphicsDevice, ushort maximumVerticesCount = DefaultMaximumVerticesCount, ushort maximumIndicesCount = DefaultMaximumIndicesCount, int maximumDrawCallsCount = DefaultBatchMaximumDrawCallsCount) : base( graphicsDevice, new DefaultEffect(graphicsDevice) { TextureEnabled = true, VertexColorEnabled = true }, maximumDrawCallsCount) { _vertices = new VertexPositionColorTexture[maximumVerticesCount]; _vertexBuffer = new DynamicVertexBuffer(graphicsDevice, VertexPositionColorTexture.VertexDeclaration, maximumVerticesCount, BufferUsage.WriteOnly); _indices = new ushort[maximumIndicesCount]; _sortedIndices = new ushort[maximumIndicesCount]; _indexBuffer = new DynamicIndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, maximumIndicesCount, BufferUsage.WriteOnly); _geometryBuilder = new GeometryBuilder2D(4, 6); } protected override void SortDrawCallsAndBindBuffers() { // Upload the vertices to the GPU and then select that vertex stream for drawing _vertexBuffer.SetData(_vertices, 0, _vertexCount); GraphicsDevice.SetVertexBuffer(_vertexBuffer); Array.Sort(DrawCalls, 0, EnqueuedDrawCallCount); BuildSortedIndices(); // Upload the indices to the GPU and then select that index stream for drawing _indexBuffer.SetData(_sortedIndices, 0, _indexCount); GraphicsDevice.Indices = _indexBuffer; _indexCount = 0; _vertexCount = 0; } private void BuildSortedIndices() { var newDrawCallsCount = 0; DrawCalls[0].StartIndex = 0; var currentDrawCall = DrawCalls[0]; DrawCalls[newDrawCallsCount++] = DrawCalls[0]; var drawCallIndexCount = currentDrawCall.PrimitiveCount * 3; Array.Copy(_indices, currentDrawCall.StartIndex, _sortedIndices, 0, drawCallIndexCount); var sortedIndexCount = drawCallIndexCount; // iterate through sorted draw calls checking if any can now be merged to reduce expensive draw calls to the graphics API // this might need to be changed for next-gen graphics API (Vulkan, Metal, DirectX 12) where the draw calls are not so expensive for (var i = 1; i < EnqueuedDrawCallCount; i++) { currentDrawCall = DrawCalls[i]; drawCallIndexCount = currentDrawCall.PrimitiveCount * 3; Array.Copy(_indices, currentDrawCall.StartIndex, _sortedIndices, sortedIndexCount, drawCallIndexCount); sortedIndexCount += drawCallIndexCount; if (currentDrawCall.TryMerge(ref DrawCalls[newDrawCallsCount - 1])) continue; currentDrawCall.StartIndex = sortedIndexCount; DrawCalls[newDrawCallsCount++] = currentDrawCall; } EnqueuedDrawCallCount = newDrawCallsCount; } /// /// Submits a draw operation to the using the specified . /// /// The draw call information. protected override void InvokeDrawCall(ref DrawCallInfo drawCall) { GraphicsDevice.DrawIndexedPrimitives(drawCall.PrimitiveType, 0, drawCall.StartIndex, drawCall.PrimitiveCount); } /// /// Draws a sprite using a specified , transform , source /// , and an optional /// , origin , , and depth . /// /// The . /// The transform . /// /// The texture region of the . Use /// null to use the entire . /// /// The . Use null to use the default . /// The . The default value is . /// The depth . The default value is 0. /// The method has not been called. /// is null. public void DrawSprite(Texture2D texture, ref Matrix2 transformMatrix, ref Rectangle sourceRectangle, Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0) { _geometryBuilder.BuildSprite(_vertexCount, ref transformMatrix, texture, ref sourceRectangle, color, flags, depth); EnqueueBuiltGeometry(texture, depth); } /// /// Draws a using the specified transform and an optional /// , origin , , and depth . /// /// The . /// The transform . /// The . Use null to use the default . /// The . The default value is . /// The depth . The default value is 0. /// The method has not been called. /// is null. public void DrawTexture(Texture2D texture, ref Matrix2 transformMatrix, Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0) { var rectangle = default(Rectangle); _geometryBuilder.BuildSprite(_vertexCount, ref transformMatrix, texture, ref rectangle, color, flags, depth); EnqueueBuiltGeometry(texture, depth); } private void EnqueueBuiltGeometry(Texture2D texture, float depth) { if ((_vertexCount + _geometryBuilder.VertexCount > _vertices.Length) || (_indexCount + _geometryBuilder.IndexCount > _indices.Length)) Flush(); var drawCall = new DrawCallInfo(texture, _geometryBuilder.PrimitiveType, _indexCount, _geometryBuilder.PrimitivesCount, depth); Array.Copy(_geometryBuilder.Vertices, 0, _vertices, _vertexCount, _geometryBuilder.VertexCount); _vertexCount += _geometryBuilder.VertexCount; Array.Copy(_geometryBuilder.Indices, 0, _indices, _indexCount, _geometryBuilder.IndexCount); _indexCount += _geometryBuilder.IndexCount; Enqueue(ref drawCall); } /// /// Draws unicode (UTF-16) characters as sprites using the specified , text /// , transform and optional , origin /// , , and depth . /// /// The . /// The text . /// The transform . /// /// The . Use null to use the default /// . /// /// The . The default value is . /// The depth . The default value is 0f. /// The method has not been called. /// is null or is null. public void DrawString(BitmapFont bitmapFont, StringBuilder text, ref Matrix2 transformMatrix, Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0f) { EnsureHasBegun(); if (bitmapFont == null) throw new ArgumentNullException(nameof(bitmapFont)); if (text == null) throw new ArgumentNullException(nameof(text)); var lineSpacing = bitmapFont.LineHeight; var offset = new Vector2(0, 0); BitmapFontRegion lastGlyph = null; for (var i = 0; i < text.Length;) { int character; if (char.IsLowSurrogate(text[i])) { character = char.ConvertToUtf32(text[i - 1], text[i]); i += 2; } else if (char.IsHighSurrogate(text[i])) { character = char.ConvertToUtf32(text[i], text[i - 1]); i += 2; } else { character = text[i]; i += 1; } // ReSharper disable once SwitchStatementMissingSomeCases switch (character) { case '\r': continue; case '\n': offset.X = 0; offset.Y += lineSpacing; lastGlyph = null; continue; } var fontRegion = bitmapFont.GetCharacterRegion(character); if (fontRegion == null) continue; var transform1Matrix = transformMatrix; transform1Matrix.M31 += offset.X + fontRegion.XOffset; transform1Matrix.M32 += offset.Y + fontRegion.YOffset; var textureRegion = fontRegion.TextureRegion; var bounds = textureRegion.Bounds; DrawSprite(textureRegion.Texture, ref transform1Matrix, ref bounds, color, flags, depth); var advance = fontRegion.XAdvance + bitmapFont.LetterSpacing; if (BitmapFont.UseKernings && lastGlyph != null) { int amount; if (lastGlyph.Kernings.TryGetValue(character, out amount)) { advance += amount; } } offset.X += i != text.Length - 1 ? advance : fontRegion.XOffset + fontRegion.Width; lastGlyph = fontRegion; } } /// /// Draws unicode (UTF-16) characters as sprites using the specified , text /// , position and optional , rotation /// , origin , scale , and /// depth . /// /// The . /// The text . /// The position . /// /// The . Use null to use the default /// . /// /// /// The angle (in radians) to rotate each sprite about its . The default /// value is 0f. /// /// /// The origin . Use null to use the default /// . /// /// /// The scale . Use null to use the default /// . /// /// The . The default value is . /// The depth . The default value is 0f /// The method has not been called. /// is null or is null. public void DrawString(BitmapFont bitmapFont, StringBuilder text, Vector2 position, Color? color = null, float rotation = 0f, Vector2? origin = null, Vector2? scale = null, FlipFlags flags = FlipFlags.None, float depth = 0f) { Matrix2 transformMatrix; Matrix2.CreateFrom(position, rotation, scale, origin, out transformMatrix); DrawString(bitmapFont, text, ref transformMatrix, color, flags, depth); } /// /// Draws unicode (UTF-16) characters as sprites using the specified , text /// , transform and optional , origin /// , , and depth . /// /// The . /// The text . /// The transform . /// /// The . Use null to use the default /// . /// /// The . The default value is . /// The depth . The default value is 0f /// The method has not been called. /// is null or is null. public void DrawString(BitmapFont bitmapFont, string text, ref Matrix2 transformMatrix, Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0f) { EnsureHasBegun(); if (bitmapFont == null) throw new ArgumentNullException(nameof(bitmapFont)); if (text == null) throw new ArgumentNullException(nameof(text)); var glyphs = bitmapFont.GetGlyphs(text); foreach (var glyph in glyphs) { var transform1Matrix = transformMatrix; transform1Matrix.M31 += glyph.Position.X; transform1Matrix.M32 += glyph.Position.Y; var texture = glyph.FontRegion.TextureRegion.Texture; var bounds = texture.Bounds; DrawSprite(texture, ref transform1Matrix, ref bounds, color, flags, depth); } } /// /// Draws unicode (UTF-16) characters as sprites using the specified , text /// , position and optional , rotation /// , origin , scale , and /// depth . /// /// The . /// The text . /// The position . /// /// The . Use null to use the default /// . /// /// /// The angle (in radians) to rotate each sprite about its . The default /// value is 0f. /// /// /// The origin . Use null to use the default /// . /// /// /// The scale . Use null to use the default /// . /// /// The . The default value is . /// The depth . The default value is 0f /// The method has not been called. /// is null or is null. public void DrawString(BitmapFont bitmapFont, string text, Vector2 position, Color? color = null, float rotation = 0f, Vector2? origin = null, Vector2? scale = null, FlipFlags flags = FlipFlags.None, float depth = 0f) { Matrix2 matrix; Matrix2.CreateFrom(position, rotation, scale, origin, out matrix); DrawString(bitmapFont, text, ref matrix, color, flags, depth); } [StructLayout(LayoutKind.Sequential, Pack = 1)] [EditorBrowsable(EditorBrowsableState.Never)] public struct DrawCallInfo : IBatchDrawCallInfo, IComparable { internal readonly PrimitiveType PrimitiveType; internal int StartIndex; internal int PrimitiveCount; internal readonly Texture2D Texture; internal readonly uint TextureKey; internal readonly uint DepthKey; internal unsafe DrawCallInfo(Texture2D texture, PrimitiveType primitiveType, int startIndex, int primitiveCount, float depth) { PrimitiveType = primitiveType; StartIndex = startIndex; PrimitiveCount = primitiveCount; Texture = texture; TextureKey = (uint)RuntimeHelpers.GetHashCode(texture); DepthKey = *(uint*)&depth; } public void SetState(Effect effect) { var textureEffect = effect as ITextureEffect; if (textureEffect != null) textureEffect.Texture = Texture; } public bool TryMerge(ref DrawCallInfo drawCall) { if (PrimitiveType != drawCall.PrimitiveType || TextureKey != drawCall.TextureKey || DepthKey != drawCall.DepthKey) return false; drawCall.PrimitiveCount += PrimitiveCount; return true; } [SuppressMessage("ReSharper", "ImpureMethodCallOnReadonlyValueField")] public int CompareTo(DrawCallInfo other) { var result = TextureKey.CompareTo(other.TextureKey);; if (result != 0) return result; result = DepthKey.CompareTo(other.DepthKey); return result != 0 ? result : ((byte)PrimitiveType).CompareTo((byte)other.PrimitiveType); } } } }