Shadow Decals
How the D3D11 projected-shadow decal pass bakes per-model silhouettes, batches by texture, and conforms decals to terrain via the heightmap.
Generals has two shadow systems in the tree: the original projected-decal system (what runs) and a GPU depth-map pass (WIP, off by default). This article is about the one that runs. The depth-map system is covered at the end.
The decal system produces a per-pixel shadow by stamping a small quad onto the terrain beneath each unit. Each quad is a screen-space decal that reads the terrain heightmap in its vertex shader and conforms to terrain contours. There is no shadow map and no per-light visibility test. A drawable registers a shadow with W3DShadowManager::addShadow() when its art asset requests one, and the decal renderer walks the resulting list once per frame.
The shadow manager
D3D11ShadowManager in D3D11Shims.cpp is a linked-list container. Each D3D11Shadow node stores a pointer back to the render object that created it, a shadow type bitfield (SHADOW_ALPHA_DECAL, SHADOW_ADDITIVE_DECAL, or plain multiplicative), a bounding-box-derived size, and an opacity byte.
Shadow *addDecalInternal(RenderObjClass *robj, Shadow::ShadowTypeInfo *shadowInfo)
{
D3D11Shadow* shadow = new D3D11Shadow(shadowInfo, robj);
shadow->m_next = m_shadowList;
if (m_shadowList) m_shadowList->m_prev = shadow;
m_shadowList = shadow;
++m_shadowCount;
return shadow;
}
Drawables call TheW3DShadowManager->addShadow(robj, shadowInfo, draw) from inside W3DModelDraw::setModelState — shadows follow the model-draw lifecycle, so a condition-state change that rebuilds the render object also tears the shadow down and re-adds it.
The render pass
RenderShadowDecalsDX11(camera) in D3D11Shims.cpp is called unconditionally each frame from W3DDisplay::draw — the option checkbox that used to gate it (m_useShadowDecals) is preserved for save-compat but no longer hides shadows. Only the debug toggle g_debugDisableShadowDecals still suppresses the pass.
The function snapshots the full shadow list into flat arrays, builds a DecalInstance per shadow, groups by (texture, blend mode), and issues one DrawInstanced per group:
for (int si = 0; si < shadowCount; ++si) { auto& inst = instances[si]; inst.posX = sX[si]; inst.posY = sY[si]; inst.sizeX = sSizeX[si]; inst.sizeY = sSizeY[si]; inst.angle = (g_useEnhancedShadows && (sTypes[si] & SHADOW_DIRECTIONAL_PROJECTION)) ? sunAzimuth : sAngle[si]; // ... color packing depends on blend mode ... }
The GPU side is a single structured-buffer instance shader. The VS reads the heightmap texture at the decal's world position and lifts the quad to terrain height. The PS samples the shadow texture and multiplies, blends, or adds it to whatever the terrain pass already wrote. Shadows use a 0.1 world-unit Z offset (shadowZOffset) so they sit flush; radius rings and command overlays use 1.25 units (decalRingZOffset) so they never clip into ridges.
The silhouette baker
For multiplicative shadows with a real render object, the preferred texture is not a generic blob. g_silhouetteBaker.GetOrBake(robj, modelName) lazily renders a top-down silhouette of the model into a cached texture the first time that model type is seen. Subsequent frames pull the cached texture straight from the decal batch. If the bake hasn't completed on the frame a shadow first appears, the code falls through to the file-texture lookup (EXShadow.dds, SCDropShadow.tga, etc.), and finally to a procedural dark circle.
The silhouette path is what makes the D3D11 shadows look sharp — the old Generals engine used the same projected-silhouette technique, but this port rebuilds the silhouettes with correct model orientation instead of reusing pre-baked art.
Enhanced Shadows toggle
g_useEnhancedShadows (default true) changes two behaviors:
- Opacity is full strength. Classic path applied a 50% multiplier cap (
1.0f - opacityF * 0.5f) which produces the ghostly look older players remember. Enhanced uses1.0f - opacityFfor darker, more realistic shadows. - Shadows tagged
SHADOW_DIRECTIONAL_PROJECTIONrotate with the sun azimuth computed fromg_fallbackShadowLightPos[0]. Other shadow types ignore the sun and use the model's heading.
The toggle is independent of the WIP shadow map — there is no code path where enhanced shadows and the GPU depth pass interact.
The WIP shadow map
Renderer::BeginShadowPass(), EndShadowPass(), and BindShadowMap() exist in Renderer.cpp, along with a 2048×2048 depth target, a PCF pixel shader (ComputeShadow in Shader3D.hlsl), and a frustum-fitted light projection in BuildCameraFitLightVP. None of them are currently called from a game path — g_debugDisableShadowMap = true is the default in W3DDisplay.cpp, and there is no invocation site that clears that flag in production code. The scaffolding is in place so the pass can be wired in once stability work is done; until then, decals carry the full shadow budget.
Quirks
Shadow geometry is registered, not culled. A unit off-screen still has a
D3D11Shadowin the list. The per-instance VS does the terrain-height sample and projects into clip space, so off-screen decals are rasterizer-clipped away — slightly wasted bandwidth but no visual artifact. There is no CPU-side frustum test on the shadow list.Multiplicative batches are skipped when
blend == Multiplicative. A note in the loop explains why: thetex * input.colorpixel shader renders multiplicative shadows as solid black squares when the silhouette baker is still warming up. Until the bake completes, the pass drops multiplicative shadows rather than flashing black rectangles. Alpha and additive batches (radius rings, general-power markers) always draw.The shadow texture list is fallback-chained.
EXShadow.dds→EXShadow.tga→shadow.dds→ ... → procedural circle. If none of the files exist, the procedural texture guarantees something draws. This is loaded lazily on first frame with shadows, not at init.Heightmap conformance is per-vertex, not per-pixel. The VS samples the heightmap once per corner of the decal quad, so a decal spanning a steep ridge clips below the triangulated surface between cell corners. The constant Z offset covers the common case but not extreme slopes — this is why command decals use a much larger offset than unit shadows.
Silhouette bakes are keyed on
Get_Name(). Models that share a name across condition states (e.g., rebuild-on-state-change) get cache hits and render the correct silhouette for whichever state was first seen. A model that renames itself mid-game would re-bake; none do in the shipping data.