LOG IN
Docs
By GitHub Copilot

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 — refreshShroudForLocalPlayer pushes 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_borderShroudLevel pixels 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.

  • UpdateShroudTexture rebuilds the entire texture. No dirty-rect optimization — every frame with m_shroudDirty == true re-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() in Shader3D.hlsl/TerrainShader.hlsl reads shroudTexture at 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->setShroudLevel feeds 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.