Files
BT411/context/rendering.md
T
arcattackandClaude Fable 5 28b01e3529 KB: truck destruction NOT fixed -- playtest falsified the harness claim
Human playtest (2026-07-19): ramming a truck makes it disappear entirely --
no rubble, no smoke/fire, no visible explosion.  The [cultvis] log evidence
verified the STATE MACHINE (pointer swaps), not pixels: the rubble child
component (DCSInstanceRenderable/DPLStaticChildRenderable) is itself
2007-stubbed and never draws, and the routed psfx 1008 produces nothing
visible.  rendering.md section corrected from 'verified live' to PARTIAL
with the three remaining gaps (rubble draw, visible explosion, burning
fire).  Gitea #3 reinstated as open/wip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 09:32:54 -05:00

23 KiB
Raw Blame History

id, title, status, source_sections, related_topics, key_terms, open_questions
id title status source_sections related_topics key_terms open_questions
rendering Rendering — the D3D9 pipeline, materials, sky, beams, performance established PROGRESS_LOG.md §10 (render-fidelity notes); docs/BGF_FORMAT.md
bgf-format
asset-formats
wintesla-port
locomotion
pod-hardware
cockpit-view
L4D3D
material-ramp
BSL
LOD
DPL
beam
Replacement-LOD for NON-additive multi-LOD models

Rendering

The port renders via the WinTesla L4D3D/L4VIDEO Direct3D9 path (wintesla-port), carrying our BT loader (bgfload.cpp) + fidelity work. The goal: match the authentic pod footage. Geometry/LOD mechanics are in bgf-format; this topic covers the D3D9 draw path + the fidelity fixes. Full detail: docs/PROGRESS_LOG.md §10.

The IG-board shading model (from libDPL DPLTYPES.H)

Shading is selected PER-GEOMETRY by vertex type: no-normal geometry (terrain/mesas/sky/buildings/ mech — WHITE baked verts) is UNLIT, colorized by the material's 2-endpoint material-ramp (dpl_SetMaterialRamp — texture luminance indexes a low→high gradient); normal-bearing geometry (~150 vehicle/missile files) is LIT by the map light. Applying the ramp to lit geometry = the "dusty white blobs" bug (gate on hasNormals). [T2] Exception: cockpit-frame (*_cop) batches are pure-emissive constant colour (0.13,0.12,0.15), BT_COP_FRAME overrides — see cockpit-view (task #55; the old BT_COP_RAMP_L ramp-index approach is retired). [T2] VERTEX-ALPHA EFFECT CARDS (2026-07-12) [T1 corpus-swept]: exactly 14 shipped BGFs carry per-vertex float RGBA with alpha < 1 (tags 0x0082/0x008A, RGBA at +12, values OUTSIDE [0,1] — clamp): FLAMEBIG/FIRE5 (flames), MUZFLASH, EXDISK_A/B/C, TMST_A/B/C, BLUEBEAM/GREEBEAM/RED_BEAM/ PPC, DECLOUDS — the EFFECT-CARD family. Their authored gradient (white-hot base → dark tip fading to alpha 0.2) IS the shading: drawn opaque with a flat batch colour they render as solid shapes (the wreck flame read as a "twisted drill bit"). The loader now keeps the authored per-vertex RGBA for any batch with vertex alpha < 1 (BgfDrawBatch.vertexAlpha) and L4D3D routes it to the ALPHA-BLEND pass, unlit, colour = texture × gradient, alpha = vertex fade (SRCALPHA/INVSRCALPHA). SKY objects are excluded from the routing (drawAsSky + alphaTest passes NEITHER pass filter — DECLOUDS must stay in the sky pass). Authored TEXTURE SCROLL (BMF TEXTURE tag 0x2037 " SCROLL u0 v0 du dv", e.g. firesmoke1 = 0.05,0.331 uv/s) is now parsed and propagated to L4TEXOP::doScroll — scrolling materials (firesmoke flame cards) animate in the model path. [T2]

