Aim coordinates: route all conversions through the true NDC (fixes the

sideways lock offset)

The windowed present stretches the fixed 800x600 backbuffer into the
client area while the projection's aspect follows the CLIENT (resize
rebuild) -- so the dpl2d/reticle frame, the client frame and NDC only
coincide for a backbuffer-shaped window. The mouse mapping and the
square-frame ray shortcut (cx = rx*tanHalfFov) were exact at screen
centre and drifted outward with a side-flipped sign: aiming left of the
enemy locked as if right of it (user report).

- BTSetAimProjection now publishes BOTH proj scales (P11 carries the
  aspect) + the backbuffer size (the dpl2d frame). NOTE: GetViewport at
  the loop top holds the PREVIOUS frame's last pass viewport (gauge
  passes shrink it) -> use the renderer's own GetWidth/GetHeight.
- BTGetAimRay / BTProjectToReticle convert reticle <-> NDC per axis
  (ndc_x = rx * vh/vw; camera x = ndc_x/P11) -- exact for any window
  shape and FOV.
- New BTClientToReticle undoes the present stretch for the mouse
  (client px -> viewport px -> reticle coords); mech4 uses it instead
  of the raw client-height mapping.
- 1Hz aim telemetry in the [target] log (hits/noRay/noPick): verified
  100% pick hit rate once the projection is live (the pre-visibility
  mission-load seconds report noRay -- that was the "flicker").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 22:13:45 -05:00
