Warp polish: animate scroll, recenter, no depth-clip, expand-only respawn

Four fixes for the user's "static / above the viewport / cavern clips it / doesn't
shoot off" report, each root-caused:

1. STATIC swirl -> now animates.  The material's SCROLL (0.0 0.0 0.1 0.5) only
   reaches the draw via a per-texture .met file (L4D3D.cpp:640) that tsphere lacks,
   and the ramp-bake path leaves doScroll=false -- so the swirl was frozen.  Set the
   authored scroll rates (u -0.1/s, v +0.5/s) + REPEAT wrap on the sphere's draw-ops
   at load, so SetTextureScrolling churns the swirl (the "spins around").

2. "Above the viewport" -> recenter.  tsphere.bgf is modelled with its centre at
   y=+8.25 (logged: center=(0,8.25,0.47) r=21.5); scaled x100-150 that threw the
   sphere ~1000 units overhead.  Translate the mesh's mCullCenter to the origin
   before scaling so it sits ON the eye/anchor.  (Added public-ish mCullCenter use;
   l4d3d.h already exposes it.)

3. Cavern clip -> Z-test off.  The arena is an enclosed cavern; a depth-tested 150x
   sphere gets occluded by the ceiling/walls.  Draw the POV overlay with ZENABLE off
   (ZWRITE already off, so no depth pollution).  BT_WARP_ZTEST=1 restores it.