The big render-fidelity fixes (dbase/desert matched to pod footage)

  • Geometry: CONN is a flat trilist (not fan); CONN+PCONN coexist — see bgf-format. Malformed fan-garbage was the "faceted mountains" bug. [T2]
  • RAMP colorize (default-on BT_RAMP) + day/night material priority (default-on BT_MATPRI for the ramps): the mat\day libs carry the warm ramps; the generic GEO libs are gray. Terrain/mesas render warm tan; the sky's dsky_mtl ramp (0→0.99) gives bright clouds. hasNormals gate keeps vehicles LIT (colored trucks). Ramp tint rule: explicit diffuse + NEUTRAL ramp ⇒ tint; coloured ramp ⇒ white. [T2]
  • Sky: a *sky.bgf dome/plane drawn in a dedicated fullbright PASS_SKY (LIGHTING off, extended far=9000 + world fog); tagged drawAsSky at load (the .x sidecar path doesn't exist). [T2]
  • The env pipeline (DPLReadEnvironmentDPLReadINIPage, reads content/BTDPL.INI via L4DPLCFG): resolves map+time → clip/fog/ambient + a directional sun. Bring-up stubs (EnsureValidProjection / per-frame ambient / uninit cloud colours) had clobbered it — now they RE-ASSERT the env values. Debug: copy BTDPL.INI → *DBG.INI with debug=True. [T2]
  • Fog — per map/time/WEATHER (task #63). BTDPL.INI authors fog=<near> <far> <r> <g> <b> (+ often nosearchlightfog=) on EVERY map/time/weather leaf. WEATHER is a fixed authored egg field (clear/fog/soup, mission->GetMissionWeather(); missing = fatal in the arcade); the env pipeline branches location→time→weather to the leaf. Shipped eggs pin cavern/night/clear → page dsnitclear (near 90, far 1100, dark blue 0.1,0.1,0.12); fogdsnitfog (500), soupdsnitsoup (300). The arcade fog model is per-PIXEL linear (dpl_fog_type_pixel_lin, dpl.h:265). It rendered INVISIBLE in the port: D3DRS_FOGTABLEMODE=D3DFOG_LINEAR (table fog) reads the perspective-NONLINEAR z-buffer, so without WFOG the authored 90..1100 range collapsed to fog-factor≈1 (no fog). FIX (L4VIDEO.cpp world pass ~8194): auto-detect D3DPRASTERCAPS_WFOG → per-pixel W-fog (smooth, == the arcade); else VERTEX fog (eye-space but per-vertex → splotchy on coarse terrain tris); FOGENABLE + mode re-asserted each world-pass frame (the old one-time set got clobbered per-pass). Task #20's "shipped maps define no fog" was WRONG — it checked .MAP/.RES, but fog lives in BTDPL.INI (the DPL env INI), as in the arcade. Night fog is dark BY DESIGN (distance→murk, not light haze); day pages author light haze. Env: BT_FOGMODE=table|vertex|off, BT_WEATHER=clear|fog|soup, BT_FOG="near far r g b" (all branches); [fog] resolved/[fog] model/[fogstyle] (BT_FOG_LOG) log lines. [T1 pipeline / T0 arcade model / T2 fix] ⚠ SEARCHLIGHT SWAP IS INERT — and it was inert in the ARCADE too (task #63 workflow, 2026-07-13). The arcade swaps the active fog between fog= (headlight ON) and nosearchlightfog= (OFF, darker/tighter) via PullFogRenderable::Execute (L4VIDRND.cpp:4601, edge-triggered on two light attrs → SetFogStyle(searchLightOn/OffFogStyle)). TWO independent reasons it never fires: (1) Port: PullFogRenderable is NEVER CONSTRUCTED (grep: only its .h decl + .cpp def; no new); searchLightOn/Off is called from nowhere else (BT_FOG_LOG: over 22s only SetFogStyle(0), zero 2/3). (2) Original 1995 LATENT BUG [T1, decomp]: even if wired, the Searchlight never lights — ToggleLamp (@004b860c) toggles commandedOn @0x1DC (part_013.c:6087) but SearchlightSimulation (@004b841c) reads requestedOn @0x1E0 (part_013.c:5978), which the ctor zeroes and NOTHING ever writes; no bridge 0x1DC→0x1E0 exists anywhere in the binary. So lightState@0x1D8 is perpetually 0. The self-consistent sibling ThermalSight reads the SAME field it toggles (0x1DC) — the tell that Searchlight's sim reads the wrong slot. The port reproduces this faithfully (searchlight.cpp:189 reads requestedOn). DECISION (2026-07-13): left as-is + documented (faithful to the buggy original); the port keeps the static lights-ON fog= values. To make the swap WORK is a DESIGNER-INTENT fix that DEVIATES from the shipped binary — repair the sim to read commandedOn (mirror ThermalSight) + construct PullFogRenderable at btl4vid.cpp MakeMechRenderables (the reticle-build/inside pass, == arcade part_014.c:5173, ExecutionType Dynamic, bound to each Searchlight lightState) + a toggle input. See open-questions. Core per-map/time/weather fog is unaffected.
  • Projection / stencil (task #55): the BTDPL.INI viewangle=60 is the HORIZONTAL FOV — authentic 60×47 frustum at 4:3; fovY derived via BTFovYFromHorizontal (L4VIDEO.cpp), applied at all 5 projection sites (it was mis-applied as vertical). The device depth-stencil is now D3DFMT_D24S8 for the cockpit punch stencil-cut (L4VIDEO.cpp:2799; sequence L4D3D.cpp:1199-1248) — see cockpit-view. [T2]

Backface culling (default CW, BT_CULL)

The bring-up D3DCULL_NONE drew interior/back faces (the "dark wedge" shapes = mesh insides). CW is correct for the BGF quad triangulation [a b c][a c d]. [T2] Exception: cockpit *_cop batches default DOUBLE-SIDED (BT_COP_SINGLE=1 restores single-siding, diag) — see cockpit-view. [T2]

LODs (default: shipping-engine first-LOD + ADDITIVE_LODS; BT_ADDLOD/BT_LOD_SCALE/BT_PUNCH)

See bgf-format for the √3 range decode, the ADDITIVE_LODS cumulative rule, PUNCH cutouts, and the coplanar-shell submission-order depth bias. Replacement LOD selection for NON-additive multi-LOD models is deferred (needs the board's hot-spot semantics). [T2]

Mech skins (BSL bit-slice — the rainbow-mech fix)

.BSL is a bit-sliced container (6 grayscale sub-images per file); the BMF 0x18 BITSLICE tag picks the slice. Reading it as RGB overlaid 2-3 gray sheets = the "rainbow/graffiti" bug. Fixed: image.cpp decodeBSL slice-decodes; bgfload threads the 0x18 channel; a cross-library RAMP registry resolves the mech-skin softer ramp (defined in OTHER libs). Every mech renders the authentic BASE gray-metal skin now. See asset-formats. [T2]

Per-pilot mech PAINT (color/badge/patch substitution) — NOT WIRED (2026-07 audit)

The "missing warning stripes / coloration" report is the per-pilot paint layer, fully authored but never engaged by the port. Mechanics (all [T1] from the BMF/BGF/vehicletable content + FUN_004d0cc0):

  • Mech BGFs reference the BASE lib only (blhskin:gen2_dz_rfoot_mtl …). The binary rewrites lib prefixes at load via the vehicletable resource [substitute] templates: skin:_%color%%serno%: (blhskin:blh_wht0:) and skin:lgo<<<emblem%serno%:%badge%_%patch%> (emblem pages → EMBLEM0.BMF lgoN_<patch>_dz_* materials). Pilot egg values (color/badge/patch) map through the [color]/[badge]/[patch] sections to the codes.
  • The COLOR libs (e.g. BLH_WHT0.BMF) are byte-identical to the base lib EXCEPT they add tags 0x23(diffuse RGB) + 0x24(ambient RGB) = the pilot color to the ~35 paintable gen*/mech*_dz_* materials (base leaves them untinted). 7 colors: wht(.99,.99,.99) red(.90,.11,.10) grn(.63,.83,.52) gry(.65,.60,.65) brn(.60,.44,.28) tan(.65,.55,.43) blk(0,0,0). All 8 sernos are byte-identical (verified: 0 of 193 groups differ) — %serno% was future variety, never used. <mech>SKIN.BMF (bare) == <mech>SKIN0.BMF.
  • WARNING STRIPES are grayscale hazard-stripe art in the shared GEN.BSL pages (slice 1 = gen2_tex, top-left), shown through the paintable-zone materials and tinted by the pilot color. EMBLEM0.BMF carries lgoN_<patchcolor>_dz_ld/rdleg materials (8 patch tints incl. yellow .89,.73,0) over LGO.BSL slices 0-5 (slice 5 = VGL winged logo).
  • WIRED 2026-07-17 (same-day fix), verified live: MP MadCat renders crimson with hip hazard stripes + yellow VGL leg emblems; Blackhawk white + emblems; replicants painted on both nodes. The reconstruction: (a) Mech::resourceNameA/B/C are real CString members (the binary's exact 16-byte CStringRepresentation {size,len,text@+8,refs@+0xc}; ctor = part_012.c:9841-71, deep-copy bind from the MakeMessage = CString::operator=(const char*) == FUN_00402a98 @10427-41, implicit member dtors = 11388-403) with paint-name accessors; (b) SetupMaterialSubstitutionList reads them (real egg names; [paint] log line); (c) dpl_SetMaterialNameCallback is REAL now (L4VIDEO.cpp registry, cleared first in TearDown as FUN_004d11e8 does) and bgfload MaterialResolver::resolve() runs every name through dpl_ApplyMaterialNameCallback; no mesh-cache issue (LoadObject/LoadBgfFile don't cache — every load resolves fresh). [T2]
  • DEVIATION (documented, btl4vid.cpp): on a vehicletable miss the binary Fail()ed ("Exiting" @BTL4VID.CPP:0xbeb); the port warns + drops that placeholder (RP analogue). Real case: dev eggs ship color=Red but the [color] table only has Crimson — MP_BHMC.EGG fixed to Crimson; LAST.EGG pilot-2 still says Red (unused slot). The 7 valid egg colors: Black/Brown/Crimson/Green/Grey/Tan/White; badges Davion/Kurita/Liao/Marik/None/Steiner/VGL; patches Black/Blue/Green/Grey/Red/Violet/White/Yellow. [T1]

The OLD-STYLE SPECIALFX layer (the weapon-impact action) — RECONSTRUCTED 2026-07-12 (firmware-decoded)

The per-weapon impact visuals are fully AUTHORED in content/BTDPL.INI [effects_to_load]: specialfx3..15 = Chunks1/Chunks2/Sparks/Explosion3/MachineGun/PPCHit/LaserHit/MissileHit/ CannonHit/GroundHit/MinorHit/MajordHit/Fireball — "old-style" (version-1) descriptor sections (texture/type/size/velocity/v_bias/y_off/cook(3f)/varience/gravity/cool(2f)/count/repeats), most on btfx:firesmoke1_scr_tex (MissileHit size 12 ×7; Fireball size 14, gravity 1.5 rises; Chunks1/2 = ballistic debris vel 40-55 grav 34-35; Sparks NOTEX). [T1: the 1995 binary parses them — part_008.c:1546-1590, field strings @004f25e4-004f2663 → installer FUN_00490210 packs a 0x44-byte board command via FUN_0048e2c0.] Simulation semantics (type/cook/cool) live in the i860 firmware content/VREND.MNG (same decode workflow as the task-#55 'damageize' handler).

  • EFFECT-NUMBER ROUTING (corrects the old claim): effect_number < 100 = the dpl board EFFECT TABLE = this specialfx list (1996 case-4 comments match: 3=Chunks, 12=GroundHit, 15=Fireball), NOT a psfx slot; >= 1000 = psfx slot n1000 (idam/ddam PFX); 100-999 = ExplosionScripts. The port's <100 → psfx slot mapping renders PPCHit(8) as ddam5 smoke and Chunks(3)/Sparks(5) as ddamN smoke — the "hits are just a little smoke" report. The 2007 WinTesla loader SKIPS all version-1 descriptors (if (version < 2) continue; — it only reads the 1998 INDIE format), which is why the layer was never seen. [T1]
  • Live hit census (mp logs): weapon hits dispatch idam bands 1009-1013 (30×1009) + raw 8/9 (PPCHit/LaserHit, mis-rendered); MissileHit(10)/CannonHit(11) never appeared — the projectile/salvo damage path likely doesn't thread the firing launcher's subsystemID into the damage records, so messmgr never queues its explosion. [T2 census; T4 cause]
  • RECONSTRUCTION (2026-07-12, firmware-first): the i860 sim decoded from VREND.MNG (dispatch table at data+0xdd0c, VA base: code 0xf0400000, data 0x1000; handlers: sfx trigger 0xf040cda0→ca48, install 0xf040cdc0→cba0, step near 0xf0413698, spawn near 0xf0414afc — string-anchored via "attempt to step a NULL sfx object"; disassembler REBUILT at scratchpad/i860dis.py from binutils i860 opcodes, scratchpad/i860_opcodes.h). The heat model [T1]: per 30Hz board frame h *= cool_a; RGB_ch = h_old*(h_new*cook_ch 0.25) + 0.25 (K=0.25 @VA 0xF080 = the ember floor); kill at h ≤ 1e-4 (@0xF0A0); alpha = cool_b per frame; pos += vel, gravity as accel. y_off = the particle KILL PLANE (Chunks 1 = die at the ground) [T3 data-indicated]. Burst frame randomly spun (quadratic small-angle rot, θ from the host-fed random table cmd 0x21). Implemented in L4VIDEO.cpp: BTInstallSfx/BTStartSfx + sfxMode particles in the BTPfx layer; parser beside the psfx page walk ([sfx] page 'effects_to_load': 13 old-style specialfx installed). Effect-number routing fixed in BOTH consumers (ExplosionClassID + DPLIndependantEffect): <100 → specialfx, ≥1000 → psfx slot n1000. Missile/cannon hits now bundle [T2]: BTProjectile carries shooter + weaponSubsys; contact routes through the SHOOTER's SubsystemMessageManager (AddDamageMessage w/ subsystemID → mslhit/acanhit explosion at the impact), mirroring the laser path's SendDamageMessage. Cone shape (v_bias as up-bias) + per-type draw weights are [T3]; the full type-0/1/2 draw-geometry decode in the firmware draw fn (0xf0416160) remains open.

Weapon-hit forensics closeout (2026-07-12, workflow-verified) [T1]

  • The HIT PACKAGES (BTL4.RES model lists, byte-verified): ppchit=[8], lzrhit=[9], mghit=[7,11], canhit=[11] (no "acanhit"), mslhit=[10, 12, 1023] (MissileHit + GroundHit + psfx23 SHKWAVE), gndhit=[12], minhit=[1002], majhit=[1003], explode=[6]; dam1-4hit=[1002-1005], desthit/stephit=[1008]; cockpit "i" variants (lzrhiti etc.) = same sfx + idam 1009-1013; ALL eight mech death lists = ["104","3","4","5","1007","15","1001"].
  • "Silver mist exactly 5×, perfectly timed" ROOT CAUSE: SHKWAVE.PFX authors maxIssue=5, releasePeriod=0.2, rate=1 — the ONE shipped .PFX where rate ≠ maxIssue/releasePeriod; the emitter trusted 'rate' → 5 huge white shells (exp 350/s) at exactly 1Hz. Emission-window rule hardened: effective rate = maxIssue/releasePeriod always (BTDrawPfx emitter).
  • SpecialFX palette [T1 firmware]: instance init @0xf04128d8: inst[0x19c]=cool_a (per board-frame heat decay), inst[0x1a4]=cool_b (× raw dt in the step = PER-SECOND fade), inst[0x188]=7.0s cap, heat=1.0. The heat channels are BRIGHTNESS over an inherently FIRE-coloured texture (firesmoke through its material ramp): sfx batch draws the FIERY-ramped sheet, psfx keeps GRAYSCALE (authored colours decide — DNBOOM orange, DDAM gray). Drawing sfx grayscale = silver mist; scaling fade ×30 = 0.1s pops ("nothing happens on laser hits").
  • Damage-band .PFX shapes are authored GENTLE (puffy clouds): bands 1-4 of ndam/idam/ddam rise at 1-4 u/s with buoyant accel; ONLY band 5 + the BOOM/SHKWAVE files are explosive-omni. idam = scaled-down WORLD venting smoke (half radius/counts of ddam), not cockpit-only. N-set = night = same physics as D-set with fewer particles. NSRM/DSRM trails = 0.1s overbright ORANGE directional puffs (never silver).
  • messmgr explosion DEDUP CONFIRMED [T1, decomp-read @0049b784]: Find (weaponExplosions vtable slot 0xC) gates AddValue (slot 8) — ONE explosion per unique weapon resource per frame, exactly as reconstructed. (A workflow agent claimed "one per record, staggered 0.1s" — REFUTED; the stagger applies to the deduped queue.) Missile multiplicity comes from the ORDNANCE side instead: Missile::MoveAndCollide @004bef78 detonates per round — ported as BTSpawnRoundDetonation (mech4) + rack-tube launch spread [T3]. Alpha-strike drama = distinct per-weapon-TYPE packages (lzrhit+ppchit) + the victim's band fire, not explosion count.
  • Remaining [T3]: sfx cone shape (v_bias as up-bias), size ×0.5 + 1.25m textured floor + hot-phase occlusion 0.45..0.9, burst scatter ×0.2; trail density is frame-rate-dependent (2/frame @60fps = 2× pod density); .PFX 'repeats' unconsumed (all shipped files author 1); replicant visual salvo chases the stale fire-time aim point (no re-lead without a target handle).

Weapon beams (the unported dpl_* layer)

The dpl_* beam renderable was never ported → beams are drawn by BTPushBeam/BTDrawBeams (L4VIDEO). The real look is the ermlaser.bgf TUBE (native Z 0..2000, UVs tiled) with the scrolling bexp grit texture (from the .VMF SCROLL). ER lasers red, PPC blue, standard yellow. The beam origin re-anchors to the live muzzle every frame (can't run out from under your own beam). See combat-damage for fire-rate/muzzle. [T2]

Translocation warp (the respawn vortex) — see translocation-warp

The death/respawn "blue whirlwind" is tsphere.bgf (a 12-facet bicone) spun on-axis with the bintA cloud through a wide lavender ramp, drawn as SKY. Full geometry/material/lifecycle/env in translocation-warp. ⚠ Rendering-wide fix that came out of it: L4D3D::SetTextureScrolling computed its texture-matrix offset as scrollDelta × absolute_time, which grew unbounded and collapsed UV float precision → grainy/spoked scrolled textures the longer a session ran. Wrapped with fmodf(…, 1.0f). This also cleans up the scrolling bexp beam grit and any other SCROLL material. [T2] (Bug class: reconstruction-gotchas §13.)

Performance — the 10fps→60fps fix (~50-80× render speedup)

The 10fps baseline had THREE stacked causes, all fixed (engine tree): (1) D3DCREATE_SOFTWARE_ VERTEXPROCESSING — every vertex transformed+lit on the CPU → HARDWARE first; (2) D3DX DrawSubset attribute-table scan (our double-sided tris broke OptimizeInplace) → explicit SetAttributeTable + direct DrawIndexedPrimitive; (3) no frustum culling → bounding-sphere culling per frame. ⚠ LESSON: the user plays the DEBUG build, but the 10fps was NOT the debug build (RelWithDebInfo identical) — split draw-vs-present + check the DEVICE flags before blaming the compiler. Diagnostics kept: [spike]/[loadobj]/[rstat]. [T2]

Cultural-icon (map prop) DESTRUCTION — revived 2026-07-19 (Gitea issue #3) [T2]

Trucks/props/buildings are CulturalIcon/Landmark entities (engine/MUNGA/cultural.h, map- streamed via INTEREST.cpp; ARENA1 = 124 icons, each with a damage zone, ExplosionModelFile, removeOnDeath). The SIM was always complete [T0]: ram/weapon damage → CulturalIcon:: TakeDamageMessageHandler → zone damageLevel ≥ 1Explosion entity (Crunch variant for collision) + WaitingToBurn (+timeDelay) → BurningState + UpdateCollisionVolumes (boxes DELETED on removeOnDeath, else sunk by destroyedYTranslation). The mech-side crunch dispatch (mech4.cpp ProcessCollisionBTDispatchCollisionDamage, ×0.001 ram economy) reaches icons (≈19 walking-speed bumps kill the spawn-yard truck). What was BROKEN (the "no state change" report): the 2007 port fully stubbed StateInstanceSwitchRenderable ("STUBBED: DPL RB 1/14/07") — the intact→rubble visual swap NEVER fired. Revived D3D9-native: the switch now controls the draw component via the SetDrawObj in-place drawable swap (the mech RemakeEntity mechanism); [video] Object hides at BurningState, Rubble shows; watcher = StateIndicator::AddVideoWatcher (SetState DOES fire video watchers). ⚠ REQUIRED gotcha: cultural objects must stay OUT of static-mesh consolidation (RecurseStaticObject — same exclusion class as banded LODs/shadows), else the merged mesh draws the intact model forever regardless of DrawObj. ⚠ STATUS (human playtest 2026-07-19): PARTIAL — the intact model now hides on death (the truck "disappears"), but the RUBBLE never appears and no explosion/fire is visible. The switch's SHOW sets DrawObj on the rubble CHILD component (DCSInstanceRenderable/ DPLStaticChildRenderable) — those child classes are themselves 2007-stubbed and never draw their object, so the swap is a no-op on screen; and the death Explosion routes effect 1008 (ddam5 psfx) but nothing visible appears (+ the authentic BurningState fire visual is entirely unreconstructed). REMAINING: make the rubble genuinely draw (or hang it off the drawn root), a visible death explosion, the burning-fire state. LESSON: [cultvis] log lines verify the STATE MACHINE, not pixels — only a human screen check verifies rendering. Diags: BT_CULT_LOG (census/damage/death + [cultvis]). MP: replicants transition via ReadUpdateRecord → same SetState watcher path (untested cross-pod).

Key Relationships