From b75bb4a04c0bb61b0b208db25f066bb16b4d9d4d Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Wed, 5 Aug 2026 22:21:32 -0500 Subject: [PATCH] Searchlight beam: the btfx brighten material class + BT_SPOT_SELF rig SPOT.BGF decoded: a 7-vert cone from the mount, ~50u forward and ~35deg DOWN (a ground-pool lamp, not an air beam), verts tinted cyan-white, material class 'brighten' smuggling its additive factor in DIFFUSE.r (0.25) with a warm emissive on the night page. The loader had never met the class -- it drew as an opaque dark-red blob. Now: brightenFactor parsed (name-gated to brighten*), batch -> L4DRAWOP.brightenAlpha, drawn in the blend pass as an additive veil (dest += vertexRGB x factor), unlit. [T3] tint compose (vertex cyan vs night emissive warm) noted in the draw branch -- field eyeball accepted the current look. BT_SPOT_SELF=1 (bench-only): builds the cone on the own-cockpit tree so the BT_CAM=face view can inspect it solo. KB: view toggle is BACKTICK ('V' is the rear-view hold since #68 -- the toggle skips bound keys); stale V-toggle claims swept. Co-Authored-By: Claude Fable 5 --- context/combat-damage.md | 2 +- context/decomp-reference.md | 2 +- engine/MUNGA_L4/L4D3D.cpp | 35 +++++++++++++++++++++++++++++++--- engine/MUNGA_L4/bgfload.cpp | 19 +++++++++++++++++- engine/MUNGA_L4/bgfload.h | 6 ++++++ engine/MUNGA_L4/l4d3d.h | 5 +++++ game/reconstructed/btl4vid.cpp | 7 ++++++- 7 files changed, 69 insertions(+), 7 deletions(-) diff --git a/context/combat-damage.md b/context/combat-damage.md index 2bf1278..cefec08 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -278,7 +278,7 @@ no-arc). The old hardwired lock, the ±30°-default cone, and the projectile pat fallback are all REMOVED. LMB fires lasers / RMB missiles (with SPACE/CTRL). ⚠ A view-selection bug was fixed en route: every renderable rebuild stomped `mCamera` back to the chase eye (btl4vid `mViewInside` now persists -the chosen view — the aim camera feed and the V toggle both depend on it). +the chosen view — the aim camera feed and the view toggle (backtick since task #68 took 'V' for rear-view) both depend on it). **WORLD STRUCTURES ARE TARGETABLE via the STATIC COLLISION TREE (task #50, 2026-07-15) [T2].** The `BTGroundRayHit` "terrain" tier only samples the VISUAL heightfield, which on arena1 is a diff --git a/context/decomp-reference.md b/context/decomp-reference.md index e0cedda..468f577 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -553,7 +553,7 @@ default-ON (`'0'` disables). | `BT_AIM="x y"` | pin the reticle crosshair (reticle coords) — headless aim harness | | `BT_AIM_LOG` | `[pick]` ray/box/hit diagnostics (Mech::PickRayHit) | | `BT_FIRE_ARC=` | OPT-IN external-camera fire-arc clamp (unset = authentic no-arc) | -| `BT_START_INSIDE` | begin in the cockpit view (V toggles) | +| `BT_START_INSIDE` | begin in the cockpit view (BACKTICK toggles; 'V' = the pod rear-view hold since #68 — the toggle skips bound keys, L4PADRIO.cpp:681) | | `BT_FORCE_MODEL=` | force the player mech (`madcat`/`owens`/…; btl4mssn.cpp — per-mech cockpit bring-up) | | `BT_HIDE_COCKPIT` | hide the `*_cop` canopy shell (SHOWS by default — see [[cockpit-view]]) | | `BT_COP_*` / `BT_EYE_FWD=` | cockpit canopy + eye diagnostics (`FRAME` (unlit frame colour override, default 0.13,0.12,0.15) / `SINGLE` (single-sided diag; double-sided is default) / `PLATES=1` (disable the punch stencil-cut kit) / `FLIP` / `DEBUG` / `DUMP`; eye forward-push probe) — [[cockpit-view]] | diff --git a/engine/MUNGA_L4/L4D3D.cpp b/engine/MUNGA_L4/L4D3D.cpp index 42a7faf..e691ab0 100644 --- a/engine/MUNGA_L4/L4D3D.cpp +++ b/engine/MUNGA_L4/L4D3D.cpp @@ -443,8 +443,13 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName) // drawAsSky) and vanishes -- DECLOUDS carries vertex alpha but must // stay in the sky pass. const bool vtxAlphaOp = data.batches[i].vertexAlpha && !isSkyObj; - object->mDrawOps[i].alphaTest = (object->mIsShadow != 0) || vtxAlphaOp; + // BTFX brighten veil (spot cone): route to the blend pass like the + // vertex-alpha cards; the draw-state branch in DrawMesh adds + // dest += emissive x factor instead of SRCALPHA/INVSRCALPHA. + const bool brightenOp = data.batches[i].brightenFactor > 0.0f && !isSkyObj; + object->mDrawOps[i].alphaTest = (object->mIsShadow != 0) || vtxAlphaOp || brightenOp; object->mDrawOps[i].vertexAlphaBlend = vtxAlphaOp; + object->mDrawOps[i].brightenAlpha = brightenOp ? data.batches[i].brightenFactor : 0.0f; object->mDrawOps[i].drawAsDecal = false; object->mDrawOps[i].drawAsSky = isSkyObj; // sky dome -> PASS_SKY (fullbright) // PUNCH (dpl_Punchize; BT_PUNCH=0 disables): cutout batch -- black texels @@ -1279,7 +1284,9 @@ void d3d_OBJECT::DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time target // (SRCALPHA/INVSRCALPHA). Runs only in the blend pass (the op filter // above routed it there; ALPHABLEND is on and z-write off pass-wide). DWORD sVLight = 0, sVSrc = 0, sVDst = 0, sVCop = 0, sVCa1 = 0, sVCa2 = 0, sVAop = 0, sVAa2 = 0; - const bool vtxAlphaCard = (drawOp->vertexAlphaBlend && pass == PASS_ALPHABLEND); + const bool brightenCard = (drawOp->brightenAlpha > 0.0f && pass == PASS_ALPHABLEND); + const bool vtxAlphaCard = + ((drawOp->vertexAlphaBlend && pass == PASS_ALPHABLEND) || brightenCard); if (vtxAlphaCard) { mDevice->GetRenderState(D3DRS_LIGHTING, &sVLight); @@ -1292,13 +1299,35 @@ void d3d_OBJECT::DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time target mDevice->GetTextureStageState(0, D3DTSS_ALPHAARG2, &sVAa2); mDevice->SetRenderState(D3DRS_LIGHTING, FALSE); mDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); - mDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); + mDevice->SetRenderState(D3DRS_DESTBLEND, + brightenCard ? D3DBLEND_ONE : D3DBLEND_INVSRCALPHA); + if (brightenCard) + { + // BTFX brighten veil: dest += vertexRGB x factor. SPOT.BGF's + // verts author the beam tint (cyan-white 0.12/0.98/1.0, alpha + // 1.0 -- no fade gradient); the factor rides TFACTOR alpha. + // [T3 tint compose]: the night material ALSO authors emissive + // (0.9,0.4,0.2 warm) -- whether the DIV board tinted the veil + // by vertex, emissive, or their product is unconfirmed; vertex + // (a white-blue searchlight) matches the authored geometry + // gradient model. Flip here if era-look evidence says warm. + mDevice->SetRenderState(D3DRS_TEXTUREFACTOR, + D3DCOLOR_COLORVALUE(1.0f, 1.0f, 1.0f, drawOp->brightenAlpha)); + mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); + mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); + mDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR); + mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG2); + mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TFACTOR); + } + else + { mDevice->SetTextureStageState(0, D3DTSS_COLOROP, drawOp->texture.texture != NULL ? D3DTOP_MODULATE : D3DTOP_SELECTARG2); mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); mDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG2); mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE); + } } const bool copCut = (drawOp->copRole == 2 && iOp > 0 diff --git a/engine/MUNGA_L4/bgfload.cpp b/engine/MUNGA_L4/bgfload.cpp index 0abe5c9..f8a55da 100644 --- a/engine/MUNGA_L4/bgfload.cpp +++ b/engine/MUNGA_L4/bgfload.cpp @@ -350,6 +350,12 @@ struct MatInfo { // EMISSIVE (tag 0x26): pure-emissive materials (diffuse black) render as an // unlit glow -- tex x emissive (btpolar:pintBIceEmit_mtl, the polar ice). bool hasEmissive = false; + // BTFX "brighten" class (SPOT.BGF searchlight beam cone): the material + // smuggles its ADDITIVE factor in DIFFUSE.r (brighten.25 authors diffuse + // {0.25,0,0}) and its glow colour in EMISSIVE ({0.9,0.4,0.2} on the night + // page). 0 = not a brighten material; >0 = draw as an additive veil, + // dest += emissive x factor (the 1995 DIV brighten semantics). + float brightenFactor = 0.0f; float emissive[3] = {0,0,0}; }; @@ -495,12 +501,14 @@ struct MaterialResolver { std::string name, texName, rampName; MatInfo info; bool haveColor = false; + float rawDiffuseR = -1.0f; // brighten-class factor carrier for (const Chunk& ch : c.children) { if (ch.id == TAG_NAME) name = chunkStr(ch); else if (ch.id == TAG_MATERIAL_TEXTURE && ch.len > 1) texName = chunkStr(ch, 1); else if (ch.id == TAG_RAMP_REF) rampName = chunkStr(ch); else if (ch.id == TAG_DIFFUSE && ch.len >= 12) { info.color = packColor(rdF32(ch.data), rdF32(ch.data + 4), rdF32(ch.data + 8)); + rawDiffuseR = rdF32(ch.data); haveColor = true; info.hasDiffuse = true; } else if (ch.id == TAG_AMBIENT && ch.len >= 12 && !haveColor @@ -520,6 +528,12 @@ struct MaterialResolver { info.hasEmissive = (info.emissive[0] + info.emissive[1] + info.emissive[2]) > 0.001f; } } + // BTFX brighten class: name-gated so ordinary materials keep + // their diffuse semantics untouched (the factor rides only in + // "brighten*" records -- brighten.25/.5/.75 in btfx.bmf). + if (name.compare(0, 8, "brighten") == 0 && rawDiffuseR > 0.0f) + info.brightenFactor = rawDiffuseR; + // Resolve the ramp reference to its low/high colours: this // file's own definitions first, then the cross-library // registry (mech skins reference 'softer' defined elsewhere). @@ -645,6 +659,7 @@ struct Builder { int currentCopRole = 0; // role of the pmesh being built (consumed by buildPmesh) bool currentHasEmissive = false; float currentEmissive[3] = {0,0,0}; + float currentBrighten = 0.0f; // btfx brighten-class additive factor // per-vertex scratch (normals accumulated from forward triangles only) std::vector px, py, pz, nx, ny, nz, uu, vv; @@ -926,6 +941,7 @@ struct Builder { batch.shadowMat = currentShadowMat; batch.hasEmissive = currentHasEmissive; for (int i = 0; i < 3; ++i) batch.emissive[i] = currentEmissive[i]; + batch.brightenFactor = currentBrighten; // btfx additive veil (spot cone) // SUBMISSION-ORDER DEPTH BIAS (additive objects): the content layers // EXACTLY-COPLANAR shells (AR02 LOD2: solid concrete + a coplanar // PUNCH-cutout overlay + an inner shell, plane separations @@ -1251,8 +1267,9 @@ struct Builder { for (int i = 0; i < 3; ++i) { currentRampLo[i] = info.rampLo[i]; currentRampHi[i] = info.rampHi[i]; } currentHasEmissive = info.hasEmissive; for (int i = 0; i < 3; ++i) currentEmissive[i] = info.emissive[i]; + currentBrighten = info.brightenFactor; } - else { currentColor = colorForMaterial(full); currentTex.clear(); currentHasDiffuse = false; currentTexChannel = 0; currentTexScroll = false; currentTexScrollU = currentTexScrollV = 0.0f; } + else { currentColor = colorForMaterial(full); currentTex.clear(); currentHasDiffuse = false; currentTexChannel = 0; currentTexScroll = false; currentTexScrollU = currentTexScrollV = 0.0f; currentBrighten = 0.0f; } // TRANSLOCATION WARP: override the material's coarse "sky" ramp with the // NARROW LAVENDER ramp that matched the original (capture.png). The swirl // is the bintA cloud coloured lo->hi by luminance; a wide sky ramp diff --git a/engine/MUNGA_L4/bgfload.h b/engine/MUNGA_L4/bgfload.h index a06b275..c7a7dda 100644 --- a/engine/MUNGA_L4/bgfload.h +++ b/engine/MUNGA_L4/bgfload.h @@ -44,6 +44,12 @@ struct BgfDrawBatch { // alpha). The verts keep their authored RGBA gradient and the draw routes // to the alpha-blend pass, unlit, colour = texture x vertex gradient. bool vertexAlpha = false; + // BTFX "brighten" ADDITIVE VEIL (the searchlight SPOT.BGF beam cone): the + // material class authors its factor in DIFFUSE.r and its glow colour in + // EMISSIVE; the batch draws in the blend pass as dest += emissive x factor + // (1995 DIV brighten semantics; the record also authors fog IMMUNE + + // DRAWLAST, both satisfied by the blend pass). 0 = not a brighten batch. + float brightenFactor = 0.0f; // TEXTURE ALPHA CUTOUT (issue #3, the wreck scorch splat): an RGBA4444 BSL // slice (texChannel >= 8) carries an AUTHORED alpha channel -- in the shipped // content it is a BINARY 0/240 cutout mask (bexp9 = the scorch splat diff --git a/engine/MUNGA_L4/l4d3d.h b/engine/MUNGA_L4/l4d3d.h index 5ea36ce..3d98d98 100644 --- a/engine/MUNGA_L4/l4d3d.h +++ b/engine/MUNGA_L4/l4d3d.h @@ -109,6 +109,11 @@ struct L4DRAWOP // UNLIT, colour = texture x the authored per-vertex gradient, alpha = the // per-vertex fade (SRCALPHA/INVSRCALPHA). bool vertexAlphaBlend; + // BTFX BRIGHTEN VEIL (the searchlight SPOT.BGF cone): additive brighten of + // the framebuffer, dest += material.Emissive x this factor (the class + // authors the factor in DIFFUSE.r, the glow colour in EMISSIVE). Drawn in + // the alpha-blend pass, unlit, fog-immune by authoring. 0 = off. + float brightenAlpha; // COCKPIT PUNCH STENCIL-CUT (task #55, i860-firmware-decoded): 0=normal, // 1=aperture MASK (drawn stencil-only, paired with the following hull op), // 2=HULL (drawn stencil-rejected under the mask = window cutouts). diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index 78fcf62..0049583 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -210,7 +210,12 @@ void // is how a night mission starts on nosearchlightfog. tree.searchFogCache[tree.searchLightCount] = (*attrs[sl] == 0); - if (!tree.searchIsCockpit) + // BT_SPOT_SELF=1 (bench-only): build the cone on the + // OWN-cockpit tree too, so the V-chase camera can + // eyeball it solo. Authentic builds never cone the + // cockpit tree (the 1995 view-1 branch stashes attrs + // only) -- keep this env off outside look benches. + if (!tree.searchIsCockpit || getenv("BT_SPOT_SELF")) { // NB: the renderer-level LoadObject wrapper (@00498448) is // still a btstubs no-op; load through the d3d route the