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<(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>
|
||||
/// Pixel size of each corner triangle's right-angle legs.
|
||||
/// </summary>
|
||||
public float TriangleSize { get; set; } = 8f;
|
||||
public float TriangleSize { get; set; } = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// World-space vertical offset to place the indicator box around
|
||||
/// the entity's torso/body rather than its feet. 0.9 m matches
|
||||
/// rough humanoid mid-body height.
|
||||
/// World-space height of the indicator box. The panel projects feet
|
||||
/// (at <c>WorldPosition</c>) and head (at <c>WorldPosition.Z +
|
||||
/// 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>
|
||||
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(
|
||||
Func<uint?> selectedGuidProvider,
|
||||
|
|
@ -92,36 +100,46 @@ public sealed class TargetIndicatorPanel
|
|||
var (view, projection, viewport) = _cameraProvider();
|
||||
if (viewport.X <= 0 || viewport.Y <= 0) return;
|
||||
|
||||
// Project world position (lifted to mid-body height) to clip space.
|
||||
var worldPos = new Vector4(
|
||||
var viewProj = view * projection;
|
||||
|
||||
// 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.Y,
|
||||
info.WorldPosition.Z + WorldVerticalOffset,
|
||||
1f);
|
||||
var clip = Vector4.Transform(worldPos, view * projection);
|
||||
if (clip.W <= 0.001f) return; // behind / on the near plane
|
||||
info.WorldPosition.Z + EntityHeight);
|
||||
if (!TryProjectToScreen(headWorld, viewProj, viewport, out var headScreen))
|
||||
return;
|
||||
|
||||
// NDC and viewport conversion.
|
||||
float ndcX = clip.X / clip.W;
|
||||
float ndcY = clip.Y / clip.W;
|
||||
if (ndcX < -1f || ndcX > 1f || ndcY < -1f || ndcY > 1f)
|
||||
return; // off-screen — MVP doesn't render the edge arrow.
|
||||
// Apparent height + width.
|
||||
float screenHeight = MathF.Abs(headScreen.Y - feetScreen.Y);
|
||||
if (screenHeight < MinScreenHeight) screenHeight = MinScreenHeight;
|
||||
float screenWidth = screenHeight * WidthHeightRatio;
|
||||
|
||||
// GL NDC Y points UP; screen Y points DOWN. Flip Y.
|
||||
float screenX = (ndcX * 0.5f + 0.5f) * viewport.X;
|
||||
float screenY = (1f - (ndcY * 0.5f + 0.5f)) * viewport.Y;
|
||||
// Box center = midpoint of feet/head projection. Use the X from
|
||||
// the head projection only (feet may project to a slightly
|
||||
// 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);
|
||||
uint col = MakeImGuiColor(rgba);
|
||||
|
||||
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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
// ImGui packed colour is 0xAABBGGRR (little-endian RGBA).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue