LOG IN
Docs
By Sonnet

Dazzle Renderer

How the dazzle system produces camera-facing billboard glows for radar beacons, antenna lights, and afterburners.

A dazzle is a billboard glow that faces the camera and fades with distance and viewing angle. Radar beacons, communications antenna lights, and jet afterburners all use them. The system predates the D3D11 port and lives in WW3D2 — the render objects, type registry, and per-frame visibility lists are untouched from the original W3D engine. Only the final draw call changed when D3D8 was removed.

From W3D asset to screen

When the W3D loader encounters a W3D_CHUNK_DAZZLE inside a model file, DazzleLoaderClass::Load_W3D calls DazzlePrototypeClass::Load_W3D in dazzle.cpp. The prototype records the dazzle's name and type and registers with the asset manager. Instantiating the model later instantiates a DazzleRenderObjClass bound to that type.

DazzleRenderObjClass::Render() does not draw itself during the normal scene traversal. It just appends this to the current DazzleLayerClass — a linked list of visible dazzles keyed by type index, reset each frame:

void DazzleRenderObjClass::Render(RenderInfoClass& rinfo)
{
    if (!Is_Dazzle_Rendering_Enabled()) return;
    visibility = 1.0f;
    current_vloc.Set(Transform[0][3], Transform[1][3], Transform[2][3]);
    // ... compute current_dir from transform ...
    if (s_current_dazzle_layer && !on_list)
        s_current_dazzle_layer->Add(this);
}

The actual draw happens later. DazzleLayerClass::Render walks the per-type lists; the scene dispatch in D3D11Shims.cpp filters render objects by CLASSID_DAZZLE and routes them to RenderDazzleDX11:

else if (classId == RenderObjClass::CLASSID_DAZZLE)
{
    RenderDazzleDX11(robj, rinfo);
}

The draw function

RenderDazzleDX11 at D3D11Shims.cpp:9938 extracts world position, computes intensity, builds a billboard quad, and draws:

DazzleRenderObjClass* dazzle = static_cast<DazzleRenderObjClass*>(robj);
if (!DazzleRenderObjClass::Is_Dazzle_Rendering_Enabled()) return;

const Matrix3D& camTM = rinfo.Camera.Get_Transform();
Vector3 cameraPos(camTM[0][3], camTM[1][3], camTM[2][3]);
Vector3 camForward(camTM[0][2], camTM[1][2], camTM[2][2]);

const Matrix3D& dazzleTM = dazzle->Get_Transform();
Vector3 dazzlePos(dazzleTM[0][3], dazzleTM[1][3], dazzleTM[2][3]);
Vector3 dirToDazzle = dazzlePos - cameraPos;
float distance = dirToDazzle.Length();
if (distance < 0.01f) return;
dirToDazzle *= (1.0f / distance);

dazzleType->Calculate_Intensities(dazzleIntensity, dazzleSize, haloIntensity,
    camForward, dazzleDir, dirToDazzle, distance);
if (dazzleIntensity < 0.01f) return;

float halfSize = dazzleSize * dazzle->Get_Scale() * 0.4f;
// ... build quad from camera right/up vectors, submit additive draw ...

Calculate_Intensities runs the dot product between the camera look vector and the vector to the dazzle, then applies the per-type dazzle_intensity_pow curve to shape how quickly the glow drops off when viewed from the side. A distance curve fades the glow between FadeoutStart and FadeoutEnd. The result is three scalars: dazzle intensity, dazzle size, and halo intensity.

The billboard quad uses camTM[0] (camera right) and camTM[1] (camera up) normalized and scaled by halfSize, offset from the dazzle world position. The 0.4f coefficient on halfSize matches the original W3D implementation's visual scale. Six vertices form two triangles; the pixel shader is additive-blend with the dazzle texture.

Type registry and INI

Each type's parameters live in Dazzle.INI. The [Dazzles_List] section enumerates type IDs by name:

[Dazzles_List]
0 = DEFAULT
1 = SUN

Followed by per-type sections:

[SUN]
HaloIntensity       = 0.9
HaloIntensityPow    = 3.0
DazzleIntensity     = 1.0
DazzleIntensityPow  = 2.0
FadeoutStart        = 2000.0
FadeoutEnd          = 10000.0
DazzleColor         = 1.0, 1.0, 0.9
HaloColor           = 1.0, 0.9, 0.7

Types load into a static s_types[] array at startup via DazzleRenderObjClass::Init_From_INI. A per-instance Set_Dazzle_Color(Vector3) override lets code tint a specific dazzle without touching the shared type — this is how faction-colored antenna lights work (same type, different color per player slot).

Quirks

  • Early-return history. During the D3D11 port, RenderDazzleDX11 had an early return at the top that effectively disabled the system. The full implementation existed immediately below it. The comment at the top of the function is preserved and spells out why it was re-enabled and what the risk is — any model that ships both a dazzle and a MuzzleFX sub-object will render the muzzle flash twice. The shipping Generals.dat models don't hit this overlap, but custom models can.

  • on_list guards duplicate insertion. A dazzle rendered through multiple passes (reflection + main) would otherwise get appended to the visible list twice in the same frame and double-brighten. The flag is reset by DazzleLayerClass::Render once per frame.

  • Intensity early-exits twice. distance < 0.01f skips dazzles on top of the camera (avoids the normalize divide-by-zero). dazzleIntensity < 0.01f skips fully-faded dazzles before the texture lookup and quad build — the cheap gate happens after the per-type intensity math so the side-angle fadeout still governs visibility, not just distance.

  • Visibility is not occluded. Render() sets visibility = 1.0f unconditionally. An app can install a DazzleVisibilityClass handler on the layer to run ray casts or portal queries, but this codebase installs none. Every dazzle inside the view frustum draws; walls don't block them.

  • Layer construction order matters. DazzleLayerClass allocates its per-type list head array sized by s_type_count. Constructing it before Init_From_INI finishes yields an under-sized array and silently corrupts inserts for types registered after construction. The correct order is enforced at startup — but if you add a new dazzle type from mod INI loaded post-init, you'll hit this.