Runtime Exposure Control in Source Engine with env_tonemap_controller
Source Engine exposure changes belong in map logic, not client cvars. Use env_tonemap_controller and entity I/O for runtime brightness shifts that work in multiplayer.
26 Jan 2026, 21:45 UTC

You want a Source Engine map to get brighter or darker on demand—a flashbang burst, a lights-out moment, a slow dawn—without rebuilding materials or asking players to change settings. The mechanism Source actually exposes for this is not a client console variable you can push from the server; it is the env_tonemap_controller entity, driven through the standard entity I/O system. That distinction matters, because the obvious-looking approach (having a server plugin set mat_hdr_level on clients) does not work: mat_hdr_level is a client-side rendering variable, and a server cannot force it onto connected players.
How Source Handles Exposure
Source renders HDR maps through high-dynamic-range render targets and then applies tone mapping to convert that range to what the monitor can display. Tone mapping is the step that decides overall exposure: how bright mid-tones appear and how much highlight detail survives. Whether a map uses HDR at all is largely determined by the client’s DirectX level and hardware support, not by a switch a server can flip mid-match.
The exposure-relevant pieces a mapper can actually control are:
env_tonemap_controller– a point entity placed in the map that adjusts tone mapping parameters for players (typically within a radius, or globally depending on configuration).mat_dynamic_tonemapping– a client console variable that enables automatic exposure adjustment based on scene brightness. This is a client setting; you cannot assume it is on.mat_force_tonemap_scale– a client console variable that forces a fixed exposure scale, useful for testing on your own machine.
Because the last two are client-side, the portable, shippable path is the entity. Console variables are for local experimentation, not for gameplay logic.
Setting Up env_tonemap_controller
In Hammer, place an env_tonemap_controller in your map and give it a name, for example tonemap_global. The entity accepts inputs that change exposure behavior at runtime. Commonly used inputs include SetTonemapScale (with a parameter for the desired scale) and related blend/rate inputs that control how quickly the change applies. Exact input names and availability vary between game branches, so check the entity’s I/O list in Hammer’s object properties for your specific game—do not assume inputs from one Source title exist in another.
To trigger a change, wire any standard output to it. For example, a trigger_multiple covering a doorway can have this output:
Output: OnStartTouch
Target: tonemap_global
Input: SetTonemapScale
Parameter: 0.5
Delay: 0.00
A second trigger (or the OnEndTouch output) restores the normal scale, for example 1.0. Because this flows through ordinary entity I/O, it works in multiplayer without plugins: the server runs the entity logic, and clients render the result as part of normal map state.
Worked Example: A Flash Corridor
A concrete pattern that works on stock engine features:
- Place
env_tonemap_controller, name ittonemap_global. - Place a
trigger_multipleacross a corridor entrance. - Add output
OnStartTouch → tonemap_global → SetTonemapScale → 0.3to plunge the corridor into near-darkness. - Add output
OnEndTouch → tonemap_global → SetTonemapScale → 1.0with a small delay to restore normal exposure when the player exits.
Compile and run the map, then walk the corridor and watch exposure shift as you cross the triggers. If nothing changes, check three things in order: the map was compiled with HDR lighting enabled in the compile options (tone mapping changes are far more visible on HDR maps), the entity name in the output exactly matches, and your client has not disabled dynamic tone mapping. You can sanity-check locally by setting mat_force_tonemap_scale 0.5 in your own console—if the scene darkens, tone mapping is active and the problem is in your entity wiring, not the renderer.
Trade-offs and Limitations
- Branch differences. Input names, default behavior, and whether the controller affects all players or only those in range differ across Source games (Half-Life 2 episodes, CS:S, CS:GO, Portal, etc.). Verify against your target game’s FGD and test in that game, not another one.
- Client settings can interfere. Players with unusual client configurations may see different results. There is no supported way to force client rendering variables from a server plugin; design so the effect is cosmetic rather than gameplay-critical.
- LDR fallback. On hardware or settings where HDR is unavailable, the map falls back to LDR rendering and exposure effects are reduced or absent. Test both paths if your audience includes low-end machines.
- Readability risk. Aggressive darkening or brightening can hide enemies or wash out the HUD. For competitive maps, keep shifts modest and telegraph them with audio or visual cues.
- Performance. HDR rendering costs more than LDR because of the wider render targets and bloom pass, but the exact overhead depends heavily on GPU and scene; measure on your target hardware rather than trusting a generic percentage.
What to Do Next
Prototype locally first: load your map, use mat_force_tonemap_scale to preview the exposure range you want, then bake the same values into env_tonemap_controller I/O so the effect ships with the map. Test on the exact game branch you are targeting, on both HDR and LDR settings, and keep the effect reversible through entity outputs rather than anything requiring a map reload. If you find yourself wanting per-player or server-scripted control beyond what entity I/O offers, that is the point where a server plugin becomes worth considering—but it should drive map entities, not client console variables.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.