feat(selection): port retail polygon picking and vivid marker
Replace the projected Setup-sphere rectangle and independent physics-wall ray with retail's render-coupled picker: only visible server-object parts participate, each exact drawing sphere broad-phases the camera-eye ray, and first-in-DAT-order visual polygon hits globally outrank sphere fallbacks. Replace the devtools-only procedural triangles with the retained gameplay VividTargetIndicator using retail client-enum surfaces 1..4, radar-blip colorization, Setup selection-sphere framing, and the exact eight-pixel viewport clamp. Release build succeeds with zero warnings and all 5,886 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
0f82a08f0a
commit
146a963aeb
26 changed files with 1302 additions and 1340 deletions
204
src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs
Normal file
204
src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Ui;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
public readonly record struct VividTargetInfo(
|
||||
Vector3 SelectionSphereCenter,
|
||||
float SelectionSphereRadius,
|
||||
uint ItemType,
|
||||
uint ObjectDescriptionFlags);
|
||||
|
||||
public sealed record VividTargetRuntimeBindings(
|
||||
SelectionState Selection,
|
||||
Func<uint> PlayerGuid,
|
||||
Func<bool> Enabled,
|
||||
Func<uint, VividTargetInfo?> ResolveTarget,
|
||||
Func<uint, bool> WasDrawn,
|
||||
Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> Camera);
|
||||
|
||||
/// <summary>
|
||||
/// Retained-UI port of <c>VividTargetIndicator @ 0x004F5CE0..0x004F6DF6</c>.
|
||||
/// The four corner surfaces are client-enum category 0x10000009 values 1..4, colorized
|
||||
/// with the selected object's retail radar-blip color and placed around the
|
||||
/// Setup selection sphere's SmartBox screen rectangle.
|
||||
/// </summary>
|
||||
public sealed class VividTargetIndicatorController
|
||||
{
|
||||
private const uint ClientEnumCategory = 0x10000009u;
|
||||
private const uint FirstSourceImageEnum = 1u;
|
||||
private const float ViewportMargin = 8f;
|
||||
|
||||
private readonly UiPanel _root;
|
||||
private readonly UiTextureElement[] _corners;
|
||||
private readonly VividTargetRuntimeBindings _bindings;
|
||||
private readonly (float Width, float Height)[] _sizes;
|
||||
|
||||
private VividTargetIndicatorController(
|
||||
UiPanel root,
|
||||
UiTextureElement[] corners,
|
||||
(float Width, float Height)[] sizes,
|
||||
VividTargetRuntimeBindings bindings)
|
||||
{
|
||||
_root = root;
|
||||
_corners = corners;
|
||||
_sizes = sizes;
|
||||
_bindings = bindings;
|
||||
}
|
||||
|
||||
public static VividTargetIndicatorController? Mount(
|
||||
UiRoot host,
|
||||
RetailUiAssets assets,
|
||||
VividTargetRuntimeBindings bindings)
|
||||
{
|
||||
var root = new UiPanel
|
||||
{
|
||||
Name = "VividTargetIndicator",
|
||||
BackgroundColor = Vector4.Zero,
|
||||
BorderColor = Vector4.Zero,
|
||||
ClickThrough = true,
|
||||
Visible = false,
|
||||
ZOrder = -10_000,
|
||||
Anchors = AnchorEdges.None,
|
||||
};
|
||||
|
||||
var corners = new UiTextureElement[4];
|
||||
var sizes = new (float Width, float Height)[4];
|
||||
for (uint i = 0; i < 4; i++)
|
||||
{
|
||||
uint did;
|
||||
lock (assets.DatLock)
|
||||
did = RetailDataIdResolver.Resolve(
|
||||
assets.Dats,
|
||||
enumValue: FirstSourceImageEnum + i,
|
||||
enumCategory: ClientEnumCategory);
|
||||
if (did == 0u)
|
||||
return null;
|
||||
|
||||
var resolved = assets.ResolveSprite(did);
|
||||
if (resolved.Texture == 0u || resolved.Width <= 0 || resolved.Height <= 0)
|
||||
return null;
|
||||
|
||||
sizes[i] = (resolved.Width, resolved.Height);
|
||||
corners[i] = new UiTextureElement
|
||||
{
|
||||
Name = $"VividTargetCorner{i + 1}",
|
||||
Texture = resolved.Texture,
|
||||
Width = resolved.Width,
|
||||
Height = resolved.Height,
|
||||
ClickThrough = true,
|
||||
Anchors = AnchorEdges.None,
|
||||
};
|
||||
root.AddChild(corners[i]);
|
||||
}
|
||||
|
||||
host.AddChild(root);
|
||||
return new VividTargetIndicatorController(root, corners, sizes, bindings);
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
if (!_bindings.Enabled()
|
||||
|| _bindings.Selection.SelectedObjectId is not uint guid
|
||||
|| guid == 0u
|
||||
|| guid == _bindings.PlayerGuid()
|
||||
|| !_bindings.WasDrawn(guid)
|
||||
|| _bindings.ResolveTarget(guid) is not VividTargetInfo target)
|
||||
{
|
||||
_root.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var camera = _bindings.Camera();
|
||||
if (!ScreenProjection.TryProjectSphereToScreenRect(
|
||||
target.SelectionSphereCenter,
|
||||
target.SelectionSphereRadius,
|
||||
camera.View,
|
||||
camera.Projection,
|
||||
camera.Viewport,
|
||||
out Vector2 rectMin,
|
||||
out Vector2 rectMax,
|
||||
out _,
|
||||
minSidePixels: 0f))
|
||||
{
|
||||
_root.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
VividTargetLayout layout = ComputeLayout(
|
||||
rectMin, rectMax, camera.Viewport, _sizes);
|
||||
_root.Left = layout.RootPosition.X;
|
||||
_root.Top = layout.RootPosition.Y;
|
||||
_root.Width = layout.RootSize.X;
|
||||
_root.Height = layout.RootSize.Y;
|
||||
for (int i = 0; i < _corners.Length; i++)
|
||||
SetCorner(i, layout.CornerPositions[i].X, layout.CornerPositions[i].Y);
|
||||
|
||||
RadarBlipColors.Rgba color = RadarBlipColors.For(
|
||||
target.ItemType, target.ObjectDescriptionFlags);
|
||||
var tint = new Vector4(color.Red, color.Green, color.Blue, color.Alpha);
|
||||
for (int i = 0; i < _corners.Length; i++)
|
||||
_corners[i].Tint = tint;
|
||||
_root.Visible = true;
|
||||
}
|
||||
|
||||
private void SetCorner(int index, float left, float top)
|
||||
{
|
||||
_corners[index].Left = left;
|
||||
_corners[index].Top = top;
|
||||
}
|
||||
|
||||
internal static VividTargetLayout ComputeLayout(
|
||||
Vector2 rectMin,
|
||||
Vector2 rectMax,
|
||||
Vector2 viewport,
|
||||
IReadOnlyList<(float Width, float Height)> sizes)
|
||||
{
|
||||
if (sizes.Count != 4)
|
||||
throw new ArgumentException("Retail VividTargetIndicator has exactly four corners.", nameof(sizes));
|
||||
|
||||
// VividTargetIndicator::OnDraw @ 0x004F69C8..0x004F6A99 reads
|
||||
// corner[1]'s dimensions, expands the SmartBox rectangle by one
|
||||
// corner on the top/left, then clamps each rectangle edge separately.
|
||||
// Partially off-screen targets therefore shrink the assembled region;
|
||||
// retail does not translate the whole un-clipped box back on-screen.
|
||||
float cornerWidth = sizes[0].Width;
|
||||
float cornerHeight = sizes[0].Height;
|
||||
float outerLeft = rectMin.X - cornerWidth;
|
||||
float outerTop = rectMin.Y - cornerHeight;
|
||||
float outerRight = rectMax.X;
|
||||
float outerBottom = rectMax.Y;
|
||||
|
||||
if (outerLeft > outerRight) outerLeft = outerRight - 1f;
|
||||
if (outerTop > outerBottom) outerTop = outerBottom - 1f;
|
||||
outerLeft = MathF.Max(outerLeft, ViewportMargin);
|
||||
outerTop = MathF.Max(outerTop, ViewportMargin);
|
||||
outerRight = MathF.Max(outerRight, cornerWidth + ViewportMargin);
|
||||
outerBottom = MathF.Max(outerBottom, cornerHeight + ViewportMargin);
|
||||
|
||||
float maximumRight = viewport.X - cornerWidth - ViewportMargin;
|
||||
float maximumBottom = viewport.Y - cornerHeight - ViewportMargin;
|
||||
outerLeft = MathF.Min(outerLeft, maximumRight - cornerWidth);
|
||||
outerTop = MathF.Min(outerTop, maximumBottom - cornerHeight);
|
||||
outerRight = MathF.Min(outerRight, maximumRight);
|
||||
outerBottom = MathF.Min(outerBottom, maximumBottom);
|
||||
Vector2 rootSize = new(
|
||||
MathF.Max(1f, outerRight - outerLeft + cornerWidth),
|
||||
MathF.Max(1f, outerBottom - outerTop + cornerHeight));
|
||||
return new VividTargetLayout(
|
||||
new Vector2(outerLeft, outerTop),
|
||||
rootSize,
|
||||
[
|
||||
Vector2.Zero,
|
||||
new Vector2(rootSize.X - sizes[1].Width, 0f),
|
||||
new Vector2(rootSize.X - sizes[2].Width, rootSize.Y - sizes[2].Height),
|
||||
new Vector2(0f, rootSize.Y - sizes[3].Height),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly record struct VividTargetLayout(
|
||||
Vector2 RootPosition,
|
||||
Vector2 RootSize,
|
||||
IReadOnlyList<Vector2> CornerPositions);
|
||||
|
|
@ -164,6 +164,7 @@ public sealed record RetailUiRuntimeBindings(
|
|||
MagicRuntimeBindings Magic,
|
||||
JumpPowerbarRuntimeBindings JumpPowerbar,
|
||||
FpsRuntimeBindings Fps,
|
||||
VividTargetRuntimeBindings VividTarget,
|
||||
IndicatorRuntimeBindings Indicators,
|
||||
ToolbarRuntimeBindings Toolbar,
|
||||
CharacterRuntimeBindings Character,
|
||||
|
|
@ -192,6 +193,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
private RetailItemConfirmationController? _itemConfirmationController;
|
||||
private RetailSkillTrainingConfirmationController? _skillTrainingConfirmationController;
|
||||
private UiShortcutDigitGraphics? _shortcutDigitGraphics;
|
||||
private VividTargetIndicatorController? _vividTargetIndicator;
|
||||
private bool _disposed;
|
||||
|
||||
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
||||
|
|
@ -202,6 +204,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
bindings.Host.ShowWindow,
|
||||
bindings.Host.HideWindow);
|
||||
MountFpsDisplay();
|
||||
MountVividTargetIndicator();
|
||||
MountVitals();
|
||||
MountRadar();
|
||||
MountChat();
|
||||
|
|
@ -295,6 +298,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public void Tick(double deltaSeconds)
|
||||
{
|
||||
FpsController?.Tick();
|
||||
_vividTargetIndicator?.Tick();
|
||||
SpellbookWindowController?.Tick();
|
||||
SpellcastingUiController?.Tick();
|
||||
PositiveEffectsController?.Tick();
|
||||
|
|
@ -460,6 +464,17 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Console.WriteLine("[D.2b] retail FPS display from SmartBox LayoutDesc 0x2100000F.");
|
||||
}
|
||||
|
||||
private void MountVividTargetIndicator()
|
||||
{
|
||||
_vividTargetIndicator = VividTargetIndicatorController.Mount(
|
||||
Host.Root,
|
||||
_bindings.Assets,
|
||||
_bindings.VividTarget);
|
||||
Console.WriteLine(_vividTargetIndicator is null
|
||||
? "[D.2b] vivid target indicator DAT surfaces unavailable."
|
||||
: "[D.2b] vivid target indicator mounted from client-enum category 0x10000009.");
|
||||
}
|
||||
|
||||
private void MountVitals()
|
||||
{
|
||||
ImportedLayout? layout = Import(0x2100006Cu);
|
||||
|
|
|
|||
|
|
@ -1,265 +0,0 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Ui;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// B.7 (2026-05-15) — Vivid Target Indicator. Draws four small
|
||||
/// corner triangles around the currently-selected entity, colour-coded
|
||||
/// by entity type (NPCs yellow, items white-ish, PKs red, etc.).
|
||||
/// Retail-faithful equivalent of <c>VividTargetIndicator</c>
|
||||
/// (named decomp at <c>0x004d6165</c> / <c>0x004f5ce0</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// MVP scope: on-screen indicator only, drawn via ImGui's background
|
||||
/// draw list. Deferred to follow-ups: off-screen edge arrow, DAT-loaded
|
||||
/// triangle sprite, mesh-tint highlight, player-option toggle.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The panel pulls its inputs through delegates supplied by the host
|
||||
/// (<see cref="Rendering.GameWindow"/>) so it doesn't have to depend
|
||||
/// on internal state types:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><c>selectedGuidProvider</c> — Core <c>SelectionState</c>'s current guid.</item>
|
||||
/// <item><c>entityResolver</c> — returns
|
||||
/// <see cref="TargetInfo"/> for a given guid, or <c>null</c> if
|
||||
/// the entity is no longer in the world (despawned).</item>
|
||||
/// <item><c>cameraProvider</c> — host's active camera + viewport
|
||||
/// dimensions; called once per frame.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class TargetIndicatorPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// What the panel needs to know about the selected entity per frame.
|
||||
/// <c>ItemType</c> + <c>ObjectDescriptionFlags</c> feed
|
||||
/// <see cref="RadarBlipColors.For"/> for colour selection.
|
||||
/// <c>Scale</c> multiplies the per-type base height in
|
||||
/// <see cref="EntityHeightFor"/> — a scaled-up sign or oversized NPC
|
||||
/// gets a proportionally bigger box. <c>Useability</c> (acclient.h:6478
|
||||
/// <c>ITEM_USEABLE</c> enum) discriminates real pickup items
|
||||
/// (USEABLE_REMOTE bit set, 0.8 m boxes) from same-ItemType-but-non-
|
||||
/// useable scenery like signs (USEABLE_UNDEF, 3 m boxes).
|
||||
/// </summary>
|
||||
public readonly record struct TargetInfo(
|
||||
Vector3 WorldPosition,
|
||||
uint ItemType,
|
||||
uint ObjectDescriptionFlags,
|
||||
float Scale,
|
||||
uint? Useability = null,
|
||||
// 2026-05-16: world-space SelectionSphere center + radius.
|
||||
// Comes from the Setup's baked selection_sphere (acclient.h
|
||||
// CSetup::selection_sphere) scaled by entity scale. When
|
||||
// populated, the panel projects the sphere as a screen circle
|
||||
// and uses that as the indicator rect — matches retail
|
||||
// SmartBox::GetObjectBoundingBox (decomp 0x00452e20). When
|
||||
// null, the panel falls back to the per-type height heuristic.
|
||||
Vector3? WorldSphereCenter = null,
|
||||
float? WorldSphereRadius = null);
|
||||
|
||||
private readonly Func<uint?> _selectedGuidProvider;
|
||||
private readonly Func<uint, TargetInfo?> _entityResolver;
|
||||
private readonly Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> _cameraProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Pixel size of each corner triangle's right-angle legs.
|
||||
/// Retail uses <c>UIRegion::GetWidth(m_rgOnScreenCorners.m_data[1])</c>
|
||||
/// of the triangle sprite (decomp <c>0x004f69c8</c>). The retail
|
||||
/// sprite is small — ~8 px legs. 14 was too chunky per user
|
||||
/// feedback on 2026-05-16; 8 matches the retail screenshot.
|
||||
/// </summary>
|
||||
public float TriangleSize { get; set; } = 8f;
|
||||
|
||||
/// <summary>
|
||||
/// World-space height of the indicator box for entities that don't
|
||||
/// have a more specific type tag. Items use a smaller value (see
|
||||
/// <see cref="EntityHeightFor"/>). 1.8 m matches a standing humanoid;
|
||||
/// short items still get a small box because the projection
|
||||
/// preserves apparent size.
|
||||
/// </summary>
|
||||
public float EntityHeight { get; set; } = 1.8f;
|
||||
|
||||
/// <summary>
|
||||
/// Defensive fallback height when the entity has no usable
|
||||
/// SelectionSphere (Radius ≤ 1e-4f). With B.7's sphere-projection
|
||||
/// path active (since commit f4f4143), this fallback only fires
|
||||
/// for entities whose Setup didn't bake a selection sphere —
|
||||
/// rare in practice. The single 1.5 m × scale default is a sane
|
||||
/// midpoint; per-type branches were retired in the 2026-05-16
|
||||
/// Commit B because the sphere path is authoritative.
|
||||
/// </summary>
|
||||
public float EntityHeightFor(uint itemType, uint pwdBitfield, float scale, uint? useability = null)
|
||||
{
|
||||
if (scale <= 0f) scale = 1f;
|
||||
return 1.5f * scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Box width = <see cref="EntityHeight"/> projected height ×
|
||||
/// <see cref="WidthHeightRatio"/>. Retail's Vivid Target Indicator
|
||||
/// draws a square box — four corner triangles arranged in a square —
|
||||
/// so 1.0 = width matches height. The earlier 0.5 (humanoid-ish
|
||||
/// aspect) made the box uncomfortably narrow for non-humanoids.
|
||||
/// </summary>
|
||||
public float WidthHeightRatio { get; set; } = 1.0f;
|
||||
|
||||
/// <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,
|
||||
Func<uint, TargetInfo?> entityResolver,
|
||||
Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> cameraProvider)
|
||||
{
|
||||
_selectedGuidProvider = selectedGuidProvider;
|
||||
_entityResolver = entityResolver;
|
||||
_cameraProvider = cameraProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame render call. No-op if nothing is selected, the selected
|
||||
/// entity is gone, or the entity is off-screen / behind the camera.
|
||||
/// Draws to the ImGui background draw list so it appears behind
|
||||
/// other panels.
|
||||
/// </summary>
|
||||
public void Render()
|
||||
{
|
||||
if (_selectedGuidProvider() is not uint guid) return;
|
||||
if (_entityResolver(guid) is not TargetInfo info) return;
|
||||
|
||||
var (view, projection, viewport) = _cameraProvider();
|
||||
if (viewport.X <= 0 || viewport.Y <= 0) return;
|
||||
|
||||
var viewProj = view * projection;
|
||||
|
||||
Vector2 tl, tr, br, bl;
|
||||
|
||||
if (info.WorldSphereCenter is Vector3 sphereCenter
|
||||
&& info.WorldSphereRadius is float sphereRadius
|
||||
&& AcDream.Core.Selection.ScreenProjection.TryProjectSphereToScreenRect(
|
||||
sphereCenter, sphereRadius, view, projection, viewport,
|
||||
out var rMin, out var rMax, out _,
|
||||
minSidePixels: 12f))
|
||||
{
|
||||
// 2026-05-16 — retail-faithful path per
|
||||
// SmartBox::GetObjectBoundingBox (decomp 0x00452e20).
|
||||
// Retail uses CPhysicsObj::GetSelectionSphere (the Setup's
|
||||
// baked selection_sphere) and produces the screen rect
|
||||
// from that sphere's projection — NOT from a per-mesh AABB.
|
||||
//
|
||||
// Retail INFLATES the rect by one triangle width/height on
|
||||
// every side before drawing (decomp 0x004f6a0b–0x004f6a99):
|
||||
// edi_3 = arg4->left - eax_21 (shift left by triangleW)
|
||||
// ebp_3 = arg4->top - eax_23 (shift up by triangleH)
|
||||
// width = sphere_width + 2 * triangleW
|
||||
// height = sphere_height + 2 * triangleH
|
||||
// So the four corner triangles sit OUTSIDE the projected
|
||||
// sphere by one triangle leg.
|
||||
float ts = TriangleSize;
|
||||
tl = new Vector2(rMin.X - ts, rMin.Y - ts);
|
||||
tr = new Vector2(rMax.X + ts, rMin.Y - ts);
|
||||
br = new Vector2(rMax.X + ts, rMax.Y + ts);
|
||||
bl = new Vector2(rMin.X - ts, rMax.Y + ts);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback when the AABB isn't available (no setup cached
|
||||
// yet, missing GfxObj bounds, behind the camera). Square
|
||||
// box centred at the entity origin, height from the
|
||||
// per-type heuristic.
|
||||
if (!TryProjectToScreen(info.WorldPosition, viewProj, viewport, out var feetScreen))
|
||||
return;
|
||||
float entityHeight = EntityHeightFor(info.ItemType, info.ObjectDescriptionFlags, info.Scale, info.Useability);
|
||||
var headWorld = new Vector3(
|
||||
info.WorldPosition.X,
|
||||
info.WorldPosition.Y,
|
||||
info.WorldPosition.Z + entityHeight);
|
||||
if (!TryProjectToScreen(headWorld, viewProj, viewport, out var headScreen))
|
||||
return;
|
||||
|
||||
float screenHeight = MathF.Abs(headScreen.Y - feetScreen.Y);
|
||||
if (screenHeight < MinScreenHeight) screenHeight = MinScreenHeight;
|
||||
float screenWidth = screenHeight * WidthHeightRatio;
|
||||
|
||||
Vector2 center = (feetScreen + headScreen) * 0.5f;
|
||||
float halfW = screenWidth * 0.5f;
|
||||
float halfH = screenHeight * 0.5f;
|
||||
|
||||
tl = new Vector2(center.X - halfW, center.Y - halfH);
|
||||
tr = new Vector2(center.X + halfW, center.Y - halfH);
|
||||
br = new Vector2(center.X + halfW, center.Y + halfH);
|
||||
bl = new Vector2(center.X - halfW, center.Y + halfH);
|
||||
}
|
||||
|
||||
var rgba = RadarBlipColors.For(info.ItemType, info.ObjectDescriptionFlags);
|
||||
uint col = MakeImGuiColor(rgba);
|
||||
|
||||
var drawList = ImGui.GetBackgroundDrawList();
|
||||
|
||||
float t = TriangleSize;
|
||||
|
||||
// 2026-05-16 — flipped per user feedback. Each corner triangle's
|
||||
// RIGHT-ANGLE apex now points INWARD toward the target (was at
|
||||
// the outer corner pointing outward). Combined with the
|
||||
// TriangleSize inflate on the rect, the apex of each triangle
|
||||
// lands at the projected mesh boundary while the hypotenuse
|
||||
// runs across the outer (inflated) corner — giving the retail
|
||||
// "corner-tick pointing at the entity" look.
|
||||
//
|
||||
// Geometry per corner:
|
||||
// apex = corner + (±t, ±t) ← inward, right-angle here
|
||||
// leg_a end = corner + (±t, 0) ← along horizontal edge
|
||||
// leg_b end = corner + (0, ±t) ← along vertical edge
|
||||
// Hypotenuse runs from leg_a end to leg_b end (the outer
|
||||
// diagonal of the corner).
|
||||
drawList.AddTriangleFilled(tl + new Vector2( t, t), tl + new Vector2( t, 0), tl + new Vector2(0, t), col);
|
||||
drawList.AddTriangleFilled(tr + new Vector2(-t, t), tr + new Vector2(-t, 0), tr + new Vector2(0, t), col);
|
||||
drawList.AddTriangleFilled(br + new Vector2(-t, -t), br + new Vector2(-t, 0), br + new Vector2(0, -t), col);
|
||||
drawList.AddTriangleFilled(bl + new Vector2( t, -t), 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).
|
||||
return ((uint)c.A << 24) | ((uint)c.B << 16) | ((uint)c.G << 8) | c.R;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue