Fire VISUALS wave: the authored firesmoke sheet, vertex-alpha effect cards, the case-4 wreck dressing
The "fireballs like the demo vids" arc, decomp/content-grounded end to end,
plus the live-play UX batch verified over the same sessions:
FIRESMOKE SHEET (the PFX fireball fix): every firesmokeN_scr_tex in BTFX.VMF
maps the SAME 64x64 tileable noise image bintA (variants differ only in SCROLL
rate) and firesmoke1_mtl colours it through the "fiery" ramp (0.3,0.1,0.1)->
(0.9,0.7,0.3). The particle layer now bakes ramp(lum(bintA)) as its sprite
colour (noise detail in alpha) and SCROLLS it at firesmoke1's authored rate
via a texture-transform; the port's radial soft-edge mask moved to a second
CLAMPed stage so the WRAPPED scroll rolls flame through the sprite without
scrolling the edge away. Old grit x radial bake kept as the no-BINTA fallback.
Impact hits, damage-band smoke and death booms all ride this layer.
AUTHORED TEXTURE SCROLL in the model path: the BMF TEXTURE records carry
SPECIAL " SCROLL u0 v0 du dv" (tag 0x2037); the draw path always supported
per-op scrolling (SetTextureScrolling) but the BGF loader never parsed it, so
every scrolling material rendered frozen. Wired TexRef -> MatInfo -> batch ->
L4TEXOP.doScroll: the flame cards (flamebig/fire5) now roll fire noise.
VERTEX-ALPHA EFFECT CARDS (the "twisted drill bit of fire" fix): FLAMEBIG's
verts carry authored float RGBA -- white-hot base (1.0,0.99,0.97) -> dark-red
tip fading to alpha -0.2 (the DPL clamp convention). The loader kept a flat
batch colour and drew it OPAQUE = a solid orange spike. Corpus sweep: exactly
14 shipped BGFs carry vertex alpha, ALL effect cards (flames, MUZFLASH,
EXDISK_A/B/C, TMST_A/B/C, beam models, DECLOUDS). Such batches now keep the
authored per-vertex gradient and route to the alpha-blend pass, unlit,
colour = texture x gradient, alpha = the vertex fade; sky objects excluded
(drawAsSky + alphaTest passes NEITHER pass filter -- DECLOUDS stays in the
sky pass). MUZFLASH/EXDISK render correctly for free when the muzzle-model
work lands.
WRECK DRESSING (the 1996 ExplosionScripts case-4 transcription): pieces spawn
HIDDEN and reveal 0.25s after the boom (the InstanceSwitch delay, behind the
dnboom flash); flamebig hangs over the pile, Y-BILLBOARDED at the camera
(SetOffsetYaw + a camera-pos getter -- the dpl_SetDCSReorientAxes analog);
the MakeDCSFall settle arms at the reveal with the two authored rates (hulk/
debris -0.025 t^2, fires -0.01 t^2 -- the flames ride above the sinking pile
and die with it at burial). EMPTY-PLACEHOLDER hulk guard: THRDBR.BGF is a
153-byte zero-geometry stub that "loads fine" -- vertex-count check now routes
it to the gendbr fallback (a Thor wreck was invisible). Hulk content census
recorded: AVADBR==MADDBR==VULDBR geometry (palette-only prefix diffs),
RAPDBR==SNDDBR==STIDBR byte-identical -- wreck variety is materials + the
dressing, not unique piles.
LIVE-PLAY BATCH: muzzle resolve uses the named segmentIndex (raw +0xdc read
was layout garbage); forward launch frame (authored MuzzleVelocity +Z vs the
mech's -Z facing); dock-bottom single window (gauge strip appended below the
world viewport, 1100x600 default, BT_DEV_GAUGES_WINDOW=1 restores the separate
window); portrait sec surface unrotated CW; ammo counters live via typed
bridges (BTAmmoBinCountPtr/BTAmmoBinFeeding/BTWeaponAmmoBin -- raw bin+0x180
and a hand-rolled link walk were garbage); fourth fire key ('4' = Pinky);
panel/arc probes de-aliased (%61 prime).
KB: rendering.md (vertex-alpha card family + scroll), combat-damage.md (hulk
census + THRDBR stub), gauges-hud.md (ammo bridges).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bb795e2805
commit
48c9c8444f
@@ -295,6 +295,9 @@ struct MatInfo {
|
||||
bool hasDiffuse = false; // material carried an explicit DIFFUSE/AMBIENT tag
|
||||
std::string texPath;
|
||||
int texChannel = 0; // BSL bit-slice (BMF TEXTURE tag 0x18; absent = 0)
|
||||
bool texScroll = false; // TEXTURE SPECIAL " SCROLL" (tag 0x2037): UV animation
|
||||
float texScrollU = 0.0f; // cycles/second
|
||||
float texScrollV = 0.0f;
|
||||
// RAMP (task #20): the material's 2-endpoint colour ramp (dpl_SetMaterialRamp).
|
||||
// The grayscale texture's luminance indexes a low->high colour gradient -- this
|
||||
// is how the IG board coloured terrain (rock 0.25,0.21,0.16->0.8,0.5,0.4=warm
|
||||
@@ -316,6 +319,11 @@ struct MatInfo {
|
||||
struct TexRef {
|
||||
std::string map;
|
||||
int channel = 0;
|
||||
// TEXTURE-record SPECIAL " SCROLL u0 v0 du dv" (tag 0x2037): authored UV
|
||||
// scroll rates in cycles/second (the firesmoke fire-noise roll).
|
||||
bool scroll = false;
|
||||
float scrollU = 0.0f;
|
||||
float scrollV = 0.0f;
|
||||
};
|
||||
|
||||
struct MaterialResolver {
|
||||
@@ -333,6 +341,22 @@ struct MaterialResolver {
|
||||
if (ch.id == TAG_NAME) name = chunkStr(ch);
|
||||
else if (ch.id == TAG_TEXTURE_MAP) ref.map = chunkStr(ch);
|
||||
else if (ch.id == TAG_BITSLICE && ch.len >= 1) ref.channel = ch.data[0];
|
||||
else if (ch.id == TAG_SV_SPECIAL && ch.len > 8) {
|
||||
// " SCROLL u0 v0 du dv" -- authored UV scroll (BTFX's
|
||||
// firesmoke family). u0/v0 initial offsets are 0 in every
|
||||
// shipped record; only the rates matter.
|
||||
std::string sp = chunkStr(ch);
|
||||
size_t at = sp.find("SCROLL");
|
||||
if (at != std::string::npos) {
|
||||
float u0 = 0, v0 = 0, du = 0, dv = 0;
|
||||
if (sscanf(sp.c_str() + at + 6, "%f %f %f %f", &u0, &v0, &du, &dv) == 4
|
||||
&& (du != 0.0f || dv != 0.0f)) {
|
||||
ref.scroll = true;
|
||||
ref.scrollU = du;
|
||||
ref.scrollV = dv;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!name.empty() && !ref.map.empty()) out[name] = ref;
|
||||
}
|
||||
@@ -442,7 +466,13 @@ struct MaterialResolver {
|
||||
if (tm != texMaps.end()) {
|
||||
const auto& imgs = imageIndex();
|
||||
auto f = imgs.find(stemLower(tm->second.map));
|
||||
if (f != imgs.end()) { info.texPath = f->second; info.texChannel = tm->second.channel; }
|
||||
if (f != imgs.end()) {
|
||||
info.texPath = f->second;
|
||||
info.texChannel = tm->second.channel;
|
||||
info.texScroll = tm->second.scroll;
|
||||
info.texScrollU = tm->second.scrollU;
|
||||
info.texScrollV = tm->second.scrollV;
|
||||
}
|
||||
}
|
||||
out[name] = info;
|
||||
}
|
||||
@@ -501,6 +531,8 @@ struct Builder {
|
||||
bool currentHasDiffuse = false; // material carried an explicit diffuse tag
|
||||
std::string currentTex;
|
||||
int currentTexChannel = 0; // BSL bit-slice (BMF tag 0x18)
|
||||
bool currentTexScroll = false; // TEXTURE SPECIAL " SCROLL" (tag 0x2037)
|
||||
float currentTexScrollU = 0.0f, currentTexScrollV = 0.0f;
|
||||
bool currentHasRamp = false;
|
||||
float currentRampLo[3] = {0,0,0};
|
||||
float currentRampHi[3] = {1,1,1};
|
||||
@@ -569,18 +601,32 @@ struct Builder {
|
||||
|
||||
void buildPmesh(const Chunk& pmesh) {
|
||||
std::vector<float> lx, ly, lz, lu, lv;
|
||||
// Authored per-vertex RGBA (tags 0x0082/0x008A: 4 floats at offset 12).
|
||||
// Values run OUTSIDE [0,1] (the flame card FLAMEBIG.BGF fades its tip
|
||||
// to alpha -0.2 and over-brightens its base to 1.2 -- the DPL clamp
|
||||
// convention, same as the .PFX negative colour ramps).
|
||||
std::vector<uint32_t> lrgba;
|
||||
bool vertexAlpha = false; // any authored alpha < 1 -> BLENDED batch
|
||||
uint16_t vtag = 0;
|
||||
for (const Chunk& ch : pmesh.children) {
|
||||
int stride = vertexStride(ch.id);
|
||||
if (!stride) continue;
|
||||
vtag = ch.id;
|
||||
int uvOff = uvOffset(ch.id);
|
||||
const bool hasRGBA = (ch.id == TAG_VERTEX_XYZ_RGBA || ch.id == TAG_VERTEX_XYZ_RGBA_UV);
|
||||
size_t count = ch.len / (size_t)stride;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const uint8_t* v = ch.data + i * stride;
|
||||
lx.push_back(rdF32(v)); ly.push_back(rdF32(v + 4)); lz.push_back(rdF32(v + 8));
|
||||
if (uvOff >= 0) { lu.push_back(rdF32(v + uvOff)); lv.push_back(rdF32(v + uvOff + 4)); }
|
||||
else { lu.push_back(0.0f); lv.push_back(0.0f); }
|
||||
if (hasRGBA) {
|
||||
float cr = rdF32(v + 12), cg = rdF32(v + 16), cb = rdF32(v + 20), ca = rdF32(v + 24);
|
||||
if (ca < 0.999f) vertexAlpha = true;
|
||||
auto C8 = [](float f) { int n = (int)(f * 255.0f + 0.5f); return (uint32_t)(n < 0 ? 0 : n > 255 ? 255 : n); };
|
||||
lrgba.push_back((C8(ca) << 24) | (C8(cr) << 16) | (C8(cg) << 8) | C8(cb));
|
||||
} else
|
||||
lrgba.push_back(0xFFFFFFFFu);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -670,7 +716,13 @@ struct Builder {
|
||||
px.push_back(lx[i]); py.push_back(ly[i]); pz.push_back(lz[i]);
|
||||
nx.push_back(0); ny.push_back(0); nz.push_back(0);
|
||||
uu.push_back(lu[i] * tileU + twist * lv[i]); vv.push_back(lv[i] * tileV);
|
||||
col.push_back(vcol);
|
||||
// VERTEX-ALPHA batches (the flame cards) keep their AUTHORED
|
||||
// per-vertex gradient (white-hot base -> dark faded tip) -- it IS
|
||||
// the flame's shading, modulated by the (ramped, scrolling)
|
||||
// texture in the blend pass. Opaque batches keep the flat
|
||||
// tint/diffuse rule unchanged (alpha==1 skins carry no gradient
|
||||
// the verified look depends on).
|
||||
col.push_back(vertexAlpha ? lrgba[i] : vcol);
|
||||
}
|
||||
|
||||
const uint32_t idxStart = (uint32_t)mesh->indices.size();
|
||||
@@ -729,6 +781,10 @@ struct Builder {
|
||||
: (useRamp ? rampTint : currentColor); // pure-emissive test keys on it
|
||||
batch.texPath = currentTex;
|
||||
batch.texChannel = currentTexChannel;
|
||||
batch.texScroll = currentTexScroll;
|
||||
batch.texScrollU = currentTexScrollU;
|
||||
batch.texScrollV = currentTexScrollV;
|
||||
batch.vertexAlpha = vertexAlpha;
|
||||
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
|
||||
@@ -966,6 +1022,8 @@ struct Builder {
|
||||
bool savedHasDiffuse = currentHasDiffuse;
|
||||
std::string savedTex = currentTex;
|
||||
int savedTexChannel = currentTexChannel;
|
||||
bool savedTexScroll = currentTexScroll;
|
||||
float savedTexScrollU = currentTexScrollU, savedTexScrollV = currentTexScrollV;
|
||||
bool savedHasRamp = currentHasRamp;
|
||||
bool savedPunch = currentPunch;
|
||||
bool savedShadowMat = currentShadowMat;
|
||||
@@ -1048,12 +1106,15 @@ struct Builder {
|
||||
currentColor = info.color; currentTex = info.texPath;
|
||||
currentHasDiffuse = info.hasDiffuse;
|
||||
currentTexChannel = info.texChannel;
|
||||
currentTexScroll = info.texScroll;
|
||||
currentTexScrollU = info.texScrollU;
|
||||
currentTexScrollV = info.texScrollV;
|
||||
currentHasRamp = info.hasRamp;
|
||||
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];
|
||||
}
|
||||
else { currentColor = colorForMaterial(full); currentTex.clear(); currentHasDiffuse = false; currentTexChannel = 0; }
|
||||
else { currentColor = colorForMaterial(full); currentTex.clear(); currentHasDiffuse = false; currentTexChannel = 0; currentTexScroll = false; currentTexScrollU = currentTexScrollV = 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
|
||||
@@ -1085,6 +1146,9 @@ struct Builder {
|
||||
currentHasDiffuse = savedHasDiffuse;
|
||||
currentTex = savedTex;
|
||||
currentTexChannel = savedTexChannel;
|
||||
currentTexScroll = savedTexScroll;
|
||||
currentTexScrollU = savedTexScrollU;
|
||||
currentTexScrollV = savedTexScrollV;
|
||||
currentHasRamp = savedHasRamp;
|
||||
currentPunch = savedPunch;
|
||||
currentShadowMat = savedShadowMat;
|
||||
|
||||
Reference in New Issue
Block a user