diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 16:03:34 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 16:03:34 +0100 |
| commit | 4171b26d1ce68d185f3d2611f5753f4288a92a69 (patch) | |
| tree | ee271ec4b5330598830638f208eac96e8e79e12f | |
| parent | Fix overlay-vanishing projections; ammo rule via held weapons (diff) | |
| download | hl2h-4171b26d1ce68d185f3d2611f5753f4288a92a69.tar.gz hl2h-4171b26d1ce68d185f3d2611f5753f4288a92a69.zip | |
Clip transition and radius lines at the near plane
Transition-box edges and projectile radius rings were only drawn when both
endpoints projected. An edge with one corner behind the camera vanished, so a
half-visible box or ring showed nothing. Add a segment projector that clips at
the near plane in view space, then clips the 2D result to the screen bounds,
and use it for both.
| -rw-r--r-- | CONTEXT.md | 7 | ||||
| -rw-r--r-- | src/esp.cpp | 102 |
2 files changed, 89 insertions, 20 deletions
| @@ -271,7 +271,8 @@ vocabulary / decisions. | |||
| 271 | trigger whose target equals `g_map_prev` is marked `<- map` (the way back). | 271 | trigger whose target equals `g_map_prev` is marked `<- map` (the way back). |
| 272 | - Scanning runs at 4 Hz in the ESP draw; the 8 world corners are projected and | 272 | - Scanning runs at 4 Hz in the ESP draw; the 8 world corners are projected and |
| 273 | the 12 edges are drawn as a 3D wireframe, with a 10 px tag above the highest | 273 | the 12 edges are drawn as a 3D wireframe, with a 10 px tag above the highest |
| 274 | visible corner. | 274 | visible corner. Each edge is clipped at the camera near plane and then to the |
| 275 | screen, so a box straddling the camera still draws its visible portions. | ||
| 275 | 276 | ||
| 276 | ## Bones / ESP | 277 | ## Bones / ESP |
| 277 | 278 | ||
| @@ -644,7 +645,9 @@ vocabulary / decisions. | |||
| 644 | `m_DmgRadius` (`FIELD_FLOAT`) as fallback, then 300/200/150/300 for | 645 | `m_DmgRadius` (`FIELD_FLOAT`) as fallback, then 300/200/150/300 for |
| 645 | frag/RPG/AR2/bomb. | 646 | frag/RPG/AR2/bomb. |
| 646 | - Draws a small marker and a `FRAG 1.2s`-style label; the optional radius ring | 647 | - Draws a small marker and a `FRAG 1.2s`-style label; the optional radius ring |
| 647 | is a 24-segment horizontal circle projected at the projectile height. | 648 | is a 24-segment horizontal circle projected at the projectile height, with |
| 649 | every segment near-plane/screen clipped independently, so it still draws | ||
| 650 | when the grenade itself is off-screen or behind the camera. | ||
| 648 | Toggles: Fuse timer, Blast radius. | 651 | Toggles: Fuse timer, Blast radius. |
| 649 | 652 | ||
| 650 | ## Roadmap / TODO | 653 | ## Roadmap / TODO |
diff --git a/src/esp.cpp b/src/esp.cpp index 447bee7..715dbc8 100644 --- a/src/esp.cpp +++ b/src/esp.cpp | |||
| @@ -1812,6 +1812,76 @@ static void transitions_scan() { | |||
| 1812 | } | 1812 | } |
| 1813 | } | 1813 | } |
| 1814 | 1814 | ||
| 1815 | // Clip a 2D segment to the screen (with a small margin). Liang-Barsky. | ||
| 1816 | static bool clip_line2d(float &x0, float &y0, float &x1, float &y1, | ||
| 1817 | float xmin, float ymin, float xmax, float ymax) { | ||
| 1818 | float dx = x1 - x0, dy = y1 - y0; | ||
| 1819 | float p[4] = {-dx, dx, -dy, dy}; | ||
| 1820 | float q[4] = {x0 - xmin, xmax - x0, y0 - ymin, ymax - y0}; | ||
| 1821 | float u1 = 0.0f, u2 = 1.0f; | ||
| 1822 | for (int i = 0; i < 4; i++) { | ||
| 1823 | if (fabsf(p[i]) < 1e-6f) { | ||
| 1824 | if (q[i] < 0.0f) | ||
| 1825 | return false; | ||
| 1826 | continue; | ||
| 1827 | } | ||
| 1828 | float r = q[i] / p[i]; | ||
| 1829 | if (p[i] < 0.0f) { | ||
| 1830 | if (r > u2) return false; | ||
| 1831 | if (r > u1) u1 = r; | ||
| 1832 | } else { | ||
| 1833 | if (r < u1) return false; | ||
| 1834 | if (r < u2) u2 = r; | ||
| 1835 | } | ||
| 1836 | } | ||
| 1837 | float nx0 = x0 + u1 * dx, ny0 = y0 + u1 * dy; | ||
| 1838 | float nx1 = x0 + u2 * dx, ny1 = y0 + u2 * dy; | ||
| 1839 | x0 = nx0; y0 = ny0; x1 = nx1; y1 = ny1; | ||
| 1840 | return true; | ||
| 1841 | } | ||
| 1842 | |||
| 1843 | // Project a world-space segment, clipping it at the near plane first. Without | ||
| 1844 | // this an edge with one corner behind the camera was dropped entirely, so a | ||
| 1845 | // partially visible transition box or radius ring disappeared. | ||
| 1846 | static bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0, | ||
| 1847 | int &x1, int &y1) { | ||
| 1848 | if (!g_scr_w || !g_scr_h) | ||
| 1849 | return false; | ||
| 1850 | float pitch = g_cam_view.x * PI_F / 180.0f; | ||
| 1851 | float yaw = g_cam_view.y * PI_F / 180.0f; | ||
| 1852 | float cp = cosf(pitch), sp = sinf(pitch); | ||
| 1853 | float cy = cosf(yaw), sy = sinf(yaw); | ||
| 1854 | Vector fwd{cp * cy, cp * sy, -sp}; | ||
| 1855 | Vector da{a.x - g_cam_eye.x, a.y - g_cam_eye.y, a.z - g_cam_eye.z}; | ||
| 1856 | Vector db{b.x - g_cam_eye.x, b.y - g_cam_eye.y, b.z - g_cam_eye.z}; | ||
| 1857 | const float nearz = 4.0f; | ||
| 1858 | float za = da.x * fwd.x + da.y * fwd.y + da.z * fwd.z; | ||
| 1859 | float zb = db.x * fwd.x + db.y * fwd.y + db.z * fwd.z; | ||
| 1860 | if (za <= nearz && zb <= nearz) | ||
| 1861 | return false; | ||
| 1862 | Vector ca = a, cb = b; | ||
| 1863 | if (za <= nearz) { | ||
| 1864 | float t = (nearz - za) / (zb - za); | ||
| 1865 | ca = Vector{a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, | ||
| 1866 | a.z + (b.z - a.z) * t}; | ||
| 1867 | } else if (zb <= nearz) { | ||
| 1868 | float t = (nearz - zb) / (za - zb); | ||
| 1869 | cb = Vector{b.x + (a.x - b.x) * t, b.y + (a.y - b.y) * t, | ||
| 1870 | b.z + (a.z - b.z) * t}; | ||
| 1871 | } | ||
| 1872 | float ax, ay, bx, by; | ||
| 1873 | if (!world_to_screen(ca, ax, ay) || !world_to_screen(cb, bx, by)) | ||
| 1874 | return false; | ||
| 1875 | if (!clip_line2d(ax, ay, bx, by, -8.0f, -8.0f, (float)g_scr_w + 8.0f, | ||
| 1876 | (float)g_scr_h + 8.0f)) | ||
| 1877 | return false; | ||
| 1878 | x0 = (int)ax; | ||
| 1879 | y0 = (int)ay; | ||
| 1880 | x1 = (int)bx; | ||
| 1881 | y1 = (int)by; | ||
| 1882 | return true; | ||
| 1883 | } | ||
| 1884 | |||
| 1815 | static void draw_transitions() { | 1885 | static void draw_transitions() { |
| 1816 | if (!g_surface || !g_ntrans) | 1886 | if (!g_surface || !g_ntrans) |
| 1817 | return; | 1887 | return; |
| @@ -1844,8 +1914,9 @@ static void draw_transitions() { | |||
| 1844 | surf_color(r, g, bl, 210); | 1914 | surf_color(r, g, bl, 210); |
| 1845 | for (int e = 0; e < 12; e++) { | 1915 | for (int e = 0; e < 12; e++) { |
| 1846 | int a = edges[e][0], c = edges[e][1]; | 1916 | int a = edges[e][0], c = edges[e][1]; |
| 1847 | if (ok[a] && ok[c]) | 1917 | int lx, ly, hx, hy; |
| 1848 | surf_line((int)xs[a], (int)ys[a], (int)xs[c], (int)ys[c]); | 1918 | if (project_segment(corners[a], corners[c], lx, ly, hx, hy)) |
| 1919 | surf_line(lx, ly, hx, hy); | ||
| 1849 | } | 1920 | } |
| 1850 | // Tag above the highest projected corner. | 1921 | // Tag above the highest projected corner. |
| 1851 | float topx = 0.0f, topy = 1e9f; | 1922 | float topx = 0.0f, topy = 1e9f; |
| @@ -2774,26 +2845,21 @@ void esp_draw_projectiles() { | |||
| 2774 | draw_text_small_col(tx, ty, text, r, g, b); | 2845 | draw_text_small_col(tx, ty, text, r, g, b); |
| 2775 | } | 2846 | } |
| 2776 | // The ring is useful even when the grenade itself is behind the camera or | 2847 | // The ring is useful even when the grenade itself is behind the camera or |
| 2777 | // off-screen, so it is not gated on center_ok. | 2848 | // off-screen, so it is not gated on center_ok and each segment is clipped |
| 2849 | // at the near plane independently. | ||
| 2778 | if (g_proj_radius && p.radius > 1.0f) { | 2850 | if (g_proj_radius && p.radius > 1.0f) { |
| 2779 | const int SEG = 24; | 2851 | const int SEG = 24; |
| 2780 | float lx = 0.0f, ly = 0.0f; | 2852 | Vector ring[SEG + 1]; |
| 2781 | bool have = false; | ||
| 2782 | surf_color(r, g, b, 150); | ||
| 2783 | for (int k = 0; k <= SEG; k++) { | 2853 | for (int k = 0; k <= SEG; k++) { |
| 2784 | float a = (float)k * 2.0f * PI_F / (float)SEG; | 2854 | float a = (float)k * 2.0f * PI_F / (float)SEG; |
| 2785 | Vector w{p.origin.x + cosf(a) * p.radius, | 2855 | ring[k] = Vector{p.origin.x + cosf(a) * p.radius, |
| 2786 | p.origin.y + sinf(a) * p.radius, p.origin.z}; | 2856 | p.origin.y + sinf(a) * p.radius, p.origin.z}; |
| 2787 | float rx, ry; | 2857 | } |
| 2788 | if (!world_to_screen(w, rx, ry)) { | 2858 | surf_color(r, g, b, 150); |
| 2789 | have = false; | 2859 | for (int k = 0; k < SEG; k++) { |
| 2790 | continue; | 2860 | int x0, y0, x1, y1; |
| 2791 | } | 2861 | if (project_segment(ring[k], ring[k + 1], x0, y0, x1, y1)) |
| 2792 | if (have) | 2862 | surf_line(x0, y0, x1, y1); |
| 2793 | surf_line((int)lx, (int)ly, (int)rx, (int)ry); | ||
| 2794 | lx = rx; | ||
| 2795 | ly = ry; | ||
| 2796 | have = true; | ||
| 2797 | } | 2863 | } |
| 2798 | } | 2864 | } |
| 2799 | } | 2865 | } |
