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.Batching;
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 : BatchRenderer
{
internal const int DefaultMaximumVerticesCount = 8192;
internal const int DefaultMaximumIndicesCount = 12288;
private readonly DefaultEffect2D _defaultEffect;
private readonly Texture2D _pixelTexture;
private BlendState _blendState;
private DepthStencilState _depthStencilState;
private Effect _effect;
private DrawCommandData _pixelTextureDrawCommandData;
private Matrix? _projectionMatrix;
private RasterizerState _rasterizerState;
private SamplerState _samplerState;
private Batch2DSortMode _sortMode;
private Matrix _viewMatrix;
private Matrix _worldMatrix;
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 null.
///
/// 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 maximumBatchCommandsCount = DefaultMaximumBatchCommandsCount)
: base(
graphicsDevice,
new DynamicVertexBuffer(graphicsDevice, VertexPositionColorTexture.VertexDeclaration,
maximumVerticesCount, BufferUsage.WriteOnly),
new DynamicIndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, maximumIndicesCount,
BufferUsage.WriteOnly),
new GraphicsGeometryData(maximumVerticesCount, maximumIndicesCount),
maximumBatchCommandsCount)
{
_defaultEffect = new DefaultEffect2D(graphicsDevice);
_pixelTexture = new Texture2D(graphicsDevice, 1, 1);
_pixelTexture.SetData(new[]
{
Color.White
});
_pixelTextureDrawCommandData = new DrawCommandData
{
Texture = _pixelTexture
};
_spriteBuilder = new SpriteBuilderPositionColorTextureU16();
}
///
/// Releases unmanaged and - optionally - managed resources.
///
///
/// true to release both managed and unmanaged resources; false to release only
/// unmanaged resources.
///
protected override void Dispose(bool disposing)
{
if (!disposing)
return;
_pixelTextureDrawCommandData.Texture = null;
_pixelTexture?.Dispose();
}
///
/// Starts a group of two-dimensional geometry for rendering with the specified ,
/// and the optional chain of es for transforming between world, view, and
/// projection spaces.
///
/// The . Default value is .
///
/// The . Use null to use the default
/// .
///
///
/// The . Use null to use the default
/// .
///
///
/// The . Use null to use the default
/// .
///
///
/// The . Use null to use the default
/// .
///
///
/// The . Use null to use the default .
///
///
/// The to transform vertices from a common model-space to world-space. Use null to
/// use .
///
///
/// The to transform vertices from the world-space to view-space. Use null to
/// use .
///
///
/// The to transform vertices from the view-space to projection-space. Use null to
/// use an orthographic projection (a 2D projection) with (0,0) as the top-left and (x,y) as
/// the bottom-right of the viewport.
///
public void Begin(Batch2DSortMode sortMode = Batch2DSortMode.Deferred, BlendState blendState = null,
SamplerState samplerState = null, DepthStencilState depthStencilState = null,
RasterizerState rasterizerState = null, Effect effect = null,
Matrix? worldMatrix = null,
Matrix? viewMatrix = null, Matrix? projectionMatrix = null)
{
_blendState = blendState ?? BlendState.AlphaBlend;
_samplerState = samplerState ?? SamplerState.LinearClamp;
_depthStencilState = depthStencilState ?? DepthStencilState.None;
_rasterizerState = rasterizerState ?? RasterizerState.CullCounterClockwise;
_effect = effect ?? _defaultEffect;
_worldMatrix = worldMatrix ?? Matrix.Identity;
_viewMatrix = viewMatrix ?? Matrix.Identity;
_projectionMatrix = projectionMatrix;
BatchSortMode batchSortMode;
switch (sortMode)
{
case Batch2DSortMode.Texture:
case Batch2DSortMode.FrontToBack:
case Batch2DSortMode.BackToFront:
batchSortMode = BatchSortMode.DeferredSorted;
break;
case Batch2DSortMode.Deferred:
batchSortMode = BatchSortMode.Deferred;
break;
case Batch2DSortMode.Immediate:
batchSortMode = BatchSortMode.Immediate;
ApplyStates();
break;
default:
throw new ArgumentOutOfRangeException(nameof(sortMode), sortMode, null);
}
_sortMode = sortMode;
Begin(_effect, batchSortMode);
}
///
/// Ends and submits the group of geometry to the
/// for rendering.
///
///
/// This method must be called after all enqueuing of draw calls.
///
public override void End()
{
if (_sortMode != Batch2DSortMode.Immediate)
ApplyStates();
base.End();
GeometryData.VerticesCount = 0;
GeometryData.IndicesCount = 0;
}
protected override void Flush()
{
base.Flush();
GeometryData.VerticesCount = 0;
GeometryData.IndicesCount = 0;
}
private void ApplyStates()
{
var graphicsDevice = GraphicsDevice;
graphicsDevice.BlendState = _blendState;
graphicsDevice.SamplerStates[0] = _samplerState;
graphicsDevice.DepthStencilState = _depthStencilState;
graphicsDevice.RasterizerState = _rasterizerState;
var matrixChainEffect = _effect as IMatrixChainEffect;
if (matrixChainEffect == null)
return;
matrixChainEffect.SetWorld(ref _worldMatrix);
matrixChainEffect.SetView(ref _viewMatrix);
if (_projectionMatrix.HasValue)
{
matrixChainEffect.Projection = _projectionMatrix.Value;
}
else
{
var viewport = GraphicsDevice.Viewport;
var projectionOffset = Matrix.CreateTranslation(-0.5f, -0.5f, 0);
var projection = Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, -1);
Matrix.Multiply(ref projectionOffset, ref projection, out projection);
matrixChainEffect.SetProjection(ref projection);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private float GetSortKey(float depth)
{
// the sort key acts as the primary sort key
// the secondary sort key is always the texture
// ReSharper disable once SwitchStatementMissingSomeCases
switch (_sortMode)
{
case Batch2DSortMode.FrontToBack:
return depth;
case Batch2DSortMode.BackToFront:
return -depth;
default:
return 0;
}
}
///
/// Draws a sprite using a specified , transform and an optional
/// source , , origin ,
/// , and depth .
///
/// The .
///
/// The destination that specifies the world destination for
/// drawing the sprite. If this rectangle is not the same size as the , the sprite
/// will be scaled to fit.
///
///
/// The texture region of the . Use
/// null to use the entire .
///
/// The . Use null to use the default .
///
/// The angle (in radians) to rotate the sprite about its . The default
/// value is 0f.
///
///
/// The origin . 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.
[Obsolete("The Draw method is deprecated, please use the DrawSprite method instead.")]
public void Draw(Texture2D texture, Rectangle destinationRectangle, Rectangle? sourceRectangle = null,
Color? color = null, float rotation = 0f, Vector2? origin = null, SpriteEffects effects = SpriteEffects.None,
float depth = 0)
{
DrawSprite(texture, destinationRectangle, sourceRectangle, color, rotation, origin, effects, depth);
}
///
/// Draws a sprite using a specified , transform and an optional
/// source , , origin ,
/// , and depth .
///
/// The .
///
/// The destination that specifies the world destination for
/// drawing the sprite. If this rectangle is not the same size as the , the sprite
/// will be scaled to fit.
///
///
/// The texture region of the . Use
/// null to use the entire .
///
/// The . Use null to use the default .
///
/// The angle (in radians) to rotate the sprite about its . The default
/// value is 0f.
///
///
/// The origin . 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, Rectangle destinationRectangle, Rectangle? sourceRectangle = null,
Color? color = null, float rotation = 0f, Vector2? origin = null, SpriteEffects effects = SpriteEffects.None,
float depth = 0)
{
var position = new Vector2(destinationRectangle.X, destinationRectangle.Y);
var size = new Size(destinationRectangle.Width, destinationRectangle.Height);
Matrix2D transformMatrix;
Matrix2D.CreateFrom(position, rotation, null, origin, out transformMatrix);
_spriteBuilder.BuildSprite(ref transformMatrix, texture, sourceRectangle, color, size, effects, depth);
var commandData = new DrawCommandData(texture);
DrawGeometry(_spriteBuilder, ref commandData, depth);
}
///
/// Draws a sprite using a specified , transform and an optional
/// source , , 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, Rectangle? sourceRectangle = null,
Color? color = null, SpriteEffects effects = SpriteEffects.None, float depth = 0)
{
_spriteBuilder.BuildSprite(ref transformMatrix, texture, sourceRectangle, color, null, effects, depth);
var commandData = new DrawCommandData(texture);
DrawGeometry(_spriteBuilder, ref commandData, depth);
}
private unsafe void DrawGeometry(GraphicsGeometryBuilder geometryBuilder, ref DrawCommandData drawCommandData, float depth)
{
if (GeometryData.VerticesCount + geometryBuilder.Vertices.Length > GeometryData.Vertices.Length || GeometryData.IndicesCount + geometryBuilder.Indices.Length > GeometryData.Indices.Length)
{
Flush();
}
var startVertex = GeometryData.VerticesCount;
var startIndex = GeometryData.IndicesCount;
var indexOffset = (ushort)startVertex;
Array.Copy(geometryBuilder.Vertices, 0, GeometryData.Vertices, startVertex, geometryBuilder.Vertices.Length);
fixed (ushort* fixedDestinationPointer = GeometryData.Indices)
fixed (ushort* fixedSourcePointer = geometryBuilder.Indices)
{
var pointerDestination = fixedDestinationPointer + startIndex;
var pointerDestinationEnd = pointerDestination + geometryBuilder.Indices.Length;
var pointerSource = fixedSourcePointer;
while (pointerDestination < pointerDestinationEnd)
{
*pointerDestination++ = (ushort)(*pointerSource++ + indexOffset);
}
}
GeometryData.VerticesCount += geometryBuilder.Vertices.Length;
GeometryData.IndicesCount += geometryBuilder.Indices.Length;
var sortKey = GetSortKey(depth);
Draw(geometryBuilder.PrimitiveType, startIndex, geometryBuilder.PrimitivesCount, sortKey, ref drawCommandData);
}
///
/// Draws a sprite using a specified , position and an optional source
/// , , rotation , origin , scale
/// , , and depth .
///
/// The .
/// The world position .
///
/// The texture region of the . Use
/// null to use the entire .
///
/// The . Use null to use the default .
///
/// The angle (in radians) to rotate the 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.
public void DrawSprite(Texture2D texture, Vector2 position, Rectangle? sourceRectangle = null,
Color? color = null, float rotation = 0f, Vector2? origin = null, Vector2? scale = null,
SpriteEffects effects = SpriteEffects.None,
float depth = 0)
{
Matrix2D transformMatrix;
Matrix2D.CreateFrom(position, rotation, scale, origin, out transformMatrix);
DrawSprite(texture, ref transformMatrix, sourceRectangle, color, effects, depth);
}
///
/// Draws a sprite using a specified , position and an optional source
/// , , rotation , origin , scale
/// , , and depth .
///
/// The .
/// The world position .
///
/// The texture region of the . Use
/// null to use the entire .
///
/// The . Use null to use the default .
///
/// The angle (in radians) to rotate the 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.
[Obsolete("The Draw method is deprecated, please use the DrawSprite method instead.")]
public void Draw(Texture2D texture, Vector2 position, Rectangle? sourceRectangle = null,
Color? color = null, float rotation = 0f, Vector2? origin = null, Vector2? scale = null,
SpriteEffects effects = SpriteEffects.None,
float depth = 0)
{
DrawSprite(texture, position, sourceRectangle, color, rotation, origin, scale, effects, 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, StringBuilder text, ref Matrix2D transformMatrix,
Color? color = null, SpriteEffects effects = SpriteEffects.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, bounds, color, effects, 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,
SpriteEffects effects = SpriteEffects.None, float depth = 0f)
{
Matrix2D transformMatrix;
Matrix2D.CreateFrom(position, rotation, scale, origin, out transformMatrix);
DrawString(bitmapFont, text, ref transformMatrix, color, effects, 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, SpriteEffects effects = SpriteEffects.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, bounds, color, effects, 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,
SpriteEffects effects = SpriteEffects.None, float depth = 0f)
{
Matrix2D matrix;
Matrix2D.CreateFrom(position, rotation, scale, origin, out matrix);
DrawString(bitmapFont, text, ref matrix, color, effects, depth);
}
///
/// Defines a drawing context for two-dimensional geometric objects.
///
[StructLayout(LayoutKind.Sequential, Pack = 1)]
[EditorBrowsable(EditorBrowsableState.Never)]
public struct DrawCommandData : IBatchDrawCommandData
{
public Texture2D Texture;
public int TextureKey;
internal DrawCommandData(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 DrawCommandData other)
{
return Texture == other.Texture;
}
public int CompareTo(DrawCommandData other)
{
return TextureKey.CompareTo(other.TextureKey);
}
}
}
}