Issue #3: wreck scorch quad ramp + RGBA4444 alpha cutout (the 'dark square')

MECHMD's scorch base (basev:bvx9_mtl, bexp9_tex = BEXP.BSL RGBA4444
slice 8, ramp cdusty) drew as a hard-edged dark square for two stacked
reasons:

1. The blanket 'truecolor BSL slice (channel >= 6) never ramps' gate
   blocked its cdusty ramp.  Corpus scan: only 4 shipped textures use
   truecolor slices; bexp9/bdet9 are grayscale in RGB (100% / 98.8%
   r==g==b) and their materials author ramps -- only bdam8 (damage
   sheet) is truly coloured.  bgfload rampableSlice() now probes the
   decoded slice: a truecolor slice ramps iff effectively gray (>=95%),
   keeping bdam8's colour protected.

2. The RGBA4444 authored alpha channel -- a binary 0/240 cutout mask
   (the splat silhouette, 78.5% transparent) -- was never alpha-tested,
   so the quad's transparent surround rendered as an opaque rectangle.
   New BgfDrawBatch.texAlpha (channel >= 8) routes these batches
   through the PUNCH alpha-test draw states, WITHOUT the black-texel
   keying (which would hole the near-black charred centre).  Side
   benefit: tree9 (tree/leaf cards) + bdet9 (trans-rail lattice) get
   their authored cutouts too.

Pixel-verified (ram-kill run, BT_SHOT): irregular char splat, dark
brown (71,44,34) lifting to near-terrain tan (115,100,91 vs terrain
131,119,108), no rectangle; pre-death frames unregressed (vehicles
stay lit/diffuse -- hasNormals gate untouched).  Awaiting human
verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-19 10:39:43 -05:00
co-authored by Claude Fable 5
parent 557e9fd243
commit 9fff079df4
4 changed files with 83 additions and 13 deletions
+17 -7
View File
@@ -264,13 +264,23 @@ rubble originally never appeared because additional cultural objects were built
DPLStaticChildRenderable/DCSInstanceRenderable at TOP level — those read the bare matrix stack
(identity), so the rubble drew at the WORLD ORIGIN. Every cultural video object is now a
`RootRenderable` (places from `myEntity->localToWorld`). Screenshot timeline confirms: intact
MECHMOVR truck → ram death → MECHMD wreck debris scattered at the truck's spot. ⚠ STILL OPEN
(#3): (a) the wreck's scorch base quad draws as an opaque BLACK rectangle (MECHMD's own op —
material/ramp resolve); (b) the death Explosion (psfx 1008 ddam5) not visually confirmed;
(c) the authored BurningState fire visual unreconstructed. PICKUP TRAIL for (a): the black
square is MECHMD.bgf patch 0 — a 4-vtx NO-NORMAL quad (VTX_XYZ_UV), material `basev:bvx9_mtl`
(BASEV.BMF; scratchpad/dump_bmf.py truncates before its record) — the no-normal ramp/material
resolve path renders it void-black instead of the authored scorch. For (b): the ram fires the
MECHMOVR truck → ram death → MECHMD wreck debris scattered at the truck's spot.
**(a) SCORCH QUAD FIXED (2026-07-19, pixel-verified, awaiting human confirm):** the scorch base
(MECHMD patch 0, 4-vtx no-normal quad, `basev:bvx9_mtl` → bexp9_tex = BEXP.BSL **RGBA4444
slice 8**, ramp `cdusty`) drew as a hard dark SQUARE for TWO stacked reasons: (1) the blanket
"truecolor slice (channel ≥ 6) never ramps" gate blocked its cdusty ramp — but only 4 shipped
textures use truecolor slices, and bexp9/bdet9 are GRAYSCALE in RGB (100% / 98.8% r==g==b
texels; only bdam8 is truly coloured) — `bgfload rampableSlice()` now probes the decoded slice
(a truecolor slice ramps iff effectively gray, ≥95%; bdam8's colour stays protected); (2) the
RGBA4444 **authored alpha** (a binary 0/240 splat CUTOUT mask, 78.5% transparent) was never
alpha-tested — new `BgfDrawBatch.texAlpha` (channel ≥ 8) routes through the PUNCH alpha-test
draw states WITHOUT the black-texel keying (which would hole the near-black charred centre).
Side benefit: tree9 (GRASS tree/leaf cards) + bdet9 (trans-rail lattice) get their authored
cutouts too. Post-death shot: irregular char splat, dark brown (71,44,34) lifting to
near-terrain tan (115,100,91 vs terrain 131,119,108), no rectangle; pre-death frames
unregressed (vehicles stay lit/diffuse — the `hasNormals` gate is untouched). [T2] ⚠ STILL OPEN
(#3): (b) the death Explosion (psfx 1008 ddam5) not visually confirmed;
(c) the authored BurningState fire visual unreconstructed. For (b): the ram fires the
CRUNCH explosion res 31 (small, human-seen); res 32 is the icon's non-crunch explosion —
check which the weapon-kill path should fire + psfx scale vs period footage. LESSON: `[cultvis]` log lines verify
the STATE MACHINE, not pixels — only a screen check (BT_SHOT / human) verifies rendering.
+10 -2
View File
@@ -437,7 +437,12 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName)
// become alpha-0 holes below; DrawMesh alpha-TESTS them in the opaque pass.
static int s_punch = -1;
if (s_punch < 0) { const char *pv = getenv("BT_PUNCH"); s_punch = (pv == 0 || pv[0] != '0') ? 1 : 0; }
object->mDrawOps[i].punchThrough = (s_punch && data.batches[i].punch);
// PUNCH batches alpha-test load-KEYED holes (black texels). TEXTURE-ALPHA
// batches (RGBA4444 BSL slice, issue #3: the wreck scorch splat / rail
// lattice / tree cards) alpha-test their AUTHORED alpha channel -- same
// draw states, no keying (the near-black splat centre must stay opaque).
object->mDrawOps[i].punchThrough =
(s_punch && data.batches[i].punch) || data.batches[i].texAlpha;
object->mDrawOps[i].copRole = data.batches[i].copRole; // punch stencil-cut kit (task #55)
object->mDrawOps[i].texture.texture = NULL;
object->mDrawOps[i].texture.wrap_u = L4TEXOP::REPEAT;
@@ -493,7 +498,10 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName)
{
// PUNCH keying: black texels of a punch batch become alpha 0
// (holes). Keyed on the ORIGINAL texel, before any ramp.
const bool punchKey = object->mDrawOps[i].punchThrough;
// NOT applied to texture-alpha (RGBA4444) batches -- their
// cutout is the authored alpha channel, and black-keying
// would hole the scorch splat's charred (near-black) centre.
const bool punchKey = (s_punch && data.batches[i].punch);
if (useRamp)
{
// Colorize: for each texel, index the low->high ramp by
+46 -4
View File
@@ -1,6 +1,7 @@
// bgfload.cpp - see bgfload.h. Parse logic ported from port/src/bgf.cpp; directory
// scanning reimplemented on Win32 (FindFirstFile) so it builds under C++14.
#include "bgfload.h"
#include "image.h" // truecolor-slice grayscale probe (rampableSlice, issue #3)
#include <windows.h>
#include <cstdio>
@@ -247,6 +248,40 @@ bool hasNormals(uint16_t id) {
return id == TAG_VERTEX_XYZ_N || id == TAG_VERTEX_XYZ_N_UV;
}
// TRUECOLOR-slice ramp eligibility (issue #3, the wreck scorch quad). Only 4
// shipped textures use a truecolor BSL slice (bdam8=RGB444; bdet9/bexp9/tree9=
// RGBA4444). bexp9 (MECHMD's scorch splat, material basev:bvx9_mtl ramp
// 'cdusty') and bdet9 (the trans-rail lattice, ramps 'softer'/'cdusty') are
// GRAYSCALE in their RGB (100% / 98.8% r==g==b texels, corpus-measured) -- they
// author the same luminance->ramp colouring as every mono slice, and the old
// blanket "channel < 6" gate left the scorch a raw dark quad instead of the
// cdusty dark-brown->dusty-tan gradient. bdam8 (the damage sheet) is genuinely
// COLOURED (33% non-gray texels) -- luminance-ramping it would destroy its
// colour, which is what the blanket gate was protecting. Gate on the actual
// slice content: a truecolor slice ramps only when its RGB is effectively
// grayscale. Cached per path#channel (decode runs once per texture).
bool rampableSlice(const std::string& texPath, int channel) {
if (channel < 6 || texPath.empty()) return true; // mono slices always ramp-eligible
static std::map<std::string, bool> cache;
std::string key = texPath;
key += '#';
key += (char)('0' + (channel % 10));
auto it = cache.find(key);
if (it != cache.end()) return it->second;
bool gray = false;
Image img = decodeImage(texPath, channel);
if (img.ok && !img.argb.empty()) {
size_t same = 0;
for (uint32_t p : img.argb) {
const uint8_t r = (uint8_t)(p >> 16), g = (uint8_t)(p >> 8), b = (uint8_t)p;
if (r == g && g == b) ++same;
}
gray = ((float)same / (float)img.argb.size()) >= 0.95f;
}
cache[key] = gray;
return gray;
}
struct Chunk {
uint16_t id = 0;
const uint8_t* data = nullptr;
@@ -689,10 +724,13 @@ struct Builder {
// RAMP applies ONLY to no-normal geometry (see hasNormals()): normal-bearing
// vehicle/missile batches keep their material diffuse colour + texture (lit path),
// so they must NOT be ramped even when the material carries a ramp ref (cdusty).
// 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) || currentTSphere) && currentTexChannel < 6;
// TRUECOLOR BSL slices (channel >= 6: RGB444/RGBA4444) ramp only when their RGB
// is effectively GRAYSCALE (rampableSlice): bexp9/bdet9 are authored gray + ramp
// (the wreck scorch splat's cdusty gradient, issue #3); damcolor's bdam8 damage
// sheet is a real colour image -- luminance-ramping it would destroy its colour,
// so it takes the plain textured path.
const bool useRamp = currentHasRamp && (!hasNormals(vtag) || currentTSphere)
&& rampableSlice(currentTex, currentTexChannel);
const uint32_t base = (uint32_t)px.size();
const uint32_t localCount = (uint32_t)lx.size();
@@ -839,6 +877,10 @@ struct Builder {
batch.texScrollU = currentTexScrollU;
batch.texScrollV = currentTexScrollV;
batch.vertexAlpha = vertexAlpha;
// RGBA4444 slice (channel >= 8): the texture's AUTHORED alpha channel is
// a cutout mask (bexp9 scorch splat / bdet9 rail lattice / tree9 cards)
// -- the renderer alpha-tests it in the opaque pass (see bgfload.h).
batch.texAlpha = (currentTexChannel >= 8) && !currentTex.empty();
batch.hasRamp = useRamp;
// tsphere warp flag: warpBands>0 signals L4D3D's ramp bake to CONTRAST-
// COMPRESS the bintA cloud toward mid-grey (BT_WARP_CONTRAST) before applying
+10
View File
@@ -44,6 +44,16 @@ 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;
// 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
// silhouette, 78.5% transparent; bdet9 = the trans-rail lattice; tree9 = the
// tree/leaf cards). Drawn opaque, the quad's transparent surround renders as
// a hard-edged rectangle (the "dark square" under the wreck). The renderer
// alpha-TESTS these batches in the opaque pass (same states as PUNCH, but the
// alpha comes from the texture itself -- no black-texel keying, which would
// hole the near-black charred splat centre).
bool texAlpha = false;
// RAMP (task #20): colorize the grayscale texture by luminance across
// rampLo->rampHi (the authentic IG-board terrain colouring). When set, the
// renderer bakes lerp(rampLo, rampHi, texLum) into the texture and draws with