Update README.md to include project description, developer documentation links, and license information.
CI / build-test (push) Successful in 1m6s
CI / build-test (push) Successful in 1m6s
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
||||
namespace MrGameEng.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// The engine's 2D renderer: a sprite batcher over dynamic vertex buffers.
|
||||
/// Per frame: <see cref="BeginFrame"/> (camera) → <see cref="Submit"/> per sprite (with culling)
|
||||
/// → <see cref="EndFrame"/> (sort layer → depth → texture, build vertices, issue draw calls).
|
||||
/// Registered as a service; scenes attach it via <c>scene.UseRenderer2D()</c>.
|
||||
/// </summary>
|
||||
public sealed class Renderer2D : IDisposable
|
||||
{
|
||||
private const int MaxQuadsPerDraw = 8192;
|
||||
|
||||
/// <summary>Render layer registry. Register layers before the first frame.</summary>
|
||||
public LayerRegistry Layers { get; } = new();
|
||||
|
||||
/// <summary>Camera state of the current frame. Valid between BeginFrame and the next BeginFrame.</summary>
|
||||
public CameraState Camera { get; private set; }
|
||||
|
||||
/// <summary>Draw calls issued by the last <see cref="EndFrame"/>.</summary>
|
||||
public int DrawCalls { get; private set; }
|
||||
|
||||
/// <summary>Sprites accepted by <see cref="Submit"/> this frame.</summary>
|
||||
public int SubmittedSprites { get; private set; }
|
||||
|
||||
/// <summary>Sprites rejected by culling this frame.</summary>
|
||||
public int CulledSprites { get; private set; }
|
||||
|
||||
private readonly GraphicsDevice _device;
|
||||
private readonly Renderer2DOptions _options;
|
||||
private readonly SpriteBatcher _batcher;
|
||||
private readonly BasicEffect _effect;
|
||||
private readonly IndexBuffer _indexBuffer;
|
||||
private DynamicVertexBuffer _vertexBuffer;
|
||||
private VertexPositionColorTexture[] _vertices;
|
||||
private CameraState _screenCamera;
|
||||
private bool _begun;
|
||||
|
||||
/// <summary>Creates the renderer. One instance per game is enough.</summary>
|
||||
public Renderer2D(GraphicsDevice device, Renderer2DOptions? options = null)
|
||||
{
|
||||
_device = device;
|
||||
_options = options ?? new Renderer2DOptions();
|
||||
_batcher = new SpriteBatcher(_options.InitialCapacity);
|
||||
_vertices = new VertexPositionColorTexture[_options.InitialCapacity * 4];
|
||||
_vertexBuffer = new DynamicVertexBuffer(
|
||||
device, VertexPositionColorTexture.VertexDeclaration, _vertices.Length, BufferUsage.WriteOnly);
|
||||
|
||||
_effect = new BasicEffect(device)
|
||||
{
|
||||
TextureEnabled = true,
|
||||
VertexColorEnabled = true,
|
||||
World = Matrix.Identity,
|
||||
};
|
||||
|
||||
_indexBuffer = CreateQuadIndexBuffer(device);
|
||||
}
|
||||
|
||||
/// <summary>Begins a frame with the given camera. Called by <see cref="CameraSystem"/>.</summary>
|
||||
public void BeginFrame(in Camera camera)
|
||||
{
|
||||
var (virtualW, virtualH, mapping) = ResolveVirtualResolution();
|
||||
Camera = CameraMath.Compute(camera, virtualW, virtualH, mapping);
|
||||
_screenCamera = CameraMath.Compute(
|
||||
new Camera(new Vector2(virtualW / 2f, virtualH / 2f)), virtualW, virtualH, mapping);
|
||||
|
||||
_batcher.Clear();
|
||||
SubmittedSprites = 0;
|
||||
CulledSprites = 0;
|
||||
_begun = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins a frame with a default camera that shows the world origin at the top-left
|
||||
/// corner of the screen. Used when the scene has no camera entity.
|
||||
/// </summary>
|
||||
public void BeginFrameWithDefaultCamera()
|
||||
{
|
||||
var (virtualW, virtualH, _) = ResolveVirtualResolution();
|
||||
var camera = new Camera(new Vector2(virtualW / 2f, virtualH / 2f));
|
||||
BeginFrame(in camera);
|
||||
}
|
||||
|
||||
/// <summary>Submits one sprite. Invisible sprites (outside the camera) are culled here.</summary>
|
||||
public void Submit(in Transform2D transform, in Sprite sprite)
|
||||
{
|
||||
if (!_begun)
|
||||
{
|
||||
throw new InvalidOperationException("Submit called outside BeginFrame/EndFrame (is CameraSystem registered first?).");
|
||||
}
|
||||
|
||||
if (sprite.Region is not { } region)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var layer = Layers[sprite.Layer];
|
||||
var (center, radius) = CullingMath.SpriteBoundingCircle(transform, region.Width, region.Height, sprite.Origin);
|
||||
|
||||
if (layer.Space == LayerSpace.World &&
|
||||
!CullingMath.CircleIntersectsRect(center, radius, Camera.CullRect))
|
||||
{
|
||||
CulledSprites++;
|
||||
return;
|
||||
}
|
||||
|
||||
var depth = layer.SortMode == LayerSortMode.YSort ? center.Y : sprite.Depth;
|
||||
|
||||
_batcher.Submit(
|
||||
new SpriteInstance
|
||||
{
|
||||
Region = region,
|
||||
Center = center,
|
||||
HalfSize = new Vector2(region.Width * transform.Scale.X, region.Height * transform.Scale.Y) / 2f,
|
||||
Rotation = transform.Rotation,
|
||||
Color = sprite.Color,
|
||||
Flip = sprite.Flip,
|
||||
Layer = sprite.Layer.Value,
|
||||
},
|
||||
SpriteSortKey.Make(sprite.Layer.Value, depth, region.TextureSortKey));
|
||||
SubmittedSprites++;
|
||||
}
|
||||
|
||||
/// <summary>Sorts, builds vertices and issues draw calls. Called by <see cref="RenderFlushSystem"/>.</summary>
|
||||
public void EndFrame()
|
||||
{
|
||||
if (!_begun)
|
||||
{
|
||||
throw new InvalidOperationException("EndFrame called without BeginFrame.");
|
||||
}
|
||||
|
||||
_begun = false;
|
||||
DrawCalls = 0;
|
||||
|
||||
var order = _batcher.Sort();
|
||||
if (order.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureVertexCapacity(order.Length * 4);
|
||||
BuildVertices(order);
|
||||
_vertexBuffer.SetData(_vertices, 0, order.Length * 4, SetDataOptions.Discard);
|
||||
|
||||
_device.BlendState = BlendState.AlphaBlend;
|
||||
_device.SamplerStates[0] = _options.Sampler;
|
||||
_device.DepthStencilState = DepthStencilState.None;
|
||||
_device.RasterizerState = RasterizerState.CullNone;
|
||||
_device.SetVertexBuffer(_vertexBuffer);
|
||||
_device.Indices = _indexBuffer;
|
||||
|
||||
DrawBatches(order);
|
||||
}
|
||||
|
||||
/// <summary>Converts a physical screen point to world coordinates using the current camera.</summary>
|
||||
public Vector2 ScreenToWorld(Vector2 screen) => Camera.ScreenToWorld(screen);
|
||||
|
||||
/// <summary>Converts a world point to physical screen coordinates using the current camera.</summary>
|
||||
public Vector2 WorldToScreen(Vector2 world) => Camera.WorldToScreen(world);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_effect.Dispose();
|
||||
_vertexBuffer.Dispose();
|
||||
_indexBuffer.Dispose();
|
||||
}
|
||||
|
||||
private (int Width, int Height, ViewportMapping Mapping) ResolveVirtualResolution()
|
||||
{
|
||||
var viewport = _device.Viewport;
|
||||
if (_options.VirtualResolution is not { } virtualSize)
|
||||
{
|
||||
return (viewport.Width, viewport.Height, ViewportMapping.Identity);
|
||||
}
|
||||
|
||||
return (virtualSize.X, virtualSize.Y,
|
||||
CameraMath.ComputeMapping(viewport.Width, viewport.Height, virtualSize.X, virtualSize.Y));
|
||||
}
|
||||
|
||||
private void BuildVertices(ReadOnlySpan<int> order)
|
||||
{
|
||||
for (var i = 0; i < order.Length; i++)
|
||||
{
|
||||
ref readonly var instance = ref _batcher[order[i]];
|
||||
var bounds = instance.Region.Bounds;
|
||||
var texture = instance.Region.Texture;
|
||||
|
||||
var u0 = bounds.X / (float)texture.Width;
|
||||
var v0 = bounds.Y / (float)texture.Height;
|
||||
var u1 = (bounds.X + bounds.Width) / (float)texture.Width;
|
||||
var v1 = (bounds.Y + bounds.Height) / (float)texture.Height;
|
||||
|
||||
if ((instance.Flip & SpriteFlip.X) != 0)
|
||||
{
|
||||
(u0, u1) = (u1, u0);
|
||||
}
|
||||
|
||||
if ((instance.Flip & SpriteFlip.Y) != 0)
|
||||
{
|
||||
(v0, v1) = (v1, v0);
|
||||
}
|
||||
|
||||
var (sin, cos) = MathF.SinCos(instance.Rotation);
|
||||
var rx = new Vector2(instance.HalfSize.X * cos, instance.HalfSize.X * sin);
|
||||
var ry = new Vector2(-instance.HalfSize.Y * sin, instance.HalfSize.Y * cos);
|
||||
var center = instance.Center;
|
||||
|
||||
var vertex = i * 4;
|
||||
_vertices[vertex + 0] = Vertex(center - rx - ry, instance.Color, u0, v0);
|
||||
_vertices[vertex + 1] = Vertex(center + rx - ry, instance.Color, u1, v0);
|
||||
_vertices[vertex + 2] = Vertex(center - rx + ry, instance.Color, u0, v1);
|
||||
_vertices[vertex + 3] = Vertex(center + rx + ry, instance.Color, u1, v1);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBatches(ReadOnlySpan<int> order)
|
||||
{
|
||||
var batchStart = 0;
|
||||
ref readonly var first = ref _batcher[order[0]];
|
||||
var currentTexture = first.Region.Texture;
|
||||
var currentLayer = first.Layer;
|
||||
ApplyLayerMatrices(currentLayer);
|
||||
|
||||
for (var i = 1; i <= order.Length; i++)
|
||||
{
|
||||
Texture2D? texture = null;
|
||||
byte layer = 0;
|
||||
if (i < order.Length)
|
||||
{
|
||||
ref readonly var instance = ref _batcher[order[i]];
|
||||
texture = instance.Region.Texture;
|
||||
layer = instance.Layer;
|
||||
if (ReferenceEquals(texture, currentTexture) && layer == currentLayer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
DrawRange(currentTexture, batchStart, i - batchStart);
|
||||
batchStart = i;
|
||||
|
||||
if (i < order.Length)
|
||||
{
|
||||
currentTexture = texture!;
|
||||
if (layer != currentLayer)
|
||||
{
|
||||
currentLayer = layer;
|
||||
ApplyLayerMatrices(currentLayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLayerMatrices(byte layer)
|
||||
{
|
||||
var state = Layers[new LayerId(layer)].Space == LayerSpace.Screen ? _screenCamera : Camera;
|
||||
_effect.View = state.View;
|
||||
_effect.Projection = state.Projection;
|
||||
}
|
||||
|
||||
private void DrawRange(Texture2D texture, int firstQuad, int quadCount)
|
||||
{
|
||||
_effect.Texture = texture;
|
||||
|
||||
while (quadCount > 0)
|
||||
{
|
||||
var quads = Math.Min(quadCount, MaxQuadsPerDraw);
|
||||
foreach (var pass in _effect.CurrentTechnique.Passes)
|
||||
{
|
||||
pass.Apply();
|
||||
_device.DrawIndexedPrimitives(PrimitiveType.TriangleList, firstQuad * 4, 0, quads * 2);
|
||||
DrawCalls++;
|
||||
}
|
||||
|
||||
firstQuad += quads;
|
||||
quadCount -= quads;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureVertexCapacity(int vertexCount)
|
||||
{
|
||||
if (_vertices.Length >= vertexCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var capacity = _vertices.Length;
|
||||
while (capacity < vertexCount)
|
||||
{
|
||||
capacity *= 2;
|
||||
}
|
||||
|
||||
_vertices = new VertexPositionColorTexture[capacity];
|
||||
_vertexBuffer.Dispose();
|
||||
_vertexBuffer = new DynamicVertexBuffer(
|
||||
_device, VertexPositionColorTexture.VertexDeclaration, capacity, BufferUsage.WriteOnly);
|
||||
}
|
||||
|
||||
private static VertexPositionColorTexture Vertex(Vector2 position, Color color, float u, float v) =>
|
||||
new(new Vector3(position, 0f), color, new Vector2(u, v));
|
||||
|
||||
private static IndexBuffer CreateQuadIndexBuffer(GraphicsDevice device)
|
||||
{
|
||||
var indices = new ushort[MaxQuadsPerDraw * 6];
|
||||
for (var quad = 0; quad < MaxQuadsPerDraw; quad++)
|
||||
{
|
||||
var vertex = quad * 4;
|
||||
var index = quad * 6;
|
||||
indices[index + 0] = (ushort)(vertex + 0);
|
||||
indices[index + 1] = (ushort)(vertex + 1);
|
||||
indices[index + 2] = (ushort)(vertex + 2);
|
||||
indices[index + 3] = (ushort)(vertex + 2);
|
||||
indices[index + 4] = (ushort)(vertex + 1);
|
||||
indices[index + 5] = (ushort)(vertex + 3);
|
||||
}
|
||||
|
||||
var buffer = new IndexBuffer(device, IndexElementSize.SixteenBits, indices.Length, BufferUsage.WriteOnly);
|
||||
buffer.SetData(indices);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user