Files
BT411/context/translocation-warp.md
arcattackandClaude Opus 4.8 1b5c3bbbd8 KB: correct STALE "peer warp not wired" claim -> it IS wired + visible (user-caught)
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>
2026-07-15 14:46:12 -05:00

13 KiB
Raw Permalink Blame History

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)
rendering
multiplayer
combat-damage
reconstruction-gotchas
asset-formats
bgf-format
material-ramp
Peer warp IS wired + visible -- observer sees a peer's un-wreck warp (world-anchored BTStartWarpEffect, 160b78e). Remaining nuance [T3]: anchor to the peer's authentic DropZoneLocation (not replicated) vs the peer's world position.
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 DropZoneReplyBTStartWarpExpandPOV() (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]

  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.cppBTSetWorldDead bridge to L4Application::SetIsDead.

Key Relationships