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);
}
}
}
}