diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 22:09:14 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 22:09:14 +0100 |
| commit | 0cbcbfc14997eff39e0c76acf30c48b851ed1f62 (patch) | |
| tree | 19a5b0996bdf9e096f79e9636ebb48b57e10e643 | |
| parent | Chams: real additive material, addref cached materials (diff) | |
| download | hl2h-0cbcbfc14997eff39e0c76acf30c48b851ed1f62.tar.gz hl2h-0cbcbfc14997eff39e0c76acf30c48b851ed1f62.zip | |
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
<game>/<mod>/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.
| -rw-r--r-- | CONTEXT.md | 46 | ||||
| -rw-r--r-- | src/gui.cpp | 2 | ||||
| -rw-r--r-- | src/hl2h.cpp | 1 | ||||
| -rw-r--r-- | src/hl2h.h | 1 | ||||
| -rw-r--r-- | src/modules.cpp | 35 | ||||
| -rw-r--r-- | src/studio.cpp | 161 |
6 files changed, 210 insertions, 36 deletions
| @@ -27,7 +27,10 @@ covers ESP, aimbot, triggerbot and silent aim. | |||
| 27 | ## Runtime architecture | 27 | ## Runtime architecture |
| 28 | 28 | ||
| 29 | - Modules from `/proc/self/maps`: `client.so`, `engine.so`, `server.so`. | 29 | - Modules from `/proc/self/maps`: `client.so`, `engine.so`, `server.so`. |
| 30 | PROT_NONE ranges excluded; file-backed bss tails merged. | 30 | PROT_NONE ranges excluded; file-backed bss tails merged. The scan builds |
| 31 | local Module snapshots and publishes them under a mutex, because the setup | ||
| 32 | thread and the server-netvar retry can call it concurrently (resetting the | ||
| 33 | shared vectors live used to crash at startup). | ||
| 31 | - Calling convention: cdecl (i386); `this` is the first stack argument. | 34 | - Calling convention: cdecl (i386); `this` is the first stack argument. |
| 32 | - Hooks (copy-and-swap vtable, 256 slots): | 35 | - Hooks (copy-and-swap vtable, 256 slots): |
| 33 | - `CHLClient` slot 21 `CreateMove`; slot 35 `FrameStageNotify`. | 36 | - `CHLClient` slot 21 `CreateMove`; slot 35 `FrameStageNotify`. |
| @@ -294,30 +297,43 @@ vocabulary / decisions. | |||
| 294 | alpha only. The through-wall pass uses `MATERIAL_VAR_IGNOREZ` (1 << 15) | 297 | alpha only. The through-wall pass uses `MATERIAL_VAR_IGNOREZ` (1 << 15) |
| 295 | through `IMaterial::SetMaterialVarFlag` (slot 29); colours use | 298 | through `IMaterial::SetMaterialVarFlag` (slot 29); colours use |
| 296 | `$color`/`$color2` (IMaterialVar slot 11, SetVecValue 3-float slot 11) and | 299 | `$color`/`$color2` (IMaterialVar slot 11, SetVecValue 3-float slot 11) and |
| 297 | `ColorModulate` (slot 28). `$alpha` is set through IMaterialVar slot 3 | 300 | `ColorModulate` (slot 28). Alpha is applied with |
| 298 | (SetFloatValue). Shared materials are restored after each draw. | 301 | `IVRenderView::SetBlend` (`VEngineRenderView014` slot 4, saved/restored |
| 302 | around the passes) — model draws take their final alpha from the render | ||
| 303 | view, not from material `$alpha`. `$alpha`/`AlphaModulate` are still set for | ||
| 304 | completeness. Shared materials are restored after each draw. | ||
| 299 | - The GUI colour picker (`add_color_alpha`) has an alpha bar, checkerboard | 305 | - The GUI colour picker (`add_color_alpha`) has an alpha bar, checkerboard |
| 300 | preview and `#RRGGBBAA` readout; plain `add_color` keeps the old opaque | 306 | preview and `#RRGGBBAA` readout; plain `add_color` keeps the old opaque |
| 301 | three-component behaviour. Chams colours are four ints (r, g, b, a). | 307 | three-component behaviour. Chams colours are four ints (r, g, b, a). |
| 302 | - **Alpha is stored but has no visual effect yet.** The usable model | 308 | - Alpha needs a translucent model material, so the mod writes a tiny VMT: |
| 303 | materials are opaque: `debug/debugtranslucentsinglecolor` has no | 309 | `<game>/<mod>/materials/hl2hake/translucent.vmt` (and a copy under |
| 304 | `$translucent`, and toggling `MATERIAL_VAR_TRANSLUCENT` (1 << 21) or | 310 | `custom/hl2hake/...`): `UnlitGeneric` with `$basetexture sprites/white`, |
| 305 | rebuilding the state snapshot does not change the static shader combo. | 311 | `$color`, `$model 1`, `$translucent 1`, `$nocull 1`. It is written from a |
| 306 | `debug/debugtranslucentvertexcolor`/`debug/debughitbox` are translucent but | 312 | preload constructor (so it exists before the material system scans) and |
| 307 | need vertex colours and render nothing on skinned models. A custom VMT with | 313 | loaded with `FindMaterial`; styles 0/3/4/6 switch to it when the colour |
| 308 | `$model 1` + `$translucent 1` (or a white basetexture) is needed to make the | 314 | alpha is below 255. The base-game `debugtranslucentsinglecolor` has no |
| 309 | alpha control real. | 315 | `$translucent` and cannot do alpha; `debugtranslucentvertexcolor` and |
| 316 | `debug/debughitbox` need vertex colours and draw nothing on skinned models. | ||
| 310 | - Styles: 0 Flat (`debug/debugtranslucentsinglecolor` + FLAT), 1 Wireframe | 317 | - Styles: 0 Flat (`debug/debugtranslucentsinglecolor` + FLAT), 1 Wireframe |
| 311 | (`debug/debugwireframe`), 2 Additive | 318 | (`debug/debugwireframe`, brightened tint), 2 Additive |
| 312 | (`debug/debugtranslucentmodelhulls`, whose VMT has `$additive 1` + `$color`; | 319 | (`debug/debugtranslucentmodelhulls`, whose VMT has `$additive 1` + `$color`; |
| 313 | the ADDITIVE flag on the solid material did nothing), 3 Solid, 4 White | 320 | the ADDITIVE flag on the solid material did nothing), 3 Solid, 4 White |
| 314 | (solid, colour forced white), 5 Fullbright (the additive material, colour | 321 | (solid, colour forced white), 5 Fullbright (the additive material, colour |
| 315 | forced white, giving a white-hot glow), 6 Glow outline (solid plus a | 322 | forced white, giving a white-hot glow), 6 Solid + contour (solid fill plus |
| 316 | `debug/debugwireframe` contour pass drawn with ZNEARER (1 << 10) and a | 323 | a `debug/debugwireframe` contour pass drawn with ZNEARER (1 << 10) and a |
| 317 | brightened colour so the base cannot bury it). | 324 | brightened colour, so the entity is recoloured while its edges stay |
| 325 | readable). Style 6 is the requested "recolour the whole entity but show its | ||
| 326 | contours" look. | ||
| 318 | `vgui/white` is vertex-colour based and has no `$model`/`$basetexture`, so it | 327 | `vgui/white` is vertex-colour based and has no `$model`/`$basetexture`, so it |
| 319 | is never used for chams; `models/debug/debugwhite` is EP2-only and the old | 328 | is never used for chams; `models/debug/debugwhite` is EP2-only and the old |
| 320 | VGUI additive material was invisible on NPCs. | 329 | VGUI additive material was invisible on NPCs. |
| 330 | - Through-wall + visible wireframe: a wireframe visible pass leaves gaps, so | ||
| 331 | the through-wall fill showed inside the visible silhouette. When both passes | ||
| 332 | are on and the visible style is Wireframe, a solid fill of the visible | ||
| 333 | colour is drawn first, then the brightened wireframe contour. (The proper | ||
| 334 | alternative is stencil: visible writes stencil 1, through-wall tests | ||
| 335 | stencil == 0, as Amalgam does; the fill is simpler and gives the | ||
| 336 | solid+contour look.) | ||
| 321 | - Style materials are preloaded from `outer_draw` (throttled) and cached; the | 337 | - Style materials are preloaded from `outer_draw` (throttled) and cached; the |
| 322 | render hook never calls FindMaterial itself. A missing style uses solid. | 338 | render hook never calls FindMaterial itself. A missing style uses solid. |
| 323 | `FindMaterial` can return an unreferenced material, so each cached material | 339 | `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() { | |||
| 459 | "Per-relationship model colours.", true); | 459 | "Per-relationship model colours.", true); |
| 460 | static const char *const chams_mat_names[] = { | 460 | static const char *const chams_mat_names[] = { |
| 461 | "Flat", "Wireframe", "Additive glow", "Solid", "White", "Fullbright", | 461 | "Flat", "Wireframe", "Additive glow", "Solid", "White", "Fullbright", |
| 462 | "Glow outline"}; | 462 | "Solid + contour"}; |
| 463 | auto add_chams_group = [&](const char *title, ChamsGroup &g) { | 463 | auto add_chams_group = [&](const char *title, ChamsGroup &g) { |
| 464 | add_section(chams, title); | 464 | add_section(chams, title); |
| 465 | add_toggle(chams, "Visible", "Draw the normal-depth pass.", | 465 | 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 *) { | |||
| 1149 | g_iface.debugoverlay = ef("VDebugOverlay003", nullptr); | 1149 | g_iface.debugoverlay = ef("VDebugOverlay003", nullptr); |
| 1150 | g_iface.enginetrace = ef("EngineTraceClient003", nullptr); | 1150 | g_iface.enginetrace = ef("EngineTraceClient003", nullptr); |
| 1151 | g_iface.modelrender = ef("VEngineModel016", nullptr); | 1151 | g_iface.modelrender = ef("VEngineModel016", nullptr); |
| 1152 | g_iface.renderview = ef("VEngineRenderView014", nullptr); | ||
| 1152 | static const char *cvar_names[] = {"VEngineCvar004", "VEngineCvar007", | 1153 | static const char *cvar_names[] = {"VEngineCvar004", "VEngineCvar007", |
| 1153 | "VEngineCvar003", "IVEngineCvar004", | 1154 | "VEngineCvar003", "IVEngineCvar004", |
| 1154 | nullptr}; | 1155 | nullptr}; |
| @@ -69,6 +69,7 @@ struct GameInterfaces { | |||
| 69 | void *enginetrace = nullptr; // EngineTraceClient003 (world vischeck) | 69 | void *enginetrace = nullptr; // EngineTraceClient003 (world vischeck) |
| 70 | void *cvar = nullptr; // VEngineCvar004 (mat_fullbright etc.) | 70 | void *cvar = nullptr; // VEngineCvar004 (mat_fullbright etc.) |
| 71 | void *modelrender = nullptr; // VEngineModel016 (chams) | 71 | void *modelrender = nullptr; // VEngineModel016 (chams) |
| 72 | void *renderview = nullptr; // VEngineRenderView014 (alpha/color modulation) | ||
| 72 | }; | 73 | }; |
| 73 | extern GameInterfaces g_iface; | 74 | extern GameInterfaces g_iface; |
| 74 | 75 | ||
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 @@ | |||
| 1 | // modules.cpp — /proc/self/maps parsing and module discovery | 1 | // modules.cpp — /proc/self/maps parsing and module discovery |
| 2 | #include "hl2h.h" | 2 | #include "hl2h.h" |
| 3 | #include <elf.h> | 3 | #include <elf.h> |
| 4 | #include <mutex> | ||
| 4 | #include <vector> | 5 | #include <vector> |
| 5 | 6 | ||
| 7 | static std::mutex g_modules_mutex; | ||
| 8 | |||
| 6 | static bool basename_is(const char *path, const char *base) { | 9 | static bool basename_is(const char *path, const char *base) { |
| 7 | size_t pl = strlen(path), bl = strlen(base); | 10 | size_t pl = strlen(path), bl = strlen(base); |
| 8 | if (pl < bl || strcmp(path + pl - bl, base) != 0) | 11 | if (pl < bl || strcmp(path + pl - bl, base) != 0) |
| @@ -11,9 +14,7 @@ static bool basename_is(const char *path, const char *base) { | |||
| 11 | } | 14 | } |
| 12 | 15 | ||
| 13 | void find_modules() { | 16 | void find_modules() { |
| 14 | g_client = Module(); | 17 | Module client, engine, server; |
| 15 | g_engine = Module(); | ||
| 16 | g_server = Module(); | ||
| 17 | FILE *f = fopen("/proc/self/maps", "r"); | 18 | FILE *f = fopen("/proc/self/maps", "r"); |
| 18 | if (!f) | 19 | if (!f) |
| 19 | return; | 20 | return; |
| @@ -41,19 +42,19 @@ void find_modules() { | |||
| 41 | memcpy(clean, path, pl); | 42 | memcpy(clean, path, pl); |
| 42 | clean[pl] = 0; | 43 | clean[pl] = 0; |
| 43 | if (basename_is(clean, "client.so")) | 44 | if (basename_is(clean, "client.so")) |
| 44 | m = &g_client; | 45 | m = &client; |
| 45 | else if (basename_is(clean, "engine.so")) | 46 | else if (basename_is(clean, "engine.so")) |
| 46 | m = &g_engine; | 47 | m = &engine; |
| 47 | else if (basename_is(clean, "server.so")) | 48 | else if (basename_is(clean, "server.so")) |
| 48 | m = &g_server; | 49 | m = &server; |
| 49 | } else { | 50 | } else { |
| 50 | // anon: absorb only as .bss tail after a file-backed range | 51 | // anon: absorb only as .bss tail after a file-backed range |
| 51 | if (g_client.valid() && g_client.ranges.back().file && | 52 | if (client.valid() && client.ranges.back().file && |
| 52 | lo == g_client.ranges.back().hi && hi - lo <= 0x1000000) | 53 | lo == client.ranges.back().hi && hi - lo <= 0x1000000) |
| 53 | m = &g_client; | 54 | m = &client; |
| 54 | else if (g_engine.valid() && g_engine.ranges.back().file && | 55 | else if (engine.valid() && engine.ranges.back().file && |
| 55 | lo == g_engine.ranges.back().hi && hi - lo <= 0x1000000) | 56 | lo == engine.ranges.back().hi && hi - lo <= 0x1000000) |
| 56 | m = &g_engine; | 57 | m = &engine; |
| 57 | } | 58 | } |
| 58 | if (!m) | 59 | if (!m) |
| 59 | continue; | 60 | continue; |
| @@ -71,6 +72,16 @@ void find_modules() { | |||
| 71 | } | 72 | } |
| 72 | } | 73 | } |
| 73 | fclose(f); | 74 | fclose(f); |
| 75 | // setup_thread and resolve_server_netvars can both call this; the old code | ||
| 76 | // reset the shared Module globals and reallocated their vectors live, which | ||
| 77 | // crashed under the startup thread. Publish complete snapshots instead. | ||
| 78 | std::lock_guard<std::mutex> lock(g_modules_mutex); | ||
| 79 | if (client.valid()) | ||
| 80 | g_client = std::move(client); | ||
| 81 | if (engine.valid()) | ||
| 82 | g_engine = std::move(engine); | ||
| 83 | if (server.valid()) | ||
| 84 | g_server = std::move(server); | ||
| 74 | } | 85 | } |
| 75 | 86 | ||
| 76 | // --------------------------------------------------------------------------- | 87 | // --------------------------------------------------------------------------- |
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 @@ | |||
| 19 | #include <cstdio> | 19 | #include <cstdio> |
| 20 | #include <cstring> | 20 | #include <cstring> |
| 21 | #include <dlfcn.h> | 21 | #include <dlfcn.h> |
| 22 | #include <string> | ||
| 23 | #include <sys/stat.h> | ||
| 22 | #include <time.h> | 24 | #include <time.h> |
| 25 | #include <unistd.h> | ||
| 23 | 26 | ||
| 24 | namespace { | 27 | namespace { |
| 25 | 28 | ||
| @@ -197,7 +200,7 @@ using ForceOverrideFn = void (*)(void *, void *, int); | |||
| 197 | void *g_modelrender = nullptr; | 200 | void *g_modelrender = nullptr; |
| 198 | ModelExecFn g_orig_model_exec = nullptr; | 201 | ModelExecFn g_orig_model_exec = nullptr; |
| 199 | ForceOverrideFn g_force_override = nullptr; | 202 | ForceOverrideFn g_force_override = nullptr; |
| 200 | void *g_style_mats[7] = {}; | 203 | void *g_style_mats[8] = {}; |
| 201 | 204 | ||
| 202 | const char *chams_material_name(int idx) { | 205 | const char *chams_material_name(int idx) { |
| 203 | switch (idx) { | 206 | switch (idx) { |
| @@ -257,6 +260,12 @@ void chams_apply(void *mat, bool ignorez, int style, bool force_white, | |||
| 257 | float b = force_white ? 1.0f : (float)rgb[2] / 255.0f; | 260 | float b = force_white ? 1.0f : (float)rgb[2] / 255.0f; |
| 258 | chams_color(mat, r, g, b); | 261 | chams_color(mat, r, g, b); |
| 259 | chams_alpha(mat, (float)rgb[3] / 255.0f); | 262 | chams_alpha(mat, (float)rgb[3] / 255.0f); |
| 263 | { | ||
| 264 | // Entity-style alpha fade; UnlitGeneric takes translucency from the base | ||
| 265 | // texture/vertex alpha, so $alpha alone is not enough. | ||
| 266 | using AlphaFn = void (*)(void *, float); | ||
| 267 | vfunc<AlphaFn>(mat, 27)(mat, (float)rgb[3] / 255.0f); | ||
| 268 | } | ||
| 260 | // 0 Flat, 1 Wireframe, 2 Additive, 3 Solid, 4 White, 5 Fullbright | 269 | // 0 Flat, 1 Wireframe, 2 Additive, 3 Solid, 4 White, 5 Fullbright |
| 261 | // (additive white glow), 6 Glow outline (solid base; the wireframe overlay | 270 | // (additive white glow), 6 Glow outline (solid base; the wireframe overlay |
| 262 | // is a second draw with ZNEARER so its contour is not hidden by the base). | 271 | // 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, | |||
| 270 | vfunc<FlagFn>(mat, 29)(mat, 1 << 6 /* SELFILLUM */, style == 5); | 279 | vfunc<FlagFn>(mat, 29)(mat, 1 << 6 /* SELFILLUM */, style == 5); |
| 271 | } | 280 | } |
| 272 | 281 | ||
| 273 | void chams_reset(void *mat) { | 282 | void chams_reset(void *mat) { using FlagFn = void (*)(void *, int, bool); |
| 274 | using FlagFn = void (*)(void *, int, bool); | ||
| 275 | vfunc<FlagFn>(mat, 29)(mat, 1 << 15, false); | 283 | vfunc<FlagFn>(mat, 29)(mat, 1 << 15, false); |
| 276 | vfunc<FlagFn>(mat, 29)(mat, 1 << 10, false); | 284 | vfunc<FlagFn>(mat, 29)(mat, 1 << 10, false); |
| 277 | vfunc<FlagFn>(mat, 29)(mat, 1 << 28, false); | 285 | vfunc<FlagFn>(mat, 29)(mat, 1 << 28, false); |
| @@ -282,6 +290,22 @@ void chams_reset(void *mat) { | |||
| 282 | chams_alpha(mat, 1.0f); | 290 | chams_alpha(mat, 1.0f); |
| 283 | } | 291 | } |
| 284 | 292 | ||
| 293 | // Model draws take their final alpha from IVRenderView::SetBlend; material | ||
| 294 | // $alpha alone is ignored by UnlitGeneric. Save/restore it around our passes. | ||
| 295 | float chams_get_blend() { | ||
| 296 | if (!g_iface.renderview) | ||
| 297 | return 1.0f; | ||
| 298 | using GetFn = float (*)(void *); | ||
| 299 | return vfunc<GetFn>(g_iface.renderview, 5)(g_iface.renderview); | ||
| 300 | } | ||
| 301 | |||
| 302 | void chams_set_blend(float a) { | ||
| 303 | if (!g_iface.renderview) | ||
| 304 | return; | ||
| 305 | using SetFn = void (*)(void *, float); | ||
| 306 | vfunc<SetFn>(g_iface.renderview, 4)(g_iface.renderview, a); | ||
| 307 | } | ||
| 308 | |||
| 285 | void hkdModelExecute(void *self, const void *state, const void *info, | 309 | void hkdModelExecute(void *self, const void *state, const void *info, |
| 286 | void *bone) { | 310 | void *bone) { |
| 287 | if (!g_orig_model_exec) | 311 | if (!g_orig_model_exec) |
| @@ -304,21 +328,35 @@ void hkdModelExecute(void *self, const void *state, const void *info, | |||
| 304 | if (style < 0) style = 0; | 328 | if (style < 0) style = 0; |
| 305 | if (style > 6) style = 6; | 329 | if (style > 6) style = 6; |
| 306 | void *mat = style_material(style); | 330 | void *mat = style_material(style); |
| 331 | bool alpha = rgb[3] < 255; | ||
| 332 | if (alpha && g_style_mats[7] && | ||
| 333 | (style == 0 || style == 3 || style == 4 || style == 6)) | ||
| 334 | mat = g_style_mats[7]; | ||
| 307 | if (!mat) | 335 | if (!mat) |
| 308 | return false; | 336 | return false; |
| 309 | bool white = style == 4 || style == 5; | 337 | bool white = style == 4 || style == 5; |
| 338 | // Wireframe lines use a brighter tint so they stay readable over a | ||
| 339 | // solid fill. | ||
| 340 | int wrgb[4]; | ||
| 310 | const int *use_rgb = rgb; | 341 | const int *use_rgb = rgb; |
| 342 | if (style == 1) { | ||
| 343 | for (int i = 0; i < 3; i++) | ||
| 344 | wrgb[i] = (rgb[i] + 255) / 2; | ||
| 345 | wrgb[3] = rgb[3]; | ||
| 346 | use_rgb = wrgb; | ||
| 347 | } | ||
| 311 | chams_apply(mat, ignorez, style, white, use_rgb, false); | 348 | chams_apply(mat, ignorez, style, white, use_rgb, false); |
| 349 | chams_set_blend((float)use_rgb[3] / 255.0f); | ||
| 312 | g_force_override(self, mat, 0 /* OVERRIDE_NORMAL */); | 350 | g_force_override(self, mat, 0 /* OVERRIDE_NORMAL */); |
| 313 | g_orig_model_exec(self, state, info, bone); | 351 | g_orig_model_exec(self, state, info, bone); |
| 314 | if (style == 6) { | 352 | if (style == 6) { |
| 315 | // Solid base plus a wireframe contour pass for depth. Brighten | 353 | // Solid base plus a wireframe contour pass for depth. |
| 316 | // the contour and bias it forward so the base cannot bury it. | ||
| 317 | void *wire = g_style_mats[1]; | 354 | void *wire = g_style_mats[1]; |
| 318 | if (wire && wire != mat) { | 355 | if (wire && wire != mat) { |
| 319 | int wrgb[4] = {(rgb[0] + 255) / 2, (rgb[1] + 255) / 2, | 356 | int crgb[4] = {(rgb[0] + 255) / 2, (rgb[1] + 255) / 2, |
| 320 | (rgb[2] + 255) / 2, rgb[3]}; | 357 | (rgb[2] + 255) / 2, rgb[3]}; |
| 321 | chams_apply(wire, ignorez, 1, false, wrgb, true); | 358 | chams_apply(wire, ignorez, 1, false, crgb, true); |
| 359 | chams_set_blend((float)crgb[3] / 255.0f); | ||
| 322 | g_force_override(self, wire, 0); | 360 | g_force_override(self, wire, 0); |
| 323 | g_orig_model_exec(self, state, info, bone); | 361 | g_orig_model_exec(self, state, info, bone); |
| 324 | chams_reset(wire); | 362 | chams_reset(wire); |
| @@ -327,17 +365,24 @@ void hkdModelExecute(void *self, const void *state, const void *info, | |||
| 327 | chams_reset(mat); | 365 | chams_reset(mat); |
| 328 | return true; | 366 | return true; |
| 329 | }; | 367 | }; |
| 368 | float old_blend = chams_get_blend(); | ||
| 330 | bool did = false; | 369 | bool did = false; |
| 331 | if (is_vm) { | 370 | if (is_vm) { |
| 332 | did = draw_pass(g_chams_vm_mat, g_chams_vm_rgb, false); | 371 | did = draw_pass(g_chams_vm_mat, g_chams_vm_rgb, false); |
| 333 | } else { | 372 | } else { |
| 334 | if (grp->inv_on) | 373 | if (grp->inv_on) |
| 335 | did |= draw_pass(grp->inv_mat, grp->inv_rgb, true); | 374 | did |= draw_pass(grp->inv_mat, grp->inv_rgb, true); |
| 336 | if (grp->vis_on) | 375 | if (grp->vis_on) { |
| 376 | // A wireframe visible pass leaves gaps, so the through-wall fill | ||
| 377 | // would show inside the visible silhouette. Fill it first. | ||
| 378 | if (grp->inv_on && grp->vis_mat == 1) | ||
| 379 | did |= draw_pass(3, grp->vis_rgb, false); | ||
| 337 | did |= draw_pass(grp->vis_mat, grp->vis_rgb, false); | 380 | did |= draw_pass(grp->vis_mat, grp->vis_rgb, false); |
| 381 | } | ||
| 338 | } | 382 | } |
| 339 | if (did) { | 383 | if (did) { |
| 340 | g_force_override(self, nullptr, 0); | 384 | g_force_override(self, nullptr, 0); |
| 385 | chams_set_blend(old_blend); | ||
| 341 | return; | 386 | return; |
| 342 | } | 387 | } |
| 343 | } | 388 | } |
| @@ -354,6 +399,96 @@ static double chams_now() { | |||
| 354 | return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; | 399 | return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; |
| 355 | } | 400 | } |
| 356 | 401 | ||
| 402 | // The base-game model debug materials are opaque. Drop a tiny VMT with | ||
| 403 | // $model + $translucent into the game's custom/materials folders so the alpha | ||
| 404 | // channel in the colour picker has something to drive. Written from a | ||
| 405 | // constructor too, so it exists before the material system scans its paths. | ||
| 406 | // Only our own hl2hake folder is touched. | ||
| 407 | static void chams_mkdirs(const std::string &path) { | ||
| 408 | std::string cur; | ||
| 409 | for (size_t i = 0; i < path.size(); i++) { | ||
| 410 | cur += path[i]; | ||
| 411 | if (path[i] == '/') | ||
| 412 | mkdir(cur.c_str(), 0755); | ||
| 413 | } | ||
| 414 | mkdir(path.c_str(), 0755); | ||
| 415 | } | ||
| 416 | |||
| 417 | static bool chams_write_vmt_at(const std::string &dir) { | ||
| 418 | chams_mkdirs(dir); | ||
| 419 | std::string path = dir + "/translucent.vmt"; | ||
| 420 | FILE *f = fopen(path.c_str(), "w"); | ||
| 421 | if (!f) | ||
| 422 | return false; | ||
| 423 | fputs("\"UnlitGeneric\"\n" | ||
| 424 | "{\n" | ||
| 425 | " \"$basetexture\" \"sprites/white\"\n" | ||
| 426 | " \"$color\" \"[1 1 1]\"\n" | ||
| 427 | " \"$alpha\" \"1\"\n" | ||
| 428 | " \"$translucent\" \"1\"\n" | ||
| 429 | " \"$model\" \"1\"\n" | ||
| 430 | " \"$nocull\" \"1\"\n" | ||
| 431 | " \"%noToolTexture\" \"1\"\n" | ||
| 432 | "}\n", | ||
| 433 | f); | ||
| 434 | fclose(f); | ||
| 435 | return true; | ||
| 436 | } | ||
| 437 | |||
| 438 | static std::string chams_game_arg() { | ||
| 439 | FILE *f = fopen("/proc/self/cmdline", "rb"); | ||
| 440 | if (!f) | ||
| 441 | return std::string(); | ||
| 442 | std::string data; | ||
| 443 | int c; | ||
| 444 | while ((c = fgetc(f)) != EOF) | ||
| 445 | data += (char)c; | ||
| 446 | fclose(f); | ||
| 447 | size_t pos = 0; | ||
| 448 | while (pos < data.size()) { | ||
| 449 | size_t end = data.find('\0', pos); | ||
| 450 | if (end == std::string::npos) | ||
| 451 | end = data.size(); | ||
| 452 | if (data.compare(pos, end - pos, "-game") == 0) { | ||
| 453 | size_t n = end + 1; | ||
| 454 | size_t e2 = data.find('\0', n); | ||
| 455 | if (e2 == std::string::npos) | ||
| 456 | e2 = data.size(); | ||
| 457 | return data.substr(n, e2 - n); | ||
| 458 | } | ||
| 459 | pos = end + 1; | ||
| 460 | } | ||
| 461 | return std::string(); | ||
| 462 | } | ||
| 463 | |||
| 464 | static void chams_write_translucent_vmt() { | ||
| 465 | static bool done = false; | ||
| 466 | if (done) | ||
| 467 | return; | ||
| 468 | char cwd[4096]; | ||
| 469 | ssize_t n = readlink("/proc/self/cwd", cwd, sizeof(cwd) - 1); | ||
| 470 | if (n <= 0) | ||
| 471 | return; | ||
| 472 | cwd[n] = 0; | ||
| 473 | std::string root(cwd); | ||
| 474 | std::string mod = chams_game_arg(); | ||
| 475 | if (mod.empty()) | ||
| 476 | mod = "hl2"; | ||
| 477 | std::string base = (!mod.empty() && mod[0] == '/') ? mod : root + "/" + mod; | ||
| 478 | // Only write when this really is a Source game directory (the wrapper | ||
| 479 | // processes also preload us, from the repo directory). | ||
| 480 | if (access((base + "/gameinfo.txt").c_str(), R_OK) != 0) | ||
| 481 | return; | ||
| 482 | bool ok = false; | ||
| 483 | ok |= chams_write_vmt_at(base + "/custom/hl2hake/materials/hl2hake"); | ||
| 484 | ok |= chams_write_vmt_at(base + "/materials/hl2hake"); | ||
| 485 | done = ok; | ||
| 486 | } | ||
| 487 | |||
| 488 | __attribute__((constructor)) static void chams_early_vmt() { | ||
| 489 | chams_write_translucent_vmt(); | ||
| 490 | } | ||
| 491 | |||
| 357 | // Called from outer_draw (outside the model draw) so FindMaterial can load | 492 | // Called from outer_draw (outside the model draw) so FindMaterial can load |
| 358 | // the style VMTs. The render hook only reads the cached pointers. | 493 | // the style VMTs. The render hook only reads the cached pointers. |
| 359 | void chams_preload() { | 494 | void chams_preload() { |
| @@ -375,6 +510,16 @@ void chams_preload() { | |||
| 375 | logf("chams: preloaded style %d %s = %p", i, chams_material_name(i), m); | 510 | logf("chams: preloaded style %d %s = %p", i, chams_material_name(i), m); |
| 376 | } | 511 | } |
| 377 | } | 512 | } |
| 513 | if (!g_style_mats[7]) { | ||
| 514 | chams_write_translucent_vmt(); | ||
| 515 | void *m = world_find_material("hl2hake/translucent"); | ||
| 516 | if (m) { | ||
| 517 | using RefFn = void (*)(void *); | ||
| 518 | vfunc<RefFn>(m, 12)(m); | ||
| 519 | g_style_mats[7] = m; | ||
| 520 | logf("chams: preloaded translucent material = %p", m); | ||
| 521 | } | ||
| 522 | } | ||
| 378 | } | 523 | } |
| 379 | 524 | ||
| 380 | void chams_init() { | 525 | void chams_init() { |