co-authored by Claude Fable 5
parent 9666dc5b5e
commit cf9f56044e
2 changed files with 98 additions and 30 deletions
+73 -18
View File
@@ -96,11 +96,23 @@ void BTSetLodEye(float x, float y, float z)
// convention = the dpl2d frame: centered origin, +y down, unit = half the
// viewport height (see dpl2d.cpp MapX/MapY).
// ---------------------------------------------------------------------------
// COORDINATE MODEL: everything talks in the DPL2D/RETICLE frame -- centered
// origin, +y down, unit = half the D3D VIEWPORT height (dpl2d MapX/MapY).
// The windowed present STRETCHES the fixed backbuffer into the client area,
// and the projection's aspect follows the CLIENT (the resize rebuild) -- so
// the viewport frame, the client frame and the NDC frame all differ unless
// the window happens to be exactly backbuffer-shaped. Every conversion below
// goes through the true NDC (per-axis P11/P22), NOT a square-frame shortcut:
// a shortcut is exact at screen centre and drifts outward with a side-flipped
// sign (the "locks left of the mech when I aim right of it" report).
static float gBTAimCamPos[3] = {0, 0, 0};
static float gBTAimCamX[3] = {1, 0, 0}; // camera right (LookAtRH xaxis)
static float gBTAimCamY[3] = {0, 1, 0}; // camera up (LookAtRH yaxis)
static float gBTAimCamZ[3] = {0, 0, 1}; // camera BACK (LookAtRH zaxis; view dir = -Z)
static float gBTAimTanHalf = 0.0f; // tan(fovY/2) = 1 / proj._22
static float gBTAimP11 = 0.0f; // proj._11 (x scale; carries the aspect)
static float gBTAimP22 = 0.0f; // proj._22 (y scale = 1/tan(fovY/2))
static float gBTAimVpW = 0.0f; // D3D viewport (backbuffer) size --
static float gBTAimVpH = 0.0f; // the dpl2d frame's pixel space
static int gBTAimCamValid = 0;
void BTSetAimCamera(const float pos[3], const float xax[3],
@@ -116,24 +128,58 @@ void BTSetAimCamera(const float pos[3], const float xax[3],
gBTAimCamValid = 1;
}
void BTSetAimProjection(float proj22)
void BTSetAimProjection(float p11, float p22, float vpW, float vpH)
{
if (proj22 > 1e-6f)
gBTAimTanHalf = 1.0f / proj22;
if (p11 > 1e-6f && p22 > 1e-6f && vpW > 0.0f && vpH > 0.0f)
{
gBTAimP11 = p11;
gBTAimP22 = p22;
gBTAimVpW = vpW;
gBTAimVpH = vpH;
}
}
//
// Crosshair (reticle coords) -> world pick ray. RH camera looks down -Z;
// a point at reticle (rx, ry) sits at camera-space (rx*t, -ry*t, -1)
// (reticle +y is DOWN, camera +y is UP; t = tan(fovY/2), and the reticle's
// half-viewport-height unit cancels the aspect term exactly).
// Client-area mouse position -> reticle coords: undo the present stretch
// (client px -> viewport px), then centre/scale by the dpl2d unit. Clamped
// to the visible frame.
//
void BTClientToReticle(float mx, float my, float cw, float ch,
float *rx, float *ry)
{
if (cw <= 0.0f || ch <= 0.0f || gBTAimVpW <= 0.0f || gBTAimVpH <= 0.0f)
{
*rx = 0.0f; *ry = 0.0f;
return;
}
const float vx = mx * (gBTAimVpW / cw);
const float vy = my * (gBTAimVpH / ch);
float x = (vx - gBTAimVpW * 0.5f) / (gBTAimVpH * 0.5f);
float y = (vy - gBTAimVpH * 0.5f) / (gBTAimVpH * 0.5f);
const float xmax = gBTAimVpW / gBTAimVpH;
if (x < -xmax) x = -xmax;
if (x > xmax) x = xmax;
if (y < -1.0f) y = -1.0f;
if (y > 1.0f) y = 1.0f;
*rx = x;
*ry = y;
}
//
// Crosshair (reticle coords) -> world pick ray. reticle -> NDC:
// ndc_x = rx * vh/vw (the reticle x unit is half the viewport HEIGHT),
// ndc_y = -ry (reticle +y is down). NDC -> camera: divide by the true
// per-axis projection scales; RH camera looks down -Z.
//
int BTGetAimRay(float rx, float ry, float outStart[3], float outDir[3])
{
if (!gBTAimCamValid || gBTAimTanHalf <= 0.0f)
if (!gBTAimCamValid || gBTAimP11 <= 0.0f || gBTAimP22 <= 0.0f
|| gBTAimVpW <= 0.0f)
return 0;
const float cx = rx * gBTAimTanHalf;
const float cy = -ry * gBTAimTanHalf;
const float ndcX = rx * (gBTAimVpH / gBTAimVpW);
const float ndcY = -ry;
const float cx = ndcX / gBTAimP11;
const float cy = ndcY / gBTAimP22;
float d[3];
float len = 0.0f;
for (int i = 0; i < 3; ++i)
@@ -159,7 +205,8 @@ int BTGetAimRay(float rx, float ry, float outStart[3], float outDir[3])
//
int BTProjectToReticle(const float world[3], float *rx, float *ry)
{
if (!gBTAimCamValid || gBTAimTanHalf <= 0.0f)
if (!gBTAimCamValid || gBTAimP11 <= 0.0f || gBTAimP22 <= 0.0f
|| gBTAimVpH <= 0.0f)
{
*rx = 0.0f; *ry = 0.0f;
return 0;
@@ -177,8 +224,10 @@ int BTProjectToReticle(const float world[3], float *rx, float *ry)
*ry = 0.0f;
return 0;
}
*rx = xc / (gBTAimTanHalf * depth);
*ry = -yc / (gBTAimTanHalf * depth);
const float ndcX = (xc * gBTAimP11) / depth;
const float ndcY = (yc * gBTAimP22) / depth;
*rx = ndcX * (gBTAimVpW / gBTAimVpH);
*ry = -ndcY;
return 1;
}
@@ -7254,11 +7303,17 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
// the render lists
memset(mRenderLists, 0, sizeof(mRenderLists));
// aim-ray feed (task #36): publish the live projection's FOV term so the
// game side can build the reticle pick ray / designator projection.
// aim-ray feed (task #36): publish the live projection's per-axis scales +
// the BACKBUFFER size (= the dpl2d reticle frame -- the full-screen
// viewport the 2D layer draws in) so the game side can build the reticle
// pick ray / designator projection through the TRUE NDC. NOTE: do NOT
// poll GetViewport here -- at this point it still holds whatever viewport
// the PREVIOUS frame's last pass set (gauge/MFD passes shrink it), which
// made the pick ray flicker frame to frame.
{
extern void BTSetAimProjection(float proj22);
BTSetAimProjection(mProjectionMatrix._22);
extern void BTSetAimProjection(float p11, float p22, float vpW, float vpH);
BTSetAimProjection(mProjectionMatrix._11, mProjectionMatrix._22,
(float)GetWidth(), (float)GetHeight());
}
HierarchicalDrawComponent *drawComp;
+25 -12
View File
@@ -1272,17 +1272,17 @@ void
pCur(&cp) && pStc(wnd, &cp) && pCr(wnd, &rc) &&
rc.right > rc.left && rc.bottom > rc.top)
{
const float w = (float)(rc.right - rc.left);
const float h = (float)(rc.bottom - rc.top);
const float hh = h * 0.5f;
float rx = ((float)cp.x - w * 0.5f) / hh;
float ry = ((float)cp.y - h * 0.5f) / hh;
// clamp to the visible frame (aspect-wide in x)
const float xmax = (w / h);
if (rx < -xmax) rx = -xmax;
if (rx > xmax) rx = xmax;
if (ry < -1.0f) ry = -1.0f;
if (ry > 1.0f) ry = 1.0f;
// client px -> reticle coords through the RENDERER
// (it undoes the backbuffer->client present stretch;
// a raw client-height mapping is only exact for a
// backbuffer-shaped window and skews the pick
// sideways otherwise).
extern void BTClientToReticle(float mx, float my,
float cw, float ch, float *rx, float *ry);
float rx = 0.0f, ry = 0.0f;
BTClientToReticle((float)cp.x, (float)cp.y,
(float)(rc.right - rc.left),
(float)(rc.bottom - rc.top), &rx, &ry);
gBTAimX = rx;
gBTAimY = ry;
}
@@ -2311,15 +2311,25 @@ void
// resolves damage to the zone under the crosshair (aimed fire).
Entity *hotTarget = 0; // mech under the crosshair NOW
Point3D hotPoint; // picked world point on its hull
static int gAimNoRay = 0, gAimNoPick = 0, gAimHits = 0; // 1Hz diagnostics
{
extern int BTGetAimRay(float rx, float ry, float outStart[3], float outDir[3]);
float rs[3], rd[3];
if (BTGetAimRay(gBTAimX, gBTAimY, rs, rd) && gEnemyMech != 0)
if (!BTGetAimRay(gBTAimX, gBTAimY, rs, rd))
{
++gAimNoRay;
}
else if (gEnemyMech != 0)
{
Point3D rayStart(rs[0], rs[1], rs[2]);
Vector3D rayDir(rd[0], rd[1], rd[2]);
if (((Mech *)gEnemyMech)->PickRayHit(rayStart, rayDir, 4000.0f, &hotPoint))
{
hotTarget = gEnemyMech;
++gAimHits;
}
else
++gAimNoPick;
}
// The Reticle struct (the mech's TargetReticle attribute): position,
@@ -2396,7 +2406,10 @@ void
: (designated ? " designated (off-crosshair)" : " no target"))
<< " range=" << range
<< (range <= kWeaponRange ? " IN RANGE" : "")
<< " [hits=" << gAimHits << " noRay=" << gAimNoRay
<< " noPick=" << gAimNoPick << "]"
<< "\n" << std::flush;
gAimHits = 0; gAimNoRay = 0; gAimNoPick = 0;
}
// --- FIRING (bring-up): on the trigger, with a target in range and the