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:
co-authored by
Claude Opus 4.8
parent
160b78e38d
commit
a9d1534292
@@ -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]; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user