From 0cbcbfc14997eff39e0c76acf30c48b851ed1f62 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 22:09:14 +0100 Subject: Chams: working alpha via RenderView blend and a custom VMT Testing on :99 showed the material $alpha path is a dead end: UnlitGeneric takes translucency from the base texture/vertex alpha, and the debug materials have neither. Follow Amalgam: apply alpha with IVRenderView::SetBlend (VEngineRenderView014 slot 4) around the model draw, saving/restoring it. To have something translucent to blend, write a tiny UnlitGeneric VMT with $model 1 + $translucent 1 + a white base texture to //materials/hl2hake/translucent.vmt (plus a custom/ copy). It is written by a preload constructor so it exists before the material system scans its search paths, then loaded once with FindMaterial. Styles 0/3/4/6 use it when the colour alpha is below 255. Also: - Visible wireframe + through-wall: fill the visible silhouette first so the through-wall pass cannot show in the wireframe gaps, then draw a brightened contour. Style 6 is now "Solid + contour" (recolour + visible edges). - find_modules builds local snapshots and publishes them under a mutex; the setup thread and the server retry could reset the shared vectors live and crash at startup. Verified on :99: Solid + alpha 64 renders a see-through viewmodel, Solid + contour shows a blue fill with bright blue contours, Additive is a translucent coloured glow and Fullbright is a white additive glow. --- CONTEXT.md | 46 ++++++++++------ src/gui.cpp | 2 +- src/hl2h.cpp | 1 + src/hl2h.h | 1 + src/modules.cpp | 35 +++++++----- src/studio.cpp | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 210 insertions(+), 36 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 5b96c33..1e860bf 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -27,7 +27,10 @@ covers ESP, aimbot, triggerbot and silent aim. ## Runtime architecture - Modules from `/proc/self/maps`: `client.so`, `engine.so`, `server.so`. - PROT_NONE ranges excluded; file-backed bss tails merged. + PROT_NONE ranges excluded; file-backed bss tails merged. The scan builds + local Module snapshots and publishes them under a mutex, because the setup + thread and the server-netvar retry can call it concurrently (resetting the + shared vectors live used to crash at startup). - Calling convention: cdecl (i386); `this` is the first stack argument. - Hooks (copy-and-swap vtable, 256 slots): - `CHLClient` slot 21 `CreateMove`; slot 35 `FrameStageNotify`. @@ -294,30 +297,43 @@ vocabulary / decisions. alpha only. The through-wall pass uses `MATERIAL_VAR_IGNOREZ` (1 << 15) through `IMaterial::SetMaterialVarFlag` (slot 29); colours use `$color`/`$color2` (IMaterialVar slot 11, SetVecValue 3-float slot 11) and - `ColorModulate` (slot 28). `$alpha` is set through IMaterialVar slot 3 - (SetFloatValue). Shared materials are restored after each draw. + `ColorModulate` (slot 28). Alpha is applied with + `IVRenderView::SetBlend` (`VEngineRenderView014` slot 4, saved/restored + around the passes) — model draws take their final alpha from the render + view, not from material `$alpha`. `$alpha`/`AlphaModulate` are still set for + completeness. Shared materials are restored after each draw. - The GUI colour picker (`add_color_alpha`) has an alpha bar, checkerboard preview and `#RRGGBBAA` readout; plain `add_color` keeps the old opaque three-component behaviour. Chams colours are four ints (r, g, b, a). -- **Alpha is stored but has no visual effect yet.** The usable model - materials are opaque: `debug/debugtranslucentsinglecolor` has no - `$translucent`, and toggling `MATERIAL_VAR_TRANSLUCENT` (1 << 21) or - rebuilding the state snapshot does not change the static shader combo. - `debug/debugtranslucentvertexcolor`/`debug/debughitbox` are translucent but - need vertex colours and render nothing on skinned models. A custom VMT with - `$model 1` + `$translucent 1` (or a white basetexture) is needed to make the - alpha control real. +- Alpha needs a translucent model material, so the mod writes a tiny VMT: + `//materials/hl2hake/translucent.vmt` (and a copy under + `custom/hl2hake/...`): `UnlitGeneric` with `$basetexture sprites/white`, + `$color`, `$model 1`, `$translucent 1`, `$nocull 1`. It is written from a + preload constructor (so it exists before the material system scans) and + loaded with `FindMaterial`; styles 0/3/4/6 switch to it when the colour + alpha is below 255. The base-game `debugtranslucentsinglecolor` has no + `$translucent` and cannot do alpha; `debugtranslucentvertexcolor` and + `debug/debughitbox` need vertex colours and draw nothing on skinned models. - Styles: 0 Flat (`debug/debugtranslucentsinglecolor` + FLAT), 1 Wireframe - (`debug/debugwireframe`), 2 Additive + (`debug/debugwireframe`, brightened tint), 2 Additive (`debug/debugtranslucentmodelhulls`, whose VMT has `$additive 1` + `$color`; the ADDITIVE flag on the solid material did nothing), 3 Solid, 4 White (solid, colour forced white), 5 Fullbright (the additive material, colour - forced white, giving a white-hot glow), 6 Glow outline (solid plus a - `debug/debugwireframe` contour pass drawn with ZNEARER (1 << 10) and a - brightened colour so the base cannot bury it). + forced white, giving a white-hot glow), 6 Solid + contour (solid fill plus + a `debug/debugwireframe` contour pass drawn with ZNEARER (1 << 10) and a + brightened colour, so the entity is recoloured while its edges stay + readable). Style 6 is the requested "recolour the whole entity but show its + contours" look. `vgui/white` is vertex-colour based and has no `$model`/`$basetexture`, so it is never used for chams; `models/debug/debugwhite` is EP2-only and the old VGUI additive material was invisible on NPCs. +- Through-wall + visible wireframe: a wireframe visible pass leaves gaps, so + the through-wall fill showed inside the visible silhouette. When both passes + are on and the visible style is Wireframe, a solid fill of the visible + colour is drawn first, then the brightened wireframe contour. (The proper + alternative is stencil: visible writes stencil 1, through-wall tests + stencil == 0, as Amalgam does; the fill is simpler and gives the + solid+contour look.) - Style materials are preloaded from `outer_draw` (throttled) and cached; the render hook never calls FindMaterial itself. A missing style uses solid. `FindMaterial` can return an unreferenced material, so each cached material diff --git a/src/gui.cpp b/src/gui.cpp index 89888da..fe82762 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -459,7 +459,7 @@ static void build_cards() { "Per-relationship model colours.", true); static const char *const chams_mat_names[] = { "Flat", "Wireframe", "Additive glow", "Solid", "White", "Fullbright", - "Glow outline"}; + "Solid + contour"}; auto add_chams_group = [&](const char *title, ChamsGroup &g) { add_section(chams, title); add_toggle(chams, "Visible", "Draw the normal-depth pass.", diff --git a/src/hl2h.cpp b/src/hl2h.cpp index 067b11f..ddfc02a 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp @@ -1149,6 +1149,7 @@ static void *setup_thread(void *) { g_iface.debugoverlay = ef("VDebugOverlay003", nullptr); g_iface.enginetrace = ef("EngineTraceClient003", nullptr); g_iface.modelrender = ef("VEngineModel016", nullptr); + g_iface.renderview = ef("VEngineRenderView014", nullptr); static const char *cvar_names[] = {"VEngineCvar004", "VEngineCvar007", "VEngineCvar003", "IVEngineCvar004", nullptr}; diff --git a/src/hl2h.h b/src/hl2h.h index f7274f1..34bf04f 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -69,6 +69,7 @@ struct GameInterfaces { void *enginetrace = nullptr; // EngineTraceClient003 (world vischeck) void *cvar = nullptr; // VEngineCvar004 (mat_fullbright etc.) void *modelrender = nullptr; // VEngineModel016 (chams) + void *renderview = nullptr; // VEngineRenderView014 (alpha/color modulation) }; extern GameInterfaces g_iface; diff --git a/src/modules.cpp b/src/modules.cpp index b832694..7a82028 100644 --- a/src/modules.cpp +++ b/src/modules.cpp @@ -1,8 +1,11 @@ // modules.cpp — /proc/self/maps parsing and module discovery #include "hl2h.h" #include +#include #include +static std::mutex g_modules_mutex; + static bool basename_is(const char *path, const char *base) { size_t pl = strlen(path), bl = strlen(base); if (pl < bl || strcmp(path + pl - bl, base) != 0) @@ -11,9 +14,7 @@ static bool basename_is(const char *path, const char *base) { } void find_modules() { - g_client = Module(); - g_engine = Module(); - g_server = Module(); + Module client, engine, server; FILE *f = fopen("/proc/self/maps", "r"); if (!f) return; @@ -41,19 +42,19 @@ void find_modules() { memcpy(clean, path, pl); clean[pl] = 0; if (basename_is(clean, "client.so")) - m = &g_client; + m = &client; else if (basename_is(clean, "engine.so")) - m = &g_engine; + m = &engine; else if (basename_is(clean, "server.so")) - m = &g_server; + m = &server; } else { // anon: absorb only as .bss tail after a file-backed range - if (g_client.valid() && g_client.ranges.back().file && - lo == g_client.ranges.back().hi && hi - lo <= 0x1000000) - m = &g_client; - else if (g_engine.valid() && g_engine.ranges.back().file && - lo == g_engine.ranges.back().hi && hi - lo <= 0x1000000) - m = &g_engine; + if (client.valid() && client.ranges.back().file && + lo == client.ranges.back().hi && hi - lo <= 0x1000000) + m = &client; + else if (engine.valid() && engine.ranges.back().file && + lo == engine.ranges.back().hi && hi - lo <= 0x1000000) + m = &engine; } if (!m) continue; @@ -71,6 +72,16 @@ void find_modules() { } } fclose(f); + // setup_thread and resolve_server_netvars can both call this; the old code + // reset the shared Module globals and reallocated their vectors live, which + // crashed under the startup thread. Publish complete snapshots instead. + std::lock_guard lock(g_modules_mutex); + if (client.valid()) + g_client = std::move(client); + if (engine.valid()) + g_engine = std::move(engine); + if (server.valid()) + g_server = std::move(server); } // --------------------------------------------------------------------------- diff --git a/src/studio.cpp b/src/studio.cpp index cf145d1..3ee6cc5 100644 --- a/src/studio.cpp +++ b/src/studio.cpp @@ -19,7 +19,10 @@ #include #include #include +#include +#include #include +#include namespace { @@ -197,7 +200,7 @@ using ForceOverrideFn = void (*)(void *, void *, int); void *g_modelrender = nullptr; ModelExecFn g_orig_model_exec = nullptr; ForceOverrideFn g_force_override = nullptr; -void *g_style_mats[7] = {}; +void *g_style_mats[8] = {}; const char *chams_material_name(int idx) { switch (idx) { @@ -257,6 +260,12 @@ void chams_apply(void *mat, bool ignorez, int style, bool force_white, float b = force_white ? 1.0f : (float)rgb[2] / 255.0f; chams_color(mat, r, g, b); chams_alpha(mat, (float)rgb[3] / 255.0f); + { + // Entity-style alpha fade; UnlitGeneric takes translucency from the base + // texture/vertex alpha, so $alpha alone is not enough. + using AlphaFn = void (*)(void *, float); + vfunc(mat, 27)(mat, (float)rgb[3] / 255.0f); + } // 0 Flat, 1 Wireframe, 2 Additive, 3 Solid, 4 White, 5 Fullbright // (additive white glow), 6 Glow outline (solid base; the wireframe overlay // is a second draw with ZNEARER so its contour is not hidden by the base). @@ -270,8 +279,7 @@ void chams_apply(void *mat, bool ignorez, int style, bool force_white, vfunc(mat, 29)(mat, 1 << 6 /* SELFILLUM */, style == 5); } -void chams_reset(void *mat) { - using FlagFn = void (*)(void *, int, bool); +void chams_reset(void *mat) { using FlagFn = void (*)(void *, int, bool); vfunc(mat, 29)(mat, 1 << 15, false); vfunc(mat, 29)(mat, 1 << 10, false); vfunc(mat, 29)(mat, 1 << 28, false); @@ -282,6 +290,22 @@ void chams_reset(void *mat) { chams_alpha(mat, 1.0f); } +// Model draws take their final alpha from IVRenderView::SetBlend; material +// $alpha alone is ignored by UnlitGeneric. Save/restore it around our passes. +float chams_get_blend() { + if (!g_iface.renderview) + return 1.0f; + using GetFn = float (*)(void *); + return vfunc(g_iface.renderview, 5)(g_iface.renderview); +} + +void chams_set_blend(float a) { + if (!g_iface.renderview) + return; + using SetFn = void (*)(void *, float); + vfunc(g_iface.renderview, 4)(g_iface.renderview, a); +} + void hkdModelExecute(void *self, const void *state, const void *info, void *bone) { if (!g_orig_model_exec) @@ -304,21 +328,35 @@ void hkdModelExecute(void *self, const void *state, const void *info, if (style < 0) style = 0; if (style > 6) style = 6; void *mat = style_material(style); + bool alpha = rgb[3] < 255; + if (alpha && g_style_mats[7] && + (style == 0 || style == 3 || style == 4 || style == 6)) + mat = g_style_mats[7]; if (!mat) return false; bool white = style == 4 || style == 5; + // Wireframe lines use a brighter tint so they stay readable over a + // solid fill. + int wrgb[4]; const int *use_rgb = rgb; + if (style == 1) { + for (int i = 0; i < 3; i++) + wrgb[i] = (rgb[i] + 255) / 2; + wrgb[3] = rgb[3]; + use_rgb = wrgb; + } chams_apply(mat, ignorez, style, white, use_rgb, false); + chams_set_blend((float)use_rgb[3] / 255.0f); g_force_override(self, mat, 0 /* OVERRIDE_NORMAL */); g_orig_model_exec(self, state, info, bone); if (style == 6) { - // Solid base plus a wireframe contour pass for depth. Brighten - // the contour and bias it forward so the base cannot bury it. + // Solid base plus a wireframe contour pass for depth. void *wire = g_style_mats[1]; if (wire && wire != mat) { - int wrgb[4] = {(rgb[0] + 255) / 2, (rgb[1] + 255) / 2, + int crgb[4] = {(rgb[0] + 255) / 2, (rgb[1] + 255) / 2, (rgb[2] + 255) / 2, rgb[3]}; - chams_apply(wire, ignorez, 1, false, wrgb, true); + chams_apply(wire, ignorez, 1, false, crgb, true); + chams_set_blend((float)crgb[3] / 255.0f); g_force_override(self, wire, 0); g_orig_model_exec(self, state, info, bone); chams_reset(wire); @@ -327,17 +365,24 @@ void hkdModelExecute(void *self, const void *state, const void *info, chams_reset(mat); return true; }; + float old_blend = chams_get_blend(); bool did = false; if (is_vm) { did = draw_pass(g_chams_vm_mat, g_chams_vm_rgb, false); } else { if (grp->inv_on) did |= draw_pass(grp->inv_mat, grp->inv_rgb, true); - if (grp->vis_on) + if (grp->vis_on) { + // A wireframe visible pass leaves gaps, so the through-wall fill + // would show inside the visible silhouette. Fill it first. + if (grp->inv_on && grp->vis_mat == 1) + did |= draw_pass(3, grp->vis_rgb, false); did |= draw_pass(grp->vis_mat, grp->vis_rgb, false); + } } if (did) { g_force_override(self, nullptr, 0); + chams_set_blend(old_blend); return; } } @@ -354,6 +399,96 @@ static double chams_now() { return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; } +// The base-game model debug materials are opaque. Drop a tiny VMT with +// $model + $translucent into the game's custom/materials folders so the alpha +// channel in the colour picker has something to drive. Written from a +// constructor too, so it exists before the material system scans its paths. +// Only our own hl2hake folder is touched. +static void chams_mkdirs(const std::string &path) { + std::string cur; + for (size_t i = 0; i < path.size(); i++) { + cur += path[i]; + if (path[i] == '/') + mkdir(cur.c_str(), 0755); + } + mkdir(path.c_str(), 0755); +} + +static bool chams_write_vmt_at(const std::string &dir) { + chams_mkdirs(dir); + std::string path = dir + "/translucent.vmt"; + FILE *f = fopen(path.c_str(), "w"); + if (!f) + return false; + fputs("\"UnlitGeneric\"\n" + "{\n" + " \"$basetexture\" \"sprites/white\"\n" + " \"$color\" \"[1 1 1]\"\n" + " \"$alpha\" \"1\"\n" + " \"$translucent\" \"1\"\n" + " \"$model\" \"1\"\n" + " \"$nocull\" \"1\"\n" + " \"%noToolTexture\" \"1\"\n" + "}\n", + f); + fclose(f); + return true; +} + +static std::string chams_game_arg() { + FILE *f = fopen("/proc/self/cmdline", "rb"); + if (!f) + return std::string(); + std::string data; + int c; + while ((c = fgetc(f)) != EOF) + data += (char)c; + fclose(f); + size_t pos = 0; + while (pos < data.size()) { + size_t end = data.find('\0', pos); + if (end == std::string::npos) + end = data.size(); + if (data.compare(pos, end - pos, "-game") == 0) { + size_t n = end + 1; + size_t e2 = data.find('\0', n); + if (e2 == std::string::npos) + e2 = data.size(); + return data.substr(n, e2 - n); + } + pos = end + 1; + } + return std::string(); +} + +static void chams_write_translucent_vmt() { + static bool done = false; + if (done) + return; + char cwd[4096]; + ssize_t n = readlink("/proc/self/cwd", cwd, sizeof(cwd) - 1); + if (n <= 0) + return; + cwd[n] = 0; + std::string root(cwd); + std::string mod = chams_game_arg(); + if (mod.empty()) + mod = "hl2"; + std::string base = (!mod.empty() && mod[0] == '/') ? mod : root + "/" + mod; + // Only write when this really is a Source game directory (the wrapper + // processes also preload us, from the repo directory). + if (access((base + "/gameinfo.txt").c_str(), R_OK) != 0) + return; + bool ok = false; + ok |= chams_write_vmt_at(base + "/custom/hl2hake/materials/hl2hake"); + ok |= chams_write_vmt_at(base + "/materials/hl2hake"); + done = ok; +} + +__attribute__((constructor)) static void chams_early_vmt() { + chams_write_translucent_vmt(); +} + // Called from outer_draw (outside the model draw) so FindMaterial can load // the style VMTs. The render hook only reads the cached pointers. void chams_preload() { @@ -375,6 +510,16 @@ void chams_preload() { logf("chams: preloaded style %d %s = %p", i, chams_material_name(i), m); } } + if (!g_style_mats[7]) { + chams_write_translucent_vmt(); + void *m = world_find_material("hl2hake/translucent"); + if (m) { + using RefFn = void (*)(void *); + vfunc(m, 12)(m); + g_style_mats[7] = m; + logf("chams: preloaded translucent material = %p", m); + } + } } void chams_init() { -- cgit v1.2.3