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. --- src/studio.cpp | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 153 insertions(+), 8 deletions(-) (limited to 'src/studio.cpp') 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