Warp: restore the translocation-vortex look + fix texture-scroll precision collapse (task #52)
The death/respawn "blue whirlwind" (tsphere) now matches the original cabinet
photo (capture.png): a smooth spinning lavender vortex with a bright core.
Root cause of the long-standing "radial spokes" artifact was NOT the warp code
but a general engine bug: L4D3D::SetTextureScrolling computed its texture-matrix
offset as scrollDelta * absolute_time, which grows unbounded and collapses UV
float precision -> a smooth scrolled cloud shatters into grainy radial steps.
Wrapped with fmodf(..., 1.0f) (identical under REPEAT tiling, full precision).
This also cleans the scrolling bexp beam grit and any other SCROLL material.
Visual reconstruction (verified against the real 45-vtx TSPHERE.BGF bicone,
offline-rasterized then ported):
- view ON-AXIS (eye centred on the throat) + spin in place -> concentric rings
(decomp FUN_00453dc4 does spin-about-local-Z + submit; the port had stubbed it)
- bintA cloud through a WIDE lavender ramp at full contrast (drawn as SKY);
no geometry "bands", no log-polar twist, no off-axis tornado (all discarded)
- tessellate the 12-facet bicone smooth; isotropic + trilinear; ramp baked into
the texture and drawn SELECTARG1(TEXTURE) to avoid double-tinting
Env knobs (BT_WARP_*) default to the verified values; BT_WARP_SELFTEST/SELFSHOT
are an off-by-default visual-verification harness (backbuffer frame dump).
Docs: new context/translocation-warp.md (geometry/material/visual/lifecycle/env);
reconstruction-gotchas.md gains the accumulated-time precision-collapse bug class;
rendering.md / multiplayer.md / decomp-reference.md / CLAUDE.md cross-linked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a35f321ba1
commit
0bfb3d4ab3
@@ -441,6 +441,12 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName)
|
||||
// texChannel: which BSL bit-slice this texture samples (BMF tag 0x18;
|
||||
// mech skins pack 4+ grayscale sheets per .BSL). Ignored for VTX/TGA.
|
||||
Image img = decodeImage(tp, data.batches[i].texChannel);
|
||||
if (getenv("BT_TLOC_LOG") && data.batches[i].warpBands > 0)
|
||||
DEBUG_STREAM << "[tloc-bake] tsphere batch#" << i << " useRamp=" << useRamp
|
||||
<< " hasRamp=" << data.batches[i].hasRamp << " tex='" << tp << "' img.ok=" << img.ok
|
||||
<< " w=" << img.w << " h=" << img.h
|
||||
<< " lo=(" << data.batches[i].rampLo[0] << "," << data.batches[i].rampLo[1] << "," << data.batches[i].rampLo[2] << ")"
|
||||
<< "\n" << std::flush;
|
||||
if (img.ok && img.w > 0 && img.h > 0)
|
||||
{
|
||||
LPDIRECT3DTEXTURE9 tex = NULL;
|
||||
@@ -480,6 +486,64 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName)
|
||||
// the texel LUMINANCE. argb = lerp(rampLo, rampHi, lum).
|
||||
const float *lo = data.batches[i].rampLo;
|
||||
const float *hi = data.batches[i].rampHi;
|
||||
// TRANSLOCATION WARP (tsphere_mtl, warpBands>0): the swirl is the
|
||||
// bintA cloud sampled in the bicone's cylindrical UVs (U=angle,
|
||||
// V=axial) which, viewed down the throat axis, ARE log-polar
|
||||
// coordinates -- exactly the mapping that matched the original
|
||||
// (capture.png). Colour it by a NARROW lavender ramp at LOW
|
||||
// contrast: compress the cloud luminance toward mid-grey before the
|
||||
// ramp so the bands read as soft, intersecting spiral streaks (the
|
||||
// "looking down a tornado" look) rather than hard concentric rings.
|
||||
// BT_WARP_CONTRAST (default 0.55). Terrain/sky ramps (warpBands==0)
|
||||
// stay full-contrast.
|
||||
float warpContrast = 0.0f;
|
||||
if (data.batches[i].warpBands > 0) {
|
||||
static float s_ct = -1.0f;
|
||||
// Full contrast (1.0 = cloud luminance untouched): the original's
|
||||
// rings span the whole range dark->bright. Compressing toward mid-
|
||||
// grey (<1) washed the swirl flat; 1.0 matched (cmp_final2.png).
|
||||
if (s_ct < 0.0f) { const char *cv = getenv("BT_WARP_CONTRAST"); s_ct = cv ? (float)atof(cv) : 1.0f; if (s_ct < 0) s_ct = 0; if (s_ct > 4) s_ct = 4; }
|
||||
warpContrast = s_ct;
|
||||
}
|
||||
// PRE-BLUR (tsphere only): the 64x64 bintA cloud is far too high-
|
||||
// frequency for the huge throat wall -- minified there it ALIASES into
|
||||
// the grainy radial "spokes" (the modern high-res render exposing what
|
||||
// the 1995 low-res IG board low-passed away; the original swirl is soft
|
||||
// concentric rings). Low-pass the cloud FIRST. Separable box blur,
|
||||
// wrap-around (the texture tiles), 2 passes. BT_WARP_BLUR = radius
|
||||
// (default 3; 0 = off).
|
||||
std::vector<float> blurLum;
|
||||
if (warpContrast > 0.0f)
|
||||
{
|
||||
static int s_blur = -1;
|
||||
// Default OFF: with the scroll precision bug fixed the raw cloud reads
|
||||
// as clean concentric rings (cmp_blur.png). Blur only ever masked the
|
||||
// scroll "spokes"; >0 now just softens the rings. BT_WARP_BLUR=radius.
|
||||
if (s_blur < 0) { const char *bv = getenv("BT_WARP_BLUR"); s_blur = bv ? atoi(bv) : 0; if (s_blur < 0) s_blur = 0; if (s_blur > 31) s_blur = 31; }
|
||||
const int BW = img.w, BH = img.h;
|
||||
blurLum.resize((size_t)BW * BH);
|
||||
for (int y = 0; y < BH; y++)
|
||||
for (int x = 0; x < BW; x++) {
|
||||
uint32_t p = img.argb[(size_t)y * BW + x];
|
||||
blurLum[(size_t)y*BW+x] = (0.299f*((p>>16)&0xFF)+0.587f*((p>>8)&0xFF)+0.114f*(p&0xFF))/255.0f;
|
||||
}
|
||||
if (s_blur > 0) {
|
||||
std::vector<float> tmp((size_t)BW * BH);
|
||||
const int R = s_blur; const float inv = 1.0f/(2*R+1);
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
for (int y = 0; y < BH; y++) // horizontal (wrap)
|
||||
for (int x = 0; x < BW; x++) {
|
||||
float s = 0; for (int k=-R;k<=R;k++){int xx=((x+k)%BW+BW)%BW; s+=blurLum[(size_t)y*BW+xx];}
|
||||
tmp[(size_t)y*BW+x] = s*inv;
|
||||
}
|
||||
for (int y = 0; y < BH; y++) // vertical (wrap)
|
||||
for (int x = 0; x < BW; x++) {
|
||||
float s = 0; for (int k=-R;k<=R;k++){int yy=((y+k)%BH+BH)%BH; s+=tmp[(size_t)yy*BW+x];}
|
||||
blurLum[(size_t)y*BW+x] = s*inv;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int y = 0; y < img.h; y++)
|
||||
{
|
||||
unsigned char *dst = (unsigned char*)lr.pBits + y * lr.Pitch;
|
||||
@@ -487,7 +551,13 @@ d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName)
|
||||
for (int x = 0; x < img.w; x++)
|
||||
{
|
||||
uint32_t p = src[x];
|
||||
float lum = (0.299f*((p>>16)&0xFF) + 0.587f*((p>>8)&0xFF) + 0.114f*(p&0xFF)) / 255.0f;
|
||||
float lum = blurLum.empty()
|
||||
? (0.299f*((p>>16)&0xFF) + 0.587f*((p>>8)&0xFF) + 0.114f*(p&0xFF)) / 255.0f
|
||||
: blurLum[(size_t)y*img.w + x];
|
||||
if (warpContrast > 0.0f) { // soften the cloud toward mid-grey
|
||||
lum = (lum - 0.5f) * warpContrast + 0.5f;
|
||||
if (lum < 0.0f) lum = 0.0f; else if (lum > 1.0f) lum = 1.0f;
|
||||
}
|
||||
int rr = (int)((lo[0] + (hi[0]-lo[0])*lum) * 255.0f + 0.5f);
|
||||
int gg = (int)((lo[1] + (hi[1]-lo[1])*lum) * 255.0f + 0.5f);
|
||||
int bb = (int)((lo[2] + (hi[2]-lo[2])*lum) * 255.0f + 0.5f);
|
||||
@@ -1179,8 +1249,14 @@ void d3d_OBJECT::SetTextureScrolling(const L4TEXOP *texture, Time targetRenderFr
|
||||
|
||||
D3DXMATRIX translate;
|
||||
D3DXMatrixIdentity(&translate);
|
||||
translate._31 = -texture->scrollUDelta * time;
|
||||
translate._32 = texture->scrollVDelta * time;
|
||||
// WRAP the scroll offset into [0,1) (the texture REPEAT-tiles, so only the
|
||||
// fractional part is meaningful). Without this, scrollDelta*time grows without
|
||||
// bound as the app runs; once the offset is large, adding it to the per-pixel UV
|
||||
// COLLAPSES float precision -> the UVs quantise into coarse steps and a smooth
|
||||
// texture shatters into grainy radial "spokes" (this was the translocation-warp
|
||||
// swirl artifact, and would degrade every scrolling texture over a long session).
|
||||
translate._31 = -fmodf(texture->scrollUDelta * time, 1.0f);
|
||||
translate._32 = fmodf(texture->scrollVDelta * time, 1.0f);
|
||||
mDevice->SetTransform(D3DTS_TEXTURE0, &translate);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user