LOG IN
Docs
By GitHub Copilot

W3D Model Draw Modules

How INI-driven model condition states become render objects, animation transitions, shadows, track marks, and unit-specific draw behavior.

The W3D draw-module stack is the bridge between gameplay-facing model conditions and actual render objects in the scene. The key idea is that art does not talk directly to a renderer object. It defines condition states, transitions, recoil parameters, track marks, particle attachments, and public bones in INI, and W3DModelDraw turns that declarative data into runtime state.

INI parse boundary

W3DModelDrawModuleData::buildFieldParse in W3DModelDraw.cpp registers the full INI surface area — recoil values, color-change permission, animated-bone particle flags, minimum LOD, projectile-bone feedback slots, condition states, transition states, track-mark file names, extra public bones, and more. This is the same field-parse table pattern used elsewhere in the engine, but here it matters more because almost every art-side draw behavior enters through that table.

Those parsed blocks become ModelConditionInfo records. Each record can carry a model name, hide-show subobject rules, animation lists, transition keys, allow-to-finish keys, turret metadata, projectile launch bones, muzzle-flash bones, particle systems, and cached validation results. One condition-state entry is already much closer to a fully prepared render recipe than to a loose tag.

Runtime selection

replaceModelConditionState(const ModelConditionFlags& c) resolves a condition bitfield to the best ModelConditionInfo and forwards to setModelState. setModelState handles four cases before touching the renderer:

  1. Duplicate request. If the requested state matches current or queued next state, it returns early.
  2. Allow-to-finish. If the current state is marked as must-finish and the request doesn't override, the new state is queued as m_nextState and the current animation runs to completion.
  3. Transition clip lookup. If both states declare TransitionKey values, the code builds a TransitionSig (a packed pair of keys) and looks up a transition state:
TransitionSig sig = buildTransitionSig(
    m_curState->m_transitionKey, newState->m_transitionKey);
const ModelConditionInfo* transState = findTransitionForSig(sig);
if (transState != nullptr) {
    nextState = newState;
    newState = transState;  // play transition first
}

This is why art can insert a turret-rotation or door-open animation between two condition states without gameplay code knowing it exists. The transition plays, then m_nextState fires automatically when the animation completes.

  1. Particle-bone preservation. Before stopping particles, the code checks whether the old and new states reference identical particle-system templates and bones:
bool bonesIdentical = false;
if (m_curState != nullptr && m_curState != newState) {
    const auto& oldBones = m_curState->m_particleSysBones;
    const auto& newBones = newState->m_particleSysBones;
    if (oldBones.size() == newBones.size()) {
        bonesIdentical = true;
        for (size_t i = 0; i < oldBones.size(); ++i) {
            if (oldBones[i].particleSystemTemplate != newBones[i].particleSystemTemplate ||
                oldBones[i].boneName != newBones[i].boneName) {
                bonesIdentical = false;
                break;
            }
        }
    }
}
if (!bonesIdentical)
    stopClientParticleSystems();

The comment explains the bug this prevents: toxin tractor, anthrax tank, and flame tank cycle FIRING_B → BETWEEN_FIRING_SHOTS_B every weapon shot. The matched ModelConditionInfo changes pointer even when both states reference identical particle bones. Tearing down and recreating the ground-aligned toxin puddle forces every new particle to start from its first keyframe — which is RGB(0,0,0) for ToxinPuddleContinuous, invisible under additive blending. The shot cycle produces a visible strobe. The fix keeps the particle systems alive when bones match, so the persistent ground effect rides through state cycles unchanged.

Render object rebuild

When bones, model name, or turret layout change, the render object is torn down and recreated. The code preserves the previous transform across the swap so the visual handoff stays smooth:

  • Save previous transform from the old render object.
  • Destroy the old render object.
  • Call Create_Render_Obj(modelName, scale, color).
  • Validate bones, public-bone cache, turret info, and weapon-barrel info against the new model.
  • Optionally attach terrain track marks through TheTerrainTracksRenderObjClassSystem.
  • Create shadow via TheW3DShadowManager->addShadow(robj, shadowInfo, draw) if requested.
  • Set collision pick types on the render object (rubble, shrubs, mines, force-attack targets, click-through objects all have specific rules).
  • Add to scene.
  • Restore hidden state and re-apply the previous transform.

None of this logic lives in gameplay modules. It is part of draw-module state realization.

Per-frame drive

doDrawModule runs every frame. It pauses animation when the drawable shouldn't animate, handles per-instance scale, completes any pending m_nextState transitions, randomizes idle animation rollover, adjusts animation speed to movement speed, updates the render-object transform, repositions turret bones, updates client particle systems, and applies recoil.

The transition-completion check is simple:

if (m_curState != nullptr && m_nextState != nullptr) {
    const ModelConditionInfo* nextState = m_nextState;
    UnsignedInt nextDuration = m_nextStateAnimLoopDuration;
    m_nextState = nullptr;
    m_nextStateAnimLoopDuration = NO_NEXT_DURATION;
    setModelState(nextState);
}

This fires when the current animation finishes naturally — the draw module is not just a draw-call wrapper, it's the per-frame owner of model presentation.

Specialized subclasses

Vehicle-specific behavior layers on top via subclassing rather than forking. W3DTankDrawModuleData::buildFieldParse appends tread debris names and tread UV animation parameters to the base table. W3DTankDraw::doDrawModule creates debris particle systems and scrolls tread UVs to fake track motion on top of the base draw. The base module still owns state changes, object recreation, and general animation plumbing.

Quirks

  • Condition-state changes are expensive. A model swap can recreate the render object, rebuild recoil data, reattach shadows, re-register scene state, and validate bones in one step. Units that cycle states every logic frame pay this cost every frame. Authors should prefer animation-only transitions where possible.

  • W3DTreeDraw::reactToTransformChange was missing. The original art code called addToTreeBuffer from the destructor instead of reactToTransformChange. OPTIMIZED_TREE props were never rendered — USA01's fir trees appeared as dust. Fixed by moving the add into the transform-change callback. See bug_w3dtreedraw_constructor_destructor_swap.md.

  • Transition clips are art-authored. The renderer inserts them by matching TransitionKey pairs; gameplay doesn't know the transition animation exists. A state with no TransitionKey falls through directly to setModelState on the target state with no intermediate clip.

  • Particle-system preservation is pointer-and-name matching. particleSystemTemplate is compared by pointer, not by value. Two entries pointing at different instances of the same particle definition (e.g., a reloaded INI with a pointer change) would trigger a teardown even though the visual is unchanged. This is theoretically possible during hot-reload but not during normal gameplay.

  • m_nextState queue is one-deep. Queuing two setModelState calls in quick succession drops the first — the second overwrites m_nextState before it's consumed. The engine never stacks transition states, which is why complex state chains (ambient → alert → fire → reload) use separate TransitionKey pairs rather than nesting.