Warp: faithful POVTranslocateRenderable -- eye-centered, ramp color, no spin

Deep-dived the authentic engine effect (L4VIDRND.cpp POVTranslocateRenderable, T0)
and reworked our one-shot to match it, fixing the user's "blob from my own POV /
not aligned to my orientation" + "gray, not blue" reports:

1. PLACEMENT was the misalignment. The engine parents the sphere to the VTV/eye
   ("rotated and scaled around the VTV", L4VIDRND.cpp:1812) -- it is centered on and
   oriented to the viewpoint. We world-fixed it at the mech's feet, so from your own
   camera it sat off-axis and, once expanded, engulfed the eye as a shapeless blob.
   Now: the own-respawn warp (BTStartWarpEffectPOV, from btplayer) uses
   world = Scale(s) * inverse(view) -> centered on + carried by your eye (the tunnel
   wraps you). Observing a PEER (BTStartWarpEffect from mechdmg) stays world-anchored
   at the peer's point.

2. GEOMETRY SPIN was inauthentic. myRotateY/myRotateYSpeed are set in the engine
   ctor but NEVER applied -- the swirl's rotation is entirely the material's texture
   SCROLL (bintA, SCROLL 0.0 0.0 0.1 0.5). Dropped our world-Y geometry spin.

3. COLOR (gray) fixed at the source. The blue/white is the "sky" RAMP remapping the
   grayscale bintA cloud per-texel (argb=lerp(0,0,0.6 -> .99,.99,.99, luminance),
   L4D3D.cpp:480). The port bakes exactly this but GATED it off for normal-bearing
   meshes; tsphere has normals so it arrived raw gray, and a MODULATE tint can't add
   the blue floor (it only multiplies) -> the wash-out. Un-gate the ramp for
   tsphere_mtl (bgfload.cpp) so the bound texture is already the blue-white swirl;
   draw it MODULATE by white (no tint), alpha from TFACTOR.

4. SCALE to authentic 100 (collapse) / 150 (expand end) -- correct now that it's
   eye-centered (you're meant to be inside it as it collapses through you and blasts
   back open, the "blast off into the distance").

Still deferred (higher risk / SimulationState-coupling trap): the WaitForReincarnate
Lissajous wobble + the SetIsDead world-mask (black-out during the dead hold). Noted
for follow-up; the placement/color/motion are the parts the user flagged.

Smoke-verified 2-node: own respawn logs POV(eye), observer logs world-anchored, no
crash. BT_WARP_ADDITIVE / BT_WARP_COLOR / BT_WARP_SCALE remain as live tuning knobs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-10 08:42:02 -05:00
co-authored by Claude Opus 4.8
parent 160b78e38d
commit a9d1534292
3 changed files with 110 additions and 54 deletions
+10 -1
View File
@@ -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]; }
+92 -46
View File
@@ -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);
+8 -7
View File
@@ -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();
}