Files
BT411/context/translocation-warp.md
T
arcattackandClaude Fable 5 3c34ae6de6 KB: full staleness audit + sweep -- 35 verified corrections across 16 topics, 15 new glossary terms
Adversarially-verified audit (18 agents) against the task #46-#56 landings:
MP/current-state understatements, superseded punch readings, retired env
gates, dead paths, line-cite drift; env-gate hub table completed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:56:51 -05:00

162 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: translocation-warp
title: "Translocation warp — the death/respawn vortex (tsphere)"
status: established
source_sections: "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)"
related_topics: [rendering, multiplayer, combat-damage, reconstruction-gotchas, asset-formats, bgf-format]
key_terms: [material-ramp]
open_questions:
- "AUTHENTIC peer path (seeing OTHERS' spheres) needs player SimulationState/DropZoneLocation replication; until then the LOCAL-player sphere stands in."
- "In the 2-node force-damage test only the EXPAND (respawn) warp logs reliably; confirm the COLLAPSE (death-side) fires on every MP death path."
---
# 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 `CONN` list = **40 single-sided triangles** (material `btfx: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 magic `DIV-BIZ2` at 0; chunk tagword id =
`tw & 0x2fff`, len-width from bits 0x8000/0x4000; vertex tag `0x0089` = 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_0040998c` axis 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 * invView` puts 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 `sky` ramp 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_TWIST` default **0**; `BT_WARP_RADFREQ`/`TILE_U` default 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]
1. **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.
2. **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 of
`capture.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).
3. **In-engine self-shot** (`BT_WARP_SELFSHOT=<prefix>` dumps backbuffer frames to disk; `BT_WARP_SELFTEST=1`
forces 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).
4. **Confirmed live** in the 2-node force-damage respawn test (`scratchpad/mp_respawn6.sh` pattern):
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`, batch `warpBands` flag.
- `engine/MUNGA_L4/L4D3D.cpp` — ramp bake (lavender + contrast + optional blur) **and the
`SetTextureScrolling` fmod fix**.
- `engine/MUNGA_L4/L4VIDRND.cpp``BTSetWorldDead` bridge to `L4Application::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).