4. "Fades in then disappears" -> expand-only.  A respawn now starts in the EXPAND
   phase (1->150 over 1s) so the swirl immediately bursts and blasts outward ("shoots
   off into the distance").  The collapse (100->1) is the DEATH warp, not the
   respawn.  BT_WARP_COLLAPSE=1 plays the full collapse->expand.

Build + render + no-crash verified 2-node; expand warp fires POV/world.  Still
deferred: the SetIsDead world-mask (black-out that makes the expand "reveal" the
world) -- carries the camera-coupling risk, add scoped if wanted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-10 09:09:21 -05:00
co-authored by Claude Opus 4.8
parent a9d1534292
commit 062c66999e
+61 -12
View File
@@ -2263,7 +2263,17 @@ static void
float v = (float)atof(s);
if (v > 0.0f) { gWarpCollapseScale = v; gWarpExpandScale = v * 1.5f; }
}
gWarpPhase = 1; // collapse
// RESPAWN = birth = the EXPAND phase (scale 1 -> 150 over 1s): the swirl bursts
// from the reinsertion point and blasts outward past you ("shoots off into the
// distance"). The COLLAPSE (100 -> 1) is the DEATH warp -- a separate trigger in
// the authentic engine (POVTranslocateRenderable), not part of the respawn -- so
// starting a respawn with a collapse read as "fades in then disappears".
// BT_WARP_COLLAPSE=1 plays the full collapse->expand cycle.
{
static int s_collapse = -1;
if (s_collapse < 0) { const char *cv = getenv("BT_WARP_COLLAPSE"); s_collapse = (cv && cv[0] == '1') ? 1 : 0; }
gWarpPhase = s_collapse ? 1 : 2; // 1 collapse->expand, 2 expand only
}
gWarpT = 0.0f;
gWarpPOV = pov;
gWarpX = x; gWarpY = y; gWarpZ = z;
@@ -2297,17 +2307,35 @@ void
gTLocSphere = d3d_OBJECT::LoadObject(device, "tsphere.bgf");
if (gTLocSphere != 0)
{
// Route the sphere's (opaque) draw-ops INTO the alpha-blend pass.
// DrawMesh skips any op whose alphaTest != (pass==PASS_ALPHABLEND)
// (L4D3D.cpp:1045), so an un-flagged tsphere renders NOTHING in the
// alpha pass -- the warp was invisible for exactly this reason.
for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op)
gTLocSphere->GetDrawOp(op)->alphaTest = true;
{
L4DRAWOP *dop = gTLocSphere->GetDrawOp(op);
// Route the sphere's ops INTO the alpha-blend pass: DrawMesh skips any
// op whose alphaTest != (pass==PASS_ALPHABLEND) (L4D3D.cpp:1045).
dop->alphaTest = true;
// THE SWIRL MOTION. The authentic material scrolls its texture
// (tsphere_scr_tex SPECIAL "SCROLL 0.0 0.0 0.1 0.5"), but the port only
// picks scroll up from a per-texture .met file (L4D3D.cpp:640), which
// tsphere has none of, and the ramp-bake path leaves doScroll=false --
// so our swirl was FROZEN. Set the authored scroll rates here so
// SetTextureScrolling animates it (u -0.1/s, v +0.5/s -> the churning
// swirl that "spins around"), REPEAT wrap so the scroll tiles.
dop->texture.doScroll = true;
dop->texture.scrollUDelta = 0.1f;
dop->texture.scrollVDelta = 0.5f;
dop->texture.wrap_u = L4TEXOP::REPEAT;
dop->texture.wrap_v = L4TEXOP::REPEAT;
}
}
if (getenv("BT_TLOC_LOG"))
DEBUG_STREAM << "[tloc] tsphere.bgf load "
<< (gTLocSphere ? "OK" : "FAILED")
<< " ops=" << (gTLocSphere ? gTLocSphere->GetDrawOpCount() : 0)
<< (gTLocSphere ? " center=(" : "")
<< (gTLocSphere ? gTLocSphere->mCullCenter.x : 0.0f) << ","
<< (gTLocSphere ? gTLocSphere->mCullCenter.y : 0.0f) << ","
<< (gTLocSphere ? gTLocSphere->mCullCenter.z : 0.0f) << ") r="
<< (gTLocSphere ? gTLocSphere->GetRadius() : 0.0f)
<< "\n" << std::flush;
}
if (gTLocSphere == 0)
@@ -2351,18 +2379,26 @@ void
// - Non-POV (observing a PEER respawn): anchor at the peer's world point so the
// swirl plays over there -- pure scale, then translate.
const float s = scale;
// Recenter on the mesh's own bounding-sphere centre: tsphere.bgf is modelled
// with its centre OFF the origin, so a raw scale pushed it "significantly above
// the viewport" once multiplied by 100-150. Translate the centre to the origin
// FIRST, then scale, so the swirl sits ON the eye / anchor.
const D3DXVECTOR3 &cc = gTLocSphere->mCullCenter;
D3DXMATRIX recenter, scaleM;
D3DXMatrixTranslation(&recenter, -cc.x, -cc.y, -cc.z);
D3DXMatrixScaling(&scaleM, s, s, s);
D3DXMATRIX m;
if (gWarpPOV && view != 0)
{
D3DXMATRIX invView, scaleM;
D3DXMATRIX invView;
D3DXMatrixInverse(&invView, 0, view);
D3DXMatrixScaling(&scaleM, s, s, s);
m = scaleM * invView; // Scale in the eye's local frame, then eye->world
m = recenter * scaleM * invView; // centre mesh, scale, then eye->world
}
else
{
D3DXMatrixScaling(&m, s, s, s);
m._41 = gWarpX; m._42 = gWarpY; m._43 = gWarpZ;
D3DXMATRIX anchorM;
D3DXMatrixTranslation(&anchorM, gWarpX, gWarpY, gWarpZ);
m = recenter * scaleM * anchorM; // centre mesh, scale, move to anchor
}
gTLocSphere->SetLocalToWorld(m);
@@ -2386,7 +2422,8 @@ void
// (ALPHABLENDENABLE + ZWRITE off, L4VIDEO.cpp:7762) but the blend/colour are
// indeterminate here (beams/pfx drew first): set them explicitly + RESTORE
// every state (a leaked LIGHTING/TFACTOR/stage-op corrupts the scene).
DWORD sSrc, sDst, sLight, sTFactor, sCOp, sCA1, sCA2, sAOp, sAA1;
DWORD sSrc, sDst, sLight, sTFactor, sCOp, sCA1, sCA2, sAOp, sAA1, sZ;
device->GetRenderState(D3DRS_ZENABLE, &sZ);
device->GetRenderState(D3DRS_SRCBLEND, &sSrc);
device->GetRenderState(D3DRS_DESTBLEND, &sDst);
device->GetRenderState(D3DRS_LIGHTING, &sLight);
@@ -2417,6 +2454,17 @@ void
s_additive ? D3DBLEND_ONE : D3DBLEND_INVSRCALPHA);
}
device->SetRenderState(D3DRS_LIGHTING, FALSE);
// Z-TEST OFF: the sphere is huge (up to 150x) and this arena is an enclosed
// CAVERN, so a depth-tested sphere gets CLIPPED by the ceiling/walls ("the cavern
// is obscuring it"). The warp is a POV overlay that engulfs you -- draw it over
// the world regardless of depth (ZWRITE is already off in the alpha pass, so it
// does not pollute the depth buffer for later draws). BT_WARP_ZTEST=1 keeps the
// depth test if the overlay-over-everything look is unwanted.
{
static int s_ztest = -1;
if (s_ztest < 0) { const char *z = getenv("BT_WARP_ZTEST"); s_ztest = (z && z[0] == '1') ? 1 : 0; }
if (!s_ztest) device->SetRenderState(D3DRS_ZENABLE, FALSE);
}
{
// TFACTOR: the RAMP now supplies the blue-white, so the tint is WHITE by
// default (MODULATE by white = the baked swirl unchanged); only the ALPHA
@@ -2439,6 +2487,7 @@ void
gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time);
device->SetRenderState(D3DRS_ZENABLE, sZ);
device->SetRenderState(D3DRS_SRCBLEND, sSrc);
device->SetRenderState(D3DRS_DESTBLEND, sDst);
device->SetRenderState(D3DRS_LIGHTING, sLight);