Update README.md to include project description, developer documentation links, and license information.
CI / build-test (push) Successful in 1m6s

This commit is contained in:
Leonid Pershin
2026-06-11 04:03:07 +03:00
parent 31aba3aeee
commit ff2231a8ab
72 changed files with 4113 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
using Microsoft.Xna.Framework;
namespace MrGameEng.Graphics;
/// <summary>Maps physical screen pixels to virtual-resolution pixels (letterbox scaling).</summary>
public readonly record struct ViewportMapping(Vector2 Offset, float Scale)
{
/// <summary>Identity mapping (no letterbox).</summary>
public static readonly ViewportMapping Identity = new(Vector2.Zero, 1f);
}
/// <summary>Per-frame camera matrices and derived data, computed by <see cref="CameraMath"/>.</summary>
public readonly struct CameraState
{
/// <summary>World → virtual-screen transform of the active camera.</summary>
public required Matrix View { get; init; }
/// <summary>Virtual-screen → NDC orthographic projection.</summary>
public required Matrix Projection { get; init; }
/// <summary>Inverse of <see cref="View"/>.</summary>
public required Matrix InverseView { get; init; }
/// <summary>World-space rectangle visible through the camera; used for culling.</summary>
public required RectF CullRect { get; init; }
/// <summary>Virtual resolution width in pixels.</summary>
public required int VirtualWidth { get; init; }
/// <summary>Virtual resolution height in pixels.</summary>
public required int VirtualHeight { get; init; }
/// <summary>Physical-screen to virtual-pixel mapping.</summary>
public required ViewportMapping Mapping { get; init; }
/// <summary>Converts a physical screen point to world coordinates.</summary>
public Vector2 ScreenToWorld(Vector2 screen)
{
var virtualPoint = (screen - Mapping.Offset) / Mapping.Scale;
return Vector2.Transform(virtualPoint, InverseView);
}
/// <summary>Converts a world point to physical screen coordinates.</summary>
public Vector2 WorldToScreen(Vector2 world)
{
var virtualPoint = Vector2.Transform(world, View);
return virtualPoint * Mapping.Scale + Mapping.Offset;
}
}
/// <summary>Pure math for the orthographic 2D camera. Y axis points down, rotation is clockwise.</summary>
public static class CameraMath
{
/// <summary>Computes the full camera state for a frame.</summary>
public static CameraState Compute(in Camera camera, int virtualWidth, int virtualHeight, ViewportMapping mapping)
{
var zoom = camera.Zoom <= 0f ? 1f : camera.Zoom;
var position = ClampToBounds(camera, virtualWidth, virtualHeight, zoom);
var view =
Matrix.CreateTranslation(-position.X, -position.Y, 0f) *
Matrix.CreateRotationZ(-camera.Rotation) *
Matrix.CreateScale(zoom, zoom, 1f) *
Matrix.CreateTranslation(virtualWidth / 2f, virtualHeight / 2f, 0f);
var inverseView = Matrix.Invert(view);
return new CameraState
{
View = view,
Projection = Matrix.CreateOrthographicOffCenter(0f, virtualWidth, virtualHeight, 0f, 0f, 1f),
InverseView = inverseView,
CullRect = ComputeCullRect(inverseView, virtualWidth, virtualHeight),
VirtualWidth = virtualWidth,
VirtualHeight = virtualHeight,
Mapping = mapping,
};
}
/// <summary>
/// Computes the letterbox mapping that fits the virtual resolution into a physical
/// viewport, preserving aspect ratio and centering.
/// </summary>
public static ViewportMapping ComputeMapping(int screenWidth, int screenHeight, int virtualWidth, int virtualHeight)
{
var scale = MathF.Min((float)screenWidth / virtualWidth, (float)screenHeight / virtualHeight);
var offset = new Vector2(screenWidth - virtualWidth * scale, screenHeight - virtualHeight * scale) / 2f;
return new ViewportMapping(offset, scale);
}
private static Vector2 ClampToBounds(in Camera camera, int virtualWidth, int virtualHeight, float zoom)
{
if (camera.Bounds is not { } bounds)
{
return camera.Position;
}
// Clamp uses unrotated view extents; with camera roll the clamp is approximate.
var halfW = virtualWidth / (2f * zoom);
var halfH = virtualHeight / (2f * zoom);
return new Vector2(
ClampAxis(camera.Position.X, bounds.Left + halfW, bounds.Right - halfW),
ClampAxis(camera.Position.Y, bounds.Top + halfH, bounds.Bottom - halfH));
}
private static float ClampAxis(float value, float min, float max) =>
min > max ? (min + max) / 2f : Math.Clamp(value, min, max);
private static RectF ComputeCullRect(in Matrix inverseView, int virtualWidth, int virtualHeight)
{
var c0 = Vector2.Transform(Vector2.Zero, inverseView);
var c1 = Vector2.Transform(new Vector2(virtualWidth, 0f), inverseView);
var c2 = Vector2.Transform(new Vector2(0f, virtualHeight), inverseView);
var c3 = Vector2.Transform(new Vector2(virtualWidth, virtualHeight), inverseView);
var min = Vector2.Min(Vector2.Min(c0, c1), Vector2.Min(c2, c3));
var max = Vector2.Max(Vector2.Max(c0, c1), Vector2.Max(c2, c3));
return RectF.FromCorners(min, max);
}
}