From cf9f56044ea9137f5b3be3f93e3961120cc7b712 Mon Sep 17 00:00:00 2001 From: arcattack Date: Wed, 8 Jul 2026 22:13:45 -0500 Subject: [PATCH] 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 --- engine/MUNGA_L4/L4VIDEO.cpp | 91 +++++++++++++++++++++++++++++------- game/reconstructed/mech4.cpp | 37 ++++++++++----- 2 files changed, 98 insertions(+), 30 deletions(-) diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 5e68173..cce38ed 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -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; diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index f20983c..33517da 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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