using System; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; 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 { /// /// Enables a group of dynamic two-dimensional geometric objects be batched together for rendering, if possible, using /// the same settings. /// /// public class DynamicBatchRenderer2D : DynamicBatchRenderer { internal const int DefaultMaximumVerticesCount = 8192; internal const int DefaultMaximumIndicesCount = 12288; private readonly SpriteBuilderPositionColorTextureU16 _spriteBuilder; /// /// Initializes a new instance of the class. /// /// The graphics device. /// The maximum number of vertices. The default value is 8192. /// The maximum number of indices. The default value is 12288. /// /// The maximum number of draw calls that can be deferred before they have to be /// submitted to the . /// /// . /// /// is less than or equal /// 0, or is less than or equal to 0, or, /// is less than or equal to 0. /// public DynamicBatchRenderer2D(GraphicsDevice graphicsDevice, ushort maximumVerticesCount = DefaultMaximumVerticesCount, ushort maximumIndicesCount = DefaultMaximumIndicesCount, int maximumDrawCallsCount = DefaultMaximumDrawCallsCount) : base( graphicsDevice, new DefaultEffect2D(graphicsDevice), new GraphicsGeometryBufferPositionColorTextureU16(graphicsDevice, maximumVerticesCount, maximumIndicesCount, true), maximumDrawCallsCount) { _spriteBuilder = new SpriteBuilderPositionColorTextureU16(); } /// /// 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 Matrix2D transformMatrix, ref Rectangle sourceRectangle, Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0) { _spriteBuilder.BuildSprite(ref transformMatrix, texture, ref sourceRectangle, color, flags, depth); var commandData = new DrawCallData(texture); DrawGeometry(_spriteBuilder, ref commandData, 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 Matrix2D transformMatrix, Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0) { _spriteBuilder.BuildSprite(ref transformMatrix, texture, color, flags, depth); var commandData = new DrawCallData(texture); DrawGeometry(_spriteBuilder, ref commandData, depth); } private void DrawGeometry(GraphicsGeometryBuilder geometryBuilder, ref DrawCallData drawCallData, float depth) { if (GeometryBuffer.VerticesCount + geometryBuilder.Vertices.Length > GeometryBuffer.Vertices.Length || GeometryBuffer.IndicesCount + geometryBuilder.Indices.Length > GeometryBuffer.Indices.Length) { Flush(); } var startVertex = GeometryBuffer.VerticesCount; var indexOffset = startVertex; Array.Copy(geometryBuilder.Vertices, 0, GeometryBuffer.Vertices, startVertex, geometryBuilder.Vertices.Length); GeometryBuffer.VerticesCount += geometryBuilder.Vertices.Length; var startIndex = GeometryBuffer.EnqueueIndicesFrom(geometryBuilder.Indices, 0, geometryBuilder.Indices.Length, indexOffset); ulong key; GenerateDrawCallKey(drawCallData.TextureKey, depth, out key); var drawCall = new DrawCall { PrimitiveType = geometryBuilder.PrimitiveType, Key = key, BaseVertex = 0, StartIndex = startIndex, PrimitiveCount = geometryBuilder.PrimitivesCount, Data = drawCallData }; if (TryEnqueueDrawCall(ref drawCall)) return; Flush(); TryEnqueueDrawCall(ref drawCall); } private static unsafe void GenerateDrawCallKey(int textureKey, float depth, out ulong key) { var depthKey = *(uint*)&depth; const ulong textureMask = 0x00000000ffffffffUL; const ulong depthMask = 0xffffffff00000000UL; // the key is used to group draw calls to minimize state changes // for general purposes we want to group draw calls by depth then by material // a more advanced renderer would consider wether the geometry is opaque and if so group by texture first instead key = depthKey & depthMask | (uint)textureKey & textureMask; } /// /// 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 Matrix2D 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); 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; 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); offset.X += i != text.Length - 1 ? fontRegion.XAdvance + bitmapFont.LetterSpacing : fontRegion.XOffset + fontRegion.Width; } } /// /// 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) { Matrix2D transformMatrix; Matrix2D.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 Matrix2D 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); 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; 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); offset.X += i != text.Length - 1 ? fontRegion.XAdvance + bitmapFont.LetterSpacing : fontRegion.XOffset + fontRegion.Width; } } /// /// 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) { Matrix2D matrix; Matrix2D.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 DrawCallData : IDrawCallData { public Texture2D Texture; public int TextureKey; internal DrawCallData(Texture2D texture) { Texture = texture; TextureKey = RuntimeHelpers.GetHashCode(texture); } public void ApplyTo(Effect effect) { var textureEffect = effect as ITexture2DEffect; if (textureEffect != null) textureEffect.Texture = Texture; } public void SetReferencesToNull() { Texture = null; } public bool Equals(ref DrawCallData other) { return Texture == other.Texture; } public int CompareTo(DrawCallData other) { return TextureKey.CompareTo(other.TextureKey); } } } }