diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 21:35:44 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 21:35:44 +0100 |
| commit | c35fc21f345ac9ecf9017115c4481a1147d05206 (patch) | |
| tree | 6907a027a097aec5af994b5bdcb20dddce6e984a | |
| parent | Chams: make Fullbright a white-hot additive glow (diff) | |
| download | hl2h-c35fc21f345ac9ecf9017115c4481a1147d05206.tar.gz hl2h-c35fc21f345ac9ecf9017115c4481a1147d05206.zip | |
Chams: real additive material, addref cached materials
Testing on :99 showed ADDITIVE on debugtranslucentsinglecolor did nothing and
alpha lost the colour, because that VMT has no $translucent and the engine
bound an unreferenced material. Use debug/debugtranslucentmodelhulls (VMT has
$additive 1 + $color) for Additive/Fullbright, and addref every preloaded
material so the engine stops warning "bad reference count 0" and applies
$color/ColorModulate. Verified: Solid is flat blue, Additive is a translucent
coloured glow, Fullbright is a white additive glow, White is opaque white.
Also drop the viewmodel additive base (the solid material is opaque, so it
made an opaque blob) and never call FindMaterial inside DrawModelExecute.
Document that the alpha control is stored but has no effect until a custom
$translucent model VMT is added.
| -rw-r--r-- | CONTEXT.md | 41 | ||||
| -rw-r--r-- | src/studio.cpp | 46 |
2 files changed, 37 insertions, 50 deletions
| @@ -294,31 +294,38 @@ vocabulary / decisions. | |||
| 294 | alpha only. The through-wall pass uses `MATERIAL_VAR_IGNOREZ` (1 << 15) | 294 | alpha only. The through-wall pass uses `MATERIAL_VAR_IGNOREZ` (1 << 15) |
| 295 | through `IMaterial::SetMaterialVarFlag` (slot 29); colours use | 295 | through `IMaterial::SetMaterialVarFlag` (slot 29); colours use |
| 296 | `$color`/`$color2` (IMaterialVar slot 11, SetVecValue 3-float slot 11) and | 296 | `$color`/`$color2` (IMaterialVar slot 11, SetVecValue 3-float slot 11) and |
| 297 | `ColorModulate` (slot 28). Alpha uses `$alpha` (IMaterialVar slot 3, | 297 | `ColorModulate` (slot 28). `$alpha` is set through IMaterialVar slot 3 |
| 298 | SetFloatValue) and `MATERIAL_VAR_TRANSLUCENT` (1 << 21) when alpha < 255. | 298 | (SetFloatValue). Shared materials are restored after each draw. |
| 299 | Shared materials are restored after each draw. | ||
| 300 | - The GUI colour picker (`add_color_alpha`) has an alpha bar, checkerboard | 299 | - The GUI colour picker (`add_color_alpha`) has an alpha bar, checkerboard |
| 301 | preview and `#RRGGBBAA` readout; plain `add_color` keeps the old opaque | 300 | preview and `#RRGGBBAA` readout; plain `add_color` keeps the old opaque |
| 302 | three-component behaviour. Chams colours are four ints (r, g, b, a). | 301 | three-component behaviour. Chams colours are four ints (r, g, b, a). |
| 303 | - Styles are built from `debug/debugtranslucentsinglecolor` (UnlitGeneric, | 302 | - **Alpha is stored but has no visual effect yet.** The usable model |
| 304 | `$color`) and material flags: | 303 | materials are opaque: `debug/debugtranslucentsinglecolor` has no |
| 305 | 0 Flat (solid + FLAT), 1 Wireframe (`debug/debugwireframe`), 2 Additive | 304 | `$translucent`, and toggling `MATERIAL_VAR_TRANSLUCENT` (1 << 21) or |
| 306 | (+ ADDITIVE), 3 Solid, 4 White (colour forced white), 5 Fullbright | 305 | rebuilding the state snapshot does not change the static shader combo. |
| 307 | (ADDITIVE|SELFILLUM, colour forced white so it reads as a white-hot glow | 306 | `debug/debugtranslucentvertexcolor`/`debug/debughitbox` are translucent but |
| 308 | instead of matching Solid), 6 Glow outline | 307 | need vertex colours and render nothing on skinned models. A custom VMT with |
| 309 | (solid plus a `debug/debugwireframe` contour pass drawn with ZNEARER | 308 | `$model 1` + `$translucent 1` (or a white basetexture) is needed to make the |
| 310 | (1 << 10) and a brightened colour so the base cannot bury it). | 309 | alpha control real. |
| 310 | - Styles: 0 Flat (`debug/debugtranslucentsinglecolor` + FLAT), 1 Wireframe | ||
| 311 | (`debug/debugwireframe`), 2 Additive | ||
| 312 | (`debug/debugtranslucentmodelhulls`, whose VMT has `$additive 1` + `$color`; | ||
| 313 | the ADDITIVE flag on the solid material did nothing), 3 Solid, 4 White | ||
| 314 | (solid, colour forced white), 5 Fullbright (the additive material, colour | ||
| 315 | forced white, giving a white-hot glow), 6 Glow outline (solid plus a | ||
| 316 | `debug/debugwireframe` contour pass drawn with ZNEARER (1 << 10) and a | ||
| 317 | brightened colour so the base cannot bury it). | ||
| 311 | `vgui/white` is vertex-colour based and has no `$model`/`$basetexture`, so it | 318 | `vgui/white` is vertex-colour based and has no `$model`/`$basetexture`, so it |
| 312 | is never used for chams; `models/debug/debugwhite` is EP2-only and the old | 319 | is never used for chams; `models/debug/debugwhite` is EP2-only and the old |
| 313 | VGUI additive material was invisible on NPCs. | 320 | VGUI additive material was invisible on NPCs. |
| 314 | - Style materials are preloaded from `outer_draw` (throttled) and cached; the | 321 | - Style materials are preloaded from `outer_draw` (throttled) and cached; the |
| 315 | render hook never calls FindMaterial itself. A missing style uses solid. | 322 | render hook never calls FindMaterial itself. A missing style uses solid. |
| 316 | - Viewmodels are collated by the engine as `RENDER_GROUP_VIEW_MODEL_OPAQUE`, | 323 | `FindMaterial` can return an unreferenced material, so each cached material |
| 317 | so an additive forced material can wash out against a bright sky or wall | 324 | is addref'd (`IncrementReferenceCount`, IMaterial slot 12); without this the |
| 318 | (and the engine may not treat the draw as translucent). Additive styles on | 325 | engine warns "bad reference count 0 when being bound" and the material's |
| 319 | the viewmodel therefore draw a faint translucent base (alpha/8) first, then | 326 | `$color`/`ColorModulate` changes are ignored. |
| 320 | the additive pass, so the model keeps a silhouette on bright backgrounds | 327 | - Additive styles still wash out against a bright sky or wall (inherent to |
| 321 | without becoming an opaque blob. | 328 | additive blending). |
| 322 | - Feature is off by default. Config sections: `[chams]` (active/bind), | 329 | - Feature is off by default. Config sections: `[chams]` (active/bind), |
| 323 | `[chams_hostile|neutral|friendly]` (`vis`, `vis_mat`, `vis_r/g/b/a`, `inv`, | 330 | `[chams_hostile|neutral|friendly]` (`vis`, `vis_mat`, `vis_r/g/b/a`, `inv`, |
| 324 | `inv_mat`, `inv_r/g/b/a`) and `[chams_vm]` (`on`, `mat`, `r/g/b/a`). The GUI | 331 | `inv_mat`, `inv_r/g/b/a`) and `[chams_vm]` (`on`, `mat`, `r/g/b/a`). The GUI |
diff --git a/src/studio.cpp b/src/studio.cpp index 086d213..cf145d1 100644 --- a/src/studio.cpp +++ b/src/studio.cpp | |||
| @@ -199,20 +199,21 @@ ModelExecFn g_orig_model_exec = nullptr; | |||
| 199 | ForceOverrideFn g_force_override = nullptr; | 199 | ForceOverrideFn g_force_override = nullptr; |
| 200 | void *g_style_mats[7] = {}; | 200 | void *g_style_mats[7] = {}; |
| 201 | 201 | ||
| 202 | void *viewmodel_material(); | ||
| 203 | |||
| 204 | const char *chams_material_name(int idx) { | 202 | const char *chams_material_name(int idx) { |
| 205 | switch (idx) { | 203 | switch (idx) { |
| 206 | case 1: return "debug/debugwireframe"; | 204 | case 1: return "debug/debugwireframe"; |
| 207 | case 2: | 205 | case 2: |
| 206 | case 5: return "debug/debugtranslucentmodelhulls"; | ||
| 208 | case 3: | 207 | case 3: |
| 209 | case 4: | 208 | case 4: |
| 210 | case 5: | ||
| 211 | case 6: | 209 | case 6: |
| 212 | default: return "debug/debugtranslucentsinglecolor"; | 210 | default: return "debug/debugtranslucentsinglecolor"; |
| 213 | } | 211 | } |
| 214 | } | 212 | } |
| 215 | 213 | ||
| 214 | // Never call FindMaterial here: this runs inside DrawModelExecute, where | ||
| 215 | // loading a material can crash the material system. chams_preload fills the | ||
| 216 | // cache from outer_draw instead. | ||
| 216 | void *style_material(int idx) { | 217 | void *style_material(int idx) { |
| 217 | if (idx < 0) idx = 0; | 218 | if (idx < 0) idx = 0; |
| 218 | if (idx > 6) idx = 6; | 219 | if (idx > 6) idx = 6; |
| @@ -220,17 +221,7 @@ void *style_material(int idx) { | |||
| 220 | return g_style_mats[idx]; | 221 | return g_style_mats[idx]; |
| 221 | if (g_style_mats[3]) | 222 | if (g_style_mats[3]) |
| 222 | return g_style_mats[3]; | 223 | return g_style_mats[3]; |
| 223 | return viewmodel_material(); | 224 | return nullptr; |
| 224 | } | ||
| 225 | |||
| 226 | // Viewmodels need a model shader; vgui materials do not draw skinned models. | ||
| 227 | void *viewmodel_material() { | ||
| 228 | if (g_style_mats[3]) | ||
| 229 | return g_style_mats[3]; | ||
| 230 | static void *mat = nullptr; | ||
| 231 | if (!mat) | ||
| 232 | mat = world_find_material("debug/debugtranslucentsinglecolor"); | ||
| 233 | return mat; | ||
| 234 | } | 225 | } |
| 235 | 226 | ||
| 236 | void chams_color(void *mat, float r, float g, float b) { | 227 | void chams_color(void *mat, float r, float g, float b) { |
| @@ -260,6 +251,7 @@ void chams_alpha(void *mat, float a) { | |||
| 260 | void chams_apply(void *mat, bool ignorez, int style, bool force_white, | 251 | void chams_apply(void *mat, bool ignorez, int style, bool force_white, |
| 261 | const int *rgb, bool znearer) { | 252 | const int *rgb, bool znearer) { |
| 262 | using FlagFn = void (*)(void *, int, bool); | 253 | using FlagFn = void (*)(void *, int, bool); |
| 254 | bool additive = style == 2 || style == 5; | ||
| 263 | float r = force_white ? 1.0f : (float)rgb[0] / 255.0f; | 255 | float r = force_white ? 1.0f : (float)rgb[0] / 255.0f; |
| 264 | float g = force_white ? 1.0f : (float)rgb[1] / 255.0f; | 256 | float g = force_white ? 1.0f : (float)rgb[1] / 255.0f; |
| 265 | float b = force_white ? 1.0f : (float)rgb[2] / 255.0f; | 257 | float b = force_white ? 1.0f : (float)rgb[2] / 255.0f; |
| @@ -268,12 +260,12 @@ void chams_apply(void *mat, bool ignorez, int style, bool force_white, | |||
| 268 | // 0 Flat, 1 Wireframe, 2 Additive, 3 Solid, 4 White, 5 Fullbright | 260 | // 0 Flat, 1 Wireframe, 2 Additive, 3 Solid, 4 White, 5 Fullbright |
| 269 | // (additive white glow), 6 Glow outline (solid base; the wireframe overlay | 261 | // (additive white glow), 6 Glow outline (solid base; the wireframe overlay |
| 270 | // is a second draw with ZNEARER so its contour is not hidden by the base). | 262 | // is a second draw with ZNEARER so its contour is not hidden by the base). |
| 263 | // Styles 2/5 use debug/debugtranslucentmodelhulls, whose VMT already has | ||
| 264 | // $additive; ADDITIVE on debugtranslucentsinglecolor did nothing. | ||
| 271 | vfunc<FlagFn>(mat, 29)(mat, 1 << 15 /* IGNOREZ */, ignorez); | 265 | vfunc<FlagFn>(mat, 29)(mat, 1 << 15 /* IGNOREZ */, ignorez); |
| 272 | vfunc<FlagFn>(mat, 29)(mat, 1 << 21 /* TRANSLUCENT */, rgb[3] < 255); | ||
| 273 | vfunc<FlagFn>(mat, 29)(mat, 1 << 10 /* ZNEARER */, znearer); | 266 | vfunc<FlagFn>(mat, 29)(mat, 1 << 10 /* ZNEARER */, znearer); |
| 274 | vfunc<FlagFn>(mat, 29)(mat, 1 << 28 /* WIREFRAME */, style == 1); | 267 | vfunc<FlagFn>(mat, 29)(mat, 1 << 28 /* WIREFRAME */, style == 1); |
| 275 | vfunc<FlagFn>(mat, 29)(mat, 1 << 7 /* ADDITIVE */, | 268 | vfunc<FlagFn>(mat, 29)(mat, 1 << 7 /* ADDITIVE */, additive); |
| 276 | style == 2 || style == 5); | ||
| 277 | vfunc<FlagFn>(mat, 29)(mat, 1 << 12 /* FLAT */, style == 0); | 269 | vfunc<FlagFn>(mat, 29)(mat, 1 << 12 /* FLAT */, style == 0); |
| 278 | vfunc<FlagFn>(mat, 29)(mat, 1 << 6 /* SELFILLUM */, style == 5); | 270 | vfunc<FlagFn>(mat, 29)(mat, 1 << 6 /* SELFILLUM */, style == 5); |
| 279 | } | 271 | } |
| @@ -281,7 +273,6 @@ void chams_apply(void *mat, bool ignorez, int style, bool force_white, | |||
| 281 | void chams_reset(void *mat) { | 273 | void chams_reset(void *mat) { |
| 282 | using FlagFn = void (*)(void *, int, bool); | 274 | using FlagFn = void (*)(void *, int, bool); |
| 283 | vfunc<FlagFn>(mat, 29)(mat, 1 << 15, false); | 275 | vfunc<FlagFn>(mat, 29)(mat, 1 << 15, false); |
| 284 | vfunc<FlagFn>(mat, 29)(mat, 1 << 21, false); | ||
| 285 | vfunc<FlagFn>(mat, 29)(mat, 1 << 10, false); | 276 | vfunc<FlagFn>(mat, 29)(mat, 1 << 10, false); |
| 286 | vfunc<FlagFn>(mat, 29)(mat, 1 << 28, false); | 277 | vfunc<FlagFn>(mat, 29)(mat, 1 << 28, false); |
| 287 | vfunc<FlagFn>(mat, 29)(mat, 1 << 7, false); | 278 | vfunc<FlagFn>(mat, 29)(mat, 1 << 7, false); |
| @@ -317,21 +308,6 @@ void hkdModelExecute(void *self, const void *state, const void *info, | |||
| 317 | return false; | 308 | return false; |
| 318 | bool white = style == 4 || style == 5; | 309 | bool white = style == 4 || style == 5; |
| 319 | const int *use_rgb = rgb; | 310 | const int *use_rgb = rgb; |
| 320 | // Additive blending vanishes against bright backgrounds (sky, | ||
| 321 | // white walls, strong fog), which made the viewmodel disappear in | ||
| 322 | // some places. Lay down a faint translucent base so the first-person | ||
| 323 | // model keeps a silhouette on bright backgrounds without turning | ||
| 324 | // into an opaque blob; the additive pass still provides the glow. | ||
| 325 | if (is_vm && (style == 2 || style == 5) && rgb[3] > 0) { | ||
| 326 | int ba = rgb[3] / 8; | ||
| 327 | if (ba < 16) | ||
| 328 | ba = 16; | ||
| 329 | int drgb[4] = {rgb[0], rgb[1], rgb[2], ba}; | ||
| 330 | chams_apply(mat, ignorez, 3, false, drgb, false); | ||
| 331 | g_force_override(self, mat, 0 /* OVERRIDE_NORMAL */); | ||
| 332 | g_orig_model_exec(self, state, info, bone); | ||
| 333 | chams_reset(mat); | ||
| 334 | } | ||
| 335 | chams_apply(mat, ignorez, style, white, use_rgb, false); | 311 | chams_apply(mat, ignorez, style, white, use_rgb, false); |
| 336 | g_force_override(self, mat, 0 /* OVERRIDE_NORMAL */); | 312 | g_force_override(self, mat, 0 /* OVERRIDE_NORMAL */); |
| 337 | g_orig_model_exec(self, state, info, bone); | 313 | g_orig_model_exec(self, state, info, bone); |
| @@ -391,6 +367,10 @@ void chams_preload() { | |||
| 391 | continue; | 367 | continue; |
| 392 | void *m = world_find_material(chams_material_name(i)); | 368 | void *m = world_find_material(chams_material_name(i)); |
| 393 | if (m) { | 369 | if (m) { |
| 370 | // FindMaterial can hand back an unreferenced material; the engine warns | ||
| 371 | // ("bad reference count 0") and may reset its vars when it is bound. | ||
| 372 | using RefFn = void (*)(void *); | ||
| 373 | vfunc<RefFn>(m, 12)(m); // IMaterial::IncrementReferenceCount | ||
| 394 | g_style_mats[i] = m; | 374 | g_style_mats[i] = m; |
| 395 | logf("chams: preloaded style %d %s = %p", i, chams_material_name(i), m); | 375 | logf("chams: preloaded style %d %s = %p", i, chams_material_name(i), m); |
| 396 | } | 376 | } |
