diff --git a/engine/MUNGA_L4/bgfload.cpp b/engine/MUNGA_L4/bgfload.cpp index f56e580..bde1db7 100644 --- a/engine/MUNGA_L4/bgfload.cpp +++ b/engine/MUNGA_L4/bgfload.cpp @@ -500,6 +500,7 @@ struct Builder { float currentLodBias = 0.0f; // (retained; per-batch bias now set in buildPmesh) bool currentPunch = false; // inside a PUNCH patch (SV_SPECIAL 0x2037, geogroup level) bool currentShadowMat = false; // material name contains "shadow" (baked ground-shadow quads) + bool currentTSphere = false; // material is tsphere_mtl (translocation warp): ramp it despite normals bool currentHasEmissive = false; float currentEmissive[3] = {0,0,0}; @@ -561,7 +562,7 @@ struct Builder { // TRUECOLOR BSL slices (channel >= 6: RGB444/RGBA4444, e.g. damcolor's bdam8 // damage sheet) are real colour images -- luminance-ramping them would // destroy their colour, so they take the plain textured path. - const bool useRamp = currentHasRamp && !hasNormals(vtag) && currentTexChannel < 6; + const bool useRamp = currentHasRamp && (!hasNormals(vtag) || currentTSphere) && currentTexChannel < 6; const uint32_t base = (uint32_t)px.size(); const uint32_t localCount = (uint32_t)lx.size(); @@ -865,6 +866,7 @@ struct Builder { bool savedHasRamp = currentHasRamp; bool savedPunch = currentPunch; bool savedShadowMat = currentShadowMat; + bool savedTSphere = currentTSphere; bool savedHasEmissive = currentHasEmissive; float savedEmissive[3] = { currentEmissive[0], currentEmissive[1], currentEmissive[2] }; float savedRampLo[3] = { currentRampLo[0], currentRampLo[1], currentRampLo[2] }; @@ -889,6 +891,12 @@ struct Builder { currentShadowMat = false; for (size_t si = 0; si + 6 <= full.size(); ++si) if (_strnicmp(full.c_str() + si, "shadow", 6) == 0) { currentShadowMat = true; break; } + // The translocation warp sphere (tsphere_mtl): its swirl COLOUR is + // the "sky" ramp remapping the grayscale bintA cloud, but tsphere.bgf + // carries vertex normals so the normals-gate below would skip the ramp + // and leave it raw gray. Flag it so the ramp bake runs anyway (the + // authentic look; matches L4VIDRND POVTranslocateRenderable). + currentTSphere = (full.find("tsphere_mtl") != std::string::npos); currentHasRamp = false; if (res) { MatInfo info = res->resolve(full); @@ -911,6 +919,7 @@ struct Builder { currentHasRamp = savedHasRamp; currentPunch = savedPunch; currentShadowMat = savedShadowMat; + currentTSphere = savedTSphere; currentHasEmissive = savedHasEmissive; for (int i = 0; i < 3; ++i) currentEmissive[i] = savedEmissive[i]; for (int i = 0; i < 3; ++i) { currentRampLo[i] = savedRampLo[i]; currentRampHi[i] = savedRampHi[i]; } diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index 3571ebe..21fb3ac 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -2207,15 +2207,15 @@ Logical // tree still builds for the replicant/POV wiring are now inert.) //===========================================================================// namespace { - // Timings are the engine's #defines (L4VIDRND.cpp:1765-1772). The authentic - // scales are 100 (collapse start) / 150 (expand end); capped smaller here so - // the sphere -- which is centred on YOUR own reinsertion point -- does not - // envelop the camera and black out the view. Tunable via BT_WARP_SCALE. + // Timings + scales are the engine's #defines verbatim (L4VIDRND.cpp:1763-1766): + // collapse from 100x down to 1x over 1.3s, then expand 1x -> 150x over 1.0s (the + // "blast off into the distance"). The big scales are CORRECT because the sphere + // is centred on your own eye (POV) -- you are meant to be deep inside it as it + // collapses through you and blasts back open. Tunable via BT_WARP_SCALE. const float TLOC_COLLAPSE_TIME = 1.3f; const float TLOC_EXPAND_TIME = 1.0f; - const float TLOC_ROTATE_RATE = 0.5f * 0.01745329222222f; // rad/frame - float gWarpCollapseScale = 30.0f; // authentic 100 - float gWarpExpandScale = 40.0f; // authentic 150 + float gWarpCollapseScale = 100.0f; // COLLAPSE_START_SCALE + float gWarpExpandScale = 150.0f; // EXPAND_END_SCALE d3d_OBJECT *gTLocSphere = 0; int gTLocSphereTried = 0; @@ -2224,7 +2224,7 @@ namespace { int gWarpPhase = 0; // 0 idle, 1 collapse, 2 expand float gWarpT = 0.0f; float gWarpX = 0.0f, gWarpY = 0.0f, gWarpZ = 0.0f; - float gWarpRot = 0.0f; + int gWarpPOV = 0; // 1 = centre on the local eye (own respawn); 0 = world-anchored (peer) } // The renderable objects the entity tree builds for the translocation wiring are @@ -2250,26 +2250,35 @@ BTTranslocationRenderable::~BTTranslocationRenderable() } // -// Kick the warp at a world position (called from the respawn path). Does NOT -// touch player state -- pure render. +// Kick the warp one-shot. Does NOT touch player state -- pure render. +// pov=1 -> centre it on the LOCAL eye (your own respawn): the authentic +// POV-tunnel that wraps your view. +// pov=0 -> anchor it at world (x,y,z): an OBSERVER seeing a peer respawn. // -void - BTStartWarpEffect(float x, float y, float z) +static void + BTKickWarp(int pov, float x, float y, float z) { if (const char *s = getenv("BT_WARP_SCALE")) { float v = (float)atof(s); - if (v > 0.0f) { gWarpCollapseScale = v; gWarpExpandScale = v * 1.33f; } + if (v > 0.0f) { gWarpCollapseScale = v; gWarpExpandScale = v * 1.5f; } } gWarpPhase = 1; // collapse gWarpT = 0.0f; + gWarpPOV = pov; gWarpX = x; gWarpY = y; gWarpZ = z; - gWarpRot = 0.0f; if (getenv("BT_TLOC_LOG")) - DEBUG_STREAM << "[tloc] warp start at (" << x << "," << y << "," << z - << ") scale=" << gWarpCollapseScale << "/" << gWarpExpandScale << "\n" << std::flush; + DEBUG_STREAM << "[tloc] warp start " << (pov ? "POV(eye)" : "world") + << " at (" << x << "," << y << "," << z << ") scale=" + << gWarpCollapseScale << "/" << gWarpExpandScale << "\n" << std::flush; } +// World-anchored warp (observer sees a peer respawn over there). +void BTStartWarpEffect(float x, float y, float z) { BTKickWarp(0, x, y, z); } + +// Eye-centred warp (the local player's OWN respawn -- the authentic POV tunnel). +void BTStartWarpEffectPOV() { BTKickWarp(1, 0.0f, 0.0f, 0.0f); } + // // Play the warp one-shot: tsphere.bgf COLLAPSES onto the respawn point (scale // -> 1 over 1.3s) then EXPANDS to reveal the reborn mech (1 -> max over 1.0s), @@ -2307,8 +2316,7 @@ void return; } - gWarpT += dt; - gWarpRot += TLOC_ROTATE_RATE; + gWarpT += dt; float scale; if (gWarpPhase == 1) // collapse: (max+1) -> 1 @@ -2332,30 +2340,49 @@ void << " at=(" << gWarpX << "," << gWarpY << "," << gWarpZ << ")\n" << std::flush; } - const float s = scale; - const float c = cosf(gWarpRot); - const float sn = sinf(gWarpRot); + // PLACEMENT (authentic POVTranslocateRenderable, L4VIDRND.cpp:1812 "rotated and + // scaled around the VTV"): the sphere is PURE SCALE -- NO geometry spin. The + // engine's myRotateY is dead code; the swirl is entirely the texture SCROLL. + // - POV (your OWN respawn): centre the sphere ON your eye and orient it to the + // view: world = Scale(s) * inverse(view). In view space that puts the sphere + // at the origin, so you sit at its centre looking out through the swirl -- the + // authentic tunnel. (World-fixing it at the mech's feet was the "blob from my + // own perspective / not aligned to my orientation" the user reported.) + // - 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; D3DXMATRIX m; - m._11 = s * c; m._12 = 0.0f; m._13 = -s * sn; m._14 = 0.0f; - m._21 = 0.0f; m._22 = s; m._23 = 0.0f; m._24 = 0.0f; - m._31 = s * sn; m._32 = 0.0f; m._33 = s * c; m._34 = 0.0f; - m._41 = gWarpX; m._42 = gWarpY; m._43 = gWarpZ; m._44 = 1.0f; + if (gWarpPOV && view != 0) + { + D3DXMATRIX invView, scaleM; + D3DXMatrixInverse(&invView, 0, view); + D3DXMatrixScaling(&scaleM, s, s, s); + m = scaleM * invView; // Scale in the eye's local frame, then eye->world + } + else + { + D3DXMatrixScaling(&m, s, s, s); + m._41 = gWarpX; m._42 = gWarpY; m._43 = gWarpZ; + } gTLocSphere->SetLocalToWorld(m); - // Draw the AUTHENTIC translocation-sphere material as a shimmering swirl, not - // a flat blob. Ground truth (content/VIDEO/MAT/DAY/BTFX.VMF): tsphere_mtl = - // TEXTURE{tsphere_scr_tex -> MAP bintA, SCROLL 0.0 0.0 0.1 0.5} EMISSIVE{0.7, - // 0.5,1} RAMP{sky:(0,0,0.6)->(.99,.99,.99)}. The swirl IS the scrolling bintA - // texture; the blue/light-blue/white is the EMISSIVE colour (+ the sky ramp). - // The old code did COLOROP=SELECTARG1/ARG1=TFACTOR, which REPLACES every texel - // with one flat colour -> the "opaque blue blob" the user saw. Instead - // MODULATE the (bound, scrolling) texture by a TFACTOR tint set to the authentic - // EMISSIVE hue, so the swirl survives and reads blue-white. DrawMesh binds - // bintA + animates the scroll (SetTexture/SetTextureScrolling, L4D3D.cpp:1093, - // 1220); its SetTexture only rewrites the stage ops on a texturing on/off - // TRANSITION (cached mLastTexturingState, :1215) -- textured meshes drew first - // so the cache is 'on' and our MODULATE ops stand. We are in the alpha pass + // Draw the AUTHENTIC translocation-sphere material. Ground truth + // (content/VIDEO/MAT/DAY/BTFX.VMF): tsphere_mtl = TEXTURE{tsphere_scr_tex -> MAP + // bintA, SCROLL 0.0 0.0 0.1 0.5} EMISSIVE{0.7,0.5,1} RAMP{sky:(0,0,0.6)->(.99, + // .99,.99)}. The swirl is the scrolling grayscale bintA CLOUD; the blue/white is + // the "sky" RAMP remapping that cloud's luminance (deep-blue for dark texels, + // near-white for bright) -- NOT a flat tint. The port bakes exactly that ramp + // (L4D3D.cpp:480, argb=lerp(lo,hi,lum)) but had GATED it off for normal-bearing + // meshes; tsphere has normals, so it arrived RAW GRAY (why a pale tint washed to + // gray -- MODULATE can only multiply, never add the (0,0,0.6) blue floor the + // ramp does). We now un-gate the ramp for tsphere_mtl (bgfload.cpp), so the + // bound texture is ALREADY the blue-white swirl -- just draw it (MODULATE by a + // WHITE TFACTOR = no tint) and take alpha/intensity from TFACTOR. DrawMesh binds + // the ramped texture + animates the SCROLL (SetTexture/SetTextureScrolling, + // L4D3D.cpp:1093,1220); its SetTexture only rewrites the stage ops on a texturing + // on/off TRANSITION (cached mLastTexturingState, :1215) -- textured meshes drew + // first so the cache is 'on' and our MODULATE ops stand. We are in the alpha pass // (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). @@ -2370,26 +2397,45 @@ void device->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAOp); device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAA1); + // Blend: authentic translucency (SRCALPHA/INVSRCALPHA) by DEFAULT. When the + // sphere expands past the respawning mech's own camera it ENGULFS the eye, and + // with additive blending the near + far sphere walls stack and SATURATE to a + // flat tint -> the "purple blob from my own perspective" the user saw (from a + // distance the sphere is small on-screen so additive stayed subtle and the swirl + // read fine). Translucent blend lets the walls blend instead of accumulate, so + // the scrolling swirl shows through even when the eye is inside the sphere. + // BT_WARP_ADDITIVE=1 restores the additive glow (punchier at a distance). device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); - device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE); // additive glow + { + static int s_additive = -1; + if (s_additive < 0) + { + const char *a = getenv("BT_WARP_ADDITIVE"); + s_additive = (a && a[0] == '1') ? 1 : 0; + } + device->SetRenderState(D3DRS_DESTBLEND, + s_additive ? D3DBLEND_ONE : D3DBLEND_INVSRCALPHA); + } device->SetRenderState(D3DRS_LIGHTING, FALSE); { - // ARGB tint = the authentic EMISSIVE {0.7,0.5,1} -> R=0xB3 G=0x80 B=0xFF, - // full-intensity alpha; BT_WARP_COLOR=AARRGGBB (hex) overrides. + // 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 + // byte matters -- ~70% so the swirl reads AND you see through it. + // BT_WARP_COLOR=AARRGGBB (hex) overrides (e.g. to push the hue or opacity). static DWORD s_warpColor = 0; if (s_warpColor == 0) { const char *wc = getenv("BT_WARP_COLOR"); - s_warpColor = wc ? (DWORD)strtoul(wc, 0, 16) : 0xFFB380FF; - if (s_warpColor == 0) s_warpColor = 0xFFB380FF; + s_warpColor = wc ? (DWORD)strtoul(wc, 0, 16) : 0xB0FFFFFF; + if (s_warpColor == 0) s_warpColor = 0xB0FFFFFF; } device->SetRenderState(D3DRS_TEXTUREFACTOR, s_warpColor); } device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); - device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); // bintA swirl - device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR); // blue tint + device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); // ramped blue-white swirl + device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR); // white (no tint) unless overridden device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); - device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); // uniform intensity + device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); // uniform ~70% translucency gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time); diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 788762c..30a4462 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -1142,13 +1142,14 @@ void Mech *mech = (Mech *)playerVehicle; mech->Reset(message->dropZoneLocation, 1 /* True */); // FUN_0049fb74 (heal+move) - // WARP (task #52): the translocation sphere at our reinsertion. A - // self-contained render one-shot -- it does NOT touch SimulationState - // (that dial drives the camera/POV + targeting; pulsing it regressed all - // of those). The effect plays its own collapse->expand. - const Point3D &p = message->dropZoneLocation.linearPosition; - extern void BTStartWarpEffect(float x, float y, float z); - BTStartWarpEffect((float)p.x, (float)p.y, (float)p.z); + // WARP (task #52): the translocation sphere at our reinsertion. This is OUR + // OWN respawn, so use the POV variant -- the sphere centres on and orients to + // our eye (the authentic VTV-parented POVTranslocateRenderable), collapsing + // through us and blasting back open. A self-contained render one-shot -- it + // does NOT touch SimulationState (that dial drives the camera/POV + targeting; + // pulsing it regressed all of those). + extern void BTStartWarpEffectPOV(); + BTStartWarpEffectPOV(); } Check_Fpu(); }