Warp: exact POVTranslocateRenderable replica (collapse/throb/reveal + world mask)
Stop approximating -- replicate POVTranslocateRenderable::Execute() behaviorally,
byte-for-byte on the constants and transitions (engine T0, L4VIDRND.cpp:1763-2076),
per the user's "replicate the effect exactly". The five prior piecemeal attempts
each fixed one facet and broke another because they left out the WHOLE mechanism.
Full state machine (btl4vid.cpp), driven by two decoupled events (NOT the
SimulationState trigger -- that dial also drives camera/POV+targeting, the f053535
regression; SetIsDead is a separate pure-render flag, verified safe):
- LOCAL DEATH -> InitialCollapse: scale (pct_left*100)+1 over 1.3s (world visible),
then SetIsDead(true) + WaitForReincarnate. (btplayer VehicleDeadMessageHandler,
deathCount==-1, local-guarded.)
- WaitForReincarnate: world BLACK (mask), scale 1, Lissajous throb
(cos(t*3.33), sin(t*2.5)) x2.0 -- the "dances around playfully".
- LOCAL RESPAWN -> ExpandReveal: SetIsDead(false) + scale (pct_used*150)+1 over
1.0s -- blasts open, world revealed ("shoots off into the distance"). (btplayer
DropZoneReplyMessageHandler, local-guarded.)
Draw EXACTLY as the engine (isDeathDraw -> drawAsSky): PASS_SKY, OPAQUE,
CULLMODE=CW, z-test ON (L4VIDEO.cpp:7526/7568-7570/7693). This kills the "glitchy
funnel" -- opaque = one write/pixel (no translucent double-blend), CW = one winding
of the double-sided mesh (no coincident z-fight), z-on = the expand shell is
occluded/revealed by the returning world. Placement = Scale * inverse(view) with
NO recenter (the mesh is authored off-origin so the eye sits inside, authentic);
dropped the wrong mCullCenter recenter and the Z-off overlay hack. Colour is the
"sky" ramp (per prior commit); no geometry spin (myRotateY is dead) -- swirl is the
texture scroll. SetIsDead reached via a BTSetWorldDead bridge (L4VIDRND.cpp).
STUCK-BLACK SAFETY (the one new risk of decoupling mask-on from mask-off): a 12s
WaitForReincarnate render-loop timeout un-masks if no respawn arrives, plus
BTWarpForceUnmask on the mission-ending + no-DropZones abort paths. Smoke-verified
2-node: collapse->wait(masked=1)->expand(masked=0), ends masked=0, no crash.
The peer-observer warp stays a world-anchored translucent alpha draw (port
extension; the authentic effect is POV-only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
062c66999e
commit
a35f321ba1
@@ -7788,9 +7788,10 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
||||
BTDrawPfx(mDevice, &viewTransform, (float)dT);
|
||||
}
|
||||
|
||||
// BT translocation spheres (task #52): the "blue warp" that collapses onto a
|
||||
// dying mech and expands to reveal the reborn one -- one per watched player,
|
||||
// keyed on its SimulationState dial. Same alpha pass as the beams/pfx.
|
||||
// BT translocation spheres (task #52): the authentic POVTranslocateRenderable
|
||||
// warp -- collapse onto the dying eye, throb black, then expand-reveal the reborn
|
||||
// world. Drawn out-of-band here (as SKY, opaque) even while IsDead so the sphere
|
||||
// survives the world mask it itself raises via BTSetWorldDead (below).
|
||||
{
|
||||
extern void BTDrawTranslocationSpheres(LPDIRECT3DDEVICE9 dev,
|
||||
const D3DXMATRIX *view, float dt, Time frame_time);
|
||||
|
||||
@@ -1746,6 +1746,22 @@ void
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Constructor for POVTranslocateRenderable
|
||||
//
|
||||
// BT (task #52): world-mask bridge for the reconstructed translocation warp. The
|
||||
// authentic POVTranslocateRenderable toggles l4_application->SetIsDead around its
|
||||
// collapse/expand (L4VIDRND.cpp:1947/1989) to black out the world during the death
|
||||
// throb and reveal it on rebirth. Our reconstruction drives the warp from
|
||||
// btl4vid.cpp (no L4Application handle there), so expose the same flag through this
|
||||
// bridge. It is a PURE render-pass gate (verified: every IsDead() reader is a draw
|
||||
// loop -- terrain/decal/sky/alpha/particles/reticle; no camera/input/targeting), so
|
||||
// driving it directly is safe -- unlike pulsing the SimulationState trigger, which
|
||||
// also drives the camera/POV + targeting (the f053535 regression).
|
||||
void BTSetWorldDead(int dead)
|
||||
{
|
||||
L4Application *l4_application = Cast_Object(L4Application*, application);
|
||||
if (l4_application != 0)
|
||||
l4_application->SetIsDead(dead != 0);
|
||||
}
|
||||
|
||||
POVTranslocateRenderable::POVTranslocateRenderable(
|
||||
Entity *entity, // Entity to attach the renderable to
|
||||
ExecutionType execution_type, // How/when to execute the renderable
|
||||
|
||||
+235
-154
@@ -2207,26 +2207,32 @@ Logical
|
||||
// tree still builds for the replicant/POV wiring are now inert.)
|
||||
//===========================================================================//
|
||||
namespace {
|
||||
// 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;
|
||||
// The engine #defines VERBATIM (L4VIDRND.cpp:1763-1770): collapse from 100x down
|
||||
// to 1x over 1.3s, throb at 1x, then expand 1x -> 150x over 1.0s. TRANSLATE_LIMIT
|
||||
// is the WaitForReincarnate wobble amplitude. Only these five are live (the
|
||||
// ROTATE_* / TRANSLATE_RATE members are dead -- no geometry rotation).
|
||||
const float TLOC_COLLAPSE_TIME = 1.3f; // COLLAPSE_TIME
|
||||
const float TLOC_EXPAND_TIME = 1.0f; // EXPAND_TIME
|
||||
const float TLOC_TRANSLATE_LIMIT = 2.0f; // wait wobble amplitude
|
||||
float gWarpCollapseScale = 100.0f; // COLLAPSE_START_SCALE
|
||||
float gWarpExpandScale = 150.0f; // EXPAND_END_SCALE
|
||||
|
||||
d3d_OBJECT *gTLocSphere = 0;
|
||||
int gTLocSphereTried = 0;
|
||||
|
||||
// The single active warp one-shot (one local player per node).
|
||||
int gWarpPhase = 0; // 0 idle, 1 collapse, 2 expand
|
||||
float gWarpT = 0.0f;
|
||||
// The single active warp one-shot (one local player per node). Faithful state
|
||||
// machine: 0 Idle, 1 InitialCollapse, 3 WaitForReincarnate (world masked, wobble),
|
||||
// 2 ExpandReveal (L4VIDRND.cpp POVTranslocateRenderable states).
|
||||
int gWarpPhase = 0;
|
||||
float gWarpT = 0.0f; // phase clock (collapse/expand)
|
||||
float gWaitClock = 0.0f; // WaitForReincarnate elapsed (wobble + stuck-black failsafe)
|
||||
float gWarpX = 0.0f, gWarpY = 0.0f, gWarpZ = 0.0f;
|
||||
int gWarpPOV = 0; // 1 = centre on the local eye (own respawn); 0 = world-anchored (peer)
|
||||
int gWarpPOV = 0; // 1 = centre on the local eye (own death/respawn); 0 = world-anchored (peer)
|
||||
int gWarpMasked = 0; // 1 while we have raised the SetIsDead world mask
|
||||
}
|
||||
|
||||
extern void BTSetWorldDead(int dead); // L4VIDRND.cpp bridge -> l4_application->SetIsDead
|
||||
|
||||
// The renderable objects the entity tree builds for the translocation wiring are
|
||||
// inert now (the warp is the self-contained one-shot below); the ctor/dtor just
|
||||
// satisfy MakeEntityRenderables.
|
||||
@@ -2249,45 +2255,76 @@ BTTranslocationRenderable::~BTTranslocationRenderable()
|
||||
{
|
||||
}
|
||||
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
static void
|
||||
BTKickWarp(int pov, float x, float y, float z)
|
||||
static void BTWarpApplyScaleEnv()
|
||||
{
|
||||
if (const char *s = getenv("BT_WARP_SCALE"))
|
||||
{
|
||||
float v = (float)atof(s);
|
||||
if (v > 0.0f) { gWarpCollapseScale = v; gWarpExpandScale = v * 1.5f; }
|
||||
}
|
||||
// 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;
|
||||
if (getenv("BT_TLOC_LOG"))
|
||||
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); }
|
||||
//
|
||||
// LOCAL DEATH -> InitialCollapse (engine: trigger becomes == control state, Idle ->
|
||||
// InitialCollapse, L4VIDRND.cpp:1900-1911). The sphere collapses 100x -> 1x onto
|
||||
// your own eye over 1.3s (world still visible), then raises the SetIsDead world mask
|
||||
// and THROBS (WaitForReincarnate) until the respawn kicks the expand. POV only.
|
||||
//
|
||||
void BTStartWarpCollapsePOV()
|
||||
{
|
||||
BTWarpApplyScaleEnv();
|
||||
gWarpPhase = 1; // InitialCollapse
|
||||
gWarpT = 0.0f;
|
||||
gWaitClock = 0.0f;
|
||||
gWarpPOV = 1;
|
||||
gWarpX = gWarpY = gWarpZ = 0.0f;
|
||||
if (getenv("BT_TLOC_LOG"))
|
||||
DEBUG_STREAM << "[tloc] warp COLLAPSE (POV) start\n" << std::flush;
|
||||
}
|
||||
|
||||
// Eye-centred warp (the local player's OWN respawn -- the authentic POV tunnel).
|
||||
void BTStartWarpEffectPOV() { BTKickWarp(1, 0.0f, 0.0f, 0.0f); }
|
||||
//
|
||||
// LOCAL RESPAWN -> ExpandReveal (engine: trigger becomes != control state,
|
||||
// WaitForReincarnate -> ExpandReveal + SetIsDead(false), L4VIDRND.cpp:1987-1989).
|
||||
// Drops the world mask and blasts the sphere 1x -> 150x, revealing the reborn world.
|
||||
//
|
||||
void BTStartWarpExpandPOV()
|
||||
{
|
||||
BTWarpApplyScaleEnv();
|
||||
if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } // == SetIsDead(false) :1989
|
||||
gWarpPhase = 2; // ExpandReveal
|
||||
gWarpT = 0.0f;
|
||||
gWarpPOV = 1;
|
||||
gWarpX = gWarpY = gWarpZ = 0.0f;
|
||||
if (getenv("BT_TLOC_LOG"))
|
||||
DEBUG_STREAM << "[tloc] warp EXPAND (POV) start\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// World-anchored warp (a PORT EXTENSION -- the authentic effect is POV only): an
|
||||
// OBSERVER seeing a peer respawn over THERE. No world mask (the observer is alive);
|
||||
// expand-reveal at the peer's world point.
|
||||
//
|
||||
void BTStartWarpEffect(float x, float y, float z)
|
||||
{
|
||||
BTWarpApplyScaleEnv();
|
||||
gWarpPhase = 2; // ExpandReveal
|
||||
gWarpT = 0.0f;
|
||||
gWarpPOV = 0;
|
||||
gWarpX = x; gWarpY = y; gWarpZ = z;
|
||||
if (getenv("BT_TLOC_LOG"))
|
||||
DEBUG_STREAM << "[tloc] warp EXPAND (world) at (" << x << "," << y << "," << z << ")\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// FAILSAFE: drop the world mask + end the effect. MUST be called on every path
|
||||
// where a collapse fired but no respawn/expand can follow (mission end, out of
|
||||
// lives, dropped DropZoneReply) -- otherwise the SetIsDead world stays BLACK forever.
|
||||
//
|
||||
void BTWarpForceUnmask()
|
||||
{
|
||||
if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; }
|
||||
if (gWarpPhase == 3) gWarpPhase = 0;
|
||||
}
|
||||
|
||||
//
|
||||
// Play the warp one-shot: tsphere.bgf COLLAPSES onto the respawn point (scale
|
||||
@@ -2310,9 +2347,6 @@ void
|
||||
for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op)
|
||||
{
|
||||
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
|
||||
@@ -2325,6 +2359,8 @@ void
|
||||
dop->texture.scrollVDelta = 0.5f;
|
||||
dop->texture.wrap_u = L4TEXOP::REPEAT;
|
||||
dop->texture.wrap_v = L4TEXOP::REPEAT;
|
||||
// The pass routing (drawAsSky for POV / alphaTest for the peer overlay)
|
||||
// is set per-frame below, since it differs by mode.
|
||||
}
|
||||
}
|
||||
if (getenv("BT_TLOC_LOG"))
|
||||
@@ -2344,20 +2380,47 @@ void
|
||||
return;
|
||||
}
|
||||
|
||||
gWarpT += dt;
|
||||
|
||||
float scale;
|
||||
if (gWarpPhase == 1) // collapse: (max+1) -> 1
|
||||
// ===== The POVTranslocateRenderable::Execute() state machine (L4VIDRND.cpp) =====
|
||||
float scale = 1.0f;
|
||||
if (gWarpPhase == 1) // InitialCollapse (:1928): 101 -> 1
|
||||
{
|
||||
gWarpT += dt;
|
||||
float left = 1.0f - (gWarpT / TLOC_COLLAPSE_TIME);
|
||||
if (left <= 0.0f) { gWarpPhase = 2; gWarpT = 0.0f; scale = 1.0f; }
|
||||
else scale = left * gWarpCollapseScale + 1.0f;
|
||||
if (left <= 0.0f) // collapse finished (:1935)
|
||||
{
|
||||
scale = 1.0f;
|
||||
gWarpPhase = 3; // -> WaitForReincarnate (:1946)
|
||||
gWaitClock = 0.0f; // rebaseline for the wobble (:1945)
|
||||
if (gWarpPOV) { BTSetWorldDead(1); gWarpMasked = 1; } // SetIsDead(true) (:1947)
|
||||
}
|
||||
else
|
||||
scale = left * gWarpCollapseScale + 1.0f; // (pct_left*100)+1
|
||||
}
|
||||
else // expand: 1 -> (max+1)
|
||||
else if (gWarpPhase == 3) // WaitForReincarnate (:1978): throb black
|
||||
{
|
||||
gWaitClock += dt;
|
||||
scale = 1.0f;
|
||||
// FAILSAFE: the respawn kicks Wait->Expand (BTStartWarpExpandPOV). If it never
|
||||
// arrives (mission end / out of lives / dropped DropZoneReply) the SetIsDead
|
||||
// world would stay BLACK forever -- so time out and un-mask after 12s.
|
||||
if (gWaitClock > 12.0f)
|
||||
{
|
||||
if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; }
|
||||
gWarpPhase = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else // ExpandReveal (:2013): 1 -> 151
|
||||
{
|
||||
gWarpT += dt;
|
||||
float used = gWarpT / TLOC_EXPAND_TIME;
|
||||
if (used >= 1.0f) { gWarpPhase = 0; return; } // done
|
||||
scale = used * gWarpExpandScale + 1.0f;
|
||||
if (used >= 1.0f) // reveal done (:2030)
|
||||
{
|
||||
if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } // safety (mask should already be off)
|
||||
gWarpPhase = 0;
|
||||
return;
|
||||
}
|
||||
scale = used * gWarpExpandScale + 1.0f; // (pct_used*150)+1
|
||||
}
|
||||
|
||||
if (getenv("BT_TLOC_LOG"))
|
||||
@@ -2365,7 +2428,7 @@ void
|
||||
static int s_lt = 0;
|
||||
if ((++s_lt % 15) == 1)
|
||||
DEBUG_STREAM << "[tloc] warp phase=" << gWarpPhase << " scale=" << scale
|
||||
<< " at=(" << gWarpX << "," << gWarpY << "," << gWarpZ << ")\n" << std::flush;
|
||||
<< " pov=" << gWarpPOV << " masked=" << gWarpMasked << "\n" << std::flush;
|
||||
}
|
||||
|
||||
// PLACEMENT (authentic POVTranslocateRenderable, L4VIDRND.cpp:1812 "rotated and
|
||||
@@ -2379,124 +2442,142 @@ 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;
|
||||
// PLACEMENT -- the authentic VTV/eye parenting (L4VIDRND.cpp:2069-2074): the mesh
|
||||
// LOCAL ORIGIN is the eye, so world = localToWorld * inverse(view) with NO recenter.
|
||||
// tsphere is authored OFF-origin ON PURPOSE (centre +8.25y) so the eye sits ~0.38r
|
||||
// low INSIDE the sphere; recentring it to dead-centre (my earlier attempt) was WRONG
|
||||
// and only worsened the funnel. In WaitForReincarnate the localToWorld is a pure
|
||||
// Lissajous TRANSLATION (the throb, :1996-2003); otherwise a pure SCALE (no spin --
|
||||
// myRotateY is dead; the swirl is the texture scroll).
|
||||
D3DXMATRIX local;
|
||||
if (gWarpPhase == 3) // WaitForReincarnate throb
|
||||
D3DXMatrixTranslation(&local,
|
||||
(float)(cos(gWaitClock * 3.33) * TLOC_TRANSLATE_LIMIT),
|
||||
(float)(sin(gWaitClock * 2.5) * TLOC_TRANSLATE_LIMIT), 0.0f);
|
||||
else
|
||||
D3DXMatrixScaling(&local, s, s, s);
|
||||
|
||||
D3DXMATRIX world;
|
||||
if (gWarpPOV && view != 0)
|
||||
{
|
||||
D3DXMATRIX invView;
|
||||
D3DXMatrixInverse(&invView, 0, view);
|
||||
m = recenter * scaleM * invView; // centre mesh, scale, then eye->world
|
||||
world = local * invView; // scale/throb about the eye, then eye->world
|
||||
}
|
||||
else
|
||||
{
|
||||
D3DXMATRIX anchorM;
|
||||
D3DXMatrixTranslation(&anchorM, gWarpX, gWarpY, gWarpZ);
|
||||
m = recenter * scaleM * anchorM; // centre mesh, scale, move to anchor
|
||||
world = local * anchorM; // peer overlay: scale about the world anchor
|
||||
}
|
||||
gTLocSphere->SetLocalToWorld(world);
|
||||
|
||||
// The swirl COLOUR is the material's "sky" ramp remapping the grayscale bintA
|
||||
// cloud per-texel (un-gated for tsphere_mtl in bgfload.cpp; L4D3D.cpp:480), so the
|
||||
// bound texture is ALREADY blue-white -- MODULATE by WHITE = the ramp unchanged.
|
||||
static DWORD s_warpColor = 0;
|
||||
if (s_warpColor == 0)
|
||||
{
|
||||
const char *wc = getenv("BT_WARP_COLOR");
|
||||
s_warpColor = wc ? (DWORD)strtoul(wc, 0, 16) : 0xFFFFFFFF;
|
||||
if (s_warpColor == 0) s_warpColor = 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
gTLocSphere->SetLocalToWorld(m);
|
||||
|
||||
// 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).
|
||||
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);
|
||||
device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTFactor);
|
||||
device->GetTextureStageState(0, D3DTSS_COLOROP, &sCOp);
|
||||
device->GetTextureStageState(0, D3DTSS_COLORARG1, &sCA1);
|
||||
device->GetTextureStageState(0, D3DTSS_COLORARG2, &sCA2);
|
||||
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);
|
||||
if (gWarpPOV)
|
||||
{
|
||||
static int s_additive = -1;
|
||||
if (s_additive < 0)
|
||||
// ===== POV: draw as SKY, exactly like the engine (isDeathDraw -> drawAsSky,
|
||||
// L4VIDRND.cpp:1802-1808). OPAQUE + backface-cull (CW) + z-test ON is what makes
|
||||
// an inside-viewed dome render CLEAN: no translucent double-blend, no coincident
|
||||
// double-winding z-fight (that WAS the "glitchy funnel"), and the huge expand
|
||||
// shell is occluded by the world coming back on = the reveal. The world itself
|
||||
// is blacked by the SetIsDead mask (raised at collapse-end), so during the throb
|
||||
// you see ONLY the swirling dome. Sky-pass states: L4VIDEO.cpp:7526 (cull CW),
|
||||
// 7568-7570 (zwrite on / blend off), 7693 (light off). Leave ZENABLE default. =====
|
||||
for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op)
|
||||
{
|
||||
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);
|
||||
// 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
|
||||
// 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) : 0xB0FFFFFF;
|
||||
if (s_warpColor == 0) s_warpColor = 0xB0FFFFFF;
|
||||
L4DRAWOP *dop = gTLocSphere->GetDrawOp(op);
|
||||
dop->drawAsSky = true; dop->alphaTest = false; dop->drawAsDecal = false;
|
||||
}
|
||||
DWORD sZW, sAB, sCull, sLight, sTF, sCOp, sCA1, sCA2, sAOp, sAA1;
|
||||
device->GetRenderState(D3DRS_ZWRITEENABLE, &sZW);
|
||||
device->GetRenderState(D3DRS_ALPHABLENDENABLE, &sAB);
|
||||
device->GetRenderState(D3DRS_CULLMODE, &sCull);
|
||||
device->GetRenderState(D3DRS_LIGHTING, &sLight);
|
||||
device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTF);
|
||||
device->GetTextureStageState(0, D3DTSS_COLOROP, &sCOp);
|
||||
device->GetTextureStageState(0, D3DTSS_COLORARG1, &sCA1);
|
||||
device->GetTextureStageState(0, D3DTSS_COLORARG2, &sCA2);
|
||||
device->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAOp);
|
||||
device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAA1);
|
||||
|
||||
device->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);
|
||||
device->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); // OPAQUE -> single write/pixel
|
||||
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); // one winding -> no z-fight
|
||||
device->SetRenderState(D3DRS_LIGHTING, FALSE);
|
||||
device->SetRenderState(D3DRS_TEXTUREFACTOR, s_warpColor);
|
||||
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); // ramped blue-white swirl
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR);
|
||||
|
||||
gTLocSphere->Draw(PASS_SKY, view, frame_time);
|
||||
|
||||
device->SetRenderState(D3DRS_ZWRITEENABLE, sZW);
|
||||
device->SetRenderState(D3DRS_ALPHABLENDENABLE, sAB);
|
||||
device->SetRenderState(D3DRS_CULLMODE, sCull);
|
||||
device->SetRenderState(D3DRS_LIGHTING, sLight);
|
||||
device->SetRenderState(D3DRS_TEXTUREFACTOR, sTF);
|
||||
device->SetTextureStageState(0, D3DTSS_COLOROP, sCOp);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG1, sCA1);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG2, sCA2);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAOP, sAOp);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAA1);
|
||||
}
|
||||
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
||||
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 ~70% translucency
|
||||
else
|
||||
{
|
||||
// ===== Peer overlay (port extension): the observer is ALIVE (no world mask), so
|
||||
// a sky-drawn sphere would be occluded by the world -> invisible. Draw it in the
|
||||
// alpha pass, Z-tested + translucent, so the swirl reads at the peer's spot. =====
|
||||
for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op)
|
||||
{
|
||||
L4DRAWOP *dop = gTLocSphere->GetDrawOp(op);
|
||||
dop->alphaTest = true; dop->drawAsSky = false; dop->drawAsDecal = false;
|
||||
}
|
||||
const DWORD peerColor = (s_warpColor == 0xFFFFFFFF) ? 0xB0FFFFFF : s_warpColor; // ~70% alpha
|
||||
DWORD sSrc, sDst, sLight, sTF, sCOp, sCA1, sCA2, sAOp, sAA1;
|
||||
device->GetRenderState(D3DRS_SRCBLEND, &sSrc);
|
||||
device->GetRenderState(D3DRS_DESTBLEND, &sDst);
|
||||
device->GetRenderState(D3DRS_LIGHTING, &sLight);
|
||||
device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTF);
|
||||
device->GetTextureStageState(0, D3DTSS_COLOROP, &sCOp);
|
||||
device->GetTextureStageState(0, D3DTSS_COLORARG1, &sCA1);
|
||||
device->GetTextureStageState(0, D3DTSS_COLORARG2, &sCA2);
|
||||
device->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAOp);
|
||||
device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAA1);
|
||||
|
||||
gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time);
|
||||
device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
|
||||
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
|
||||
device->SetRenderState(D3DRS_LIGHTING, FALSE);
|
||||
device->SetRenderState(D3DRS_TEXTUREFACTOR, peerColor);
|
||||
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR);
|
||||
|
||||
device->SetRenderState(D3DRS_ZENABLE, sZ);
|
||||
device->SetRenderState(D3DRS_SRCBLEND, sSrc);
|
||||
device->SetRenderState(D3DRS_DESTBLEND, sDst);
|
||||
device->SetRenderState(D3DRS_LIGHTING, sLight);
|
||||
device->SetRenderState(D3DRS_TEXTUREFACTOR, sTFactor);
|
||||
device->SetTextureStageState(0, D3DTSS_COLOROP, sCOp);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG1, sCA1);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG2, sCA2);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAOP, sAOp);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAA1);
|
||||
gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time);
|
||||
|
||||
device->SetRenderState(D3DRS_SRCBLEND, sSrc);
|
||||
device->SetRenderState(D3DRS_DESTBLEND, sDst);
|
||||
device->SetRenderState(D3DRS_LIGHTING, sLight);
|
||||
device->SetRenderState(D3DRS_TEXTUREFACTOR, sTF);
|
||||
device->SetTextureStageState(0, D3DTSS_COLOROP, sCOp);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG1, sCA1);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG2, sCA2);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAOP, sAOp);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAA1);
|
||||
}
|
||||
}
|
||||
|
||||
//===========================================================================//
|
||||
|
||||
@@ -314,6 +314,10 @@ void
|
||||
//
|
||||
if (GetSimulationState() == MissionEndingState)
|
||||
{
|
||||
// If our death raised the warp world-mask but the mission is tearing down
|
||||
// before we respawn, drop it now so the world can never stay black.
|
||||
if (this == (BTPlayer *)application->GetMissionPlayer())
|
||||
{ extern void BTWarpForceUnmask(); BTWarpForceUnmask(); }
|
||||
Check_Fpu();
|
||||
return;
|
||||
}
|
||||
@@ -357,6 +361,10 @@ void
|
||||
if (entity_manager == 0
|
||||
|| entity_manager->FindGroup("DropZones") == 0)
|
||||
{
|
||||
// No respawn possible -> the expand will never fire -> drop the warp
|
||||
// world-mask now so we don't stay black waiting for a reincarnation.
|
||||
if (this == (BTPlayer *)application->GetMissionPlayer())
|
||||
{ extern void BTWarpForceUnmask(); BTWarpForceUnmask(); }
|
||||
if (getenv("BT_SCORE_LOG"))
|
||||
DEBUG_STREAM << "[score] VehicleDead: no DropZones group -- "
|
||||
"respawn unavailable in this mission\n" << std::flush;
|
||||
@@ -386,6 +394,16 @@ void
|
||||
}
|
||||
deathPending = 1; // this+0x290
|
||||
|
||||
// WARP (task #52): our OWN death = the engine's Idle -> InitialCollapse (trigger
|
||||
// becomes == control state, POVTranslocateRenderable). Collapse the eye-centred
|
||||
// sphere 100x -> 1x, then it raises the SetIsDead world mask and throbs until the
|
||||
// respawn (DropZoneReply) releases the expand-reveal. LOCAL player only (POV).
|
||||
if (this == (BTPlayer *)application->GetMissionPlayer())
|
||||
{
|
||||
extern void BTStartWarpCollapsePOV();
|
||||
BTStartWarpCollapsePOV();
|
||||
}
|
||||
|
||||
if (getenv("BT_SCORE_LOG"))
|
||||
DEBUG_STREAM << "[score] VehicleDead(-1): death #" << (deathCount + 1)
|
||||
<< ", respawn hunt in 5s\n" << std::flush;
|
||||
@@ -1142,14 +1160,17 @@ void
|
||||
Mech *mech = (Mech *)playerVehicle;
|
||||
mech->Reset(message->dropZoneLocation, 1 /* True */); // FUN_0049fb74 (heal+move)
|
||||
|
||||
// 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();
|
||||
// WARP (task #52): our OWN respawn = the engine's WaitForReincarnate -> Expand
|
||||
// release (POVTranslocateRenderable): drop the SetIsDead world mask and blast
|
||||
// the eye-centred sphere open to reveal the reborn world. LOCAL player only (a
|
||||
// POV effect); a self-contained render one-shot that does NOT touch
|
||||
// SimulationState (that dial drives the camera/POV + targeting -- pulsing it
|
||||
// regressed all of those; SetIsDead is a separate pure-render flag).
|
||||
if (this == (BTPlayer *)application->GetMissionPlayer())
|
||||
{
|
||||
extern void BTStartWarpExpandPOV();
|
||||
BTStartWarpExpandPOV();
|
||||
}
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user