Last active
July 2, 2021 20:04
-
-
Save Jjagg/bd0540ded0d399e716f25e00641488e1 to your computer and use it in GitHub Desktop.
Platform agnostic primitive batcher with implementation for MonoGame.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using System.Text; | |
using Microsoft.Xna.Framework; | |
namespace MgWheels | |
{ | |
public interface IPrimitiveBatcher | |
{ | |
void DrawLine(Vector2 p1, Vector2 p2, Color color, float lineWidth); | |
void DrawLineStrip(IEnumerable<Vector2> points, Color color, float lineWidth); | |
void DrawRect(RectangleF rect, Color color, float lineWidth); | |
void DrawRoundedRect(RectangleF rectangle, float radius, int segments, Color color, int lineWidth); | |
void DrawRoundedRect(RectangleF rectangle, | |
float radiusTl, int segmentsTl, | |
float radiusTr, int segmentsTr, | |
float radiusBr, int segmentsBr, | |
float radiusBl, int segmentsBl, | |
Color color, int lineWidth = 1); | |
void FillRect(RectangleF rect, Color c); | |
void FillRoundedRect(RectangleF rectangle, float radius, int segments, Color color); | |
void DrawCircle(Vector2 center, float radius, Color color, int sides, float lineWidth); | |
void DrawCircleSegment(Vector2 center, float radius, float start, float end, Color color, int sides, float lineWidth); | |
void FillCircle(Vector2 center, float radius, Color color, int sides); | |
void FillCircleSegment(Vector2 center, float radius, float start, float end, Color color, int sides); | |
void DrawString(StringBuilder text, Vector2 position, float size, Color color); | |
void Clear(); | |
void Flush(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Text; | |
using Microsoft.Xna.Framework; | |
using Microsoft.Xna.Framework.Graphics; | |
namespace MgWheels | |
{ | |
public class MgPrimitiveBatcher : PrimitiveBatcherBase<VertexPositionColorTexture, Matrix, Texture2D> | |
{ | |
private readonly GraphicsDevice _graphicsDevice; | |
private readonly SpriteBatch _spriteBatch; | |
private readonly SpriteFont _font; | |
private readonly BasicEffect _basicEffect; | |
private readonly VertexBuffer _vb; | |
private readonly IndexBuffer _ib; | |
public Texture2D BlankTexture { get; } | |
public MgPrimitiveBatcher(GraphicsDevice gd, SpriteFont font) | |
{ | |
_graphicsDevice = gd ?? throw new ArgumentNullException(nameof(gd)); | |
_font = font ?? throw new ArgumentNullException(nameof(font)); | |
_spriteBatch = new SpriteBatch(gd); | |
_basicEffect = new BasicEffect(gd) | |
{ | |
VertexColorEnabled = true, | |
LightingEnabled = false, | |
TextureEnabled = true, | |
FogEnabled = false, | |
}; | |
BlankTexture = new Texture2D(gd, 1, 1); | |
BlankTexture.SetData(new[] {Color.White.PackedValue}); | |
Texture = BlankTexture; | |
FontTexture = _font.Texture; | |
var viewport = gd.Viewport; | |
TransformMatrix = Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, 1); | |
_vb = new VertexBuffer(gd, VertexPositionColorTexture.VertexDeclaration, DefaultMaxVertices, BufferUsage.WriteOnly); | |
_ib = new IndexBuffer(gd, IndexElementSize.ThirtyTwoBits, DefaultMaxVertices, BufferUsage.WriteOnly); | |
} | |
protected override VertexPositionColorTexture CreateVertex(Vector2 p, Vector2 uv, Color color) | |
{ | |
return new VertexPositionColorTexture(new Vector3(p.X, p.Y, 0), new Color(color.PackedValue), new Vector2(uv.X, uv.Y)); | |
} | |
protected override void TransformVertexPosition(ref VertexPositionColorTexture v, Matrix transform) | |
{ | |
v.Position = Vector3.Transform(v.Position, transform); | |
} | |
protected override void SetTexture(Texture2D texture) | |
{ | |
if (texture == null) | |
throw new ArgumentNullException(nameof(texture)); | |
if (texture != _basicEffect.Texture) | |
_basicEffect.Texture = texture; | |
} | |
protected override void BeginFlush(VertexPositionColorTexture[] vertices, int vertexCount, int[] indices, int indexCount) | |
{ | |
_graphicsDevice.BlendState = BlendState.NonPremultiplied; | |
_graphicsDevice.DepthStencilState = DepthStencilState.None; | |
_graphicsDevice.RasterizerState = RasterizerState.CullNone; | |
_graphicsDevice.SamplerStates[0] = SamplerState.LinearWrap; | |
_vb.SetData(vertices, 0, vertexCount); | |
_ib.SetData(indices, 0, indexCount); | |
_graphicsDevice.SetVertexBuffer(_vb); | |
_graphicsDevice.Indices = _ib; | |
} | |
protected override void DrawBatch(int vertexCount, int indexOffset, int indexCount) | |
{ | |
_basicEffect.CurrentTechnique.Passes[0].Apply(); | |
_graphicsDevice.DrawIndexedPrimitives( | |
PrimitiveType.TriangleList, | |
0, indexOffset, indexCount / 3); | |
} | |
public override void DrawString(StringBuilder text, Vector2 position, float size, Color color) | |
{ | |
_spriteBatch.Begin(); | |
_spriteBatch.DrawString(_font, text, new Vector2(position.X, position.Y), new Color(color.PackedValue)); | |
_spriteBatch.End(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using Microsoft.Xna.Framework; | |
namespace MgWheels | |
{ | |
public abstract class PrimitiveBatcherBase<TVertex, TMatrix, TTexture> : IPrimitiveBatcher | |
{ | |
private class DrawInfo | |
{ | |
public readonly TTexture Texture; | |
public DrawInfo(TTexture texture) | |
{ | |
Texture = texture; | |
} | |
public bool Equals(DrawInfo other) | |
{ | |
return ReferenceEquals(Texture, other.Texture); | |
} | |
} | |
private class BatchInfo | |
{ | |
public readonly DrawInfo DrawInfo; | |
public readonly int Startindex; | |
public readonly int IndexCount; | |
public BatchInfo(DrawInfo drawInfo, int startindex, int indexCount) | |
{ | |
DrawInfo = drawInfo; | |
Startindex = startindex; | |
IndexCount = indexCount; | |
} | |
} | |
public const int DefaultMaxVertices = 2048; | |
public const int DefaultMaxIndices = 4096; | |
private readonly TVertex[] _vb; | |
private readonly int[] _ib; | |
public TMatrix TransformMatrix { get; set; } | |
private int _nextToDraw; | |
private int _indicesInBatch; | |
private DrawInfo _lastDrawInfo; | |
private int _verticesSubmitted; | |
private readonly List<BatchInfo> _batches; | |
// Render state | |
public TTexture Texture { get; set; } | |
public TTexture FontTexture { get; protected set; } | |
protected PrimitiveBatcherBase() | |
{ | |
_vb = new TVertex[DefaultMaxVertices]; | |
_ib = new int[DefaultMaxIndices]; | |
_batches = new List<BatchInfo>(); | |
} | |
#region Abstract Methods | |
protected abstract TVertex CreateVertex(Vector2 p, Vector2 uv, Color color); | |
protected abstract void TransformVertexPosition(ref TVertex vertexPositionColorTexture, TMatrix matrix); | |
protected abstract void SetTexture(TTexture textureId); | |
protected abstract void BeginFlush(TVertex[] vertices, int vertexCount, int[] indices, int indexCount); | |
protected abstract void DrawBatch(int vertexCount, int indexOffset, int indexCount); | |
#endregion | |
#region Line | |
public void DrawLine(Vector2 p1, Vector2 p2, Color color, float lineWidth = 1) | |
{ | |
var d = Vector2.Normalize(p2 - p1); | |
var dt = d.RotatedQuarter() * (lineWidth / 2f); | |
var v1 = CreateVertex(p1 - dt, Vector2.Zero, color); | |
var v2 = CreateVertex(p1 + dt, Vector2.Zero, color); | |
var v3 = CreateVertex(p2 + dt, Vector2.Zero, color); | |
var v4 = CreateVertex(p2 - dt, Vector2.Zero, color); | |
FillQuad(v1, v2, v3, v4); | |
} | |
public void DrawLineStrip(IEnumerable<Vector2> points, Color color, float lineWidth = 1) | |
{ | |
if (points.CountLessThan(2)) | |
throw new Exception("Need at least 2 vertices for a line strip."); | |
using (var enumerator = points.GetEnumerator()) | |
{ | |
enumerator.MoveNext(); | |
var p1 = enumerator.Current; | |
while (enumerator.MoveNext()) | |
{ | |
var p2 = enumerator.Current; | |
DrawLine(p1, p2, color, lineWidth); | |
p1 = p2; | |
} | |
} | |
} | |
#endregion | |
#region Rectangle | |
public void DrawRect(RectangleF rect, Color color, float lineWidth = 1) | |
{ | |
var p1 = new Vector2(rect.Left, rect.Top); | |
var p2 = new Vector2(rect.Right, rect.Top); | |
var p3 = new Vector2(rect.Right, rect.Bottom); | |
var p4 = new Vector2(rect.Left, rect.Bottom); | |
DrawLine(p1, p2, color, lineWidth); | |
DrawLine(p2, p3, color, lineWidth); | |
DrawLine(p3, p4, color, lineWidth); | |
DrawLine(p4, p1, color, lineWidth); | |
} | |
public void DrawRoundedRect(RectangleF rectangle, float radius, int segments, Color color, int lineWidth = 1) | |
{ | |
DrawRoundedRect(rectangle, radius, segments, radius, segments, radius, segments, radius, segments, color, lineWidth); | |
} | |
public void DrawRoundedRect(RectangleF rectangle, | |
float radiusTl, int segmentsTl, | |
float radiusTr, int segmentsTr, | |
float radiusBr, int segmentsBr, | |
float radiusBl, int segmentsBl, | |
Color color, int lineWidth = 1) | |
{ | |
if (radiusTl > rectangle.Width / 2f || radiusTl > rectangle.Height / 2f || | |
radiusTr > rectangle.Width / 2f || radiusTr > rectangle.Height / 2f || | |
radiusBr > rectangle.Width / 2f || radiusBr > rectangle.Height / 2f || | |
radiusBl > rectangle.Width / 2f || radiusBl > rectangle.Height / 2f) | |
throw new Exception("Radius too large"); | |
if (radiusTl == 0 && radiusTr == 0 && radiusBr == 0 && radiusBl == 0) | |
{ | |
DrawRect(rectangle, color, lineWidth); | |
return; | |
} | |
var outerRect = rectangle; | |
var tl = new Vector2(outerRect.Left + radiusTl, outerRect.Top + radiusTl); | |
var tr = new Vector2(outerRect.Right - radiusTr, outerRect.Top + radiusTr); | |
var bl = new Vector2(outerRect.Left + radiusBl, outerRect.Bottom - radiusBl); | |
var br = new Vector2(outerRect.Right - radiusBr, outerRect.Bottom - radiusBr); | |
DrawLine(new Vector2(tl.X, outerRect.Top), new Vector2(tr.X, outerRect.Top), color, lineWidth); | |
DrawLine(new Vector2(outerRect.Right, tr.Y), new Vector2(outerRect.Right, br.Y), color, lineWidth); | |
DrawLine(new Vector2(br.X, outerRect.Bottom), new Vector2(bl.X, outerRect.Bottom), color, lineWidth); | |
DrawLine(new Vector2(outerRect.Left, bl.Y), new Vector2(outerRect.Left, tr.Y), color, lineWidth); | |
if (radiusTl > 0) | |
DrawCircleSegment(tl, radiusTl, LeftAngle, TopAngle, color, segmentsTl, lineWidth); | |
if (radiusTr > 0) | |
DrawCircleSegment(tr, radiusTr, TopAngle, RightEndAngle, color, segmentsTr, lineWidth); | |
if (radiusBr > 0) | |
DrawCircleSegment(br, radiusBr, RightStartAngle, BotAngle, color, segmentsBr, lineWidth); | |
if (radiusBl > 0) | |
DrawCircleSegment(bl, radiusBl, BotAngle, LeftAngle, color, segmentsBl, lineWidth); | |
} | |
public void FillQuad(TVertex v0, TVertex v1, TVertex v2, TVertex v3) | |
{ | |
var di = new DrawInfo(Texture); | |
CheckFlush(di); | |
var i1 = AddVertex(v0); | |
var i2 = AddVertex(v1); | |
var i3 = AddVertex(v2); | |
var i4 = AddVertex(v3); | |
AddIndex(i1); | |
AddIndex(i2); | |
AddIndex(i4); | |
AddIndex(i4); | |
AddIndex(i2); | |
AddIndex(i3); | |
} | |
public void FillRect(RectangleF rect, Color c) | |
{ | |
FillRect(rect, c, c, c, c); | |
} | |
public void FillRect(RectangleF rect, Color c1, Color c2, Color c3, Color c4) | |
{ | |
var v1 = CreateVertex(new Vector2(rect.Left, rect.Top), Vector2.Zero, c1); | |
var v2 = CreateVertex(new Vector2(rect.Right, rect.Top), Vector2.UnitX, c2); | |
var v3 = CreateVertex(new Vector2(rect.Right, rect.Bottom), Vector2.One, c3); | |
var v4 = CreateVertex(new Vector2(rect.Left, rect.Bottom), Vector2.UnitY, c4); | |
FillQuad(v1, v2, v3, v4); | |
} | |
public void FillRoundedRect(RectangleF rectangle, float radius, int segments, Color color) | |
{ | |
if (radius > rectangle.Width / 2f || radius > rectangle.Height / 2f) | |
throw new Exception("Radius too large"); | |
if (radius == 0) | |
{ | |
FillRect(rectangle, color); | |
return; | |
} | |
var outerRect = rectangle; | |
var innerRect = rectangle; | |
innerRect = innerRect.Inflate(-radius, -radius); | |
FillRect(innerRect, color); | |
// TODO UV's | |
FillRect(new RectangleF(outerRect.Left, innerRect.Top, radius, innerRect.Height), color); // left | |
FillRect(new RectangleF(innerRect.Right, innerRect.Top, radius, innerRect.Height), color); // right | |
FillRect(new RectangleF(innerRect.Left, outerRect.Top, innerRect.Width, radius), color); // top | |
FillRect(new RectangleF(innerRect.Left, innerRect.Bottom, innerRect.Width, radius), color); // top | |
var tl = new Vector2(innerRect.Left, innerRect.Top); | |
var tr = new Vector2(innerRect.Right, innerRect.Top); | |
var bl = new Vector2(innerRect.Left, innerRect.Bottom); | |
var br = new Vector2(innerRect.Right, innerRect.Bottom); | |
FillCircleSegment(tl, radius, LeftAngle, TopAngle, color, segments); | |
FillCircleSegment(tr, radius, TopAngle, RightEndAngle, color, segments); | |
FillCircleSegment(br, radius, RightStartAngle, BotAngle, color, segments); | |
FillCircleSegment(bl, radius, BotAngle, LeftAngle, color, segments); | |
} | |
#endregion | |
#region Circle | |
private const float LeftAngle = (float) Math.PI; | |
private const float TopAngle = (float) (1.5 * Math.PI); | |
private const float RightStartAngle = 0; | |
private const float RightEndAngle = (float) (2 * Math.PI); | |
private const float BotAngle = (float) (.5 * Math.PI); | |
public void DrawCircle(Vector2 center, float radius, Color color, int sides, float lineWidth = 1) | |
{ | |
DrawCircleSegment(center, radius, RightStartAngle, RightEndAngle, color, sides, lineWidth); | |
} | |
public void DrawCircleSegment(Vector2 center, float radius, float start, float end, Color color, int sides, float lineWidth = 1) | |
{ | |
var ps = CreateCircleSegment(center, radius, sides, start, end); | |
DrawLineStrip(ps, color, lineWidth); | |
} | |
public void FillCircle(Vector2 center, float radius, Color color, int sides) | |
{ | |
FillCircleSegment(center, radius, RightStartAngle, RightEndAngle, color, sides); | |
} | |
public void FillCircleSegment(Vector2 center, float radius, float start, float end, Color color, int sides) | |
{ | |
var ps = CreateCircleSegment(center, radius, sides, start, end); | |
FillTriangleFan(center, ps, color); | |
} | |
public abstract void DrawString(StringBuilder text, Vector2 position, float size, Color color); | |
private static IEnumerable<Vector2> CreateCircleSegment(Vector2 center, float radius, int sides, float start, float end) | |
{ | |
var step = (end - start) / sides; | |
var theta = start; | |
for (var i = 0; i < sides; i++) | |
{ | |
yield return center + new Vector2((float) (radius * Math.Cos(theta)), (float) (radius * Math.Sin(theta))); | |
theta += step; | |
} | |
yield return center + new Vector2((float) (radius * Math.Cos(end)), (float) (radius * Math.Sin(end))); | |
} | |
#endregion | |
#region Low level | |
public void FillTriangleStrip(IEnumerable<Vector2> ps, Color? c = null) | |
{ | |
var di = new DrawInfo(Texture); | |
CheckFlush(di); | |
if (ps.CountLessThan(3)) | |
throw new Exception("Need at least 3 vertices for a triangle strip."); | |
var color = c ?? Color.White; | |
using (var en = ps.GetEnumerator()) | |
{ | |
en.MoveNext(); | |
var v1 = AddVertex(en.Current, color); | |
en.MoveNext(); | |
var v2 = AddVertex(en.Current, color); | |
while (en.MoveNext()) | |
{ | |
var v3 = AddVertex(en.Current, color); | |
AddIndex(v1); | |
AddIndex(v2); | |
AddIndex(v3); | |
v1 = v2; | |
v2 = v3; | |
} | |
} | |
} | |
public void FillTriangleFan(Vector2 center, IEnumerable<Vector2> ps, Color color) | |
{ | |
// TODO UV's | |
FillTriangleFan(CreateVertex(center, Vector2.Zero, color), ps.Select(p => CreateVertex(p, Vector2.Zero, color))); | |
} | |
public void FillTriangleFan(TVertex center, IEnumerable<TVertex> ps) | |
{ | |
var di = new DrawInfo(Texture); | |
CheckFlush(di); | |
if (ps.CountLessThan(3)) | |
throw new Exception("Need at least 3 vertices for a triangle fan."); | |
using (var en = ps.GetEnumerator()) | |
{ | |
en.MoveNext(); | |
var centerIndex = AddVertex(center); | |
var v0 = AddVertex(en.Current); | |
var v1 = v0; | |
while (en.MoveNext()) | |
{ | |
var v2 = AddVertex(en.Current); | |
AddIndex(centerIndex); | |
AddIndex(v1); | |
AddIndex(v2); | |
v1 = v2; | |
} | |
} | |
} | |
#endregion | |
#region Flush | |
public void Clear() | |
{ | |
_batches.Clear(); | |
_nextToDraw = 0; | |
_indicesInBatch = 0; | |
_verticesSubmitted = 0; | |
_lastDrawInfo = null; | |
} | |
public void Flush() | |
{ | |
// register last batch | |
RegisterFlush(); | |
if (!_batches.Any()) | |
return; | |
BeginFlush(_vb, _verticesSubmitted, _ib, _nextToDraw); | |
foreach (var b in _batches) | |
{ | |
SetTexture(b.DrawInfo.Texture); | |
DrawBatch(_verticesSubmitted, b.Startindex, b.IndexCount); | |
} | |
Clear(); | |
} | |
private void CheckFlush(DrawInfo di) | |
{ | |
if (_lastDrawInfo != null && !_lastDrawInfo.Equals(di)) | |
RegisterFlush(); | |
_lastDrawInfo = di; | |
} | |
private void RegisterFlush() | |
{ | |
// if nothing to flush | |
if (_indicesInBatch == 0) | |
return; | |
var bi = new BatchInfo(_lastDrawInfo, _nextToDraw, _indicesInBatch); | |
_batches.Add(bi); | |
_nextToDraw = _nextToDraw + _indicesInBatch; | |
_indicesInBatch = 0; | |
} | |
#endregion | |
private int AddVertex(Vector2 position, Color color) | |
{ | |
return AddVertex(CreateVertex(position, Vector2.Zero, color)); | |
} | |
private int AddVertex(Vector2 position, Color color, Vector2 uv) | |
{ | |
return AddVertex(CreateVertex(position, uv, color)); | |
} | |
private int AddVertex(TVertex v) | |
{ | |
TransformVertexPosition(ref v, TransformMatrix); | |
var i = _verticesSubmitted; | |
_vb[i] = v; | |
_verticesSubmitted++; | |
return i; | |
} | |
private void AddIndex(int index) | |
{ | |
var i = _nextToDraw + _indicesInBatch; | |
_ib[i] = index; | |
_indicesInBatch++; | |
} | |
} | |
} |
What's the logic in the .RotatedQuarter function on line 81?
Thanks for answer!
@DanielVampire it’s a 90 degree rotation, although I’m not sure what direction. I think you want new Vector2(-v.X, v.Y)
where v
is the vector to rotate.
However, I’ve since refactored this and expanded it a bunch. It now lives in my OpenWheels repo: https://github.com/Jjagg/OpenWheels.
The batcher is here: https://github.com/Jjagg/OpenWheels/blob/master/src/OpenWheels.Rendering/Batcher.cs.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
As there are no Begin & End methods, how do we actually use this? I have trouble getting view transform matrix to work with this, passed standard MonoGame.Extended.Camera2D matrix in (which works nice) but that had no result.
Thanks in advance.