fix(B.7): scale indicator box from projected entity height, not fixed pixels
Visual test surfaced two B.7 MVP issues:
1. Box anchored at abdomen + fixed 48px size meant the rectangle
shrank visually as the camera approached the entity (entity got
bigger on screen, box stayed 48px → triangles ended up inside
the silhouette).
2. Origin was a single point (entity position + 0.9m WorldVerticalOffset)
so the box wasn't centred on the visible body.
Fix: project both feet (WorldPosition) and head (WorldPosition.Z +
EntityHeight=1.8m) to screen space. Apparent pixel height between the
two = box height; halve it for width (WidthHeightRatio=0.5 ≈
humanoid). Box centred at midpoint of projected feet+head.
- Closer entity → bigger projected height → bigger box. Distance
scaling is automatic from the perspective projection.
- Farther entity → smaller projected height → MinScreenHeight=16px
floor prevents the box collapsing to a point.
- Box is screen-axis-aligned (always rectangular on screen) but
sized + positioned by the entity's actual world-space silhouette.
Properties exposed (TriangleSize, EntityHeight, WidthHeightRatio,
MinScreenHeight) so the panel can be tuned per-instance if a future
caller wants short-item boxes (drop EntityHeight to ~0.3m for tapers,
keep WidthHeightRatio at 1.0 for a square box).
Stuck-on-+Je issue (clicking other things still returns +Je) is
Issue #59 — picker over-pick — and unaffected by this commit.
This commit is contained in:
parent
c7e5f9f00f
commit
4bc95eca01
1 changed files with 83 additions and 33 deletions
|
|
@ -48,25 +48,33 @@ public sealed class TargetIndicatorPanel
|
||||||
private readonly Func<uint, TargetInfo?> _entityResolver;
|
private readonly Func<uint, TargetInfo?> _entityResolver;
|
||||||
private readonly Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> _cameraProvider;
|
private readonly Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> _cameraProvider;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pixel half-extent of the bounding square. Triangles sit at the
|
|
||||||
/// four corners of a 2 × HalfExtent square centred on the entity.
|
|
||||||
/// Future: scale by apparent radius (project entity bounds + read
|
|
||||||
/// pixel size) instead of a fixed value.
|
|
||||||
/// </summary>
|
|
||||||
public float HalfExtent { get; set; } = 24f;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pixel size of each corner triangle's right-angle legs.
|
/// Pixel size of each corner triangle's right-angle legs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float TriangleSize { get; set; } = 8f;
|
public float TriangleSize { get; set; } = 10f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// World-space vertical offset to place the indicator box around
|
/// World-space height of the indicator box. The panel projects feet
|
||||||
/// the entity's torso/body rather than its feet. 0.9 m matches
|
/// (at <c>WorldPosition</c>) and head (at <c>WorldPosition.Z +
|
||||||
/// rough humanoid mid-body height.
|
/// EntityHeight</c>) to screen space and uses the projected pixel
|
||||||
|
/// distance as the box height. 1.8 m matches a standing humanoid;
|
||||||
|
/// short items still get a small box because the projection
|
||||||
|
/// preserves apparent size.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float WorldVerticalOffset { get; set; } = 0.9f;
|
public float EntityHeight { get; set; } = 1.8f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Box width = <see cref="EntityHeight"/> projected height ×
|
||||||
|
/// <see cref="WidthHeightRatio"/>. Humanoids are about half as
|
||||||
|
/// wide as tall on screen, so 0.5 is a sensible default.
|
||||||
|
/// </summary>
|
||||||
|
public float WidthHeightRatio { get; set; } = 0.5f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Floor for the projected screen height (pixels). Prevents the
|
||||||
|
/// indicator from collapsing to a point on far-away entities.
|
||||||
|
/// </summary>
|
||||||
|
public float MinScreenHeight { get; set; } = 16f;
|
||||||
|
|
||||||
public TargetIndicatorPanel(
|
public TargetIndicatorPanel(
|
||||||
Func<uint?> selectedGuidProvider,
|
Func<uint?> selectedGuidProvider,
|
||||||
|
|
@ -92,36 +100,46 @@ public sealed class TargetIndicatorPanel
|
||||||
var (view, projection, viewport) = _cameraProvider();
|
var (view, projection, viewport) = _cameraProvider();
|
||||||
if (viewport.X <= 0 || viewport.Y <= 0) return;
|
if (viewport.X <= 0 || viewport.Y <= 0) return;
|
||||||
|
|
||||||
// Project world position (lifted to mid-body height) to clip space.
|
var viewProj = view * projection;
|
||||||
var worldPos = new Vector4(
|
|
||||||
|
// Project the entity's feet and head to screen space. Apparent
|
||||||
|
// pixel height (= |headY - feetY|) gives the box height; halve
|
||||||
|
// it for width. Closer entity → bigger projected height → bigger
|
||||||
|
// box. Distance scaling is automatic from the perspective
|
||||||
|
// projection.
|
||||||
|
if (!TryProjectToScreen(info.WorldPosition, viewProj, viewport, out var feetScreen))
|
||||||
|
return;
|
||||||
|
var headWorld = new Vector3(
|
||||||
info.WorldPosition.X,
|
info.WorldPosition.X,
|
||||||
info.WorldPosition.Y,
|
info.WorldPosition.Y,
|
||||||
info.WorldPosition.Z + WorldVerticalOffset,
|
info.WorldPosition.Z + EntityHeight);
|
||||||
1f);
|
if (!TryProjectToScreen(headWorld, viewProj, viewport, out var headScreen))
|
||||||
var clip = Vector4.Transform(worldPos, view * projection);
|
return;
|
||||||
if (clip.W <= 0.001f) return; // behind / on the near plane
|
|
||||||
|
|
||||||
// NDC and viewport conversion.
|
// Apparent height + width.
|
||||||
float ndcX = clip.X / clip.W;
|
float screenHeight = MathF.Abs(headScreen.Y - feetScreen.Y);
|
||||||
float ndcY = clip.Y / clip.W;
|
if (screenHeight < MinScreenHeight) screenHeight = MinScreenHeight;
|
||||||
if (ndcX < -1f || ndcX > 1f || ndcY < -1f || ndcY > 1f)
|
float screenWidth = screenHeight * WidthHeightRatio;
|
||||||
return; // off-screen — MVP doesn't render the edge arrow.
|
|
||||||
|
|
||||||
// GL NDC Y points UP; screen Y points DOWN. Flip Y.
|
// Box center = midpoint of feet/head projection. Use the X from
|
||||||
float screenX = (ndcX * 0.5f + 0.5f) * viewport.X;
|
// the head projection only (feet may project to a slightly
|
||||||
float screenY = (1f - (ndcY * 0.5f + 0.5f)) * viewport.Y;
|
// different X if the camera looks down at an angle; midpoint is
|
||||||
|
// a stable centre regardless).
|
||||||
|
Vector2 center = (feetScreen + headScreen) * 0.5f;
|
||||||
|
|
||||||
|
float halfW = screenWidth * 0.5f;
|
||||||
|
float halfH = screenHeight * 0.5f;
|
||||||
|
|
||||||
|
Vector2 tl = new(center.X - halfW, center.Y - halfH);
|
||||||
|
Vector2 tr = new(center.X + halfW, center.Y - halfH);
|
||||||
|
Vector2 br = new(center.X + halfW, center.Y + halfH);
|
||||||
|
Vector2 bl = new(center.X - halfW, center.Y + halfH);
|
||||||
|
|
||||||
var rgba = RadarBlipColors.For(info.ItemType, info.ObjectDescriptionFlags);
|
var rgba = RadarBlipColors.For(info.ItemType, info.ObjectDescriptionFlags);
|
||||||
uint col = MakeImGuiColor(rgba);
|
uint col = MakeImGuiColor(rgba);
|
||||||
|
|
||||||
var drawList = ImGui.GetBackgroundDrawList();
|
var drawList = ImGui.GetBackgroundDrawList();
|
||||||
|
|
||||||
// Four corners of the bounding square.
|
|
||||||
Vector2 tl = new(screenX - HalfExtent, screenY - HalfExtent);
|
|
||||||
Vector2 tr = new(screenX + HalfExtent, screenY - HalfExtent);
|
|
||||||
Vector2 br = new(screenX + HalfExtent, screenY + HalfExtent);
|
|
||||||
Vector2 bl = new(screenX - HalfExtent, screenY + HalfExtent);
|
|
||||||
|
|
||||||
float t = TriangleSize;
|
float t = TriangleSize;
|
||||||
|
|
||||||
// Each corner triangle: right-angle at the corner itself, legs
|
// Each corner triangle: right-angle at the corner itself, legs
|
||||||
|
|
@ -134,6 +152,38 @@ public sealed class TargetIndicatorPanel
|
||||||
drawList.AddTriangleFilled(bl, bl + new Vector2( t, 0), bl + new Vector2(0, -t), col);
|
drawList.AddTriangleFilled(bl, bl + new Vector2( t, 0), bl + new Vector2(0, -t), col);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Project a world-space point to screen-space pixels. Returns
|
||||||
|
/// <c>false</c> if the point is behind the camera or outside the
|
||||||
|
/// expanded viewport (±20 % margin so a tall entity whose feet are
|
||||||
|
/// just off the bottom of the screen still gets its head projected).
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryProjectToScreen(
|
||||||
|
Vector3 world,
|
||||||
|
Matrix4x4 viewProj,
|
||||||
|
Vector2 viewport,
|
||||||
|
out Vector2 screen)
|
||||||
|
{
|
||||||
|
var clip = Vector4.Transform(new Vector4(world, 1f), viewProj);
|
||||||
|
if (clip.W <= 0.001f)
|
||||||
|
{
|
||||||
|
screen = Vector2.Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
float ndcX = clip.X / clip.W;
|
||||||
|
float ndcY = clip.Y / clip.W;
|
||||||
|
const float margin = 1.2f;
|
||||||
|
if (ndcX < -margin || ndcX > margin || ndcY < -margin || ndcY > margin)
|
||||||
|
{
|
||||||
|
screen = Vector2.Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
screen = new Vector2(
|
||||||
|
(ndcX * 0.5f + 0.5f) * viewport.X,
|
||||||
|
(1f - (ndcY * 0.5f + 0.5f)) * viewport.Y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static uint MakeImGuiColor(RadarBlipColors.Rgba c)
|
private static uint MakeImGuiColor(RadarBlipColors.Rgba c)
|
||||||
{
|
{
|
||||||
// ImGui packed colour is 0xAABBGGRR (little-endian RGBA).
|
// ImGui packed colour is 0xAABBGGRR (little-endian RGBA).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue