Water in Source Maps: Set It Up Correctly and Fix Bugs
Water in Source and GMod maps: how the water brush is built, expensive and cheap water, water_lod_control, cubemaps and fixes for rendering bugs.
In the Source Engine, water is a world brush whose top face carries a water material while all other sides get tools/toolsnodraw. It also needs a water_lod_control, a sealed hull of solid brushes and env_cubemap entities for the cheap rendering path. Water usually renders wrongly for one of four reasons: the brush is tied to an entity, the map has a leak, two water surfaces at different heights are visible from the same place, or the cubemaps are missing.
How a water brush is built
- Create a brush that fills the entire water volume, from the bottom to the surface.
- Apply
tools/toolsnodrawto the whole brush. - Give only the top face a water material, for example
nature/water_canals_water2. - Leave the brush as a world brush. Do not tie it to
func_detailor any other entity. - Enclose the basin with solid world brushes and place a
water_lod_controlin the map.
These rules follow from a mechanism. Water materials carry the parameter %compilewater 1. VBSP then cuts the visleafs along the water surface and stores exactly one height for each water volume. That is why the top face has to be horizontal, and why this only works with world brushes: with a water brush tied to func_detail, GMod's VBSP wrote no water volume into the BSP at all in a test (leafwaterdata 0).
Water is also not solid. If a water brush replaces an outer wall, VBSP reports **** leaked ****. See fixing the map leak error for how to find the gap.
The sides stay nodraw because they are compiled as faces otherwise. With a water material on all six sides, the test produced 60 faces instead of 40. VBSP creates the view from below itself, from the material's $bottommaterial parameter.
Expensive and cheap water
The same water can be drawn in three ways:
| Rendering | Reflection | View below the surface | Additional render passes |
|---|---|---|---|
| Expensive | Real time | Refraction | Two, for reflection and refraction |
| Medium | Cubemap | Refraction | One |
| Cheap | Cubemap | No refraction | None |
The engine decides which variant applies for every frame. The Valve Developer Community wiki (VDC) only distinguishes expensive and cheap; "medium" is our name for the state in between. The following order comes from the published client code of the Source SDK 2013. GMod uses its own engine branch, so details may differ.
- If the material sets
$forcecheap 1, the water is always cheap. One example isnature/water_canals_cheap001. - A real-time reflection needs a
$reflecttexturein the material and, in addition, either$forceexpensive 1or the player settingr_waterforceexpensive 1. Most of the stock HL2 water materials set$forceexpensive 1, includingnature/water_canals_water2. - Without a real-time reflection, the distance decides. From the end distance of the
water_lod_controlonwards the water is drawn cheap, below that with refraction and a cubemap.
So you set the rendering mainly through your choice of material. The cubemap reflection still has to work at all times, because medium and cheap water fall back on it. Expensive water draws large parts of the scene up to three times. How to measure such costs is shown in optimizing GMod map performance.
Using water_lod_control properly
This entity controls, for all water in the map, the camera distance at which the engine switches to cheap water.
| Keyvalue | Name in Hammer | Default |
|---|---|---|
cheapwaterstartdistance | Start Transition to Cheap Water | 1000 |
cheapwaterenddistance | End Transition to Cheap Water | 2000 |
Both values are in units. Between start and end the material fades smoothly, provided it contains the WaterLOD proxy, as the stock water materials do.
If the entity is missing, the compile does not abort. VBSP creates one with the default values and writes:
Water found with no water_lod_control entity, creating a default one.
Place one yourself anyway to adjust the distances to your map. Do not expect too much, though: according to the SDK code and the VDC, the distance does not apply to materials with a real-time reflection.
Water at different heights
The VDC states the rule: with expensive water there may be only one water height in a PVS, the set of areas that can be seen from one place. The SDK code shows why: for each frame the client determines a single visible water volume and renders reflection and refraction for exactly that height. A second surface at a different height then receives images that were calculated for the wrong plane. We did not test in game what exactly that looks like. There is no warning: a test map with two pools at different heights compiled without any message.
- One height per visible area. Separate pools at different heights with world brushes so that they never end up in the same PVS.
- Use the same height. The rule only concerns differing heights. Several pools at the same height share the reflection plane.
- Do not mix the types. According to the VDC, expensive and cheap water must not share a PVS either. If two heights have to stay visible together, give all of that water a material with
$forcecheap 1. It reflects through its cubemap.
Cubemaps, 3D skybox and displacements
Cubemaps. Water materials contain $envmap env_cubemap. Medium and cheap water use it to reflect the nearest env_cubemap. Place at least one in every area with water and run buildcubemaps after the final compile. Until then the BSP only contains a substitute made from the skybox texture. The procedure is described in fixing cubemap reflections, and the basics of HDR are in the Source Engine lighting guide.
3D skybox. According to the SDK code, the 3D skybox is drawn in a single pass, without its own reflection and refraction. Use a material with $forcecheap 1 there. According to the VDC, cheap water in the skybox works alongside expensive water in the map. The height follows from the scale, because the sky_camera stands for the origin of the main map: if the water there is at Z = 128 and the sky_camera has a scale of 16, the surface in the skybox belongs 8 units above the sky_camera. Conversely, according to the SDK code and the VDC the real-time reflection in the main map shows the 2D skybox and not the 3D skybox. The VDC lists a material parameter $reflect3dskybox for CS:GO and Garry's Mod that is meant to change this. We did not test it. More on that in the 3D skybox guide.
Displacements. A shore made of displacements seals the map just as little as the water itself. Continue the water brush underneath the terrain and place a solid world brush below both. Use nodraw only where the terrain hides it: the VDC warns of a trailing effect when nodraw seals off water. More on that in displacements in Hammer.
Typical errors and their cause
| Symptom | Cause | Fix |
|---|---|---|
| Water invisible or not rendered correctly | Leak in the map, or water brush tied to an entity | Close the leak, bring the brush back with Tools → Move to World |
| Water opaque or with a broken reflection | Second water height in the same PVS, or a func_viscluster or areaportal crosses the water surface | Separate the heights, keep func_viscluster and areaportals away from the surface |
| Black, purple and black checkered or far too bright reflection | Cubemaps are missing or were not built for the active mode | Place env_cubemap, run buildcubemaps for LDR and HDR |
| Water suddenly looks different in the distance | Water without a real-time reflection switches to cheap from the end distance onwards | Raise the distances in water_lod_control, build cubemaps |
**** leaked ****, line runs through the water | Water used as the outer boundary | Solid world brushes around the basin |
If water disappears completely at a distance, there is no single documented cause. First check whether a second water height comes into view. You can also paste the compile log into the Map Scanner. It detects root causes, follow-up errors and matching fixes in VBSP, VVIS and VRAD logs.
FAQ
Can the water level rise or fall?
Yes, with func_water_analog. The entity moves a water brush through the inputs Open, Close and SetPosition. The normal water shader does not work there, because it needs fixed visleafs. Use a material such as nature/water_movingplane. It only reflects through the cubemap.
Does water reflect players and props as well?
Only if the material sets $reflectentities 1 or the player has enabled r_waterforcereflectentities 1. Otherwise the real-time reflection contains only the world and the sky.
What does a custom water material need?
At least %compilewater 1, $abovewater, $fogcolor, $bottommaterial and $envmap env_cubemap. If $abovewater or $fogcolor is missing, the shader warns in the console. If $bottommaterial is missing, VBSP warns. For the smooth transition at a distance, add the WaterLOD proxy.