Terrain Smudges
How heat-haze and smear particles are converted into a one-frame terrain distortion pass.
The smudge system is the old heat-distortion path. It is not a decal system and it is not a persistent scorch-mark database. A smudge is a short-lived screen-space distortion primitive that samples the already-rendered background and writes it back with a slight UV offset. The visual result is heat shimmer, shock haze, or a smeared refractive patch over the terrain.
That distinction matters because the manager is rebuilt every frame. The core pool types in Smudge.h are just Smudge, SmudgeSet, and SmudgeManager. They store world position, UV offset, size, opacity, and a small cached vertex array. No long-term ownership chain — sets and smudges are pooled, rendered, then returned to free lists.
How particle systems feed smudges
The entry point is W3DParticleSystemManager::doParticles in W3DParticleSys.cpp. At the start of the pass it asks TheSmudgeManager for one global SmudgeSet:
SmudgeSet *set = nullptr; if (TheSmudgeManager) set = TheSmudgeManager->addSmudgeSet();
That set becomes the temporary bucket for every visible smudge in the frame. Not every particle system qualifies. The current test is a compact DWORD prefix comparison:
// Legacy "hack" check: first 4 bytes of the texture name spell "SMUD" if (*((DWORD*)sys->getParticleTypeName().str()) == 0x44554D53) { if (TheSmudgeManager && ((W3DSmudgeManager*)TheSmudgeManager)->getHardwareSupport() && TheGlobalData->m_useHeatEffects) { for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext) { // ... cull against screen + terrain extents ... Smudge *smudge = set->addSmudgeToSet(); smudge->m_pos.Set(pos->x, pos->y, pos->z); smudge->m_offset.Set( GameClientRandomValueReal(-0.06f, 0.06f), GameClientRandomValueReal(-0.03f, 0.03f)); smudge->m_size = psize; smudge->m_opacity = p->getAlpha(); } } continue; }
The 0x44554D53 constant is 'D' | 'U' << 8 | 'M' << 16 | 'S' << 24 — little-endian "SMUD". It's faster than strncmp but breaks if anyone touches the first four bytes of the texture name or if the build targets a big-endian platform (neither is a concern here).
The three preconditions are strict: the manager must exist, hardware probe must have passed, and the global heat-effects toggle must be on. All three failing silently skip the smudge and let the particle draw with its normal visuals.
How the distortion pass renders
The first non-obvious step is W3DSmudgeManager::testHardwareSupport at W3DSmudge.cpp:204. The code does not trust the card just because a render target exists. It performs a live probe: draw a known color block, copy it back, then draw again through the smudge sampling path and compare the captured buffers. If the buffers match, the hardware is marked as smudge-capable.
Actual rendering in W3DSmudgeManager::render flushes sorted translucent geometry first so the background texture is complete, then walks every SmudgeSet and every Smudge, transforms each center into view space, builds a five-vertex distortion primitive, and derives screen-space UVs against the current render target.
The important detail is that the pass samples from the already-rendered background. The smudge is a post-like pass living inside the 3D renderer, not a terrain material feature. That is why W3DShaderManager checks the previous frame's smudge count and why the system depends on render-to-texture support.
At the end of the particle pass, W3DParticleSys.cpp:388 calls W3DSmudgeManager::render, records the visible-smudge count, and immediately resets TheSmudgeManager. Nothing persists into the next frame except pooled allocations.
Quirks
Smudge detection is name-based and hex-coded. The
0x44554D53DWORD prefix comparison is faster than string compare but silently tolerates any name starting with "SMUD" and nothing else. Authoring mistakes turn into silent feature loss — a typo like "SMOG*" produces no warning, just no smudge.The D3D8 hardware probe uses
IDirect3DTexture8.testHardwareSupportinW3DSmudge.cppstill pullsW3DShaderManager::getRenderTexture()expecting a D3D8 texture and calls intoDX8Wrapper::Set_Shader. None of that exists on the D3D11 path. The manager exists but the probe never passes on the current backend, which means smudges are effectively off in this port. The particle systems with "SMUD*" names still cull-test and iterate their particles every frame — just no render.The data is one-frame-only.
W3DParticleSys.cpp:391resets the manager after rendering, so smudges must be regenerated every frame by a live particle system. There is no persisted smudge state between frames.The subsystem pools aggressively. Global free lists for both
SmudgeandSmudgeSetavoid churn during heavy particle scenes. The pool never shrinks once grown — a single big explosion inflates the pool permanently for the session.The pass depends on the background existing as a texture. If render-to-texture support is unavailable (or as currently on D3D11, not wired), the system short-circuits and the particles fall back to their ordinary visuals only. Heat shimmer silently becomes an ordinary alpha particle.
Port TODO. The smudge render would be a natural match for the D3D11 post-pass pipeline — bind the scene color as an SRV, sample it with a small UV perturbation in a full-screen or per-quad PS. The existing particle feed already builds the right data; only the draw call needs replacing.