From 062c66999e7d51e740b5676839d7df24fbeab4b0 Mon Sep 17 00:00:00 2001 From: arcattack Date: Fri, 10 Jul 2026 09:09:21 -0500 Subject: [PATCH] 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 --- game/reconstructed/btl4vid.cpp | 73 ++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index 21fb3ac..e67c14d 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -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);