acdream/src/AcDream.Core/Selection/ScreenProjection.cs

82 lines
3.8 KiB
C#

using System.Numerics;
namespace AcDream.Core.Selection;
/// <summary>
/// Screen-space selection-sphere projection used by SmartBox's vivid target
/// indicator. World picking separately ports retail's visible GfxObj polygon
/// ray tests; the persistent marker intentionally ignores world occlusion.
///
/// <para>
/// Retail equivalent: <c>SmartBox::GetObjectBoundingBox</c> at
/// <c>0x00452e20</c>, which uses
/// <c>Render::GetViewerBBox(selection_sphere, &amp;corner1, &amp;corner2)</c>
/// to compute a camera-aligned bbox of the sphere and projects the two
/// corner points. We use the mathematical equivalent (project center,
/// compute screen radius analytically) — both produce identical pixel
/// rects for a standard right-handed perspective.
/// </para>
/// </summary>
public static class ScreenProjection
{
/// <summary>
/// Project a world-space sphere to a screen-space axis-aligned square
/// rectangle.
/// </summary>
/// <param name="worldCenter">Sphere center in world space.</param>
/// <param name="worldRadius">Sphere radius in world space.</param>
/// <param name="view">View matrix (System.Numerics row-vector convention).</param>
/// <param name="projection">Projection matrix. <c>M22 = cot(fovY/2)</c>
/// for a standard right-handed perspective.</param>
/// <param name="viewport">Viewport size in pixels (X = width, Y = height).</param>
/// <param name="rectMin">Out: top-left corner of the rect in viewport pixels.</param>
/// <param name="rectMax">Out: bottom-right corner of the rect in viewport pixels.</param>
/// <param name="depth">Out: camera-space depth (<c>clip.W</c>) of the sphere
/// center — use this for nearest-first sorting when multiple rects overlap.</param>
/// <param name="minSidePixels">Minimum side length of the rect. Distant
/// entities may clamp to this for a stable presentation.</param>
/// <returns>
/// <c>true</c> if the sphere is in front of the camera and the rect was
/// produced; <c>false</c> if the center is behind the camera
/// (<c>clip.W &lt;= 0</c>) or the rect is more than a screen offset
/// from the viewport (obviously off-screen).
/// </returns>
public static bool TryProjectSphereToScreenRect(
Vector3 worldCenter, float worldRadius,
Matrix4x4 view, Matrix4x4 projection, Vector2 viewport,
out Vector2 rectMin, out Vector2 rectMax, out float depth,
float minSidePixels = 12f)
{
rectMin = default;
rectMax = default;
depth = 0f;
var viewProj = view * projection;
var clip = Vector4.Transform(new Vector4(worldCenter, 1f), viewProj);
if (clip.W <= 0.001f) return false;
depth = clip.W;
float ndcX = clip.X / clip.W;
float ndcY = clip.Y / clip.W;
float screenX = (ndcX * 0.5f + 0.5f) * viewport.X;
float screenY = (1f - (ndcY * 0.5f + 0.5f)) * viewport.Y;
// Screen-space radius. projection.M22 = cot(fovY/2). clip.W is
// the camera-space distance.
float scaleY = projection.M22;
if (scaleY <= 0f) return false;
float screenRadius = worldRadius * scaleY * viewport.Y / (2f * clip.W);
// Cull obviously-off-screen entities (more than a screen away).
if (screenX + screenRadius < -viewport.X || screenX - screenRadius > 2f * viewport.X) return false;
if (screenY + screenRadius < -viewport.Y || screenY - screenRadius > 2f * viewport.Y) return false;
// Optional presentation floor for very distant spheres.
if (screenRadius < minSidePixels * 0.5f) screenRadius = minSidePixels * 0.5f;
rectMin = new Vector2(screenX - screenRadius, screenY - screenRadius);
rectMax = new Vector2(screenX + screenRadius, screenY + screenRadius);
return true;
}
}