Shroud And Fog-Of-War Pipeline
How shroud state is tracked per player in PartitionManager, turned into a texture mask, and applied per-pixel in the terrain shader.
Shroud in this codebase is not a pure render effect. The authoritative state starts in gameplay logic; rendering consumes that state as a texture projection or per-pixel mask.
The central split is:
- Logic side decides whether each cell is clear, fogged, or shrouded for each player.
- Render side converts that into brightness values and multiplies scene output by the resulting mask.
Logic authority: PartitionManager
PartitionManager is the source of truth for visibility. PartitionManager.cpp exposes doShroudReveal, undoShroudReveal, doShroudCover, and undoShroudCover — each updates looker and shrouder layers over a discrete circle of cells.
refreshShroudForLocalPlayer is the bridge. It iterates every cell, reads PartitionCell::getShroudStatusForPlayer, then pushes that status into both display and radar:
TheDisplay->setShroudLevel(m_cellX, m_cellY, newShroud); TheRadar->setShroudLevel(m_cellX, m_cellY, newShroud);
Query helpers gate effects and interaction:
getShroudStatusForPlayer(player, x, y)for cell-level tests.getPropShroudStatusForPlayer(player, worldPos)for object-ish tests that can return partial-clear when neighboring cells disagree.
Game logic does not ask the renderer whether something is visible. The renderer follows logic state, not the reverse.
DX11 terrain path
In the modern terrain path, shroud application moved into the terrain shader rather than the old fixed-function stage stack. TerrainRenderer::UpdateShroudTexture in TerrainRenderer.cpp converts the per-cell grid to an RGBA brightness texture:
// Multiplicative blend: dest * src_color. RGB controls terrain brightness: // brightness=0 -> dest * 0 = black (fully shrouded) // brightness=127 -> dest * 0.5 = half brightness (fogged) // brightness=255 -> dest * 1 = unchanged (clear) auto packBrightness = [](uint8_t brightness) -> uint32_t { return 0xFF000000 | (static_cast<uint32_t>(brightness)) | (static_cast<uint32_t>(brightness) << 8) | (static_cast<uint32_t>(brightness) << 16); };
The texture is always 2 texels larger than the playable grid in each dimension — a one-texel border around the cells is pre-filled with m_borderShroudLevel (clamped to TheGlobalData->m_shroudAlpha) so sampling just outside map bounds returns explicit shroud state instead of edge-clamp bleed:
std::vector<uint32_t> pixels(texW * texH, packBrightness(borderBrightness)); for (int y = 0; y < m_shroudHeight; ++y) for (int x = 0; x < m_shroudWidth; ++x) pixels[(y + 1) * texW + (x + 1)] = packBrightness(m_shroudGrid[y * m_shroudWidth + x]);
TerrainRenderer::RenderShroud used to draw shroud as a geometry pass. It no longer does — the function is now a no-op other than triggering a deferred texture update when m_shroudDirty is set. The comment in the function says so directly: "Fog of war is now computed per-pixel in PSMain via ApplyShroud(). The shroud texture is bound during Render() before terrain drawing."
Classic W3D render path
The legacy shroud system in W3DShroud.cpp still exists in the tree. It owns two texture surfaces — a system-memory source (m_pSrcTexture) and a video-memory destination (m_pDstTexture) — and draws shroud as a projection pass with W3DShaderManager::ShroudTextureShader setting up camera-space texture projection.
In the D3D11 path, several of these methods are stubbed — W3DShaderManager::setShroudTex in D3D11Shims.cpp is one of them. The real path in DX11 is the terrain-shader ApplyShroud() described above.
Quirks
Shroud ownership is logic-first. Rendering can only visualize states coming from
PartitionManager; it does not decide visibility. A desync between "I can see my unit but radar can't" usually indicates update-order problems, not independent logic —refreshShroudForLocalPlayerpushes both in one pass.Multiplicative brightness, not alpha. Artist-facing values should be reasoned about as light multipliers, not alpha overlays. A shroud value of 0.5 darkens the terrain to half brightness; it doesn't blend a black overlay at 50% opacity.
Border cells matter. A one-texel border of
m_borderShroudLevelpixels surrounds the shroud texture. If this border drops out (e.g., a resize race), map-edge sampling falls back to edge-clamp and reveals unintended terrain along the border. The dynamic texture recreates when the grid size changes, but the border is always re-applied on every update.UpdateShroudTexturerebuilds the entire texture. No dirty-rect optimization — every frame withm_shroudDirty == truere-uploads all pixels. On a 512×512 map that's ~1 MB per upload; large maps are visibly heavier. A partial-update path exists in the classic code but hasn't been ported forward.The DX11 terrain shader samples the shroud directly.
ApplyShroud()inShader3D.hlsl/TerrainShader.hlslreadsshroudTextureat the current pixel's UV (computed from world XY) and multiplies into the final color. This replaces the original's multi-pass setup stack; there is no separate shroud pass to reorder against other effects.Radar shroud is a parallel state.
TheRadar->setShroudLevelfeeds the mini-map's own per-player visibility grid. The radar draws its own shroud overlay with different colors (solid black for shrouded, tinted gray for fogged). Changes to the radar shroud rendering don't affect world shroud and vice versa.