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,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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user