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