82 lines
3.8 KiB
C#
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, &corner1, &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 <= 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;
|
|
}
|
|
}
|