The death/respawn "blue whirlwind" (tsphere) now matches the original cabinet
photo (capture.png): a smooth spinning lavender vortex with a bright core.
Root cause of the long-standing "radial spokes" artifact was NOT the warp code
but a general engine bug: L4D3D::SetTextureScrolling computed its texture-matrix
offset as scrollDelta * absolute_time, which grows unbounded and collapses UV
float precision -> a smooth scrolled cloud shatters into grainy radial steps.
Wrapped with fmodf(..., 1.0f) (identical under REPEAT tiling, full precision).
This also cleans the scrolling bexp beam grit and any other SCROLL material.
Visual reconstruction (verified against the real 45-vtx TSPHERE.BGF bicone,
offline-rasterized then ported):
- view ON-AXIS (eye centred on the throat) + spin in place -> concentric rings
(decomp FUN_00453dc4 does spin-about-local-Z + submit; the port had stubbed it)
- bintA cloud through a WIDE lavender ramp at full contrast (drawn as SKY);
no geometry "bands", no log-polar twist, no off-axis tornado (all discarded)
- tessellate the 12-facet bicone smooth; isotropic + trilinear; ramp baked into
the texture and drawn SELECTARG1(TEXTURE) to avoid double-tinting
Env knobs (BT_WARP_*) default to the verified values; BT_WARP_SELFTEST/SELFSHOT
are an off-by-default visual-verification harness (backbuffer frame dump).
Docs: new context/translocation-warp.md (geometry/material/visual/lifecycle/env);
reconstruction-gotchas.md gains the accumulated-time precision-collapse bug class;
rendering.md / multiplayer.md / decomp-reference.md / CLAUDE.md cross-linked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 KiB
id, title, status, source_sections, related_topics, key_terms, open_questions
| id | title | status | source_sections | related_topics | key_terms | open_questions | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| translocation-warp | Translocation warp — the death/respawn vortex (tsphere) | established | multiplayer.md (lifecycle + trigger wiring, task #52); reference/decomp FUN_00453dc4 (translocate Execute); L4VIDRND POVTranslocateRenderable @1749/.h:638; scratchpad/render_bicone.py (offline mesh match vs capture.png) |
|
|
|
Translocation warp (the respawn vortex)
The "blue whirlwind" respawn effect from the 1995 pod: on death the pilot is sucked into a spinning
energy vortex; on respawn it blows outward and the reborn mech's world snaps back. VWE calls it
translocate (not "whirlwind" — that keyword search is why an earlier scout wrongly declared it
"not a shipped asset"). It is a pure render-side effect watching the player's SimulationState
dial; the respawn SIM path (DropZoneReply → Mech::Reset) is separate (multiplayer, combat-damage).
This topic is the VISUAL/RENDER authority. The trigger wiring + the state machine's engine grounding live in multiplayer (task #52). The systemic bug the effect exposed is in reconstruction-gotchas §13.
The asset — content/VIDEO/GEO/TSPHERE.BGF [T1, dumped]
NOT a sphere — a coarse 12-facet BICONE (two cones base-to-base along local Z), 2085 bytes:
- 45 vertices, one
CONNlist = 40 single-sided triangles (materialbtfx:tsphere_mtl). - Apexes at z = ±21 (both at x=0, y=8.25, radius 0); shared ring at z=0, radius 10.5 (a 12-gon).
- Geometric axis = the line (x=0, y=8.25, z) — the "throat." Bound center (0, 8.25, 0), r≈21.
- UVs are cylindrical: U = angle around the throat, authored into the narrow band [0.131, 0.656] (~half the texture, wrapped around the full 360°); V = axial, [0, 2] (ring at v=1.0, apexes at v≈0 / 2). Viewed straight down the throat these U,V ARE (angle, radius) — a polar map. This is why spinning it reads as concentric rings, and why UV twist shears rings into spokes.
- Dump/parse tool:
scratchpad/dump_tsphere.py(BGF magicDIV-BIZ2at 0; chunk tagword id =tw & 0x2fff, len-width from bits 0x8000/0x4000; vertex tag0x0089= XYZ|N|UV stride 32).
The loader doubles every tri (fwd a b c + rev a c b) → 80 tris (emitTri, bgfload.cpp), then
tessellateWarpCone() subdivides 4:1 × BT_WARP_TESS (default 3) projecting each new midpoint onto
the true cone surface (radius 10.5*(1-|z|/21)) → ~5120 smooth tris. Untessellated, the 12 flat
facets read as a "12-sided funnel"; tessellation is what makes the swirl smooth on a modern display.
(The 1995 low-res CRT + phosphor + fast spin hid the facets for free.)
The material — btfx:tsphere_mtl (in VIDEO/MAT/<tod>/BTFX.BMF) [T1]
tsphere_mtl → texture tsphere_scr_tex (a scrolling texture, SPECIAL SCROLL 0.0 0.0 0.1 0.5) →
image bintA (VIDEO/TEX/BINTA.VTX, DIV-VTX2, 64×64 RGB, pixels at offset 57, a smooth
cloud-noise field). Ramp ref sky; IMMUNE 1 (fog-immune). So authentically the swirl COLOUR is the
sky ramp remapping the grayscale bintA cloud, scrolled, fog-immune.
The visual model — VERIFIED against capture.png [T2, in-game + offline-matched]
The correct look = the bintA cloud, on the bicone, viewed ON-AXIS, spun in place, coloured by a wide lavender ramp. Concretely, and each of these was a hard-won correction:
- On-axis, not off-axis. The eye is parented so the bicone THROAT axis passes through it (matrix
term
eyeUp = Translate(0,-8.25,0)cancels the +8.25 authored offset). Looking straight down the throat, the cloud's polar UVs read as concentric rings converging to a bright center. Placing the eye off-axis (the authentic mesh-origin, 8.25 below the axis) gives an off-center "tornado" that does NOT match the original.BT_WARP_EYE_UP(default 8.25 = on-axis). - Spun in place (this IS the animation). The decomp translocate Execute FUN_00453dc4 does ONLY:
accumulate an angle (
+0x80 += +0x7c, wrap 2π) and write a Z-axis quaternion rotation (FUN_0040998caxis idx 2), then submit. No texcoord/palette/ramp/texture-matrix work — proof the swirl is the MATERIAL, not geometry. The WinTesla port STUBBED this spin; restoring it is essential. Matrix order matters:world = eyeUp * spin * local * tilt * invViewputs the axis ON the spin-Z BEFORE rotating, so it spins in place (no precession).BT_WARP_SPIN(rad/s, default 4). - Wide lavender ramp, full contrast — NOT the sky ramp, NOT geometry bands. The bake overrides the
material's coarse
skyramp with a NARROW-hued but FULL-RANGE lavender:LO=(0.15,0.12,0.35),HI=(0.98,0.94,1.0), contrast 1.0. The bintA luminance indexes LO→HI per texel (baked into the texture, drawn WHITE). Dark→lavender across the full range = distinct soft rings + a bright core. (BT_WARP_LO_R/G/B,BT_WARP_HI_R/G/B,BT_WARP_CONTRAST.) - No UV twist, radfreq 1.0. U += twist·V shears the concentric rings into a spiral/spokes — wrong
for this near-concentric target.
BT_WARP_TWISTdefault 0;BT_WARP_RADFREQ/TILE_Udefault 1. - Isotropic linear + trilinear mips, no blur. Anisotropic filtering smears the grazing throat wall
radially → streaks; keep isotropic. Mips help (trilinear); base-only is sharper/worse. The pre-blur
hook (
BT_WARP_BLUR) defaults 0 — it only ever existed to mask the scroll bug (below); with that fixed the raw cloud reads as clean rings.
Discarded WRONG models (do not revive): per-vertex geometry "shade bands" (concentric contours from
|z|); a screen-space log-polar billboard; a narrow low-contrast ramp; an off-axis "tornado"; a
helical UV twist; anisotropic filtering; heavy texture blur; a "garbage autogen-mip" theory. Each was
plausible and each was wrong — the real defect was the scroll bug.
The draw [T2] (btl4vid.cpp BTDrawTranslocationSpheres, POV branch)
Drawn as SKY (drawAsSky, PASS_SKY): OPAQUE, CULLMODE=D3DCULL_CW (one winding of the
double-sided mesh → no z-fight), z-test on, LIGHTING off, FOG off (IMMUNE 1). COLOROP = SELECTARG1(TEXTURE) — the ramp is already baked into the texture, so draw it DIRECT; do NOT modulate
by a TFACTOR/vertex tint (that double-tints the ramp — the ramp-bake KB note, docs/PROGRESS_LOG.md).
The world behind is blacked by the SetIsDead mask (below) so only the vortex shows during the wait.
Non-POV (observer over-the-shoulder view of a peer) is a port EXTENSION: alpha-blended in the alpha
pass so it isn't sky-occluded.
The lifecycle state machine [T1 grounding, T2 impl] — mirrors POVTranslocateRenderable
gWarpPhase: 0 idle → 1 InitialCollapse (scale COLLAPSE_START 100→1 over COLLAPSE_TIME 1.3s) →
3 WaitForReincarnate (scale 1, Lissajous wobble cos(t·3.33)/sin(t·2.5)·TRANSLATE_LIMIT 2.0,
raises the world mask) → 2 ExpandReveal (scale 1→EXPAND_END 150 over EXPAND_TIME 1.0s, drops
the mask) → 0. Spins throughout.
The world mask = L4Application::SetIsDead(bool) (L4APP.h:54), reached via the BTSetWorldDead(int)
bridge added in L4VIDRND.cpp. It is a PLAIN BOOL that gates ONLY render passes (world/decal/sky/alpha/
particles/reticle IsDead() readers in L4VIDEO.cpp) — NOT camera/input/targeting/sim. Safe to drive
directly. (⚠ An earlier regression came from pulsing the player SimulationState for the same effect —
that DID corrupt camera/targeting; SetIsDead is the correct lever. See f053535.) Failsafes: the wait
times out (12s) and force-unmasks on mission-end / no-DropZones so the world can never stay black.
THE BUG that hid the swirl for the whole effort — texture-scroll precision collapse [T2]
Symptom: grainy radial "spokes" from screen center, on black — nothing like the smooth rings.
Survived every geometry/param/filter change; my offline mesh rasterizer (scratchpad/render_bicone.py)
rendered the SAME mesh+texture+params cleanly. The one thing offline didn't do was scroll.
Cause (L4D3D.cpp SetTextureScrolling): the scroll set a texture-matrix translate
_31 = -scrollUDelta * time, _32 = scrollVDelta * time, where time = targetRenderFrame is
absolute runtime. That product grows without bound; within seconds the UV offset is large enough
that adding it to the per-pixel UV collapses float precision — the coordinates quantize into coarse
steps and the smooth cloud shatters into radial streaks + grain.
Fix: wrap each offset into [0,1) with fmodf(scrollDelta*time, 1.0f) — identical under REPEAT
tiling, full precision. Smooth swirl restored. This bug silently degraded EVERY scrolling texture
(weapon beams bexp, exhaust) the longer a session ran — the warp (full-screen, on black) just made
it impossible to ignore. Generalized in reconstruction-gotchas §13.
Trigger wiring (summary; full detail in multiplayer) [T2]
Local player's own death → BTStartWarpCollapsePOV() (btplayer.cpp VehicleDeadMessageHandler,
deathCount==-1, guarded this==GetMissionPlayer()). Respawn DropZoneReply → BTStartWarpExpandPOV()
(local-guarded). BTWarpForceUnmask() on mission-end / no-DropZones. Observer replicant un-wreck fires
the world-anchored BTStartWarpEffect(x,y,z) (mechdmg.cpp). The peer path is a stand-in until player
SimulationState/DropZoneLocation replication exists.
How it was solved (method) + verification [T2]
- Dump the real asset (
dump_tsphere.py) — established it's a bicone with cylindrical UVs, ending a long chain of wrong guesses about the geometry. - Offline mesh rasterizer (
render_bicone.py) — projected + textured the true 45-vtx mesh with the game's exact transform order and swept eye-height / spin / ramp / contrast against a crop ofcapture.png. Matched at on-axis + wide ramp + no twist (scratchpad/cmp_final2.png). This proved the LOOK is achievable and isolated the in-game defect (offline was clean → the delta is scroll). - In-engine self-shot (
BT_WARP_SELFSHOT=<prefix>dumps backbuffer frames to disk;BT_WARP_SELFTEST=1forces a held warp in a solo game) — compared in-game frames to the target WITHOUT foregrounding the window. This is how the scroll cause was pinned (scroll-off frame was instantly clean). - Confirmed live in the 2-node force-damage respawn test (
scratchpad/mp_respawn6.shpattern): victim node dies → masked swirl → EXPAND-reveal, un-regressed.
Env gates (all default to the verified values)
BT_WARP_EYE_UP=8.25 · BT_WARP_SPIN=4 · BT_WARP_TWIST=0 · BT_WARP_CONTRAST=1.0 ·
BT_WARP_LO_R/G/B=0.15/0.12/0.35 · BT_WARP_HI_R/G/B=0.98/0.94/1.0 · BT_WARP_BLUR=0 ·
BT_WARP_TESS=3 · BT_WARP_MIP=1 · BT_WARP_TILE_U=1 · BT_WARP_RADFREQ=1 · BT_WARP_ANISO=0 ·
BT_WARP_CULL=cw · BT_WARP_TINT=0 · BT_WARP_TILT=0 · BT_TLOC_LOG (lifecycle log) ·
BT_WARP_SELFTEST / BT_WARP_SELFSHOT (DIAG capture harness — off by default; retained for visual verification of the open peer/collapse work).
Key files
game/reconstructed/btl4vid.cpp— state machine, matrix, POV/peer draw, self-test/self-shot hooks.engine/MUNGA_L4/bgfload.cpp— tsphere detect (tsphere_mtl), cylindrical-UV + lavender-ramp override,tessellateWarpCone, batchwarpBandsflag.engine/MUNGA_L4/L4D3D.cpp— ramp bake (lavender + contrast + optional blur) and theSetTextureScrollingfmod fix.engine/MUNGA_L4/L4VIDRND.cpp—BTSetWorldDeadbridge toL4Application::SetIsDead.
Key Relationships
- Draws through: rendering (L4D3D sky pass, material ramp, scrolling textures).
- Triggered by / wired in: multiplayer (SimulationState dial, DropZone respawn), combat-damage (death transition, un-wreck).
- Exposed the bug class: reconstruction-gotchas §13 (accumulated-time precision collapse).
- Asset formats: asset-formats (VTX), bgf-format (BGF chunks, LOD/CONN).