User observed peer warp spheres in 2-player mode; the KB (translocation-warp.md
frontmatter+body, multiplayer.md frontmatter+body) still called the peer path a
"local-player sphere stand-in until SimulationState/DropZoneLocation replication."
STALE: mechdmg.cpp:1074 fires BTStartWarpEffect at the peer's position gated to
ReplicantInstance, and simulationState rides every update-record header -- so an
observer DOES see a peer's un-wreck/respawn warp. Committed 160b78e ("observer
sees peer un-wreck"). The only genuine remainder is a [T3] fidelity nuance: the
peer sphere is anchored to the peer's WORLD position rather than the peer's
authentic (un-replicated) DropZoneLocation. (The staleness audit itself missed
this -- it trusted the file's own stale wording over its body + the code.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
13 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. Peer warp IS wired + visible
(160b78e): an observer sees a peer's death/respawn warp — the replicant un-wreck fires the
world-anchored BTStartWarpEffect(x,y,z) at the peer's position (mechdmg.cpp:1074, gated to
ReplicantInstance; simulationState rides every update-record header so the observer tracks the
peer's state). The ONLY remaining nuance [T3, non-gating]: the observer's peer sphere is anchored to
the peer's WORLD position rather than the peer's authentic DropZoneLocation (that attribute isn't
replicated) — a fidelity refinement, not a missing effect.
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_TILE_V is an
accepted alias — bgfload.cpp:665) · BT_WARP_ANISO=0 · BT_WARP_CULL=cw · BT_WARP_TEXMOD=0
(diag: =1 re-enables the TEXTURE×DIFFUSE modulate — double-tints; btl4vid.cpp:2640) · BT_WARP_TILT=0 ·
BT_WARP_SCALE (btl4vid.cpp:2263-2266, collapse =v / expand =1.5·v) · BT_WARP_NOSCROLL (diag,
btl4vid.cpp:2373) · 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).