Files
BT412/engine/MUNGA_L4/L4D3D.cpp
T
arcattackandClaude Opus 4.8 0bfb3d4ab3 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>
2026-07-10 14:53:50 -05:00

1307 lines
50 KiB
C++

#include "mungal4.h"
#pragma hdrstop
#include "l4d3d.h"
#include "L4VIDEO.h"
#include "bgfload.h"
#include "image.h"
#include <algorithm>
#include <cstdint>
int gNumBatches = 0;
bool d3d_OBJECT::mLastTextureScrollState = false;
L4TEXOP::WrapType d3d_OBJECT::mLastWrapU = L4TEXOP::WrapType::REPEAT;
L4TEXOP::WrapType d3d_OBJECT::mLastWrapV = L4TEXOP::WrapType::REPEAT;
bool d3d_OBJECT::mLastTexturingState = true;
long d3d_OBJECT::mNextID = 1;
stdext::hash_map<std::string, L4TEXOP> d3d_OBJECT::mTextureCache;
void chgext(char *filePath, const char *newExtension)
{
int len = strlen(filePath);
for (int i=len-1; i>=0; i--)
{
if (filePath[i] == '.')
{
strcpy(&filePath[i + 1], newExtension);
return;
}
}
}
void d3d_OBJECT::ResetState(LPDIRECT3DDEVICE9 device)
{
d3d_OBJECT::mLastTextureScrollState = false;
device->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE);
d3d_OBJECT::mLastWrapU = L4TEXOP::WrapType::REPEAT;
device->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP);
d3d_OBJECT::mLastWrapV = L4TEXOP::WrapType::REPEAT;
device->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);
//
// BT port fix: the 2007 port never set filter states, so D3D9 ran at its
// default D3DTEXF_POINT (nearest-neighbour, no mip selection) -- blocky,
// shimmering textures regardless of source-art resolution. The IG board
// filtered. Linear min/mag + linear mip (trilinear where mips exist).
//
device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
d3d_OBJECT::mLastTexturingState = true;
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
}
// DIAG (turn-hitch hunt): time every runtime model load. New objects entering
// interest range load SYNCHRONOUSLY on the sim thread -- if the momentary
// freezes correlate with these lines, the hitch is lazy model loading.
struct BTLoadTimer
{
const char *name; LARGE_INTEGER t0;
BTLoadTimer(const char *n) : name(n) { QueryPerformanceCounter(&t0); }
~BTLoadTimer()
{
LARGE_INTEGER t1, f; QueryPerformanceCounter(&t1); QueryPerformanceFrequency(&f);
double ms = (double)(t1.QuadPart - t0.QuadPart) * 1000.0 / (double)f.QuadPart;
if (ms > 5.0)
DEBUG_STREAM << "[loadobj] " << name << " took " << ms << " ms\n" << std::flush;
}
};
d3d_OBJECT *d3d_OBJECT::LoadObject(LPDIRECT3DDEVICE9 device, char *fileName)
{
BTLoadTimer _lt(fileName);
char fullPath[512];
sprintf(fullPath, "VIDEO\\%s", fileName);
chgext(fullPath, "x");
//Get a ref to the renderer
DPLRenderer *renderer = l4_application->GetVideoRenderer();
LPD3DXMESH mesh;
LPD3DXBUFFER materialBuffer, adjacencyBuffer;
DWORD materialCount;
HRESULT hr = D3DXLoadMeshFromXA(fullPath, D3DXMESH_MANAGED, device, &adjacencyBuffer, &materialBuffer, NULL, &materialCount, &mesh);
if (FAILED(hr))
{
// No .x present (the pod content tree ships original .bgf, not exported .x).
// Try the native .bgf loader before falling back to point-sphere objects.
d3d_OBJECT *bgf = d3d_OBJECT::LoadObjectBGF(device, fileName);
if (bgf != NULL)
return bgf;
chgext(fullPath, "sph");
return d3d_OBJECT::LoadSpheres(device, fullPath);
}
chgext(fullPath, "det");
FILE *detFile = fopen(fullPath, "rb");
int numDetailOps = 0;
int *detailOps = NULL;
bool isSky = false;
if (detFile != NULL)
{
fread(&numDetailOps, sizeof(int), 1, detFile);
detailOps = new int[numDetailOps];
fread(detailOps, sizeof(int), numDetailOps, detFile);
fclose(detFile);
}
chgext(fullPath, "sky");
FILE *skyFile = fopen(fullPath, "rb");
if (skyFile != NULL)
{
isSky = true;
fclose(skyFile);
}
d3d_OBJECT *object = new d3d_OBJECT(device, mesh, (DWORD*)adjacencyBuffer->GetBufferPointer(), materialCount);
D3DXMATERIAL *materials = (D3DXMATERIAL*)materialBuffer->GetBufferPointer();
int nextDetailOp = 0;
for (DWORD i = 0; i < materialCount; i++)
{
chgext(fullPath, "BGF");
string path(fullPath);
string actualPath = path.substr(path.find("\\") + 1);
std::transform(actualPath.begin(), actualPath.end(), actualPath.begin(), toupper);
const char *matName = opMaterialName(actualPath.c_str(), i);
char *replaceMatName = NULL;
char *replaceTexName = NULL;
if (matName != NULL)
{
char *tmpMatName = new char[strlen(matName) + 1];
strcpy_s(tmpMatName, strlen(matName) + 1, matName);
replaceMatName = substituteMaterial(tmpMatName);
}
object->mDrawOps[i].material = materials[i].MatD3D;
if (replaceMatName != NULL)
{
hash_map<string, ReplacementMaterialData>::const_iterator iter = gReplacementData->find(string(replaceMatName));
ReplacementMaterialData data = (*iter).second;
object->mDrawOps[i].material.Diffuse.r = data.r;
object->mDrawOps[i].material.Diffuse.g = data.g;
object->mDrawOps[i].material.Diffuse.b = data.b;
replaceTexName = new char[data.texName.size() + 1];
strcpy_s(replaceTexName, data.texName.size() + 1, data.texName.c_str());
delete [] replaceMatName;
}
//TODO: set ambient of material; doesn't come back from .X file
object->mDrawOps[i].material.Ambient = object->mDrawOps[i].material.Diffuse;
object->mDrawOps[i].texture.texture = NULL;
if (materials[i].pTextureFilename || replaceTexName)
{
char textureFilename[512];
if (replaceTexName)
{
sprintf(textureFilename, "VIDEO\\%s.png", replaceTexName);
delete [] replaceTexName;
} else
{
sprintf(textureFilename, "VIDEO\\%s", materials[i].pTextureFilename);
}
object->mDrawOps[i].texture = LoadTexture(device, textureFilename);
}
if (nextDetailOp < numDetailOps && detailOps[nextDetailOp] == i)
{
object->mDrawOps[i].drawAsDecal = true;
nextDetailOp++;
}
if (abs(object->mDrawOps[i].material.Diffuse.a - 1.0f) > 0.0001f)
{
object->mDrawOps[i].alphaTest = true;
}
object->mDrawOps[i].drawAsSky = isSky;
if (isSky)
{
object->mDrawOps[i].material.Ambient.r *= renderer->GetCloudRed();
object->mDrawOps[i].material.Ambient.g *= renderer->GetCloudGreen();
object->mDrawOps[i].material.Ambient.b *= renderer->GetCloudBlue();
object->mDrawOps[i].material.Diffuse.r *= renderer->GetCloudRed();
object->mDrawOps[i].material.Diffuse.g *= renderer->GetCloudGreen();
object->mDrawOps[i].material.Diffuse.b *= renderer->GetCloudBlue();
object->mDrawOps[i].material.Emissive.r = renderer->GetCloudEmitRed();
object->mDrawOps[i].material.Emissive.g = renderer->GetCloudEmitGreen();
object->mDrawOps[i].material.Emissive.b = renderer->GetCloudEmitBlue();
}
}
materialBuffer->Release();
return object;
}
// Native .BGF (DIV-BIZ2) mesh loader. The pod content tree ships original .bgf models
// (no exported .x), so when D3DXLoadMeshFromXA fails we build the ID3DXMesh ourselves
// from the .bgf using the proven port loader logic (bgfload.cpp + image.cpp).
d3d_OBJECT* d3d_OBJECT::LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName)
{
BgfData data;
if (!LoadBgfFile(fileName ? std::string(fileName) : std::string(), data) || data.batches.empty())
return NULL;
// SKY DOME detection (task #20): the world's sky is a *sky.bgf dome
// (dbase=dsky, arena=sky, polar=psky ...) authored with a cloud TEXTURE +
// white vertex colours. The original renderer drew it in a dedicated SKY
// pass (fullbright, minimal fog); our BGF loader route defaults drawAsSky
// to false, so the dome fell into PASS_OPAQUE and was LIT + FOGGED like
// terrain -> the "dark navy half-lit dome" the pod never showed. Tag any
// *sky* object so its drawOps route to PASS_SKY (lighting-exempt).
bool isSkyObj = false;
if (fileName != NULL)
{
char lower[64]; int li = 0;
for (const char *s = fileName; *s && li < 63; ++s)
lower[li++] = (char)tolower((unsigned char)*s);
lower[li] = 0;
if (strstr(lower, "sky") != NULL)
isSkyObj = true;
}
DWORD numFaces = (DWORD)(data.indices.size() / 3);
DWORD numVerts = (DWORD)data.verts.size();
if (numFaces == 0 || numVerts == 0)
return NULL;
LPD3DXMESH mesh = NULL;
HRESULT hr = D3DXCreateMeshFVF(numFaces, numVerts,
D3DXMESH_MANAGED | D3DXMESH_32BIT, L4VERTEX_FVF, device, &mesh);
if (FAILED(hr) || mesh == NULL)
return NULL;
void *vptr = NULL;
if (SUCCEEDED(mesh->LockVertexBuffer(0, &vptr)) && vptr)
{
memcpy(vptr, data.verts.data(), (size_t)numVerts * sizeof(BgfVtx));
mesh->UnlockVertexBuffer();
}
void *iptr = NULL;
if (SUCCEEDED(mesh->LockIndexBuffer(0, &iptr)) && iptr)
{
memcpy(iptr, data.indices.data(), data.indices.size() * sizeof(unsigned int));
mesh->UnlockIndexBuffer();
}
// one attribute id per face = its batch (material) index
DWORD *attr = NULL;
if (SUCCEEDED(mesh->LockAttributeBuffer(0, &attr)) && attr)
{
for (size_t bi = 0; bi < data.batches.size(); ++bi)
{
DWORD startFace = data.batches[bi].indexStart / 3;
DWORD faceCount = data.batches[bi].indexCount / 3;
for (DWORD fct = 0; fct < faceCount; ++fct)
attr[startFace + fct] = (DWORD)bi;
}
mesh->UnlockAttributeBuffer();
}
// PORT (draw-cost fix): NO GenerateAdjacency/OptimizeInplace here. The BGF
// loader's double-sided triangles (each face emitted twice) made the adjacency
// degenerate and the ATTRSORT optimize fail silently -> no attribute table ->
// D3DX DrawSubset scanned the whole attribute buffer PER CALL (~44us x ~1400
// batches = ~60ms/frame = the 10fps baseline). The batches are already
// contiguous, face-sorted index ranges, so SET the attribute table EXPLICITLY
// (D3DXConcatenateMeshes -- the static-world consolidation -- REQUIRES one on
// its inputs; without it the whole terrain got skipped), and each draw op also
// carries its range so DrawMesh can DrawIndexedPrimitive directly.
int drawOpCount = (int)data.batches.size();
{
std::vector<D3DXATTRIBUTERANGE> atable((size_t)drawOpCount);
for (int bi = 0; bi < drawOpCount; ++bi)
{
atable[bi].AttribId = (DWORD)bi;
atable[bi].FaceStart = data.batches[bi].indexStart / 3;
atable[bi].FaceCount = data.batches[bi].indexCount / 3;
atable[bi].VertexStart = 0;
atable[bi].VertexCount = numVerts;
}
mesh->SetAttributeTable(atable.data(), (DWORD)drawOpCount);
}
d3d_OBJECT *object = new d3d_OBJECT(device, mesh, NULL, drawOpCount);
// DIAG (BT_LOD_LOG): stash the model basename for flip telemetry
{
const char *bn = fileName ? fileName : "";
for (const char *p = bn; *p; ++p) if (*p == '\\' || *p == '/') bn = p + 1;
strncpy(object->mDbgName, bn, sizeof(object->mDbgName) - 1);
object->mDbgName[sizeof(object->mDbgName) - 1] = '\0';
}
// BAKED GROUND-SHADOW models (e.g. MECHMOVS.BGF = two coplanar shadow_mtl
// quads at y=0.1 under the wreck props): drawn opaque they z-fight each
// other / the floor ("the floor tile under the wreckage flickers"), and
// look wrong (solid black tiles). A model whose EVERY batch uses a shadow
// material is routed through the mech-shadow pipeline: mIsShadow draw
// (translucent dark, depth-biased, no z-write -- cannot fight) in the
// blend pass (alphaTest routes the ops there), excluded from static
// consolidation (RecurseStaticObject). Mixed models (a vehicle with a
// baked shadow part among solid parts) are left untouched.
{
bool allShadow = !data.batches.empty();
for (size_t bi = 0; bi < data.batches.size() && allShadow; ++bi)
if (!data.batches[bi].shadowMat) allShadow = false;
// MECH SHADOW PROXIES (*_tshd.bgf) tag by FILENAME here in the loader
// (task #49b): the mech proxy uses a plain black material (not
// shadow_mtl), so the all-shadow-material detector above misses it.
// The render-tree build used to tag it at its own call site -- but the
// VIEW TOGGLE (SetViewInside) and the damage swaps RELOAD segment
// meshes through this loader and swapped in an UNTAGGED object: solid
// opaque black, no depth bias, buried on slopes (the "shadow broke
// mid-session after pressing V" report). Tagging at load covers every
// caller; the drawOps' alphaTest routing below reads the flag.
if (!allShadow && fileName != NULL && strstr(fileName, "tshd") != NULL)
allShadow = true;
if (allShadow)
{
object->SetIsShadow(1);
DEBUG_STREAM << "[shadowobj] " << object->mDbgName
<< " ops=" << (int)data.batches.size() << " -> shadow pipeline\n" << std::flush;
}
}
// cache the mesh's own buffers for the direct-draw path (AddRef'd; dtor releases)
mesh->GetVertexBuffer(&object->mBgfVB);
mesh->GetIndexBuffer((LPDIRECT3DINDEXBUFFER9 *)&object->mBgfIB);
object->mBgfStride = mesh->GetNumBytesPerVertex();
object->mBgfNumVerts = mesh->GetNumVertices();
// DIAG (BT_LOD_LOG): banded-op census at load
{
static int s_bandLog = -1;
if (s_bandLog < 0) { const char *lv = getenv("BT_LOD_LOG"); s_bandLog = (lv != 0 && lv[0] == '1') ? 1 : 0; }
if (s_bandLog)
{
int nBanded = 0;
for (size_t bi = 0; bi < data.batches.size(); ++bi)
if (data.batches[bi].lodFar > 0.0f && data.batches[bi].lodFar < 1.0e8f) ++nBanded;
if (nBanded > 0)
DEBUG_STREAM << "[lodband] " << object->mDbgName << " ops=" << (int)data.batches.size()
<< " banded=" << nBanded << "\n" << std::flush;
}
}
// RAMP colorize gate (task #20; BT_RAMP=0 disables for A/B). The IG board
// coloured terrain by the material's 2-endpoint ramp (texture luminance ->
// low..high gradient). Materials with NO diffuse (grass_mtl) relied on it and
// fell back to the gray placeholder -> the "pale ground that doesn't match the
// warm mountains". When a batch has a ramp we bake lerp(lo,hi,texLum) into the
// texture and draw with a WHITE material so the ramp colour shows directly.
static int s_ramp = -1;
if (s_ramp < 0) { const char *rv = getenv("BT_RAMP"); s_ramp = (rv == 0 || rv[0] != '0') ? 1 : 0; }
for (int i = 0; i < drawOpCount; i++)
{
// direct-draw range for this batch (bypasses D3DX DrawSubset)
object->mDrawOps[i].bgfStartIndex = (int)data.batches[i].indexStart;
object->mDrawOps[i].bgfPrimCount = (int)(data.batches[i].indexCount / 3);
// authentic LOD band (0x2046): DrawMesh selects by camera distance
object->mDrawOps[i].lodNear = data.batches[i].lodNear;
object->mDrawOps[i].lodFar = data.batches[i].lodFar;
object->mDrawOps[i].lodDepthBias = data.batches[i].lodBias;
const bool useRamp = (s_ramp && data.batches[i].hasRamp);
uint32_t c = data.batches[i].color;
// PURE-EMISSIVE materials (diffuse black + emissive set, tag 0x26 --
// btpolar:pintBIceEmit_mtl, the polar glowing ice mounds): authored as
// an UNLIT self-glow, tex x emissive. Diffuse/ambient stay black so
// the sun/ambient add nothing; the (ramped) texture is coloured by the
// Emissive term alone.
const bool pureEmissive =
data.batches[i].hasEmissive && (c & 0x00FFFFFF) == 0;
D3DMATERIAL9 &m = object->mDrawOps[i].material;
memset(&m, 0, sizeof(m));
if (pureEmissive)
{
m.Emissive.r = data.batches[i].emissive[0];
m.Emissive.g = data.batches[i].emissive[1];
m.Emissive.b = data.batches[i].emissive[2];
}
else
{
// Ramped batches arrive with batch.color already set to their tint:
// WHITE for coloured ramps (sky/rock/grass -- the ramp-baked texture
// shows untinted, the pre-BSL-decode behavior), or the material
// diffuse for NEUTRAL-ramp skins (the mech's violet-gray limb
// variants / green lgo6 leg logo / near-white searchlights -- see
// the RAMP TINT RULE in bgfload.cpp buildPmesh).
m.Diffuse.r = ((c >> 16) & 0xFF) / 255.0f;
m.Diffuse.g = ((c >> 8) & 0xFF) / 255.0f;
m.Diffuse.b = (c & 0xFF) / 255.0f;
}
m.Diffuse.a = 1.0f;
m.Ambient = m.Diffuse; // matches the .x path; lit by scene ambient even w/o direct lights
object->mDrawOps[i].alphaTest = object->mIsShadow != 0; // shadow -> blend pass
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
// 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);
object->mDrawOps[i].texture.texture = NULL;
object->mDrawOps[i].texture.wrap_u = L4TEXOP::REPEAT;
object->mDrawOps[i].texture.wrap_v = L4TEXOP::REPEAT;
object->mDrawOps[i].texture.doScroll = false;
const std::string &tp = data.batches[i].texPath;
if (!tp.empty())
{
// 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;
// BT port fix: was 1 mip level / no usage -- no mipmaps existed, so
// distant surfaces aliased at any filter mode. Auto-generate the
// full mip chain (falls back to a single level if unsupported).
// PUNCH textures build their chain MANUALLY below (mip alpha must be
// re-binarized per level -- box-filtered alpha hovers around the test
// threshold, so texels flicker in/out as the camera distance shifts
// the mip level: the "walls render with noise / diagonal patterns on
// slight movement" on the arena framework layers).
const bool punchTex = object->mDrawOps[i].punchThrough;
if (punchTex)
{
if (FAILED(device->CreateTexture(img.w, img.h, 0, 0,
D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &tex, NULL)))
tex = NULL;
}
else if (FAILED(device->CreateTexture(img.w, img.h, 0, D3DUSAGE_AUTOGENMIPMAP,
D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &tex, NULL)))
{
tex = NULL;
device->CreateTexture(img.w, img.h, 1, 0,
D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &tex, NULL);
}
if (tex)
{
D3DLOCKED_RECT lr;
if (SUCCEEDED(tex->LockRect(0, &lr, NULL, 0)))
{
// 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;
if (useRamp)
{
// Colorize: for each texel, index the low->high ramp by
// 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;
const uint32_t *src = &img.argb[(size_t)y * img.w];
for (int x = 0; x < img.w; x++)
{
uint32_t p = src[x];
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);
rr = rr<0?0:rr>255?255:rr; gg = gg<0?0:gg>255?255:gg; bb = bb<0?0:bb>255?255:bb;
uint32_t a = (p & 0xFF000000u);
if (punchKey && ((p>>16)&0xFF) < 8 && ((p>>8)&0xFF) < 8 && (p&0xFF) < 8)
a = 0;
((uint32_t*)dst)[x] = a | (rr<<16) | (gg<<8) | bb;
}
}
}
else if (punchKey)
{
for (int y = 0; y < img.h; y++)
{
unsigned char *dst = (unsigned char*)lr.pBits + y * lr.Pitch;
const uint32_t *src = &img.argb[(size_t)y * img.w];
for (int x = 0; x < img.w; x++)
{
uint32_t p = src[x];
if (((p>>16)&0xFF) < 8 && ((p>>8)&0xFF) < 8 && (p&0xFF) < 8)
p &= 0x00FFFFFFu;
((uint32_t*)dst)[x] = p;
}
}
}
else
{
for (int y = 0; y < img.h; y++)
memcpy((unsigned char*)lr.pBits + y * lr.Pitch,
&img.argb[(size_t)y * img.w], (size_t)img.w * 4);
}
tex->UnlockRect(0);
}
if (punchTex)
{
// Manual mip chain with RE-BINARIZED alpha per level: box-
// filter color from the previous level, then snap alpha to
// 0/255 at the 0x80 test threshold so the alpha-TEST result
// is stable across mip transitions (box-filtered alpha
// hovers around the threshold -> texels flicker in/out as
// slight camera movement shifts the mip level = the "walls
// render with noise / diagonal patterns" on the arena
// framework layers).
const DWORD nLevels = tex->GetLevelCount();
for (DWORD lv = 1; lv < nLevels; ++lv)
{
D3DLOCKED_RECT lrS, lrD;
D3DSURFACE_DESC dd, sd;
tex->GetLevelDesc(lv, &dd);
tex->GetLevelDesc(lv - 1, &sd);
if (FAILED(tex->LockRect(lv - 1, &lrS, NULL, D3DLOCK_READONLY))) break;
if (FAILED(tex->LockRect(lv, &lrD, NULL, 0))) { tex->UnlockRect(lv - 1); break; }
for (UINT y = 0; y < dd.Height; ++y)
{
const UINT sy0 = (y*2 < sd.Height) ? y*2 : sd.Height - 1;
const UINT sy1 = (y*2+1 < sd.Height) ? y*2+1 : sd.Height - 1;
uint32_t *dst = (uint32_t*)((unsigned char*)lrD.pBits + y * lrD.Pitch);
const uint32_t *s0 = (const uint32_t*)((const unsigned char*)lrS.pBits + sy0 * lrS.Pitch);
const uint32_t *s1 = (const uint32_t*)((const unsigned char*)lrS.pBits + sy1 * lrS.Pitch);
for (UINT x = 0; x < dd.Width; ++x)
{
const UINT sx0 = (x*2 < sd.Width) ? x*2 : sd.Width - 1;
const UINT sx1 = (x*2+1 < sd.Width) ? x*2+1 : sd.Width - 1;
const uint32_t p[4] = { s0[sx0], s0[sx1], s1[sx0], s1[sx1] };
uint32_t a = 0, r = 0, g = 0, b = 0;
for (int k = 0; k < 4; ++k)
{
a += (p[k] >> 24) & 0xFF; r += (p[k] >> 16) & 0xFF;
g += (p[k] >> 8) & 0xFF; b += p[k] & 0xFF;
}
a /= 4; r /= 4; g /= 4; b /= 4;
a = (a >= 0x80) ? 0xFF : 0x00; // re-binarize
dst[x] = (a << 24) | (r << 16) | (g << 8) | b;
}
}
tex->UnlockRect(lv);
tex->UnlockRect(lv - 1);
}
}
else
tex->GenerateMipSubLevels();
object->mDrawOps[i].texture.texture = tex;
}
}
}
}
return object;
}
d3d_OBJECT* d3d_OBJECT::LoadSpheres(LPDIRECT3DDEVICE9 device, char *fileName)
{
FILE *file;
float emissionColor[3];
int vertexCount;
if ((file = fopen(fileName, "rb")) == NULL)
return NULL;
fread(emissionColor, sizeof(float), 3, file);
fread(&vertexCount, sizeof(int), 1, file);
d3d_OBJECT *object = new d3d_OBJECT(device, vertexCount);
void *buffer = NULL;
object->mVB->Lock(0, 0, &buffer, 0);
fread(buffer, sizeof(L4POINTVERTEX), vertexCount, file);
D3DXVECTOR3 center(0, 0, 0);
D3DXComputeBoundingSphere((D3DXVECTOR3*)buffer, vertexCount, sizeof(L4POINTVERTEX), &center, &object->mRadius);
object->mVB->Unlock();
object->mDrawOps[0].material.Emissive.r = emissionColor[0];
object->mDrawOps[0].material.Emissive.g = emissionColor[1];
object->mDrawOps[0].material.Emissive.b = emissionColor[2];
object->mDrawOps[0].material.Emissive.a = 1.0f;
fclose(file);
return object;
}
L4TEXOP d3d_OBJECT::LoadTexture(LPDIRECT3DDEVICE9 device, const char *fileName)
{
L4TEXOP texOp;
FILE *file;
if (mTextureCache.find(fileName) == mTextureCache.end())
{
struct
{ unsigned char minify;
unsigned char magnify;
unsigned char alpha;
unsigned char wrap_u;
unsigned char wrap_v;
bool doScroll;
float scrollUDelta;
float scrollVDelta;
} fileTexOp;
char metFileName[512];
strcpy(metFileName, fileName);
chgext(metFileName, "met");
memset(&texOp, 0, sizeof(L4TEXOP));
if ((file = fopen(metFileName, "rb")) != NULL)
{
fread(&fileTexOp, sizeof(fileTexOp), 1, file);
fclose(file);
texOp.wrap_u = (L4TEXOP::WrapType)fileTexOp.wrap_u;
texOp.wrap_v = (L4TEXOP::WrapType)fileTexOp.wrap_v;
texOp.doScroll = fileTexOp.doScroll;
texOp.scrollUDelta = fileTexOp.scrollUDelta;
texOp.scrollVDelta = fileTexOp.scrollVDelta;
}
D3DXCreateTextureFromFileA(device, fileName, &texOp.texture);
mTextureCache.insert(std::pair<std::string,L4TEXOP>(std::string(fileName), texOp));
}
else
texOp = mTextureCache[std::string(fileName)];
texOp.texture->AddRef();
return texOp;
}
d3d_OBJECT::d3d_OBJECT(LPDIRECT3DDEVICE9 device, int vertCount)
: mDevice(device),
mVertCount(vertCount),
mDrawOpCount(1),
mImmune(false),
mIsShadow(0),
mVB(NULL),
mMesh(NULL),
mDrawOps(NULL),
mLocalToWorld(),
mOrigin(),
mRadius(0.0f),
mCullCenter(0.0f, 0.0f, 0.0f),
mCullRadius(-1.0f),
mBgfVB(NULL),
mBgfIB(NULL),
mBgfStride(0),
mBgfNumVerts(0),
mID(mNextID++)
{
HRESULT hr;
mDbgName[0] = '\0';
hr = device->CreateVertexBuffer(vertCount * sizeof(L4POINTVERTEX), D3DUSAGE_WRITEONLY, L4POINTVERTEX_FVF, D3DPOOL_MANAGED, &mVB, NULL);
mDrawOps = new L4DRAWOP();
memset(mDrawOps, 0, sizeof(L4DRAWOP));
memset(mNext, 0, sizeof(mNext));
memset(mPrev, 0, sizeof(mPrev));
D3DXMatrixIdentity(&mLocalToWorld);
}
d3d_OBJECT::d3d_OBJECT(LPDIRECT3DDEVICE9 device, LPD3DXMESH mesh, DWORD *adjacencyBuffer, int drawOpCount)
: mDevice(device),
mVertCount(-1),
mDrawOpCount(drawOpCount),
mImmune(false),
mIsShadow(0),
mVB(NULL),
mMesh(mesh),
mLocalToWorld(),
mOrigin(),
mRadius(0.0f),
mCullCenter(0.0f, 0.0f, 0.0f),
mCullRadius(-1.0f),
mBgfVB(NULL),
mBgfIB(NULL),
mBgfStride(0),
mBgfNumVerts(0),
mID(mNextID++)
{
mDbgName[0] = '\0';
mDrawOps = new L4DRAWOP[drawOpCount];
memset(mDrawOps, 0, sizeof(L4DRAWOP) * drawOpCount);
// PORT (frustum culling): compute the model-space bounding sphere once.
if (mesh != NULL)
{
void *vtx = NULL;
if (SUCCEEDED(mesh->LockVertexBuffer(D3DLOCK_READONLY, &vtx)) && vtx != NULL)
{
D3DXVECTOR3 c; FLOAT r = 0.0f;
if (SUCCEEDED(D3DXComputeBoundingSphere((const D3DXVECTOR3 *)vtx,
mesh->GetNumVertices(), mesh->GetNumBytesPerVertex(), &c, &r)))
{
mCullCenter = c;
mCullRadius = r;
}
mesh->UnlockVertexBuffer();
}
}
if (adjacencyBuffer != NULL)
{
//Null adjacency buffer means the mesh should have already been optimized
mesh->OptimizeInplace(D3DXMESHOPT_ATTRSORT | D3DXMESHOPT_VERTEXCACHE, adjacencyBuffer, NULL, NULL, NULL);
}
DWORD vertCount = mesh->GetNumVertices();
DWORD vertStride = mesh->GetNumBytesPerVertex();
D3DXVECTOR3 *data;
D3DXVECTOR3 center(0, 0, 0);
mesh->LockVertexBuffer(D3DLOCK_READONLY, (LPVOID*)&data);
D3DXComputeBoundingSphere(data, vertCount, vertStride, &center, &mRadius);
mesh->UnlockVertexBuffer();
memset(mNext, 0, sizeof(mNext));
memset(mPrev, 0, sizeof(mPrev));
D3DXMatrixIdentity(&mLocalToWorld);
}
d3d_OBJECT::~d3d_OBJECT()
{
if (mVB)
{
mVB->Release();
mVB = NULL;
}
if (mBgfVB)
{
mBgfVB->Release();
mBgfVB = NULL;
}
if (mBgfIB)
{
mBgfIB->Release();
mBgfIB = NULL;
}
if (mDrawOps)
{
delete[] mDrawOps;
mDrawOps = NULL;
}
}
// PORT (frustum culling): the frame's view frustum, set once per frame by the
// renderer (ExecuteImplementation). Only the 4 SIDE planes are used -- near/far
// are skipped so the pass-specific projection differences (decal near, sky far)
// can't wrongly cull, and the far-plane fog already hides distant pop.
static D3DXPLANE gCullPlanes[5];
static int gCullValid = 0;
int gBTNumCulled = 0; // per-frame culled-object count (diag; reset by the renderer)
static D3DXVECTOR3 gCamPos(0.0f, 0.0f, 0.0f); // camera world position (LOD distance selection)
void d3d_OBJECT::SetCameraPosition(float x, float y, float z)
{
gCamPos.x = x; gCamPos.y = y; gCamPos.z = z;
}
void d3d_OBJECT::SetCullFrustum(const D3DXMATRIX *viewProj)
{
if (viewProj == NULL) { gCullValid = 0; return; }
const D3DXMATRIX &m = *viewProj;
// standard Gribb-Hartmann plane extraction (row-major, v*M convention).
// 4 side planes + the FAR plane (beyond clip range the GPU clips the object
// anyway -- skipping it is a guaranteed no-visual-change win; the sky pass is
// excluded from culling and the decal projection shares the main far plane).
// The NEAR plane is skipped (pass-specific near differences).
gCullPlanes[0] = D3DXPLANE(m._14 + m._11, m._24 + m._21, m._34 + m._31, m._44 + m._41); // left
gCullPlanes[1] = D3DXPLANE(m._14 - m._11, m._24 - m._21, m._34 - m._31, m._44 - m._41); // right
gCullPlanes[2] = D3DXPLANE(m._14 + m._12, m._24 + m._22, m._34 + m._32, m._44 + m._42); // bottom
gCullPlanes[3] = D3DXPLANE(m._14 - m._12, m._24 - m._22, m._34 - m._32, m._44 - m._42); // top
gCullPlanes[4] = D3DXPLANE(m._14 - m._13, m._24 - m._23, m._34 - m._33, m._44 - m._43); // far
for (int i = 0; i < 5; ++i)
D3DXPlaneNormalize(&gCullPlanes[i], &gCullPlanes[i]);
gCullValid = 1;
}
void d3d_OBJECT::Draw(int pass, const D3DXMATRIX *viewTransform, Time targetRenderFrame)
{
// DIAG (BT_WATCH=<substr>): trace a named model's draw lifecycle -- proves
// whether a "popping" object stops being drawn (torn down upstream), gets
// frustum-culled, or draws-but-invisible. Logs every ~120th call + culls.
static const char *s_watch = getenv("BT_WATCH");
int watched = 0;
if (s_watch && s_watch[0] && mDbgName[0])
{
for (const char *p = mDbgName; *p && !watched; ++p)
if (_strnicmp(p, s_watch, strlen(s_watch)) == 0) watched = 1;
if (watched)
{
static int s_wn = 0;
if ((++s_wn % 120) == 1)
{
const float wdx = mLocalToWorld._41 - gCamPos.x;
const float wdz = mLocalToWorld._43 - gCamPos.z;
DEBUG_STREAM << "[watch] " << mDbgName << " id=" << mID
<< " pass=" << pass << " shadow=" << mIsShadow
<< " d=" << sqrtf(wdx*wdx + wdz*wdz)
<< " T=(" << mLocalToWorld._41 << "," << mLocalToWorld._42 << "," << mLocalToWorld._43 << ")"
<< " R0=(" << mLocalToWorld._11 << "," << mLocalToWorld._12 << "," << mLocalToWorld._13 << ")"
<< " R1=(" << mLocalToWorld._21 << "," << mLocalToWorld._22 << "," << mLocalToWorld._23 << ")"
<< " R2=(" << mLocalToWorld._31 << "," << mLocalToWorld._32 << "," << mLocalToWorld._33 << ")\n" << std::flush;
}
}
}
// PORT (frustum culling): skip objects whose world bounding sphere is fully
// outside the side planes. Sky is never culled (its dome/plane must always
// draw); unknown bounds (mCullRadius<=0, e.g. point-sphere objects) never cull.
if (gCullValid && pass != PASS_SKY && mCullRadius > 0.0f)
{
D3DXVECTOR3 wc;
D3DXVec3TransformCoord(&wc, &mCullCenter, &mLocalToWorld);
// conservative world radius: scale by the largest basis-row length
float sx = mLocalToWorld._11*mLocalToWorld._11 + mLocalToWorld._12*mLocalToWorld._12 + mLocalToWorld._13*mLocalToWorld._13;
float sy = mLocalToWorld._21*mLocalToWorld._21 + mLocalToWorld._22*mLocalToWorld._22 + mLocalToWorld._23*mLocalToWorld._23;
float sz = mLocalToWorld._31*mLocalToWorld._31 + mLocalToWorld._32*mLocalToWorld._32 + mLocalToWorld._33*mLocalToWorld._33;
float s2 = sx > sy ? (sx > sz ? sx : sz) : (sy > sz ? sy : sz);
float wr = mCullRadius * (s2 > 1.0f ? sqrtf(s2) : 1.0f);
for (int i = 0; i < 5; ++i)
if (gCullPlanes[i].a * wc.x + gCullPlanes[i].b * wc.y
+ gCullPlanes[i].c * wc.z + gCullPlanes[i].d < -wr)
{
++gBTNumCulled;
// DIAG (BT_CULL_LOG): dump big culled objects -- bad bounds show as
// a big radius culled at an implausible world centre.
if (wr > 40.0f && getenv("BT_CULL_LOG"))
{
static int s_cl = 0;
if (s_cl < 40)
{
++s_cl;
DEBUG_STREAM << "[cull] plane=" << i << " wc=(" << wc.x << ","
<< wc.y << "," << wc.z << ") wr=" << wr
<< " local=(" << mCullCenter.x << "," << mCullCenter.y << "," << mCullCenter.z
<< ") r=" << mCullRadius
<< " T=(" << mLocalToWorld._41 << "," << mLocalToWorld._42 << "," << mLocalToWorld._43
<< ")\n" << std::flush;
}
}
return; // fully outside -> skip
}
}
if (pass == PASS_OPAQUE)
{
static int dbgDraw = 0;
if (dbgDraw < 60)
{
DEBUG_STREAM << "[DRAW] opaque #" << dbgDraw << " worldT=("
<< mLocalToWorld._41 << "," << mLocalToWorld._42 << "," << mLocalToWorld._43
<< ") ops=" << mDrawOpCount << " mesh=" << (mMesh != NULL) << "\n" << std::flush;
++dbgDraw;
}
}
// set our world transform
mDevice->SetTransform(D3DTS_WORLD, &mLocalToWorld);
if (mMesh != NULL && pass != PASS_SPHERE)
return DrawMesh(pass, viewTransform, targetRenderFrame);
else if (mMesh == NULL && mVB != NULL && pass == PASS_SPHERE)
return DrawSpheres(pass, viewTransform);
}
void d3d_OBJECT::DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time targetRenderFrame)
{
// BT (task #20): the shadow proxy draws ONLY in the blend pass, as ~55%
// translucent black (constant TFACTOR color/alpha), no Z-write -- the
// authentic dark ground silhouette instead of the opaque black blob.
if (mIsShadow)
{
if (pass != PASS_ALPHABLEND)
return;
// Save EVERY state we touch and restore it exactly (a leaked LIGHTING
// FALSE / ALPHAARG1 TFACTOR darkened the whole scene on first attempt).
DWORD sBlend, sSrc, sDst, sZWrite, sLight, sTFactor;
DWORD sColorOp, sColorArg1, sAlphaOp, sAlphaArg1;
DWORD sDepthBias, sSlopeBias;
mDevice->GetRenderState(D3DRS_ALPHABLENDENABLE, &sBlend);
mDevice->GetRenderState(D3DRS_SRCBLEND, &sSrc);
mDevice->GetRenderState(D3DRS_DESTBLEND, &sDst);
mDevice->GetRenderState(D3DRS_ZWRITEENABLE, &sZWrite);
mDevice->GetRenderState(D3DRS_LIGHTING, &sLight);
mDevice->GetRenderState(D3DRS_TEXTUREFACTOR, &sTFactor);
mDevice->GetRenderState(D3DRS_DEPTHBIAS, &sDepthBias);
mDevice->GetRenderState(D3DRS_SLOPESCALEDEPTHBIAS, &sSlopeBias);
mDevice->GetTextureStageState(0, D3DTSS_COLOROP, &sColorOp);
mDevice->GetTextureStageState(0, D3DTSS_COLORARG1, &sColorArg1);
mDevice->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAlphaOp);
mDevice->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAlphaArg1);
mDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
mDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
mDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
mDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);
mDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
{
// DIAG (BT_SHADOW_COLOR=AARRGGBB hex): override the shadow tint --
// e.g. FFFF0000 opaque red proves whether the quad reaches pixels.
static DWORD s_shadowColor = 0;
if (s_shadowColor == 0)
{
const char *sc = getenv("BT_SHADOW_COLOR");
s_shadowColor = sc ? (DWORD)strtoul(sc, NULL, 16) : 0x8C000000;
if (s_shadowColor == 0) s_shadowColor = 0x8C000000;
}
mDevice->SetRenderState(D3DRS_TEXTUREFACTOR, s_shadowColor);
}
//
// DEPTH BIAS (task #20 shadow-vanish fix): the shadow proxy sits on the
// COLLISION surface (the mech origin), but the visual terrain mesh runs up
// to ~2u ABOVE the coarse collision solid on mound/mesa slopes -- a plain
// Z-test buries the whole quad there and the shadow VANISHES (and without
// its ground-contact cue, the authentic small foot-clip reads as "the mech
// is sinking"). The IG board drew ground shadows decal-style (depth-biased
// coplanar geometry -- the classic 90s blob-shadow technique; the shadow
// flicker visible in pod footage is exactly this bias fighting at edges).
// Pull the shadow toward the camera in depth so it stays ON the terrain
// wherever the surface runs slightly above the quad. BT_SHADOW_BIAS
// overrides (float, default -0.004 normalized depth ~= 2-4 world units at
// chase-view range on the 2100-far projection); =0 disables (plain Z-test).
//
{
static float s_bias = -1e9f;
if (s_bias == -1e9f)
{
const char *bv = getenv("BT_SHADOW_BIAS");
if (bv)
s_bias = (float)atof(bv);
else
{
// Default pairs with the tilt (task #49b): with the quad TILTED
// onto the slope (BT_SHADOW_TILT on, the default) only a decal
// epsilon is needed -- the old big bias (2-4 world units) also
// beat the mech's FEET in the depth test, painting the shadow
// OVER them. The flat-quad A/B fallback (BT_SHADOW_TILT=0)
// still needs the big bias to survive slope burial.
const char *tv = getenv("BT_SHADOW_TILT");
const int tiltOn = (tv == 0 || tv[0] != '0');
s_bias = tiltOn ? -0.0008f : -0.004f;
}
}
mDevice->SetRenderState(D3DRS_DEPTHBIAS, *(DWORD *)&s_bias);
float slope = s_bias == 0.0f ? 0.0f : -1.0f; // coplanar decal term
mDevice->SetRenderState(D3DRS_SLOPESCALEDEPTHBIAS, *(DWORD *)&slope);
}
mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR);
mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR);
const float _sdx = mLocalToWorld._41 - gCamPos.x;
const float _sdy = mLocalToWorld._42 - gCamPos.y;
const float _sdz = mLocalToWorld._43 - gCamPos.z;
const float shadowLodDist = sqrtf(_sdx*_sdx + _sdy*_sdy + _sdz*_sdz);
for (int iOp = 0; iOp < mDrawOpCount; iOp++)
{
if (mDrawOps[iOp].lodFar > 0.0f &&
(shadowLodDist < mDrawOps[iOp].lodNear || shadowLodDist >= mDrawOps[iOp].lodFar))
continue; // LOD band (see DrawMesh main loop)
gNumBatches++;
if (mDrawOps[iOp].bgfPrimCount > 0 && mBgfVB != NULL)
{
mDevice->SetFVF(L4VERTEX_FVF);
mDevice->SetStreamSource(0, mBgfVB, 0, mBgfStride);
mDevice->SetIndices(mBgfIB);
mDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, mBgfNumVerts,
mDrawOps[iOp].bgfStartIndex, mDrawOps[iOp].bgfPrimCount);
}
else
mMesh->DrawSubset(iOp);
}
mDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, sBlend);
mDevice->SetRenderState(D3DRS_SRCBLEND, sSrc);
mDevice->SetRenderState(D3DRS_DESTBLEND, sDst);
mDevice->SetRenderState(D3DRS_ZWRITEENABLE, sZWrite);
mDevice->SetRenderState(D3DRS_LIGHTING, sLight);
mDevice->SetRenderState(D3DRS_TEXTUREFACTOR, sTFactor);
mDevice->SetRenderState(D3DRS_DEPTHBIAS, sDepthBias);
mDevice->SetRenderState(D3DRS_SLOPESCALEDEPTHBIAS, sSlopeBias);
mDevice->SetTextureStageState(0, D3DTSS_COLOROP, sColorOp);
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, sColorArg1);
mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, sAlphaOp);
mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAlphaArg1);
return;
}
// ADDITIVE-LOD band gate: skip the ops whose [lodNear..lodFar) band excludes
// the eye distance (the BGF 0x2046 ranges pre-scaled by sqrt3 at load; see
// bgfload.cpp TAG_LOD -- under the decoded ADDITIVE rule every banded op
// carries [0..OutDist), so this is "drop the near-detail LOD once the eye
// is OutDist away"). lodFar==0 (zero-init non-BGF op) => always drawn.
// METRIC (decoded, lod-blink-decode workflow): the eye distance to the
// INSTANCE-TRANSFORMED OBJECT HOT SPOT -- default (0,0,0) = the instance
// origin (dpl_SetObjectLodHotSpot / s_dplobject.lod_hot_spot; no arena
// content carries a 0x2047 per-LOD reference). ONE shared d for ALL LODs
// of an object, so sibling bands hand off coherently (the OpenFlight "use
// previous slant range" contract). The earlier bounding-sphere-SURFACE
// metric was NOT authentic: per-piece radius offsets desynchronized the
// shared measurement point (each piece switched at its own position-
// dependent radius). With the sqrt3 range decode the authored massing
// ranges (2000) exceed the arena's reachable distances + clip (1300), so
// origin ranging no longer drops anything large in play.
const float _ldx = mLocalToWorld._41 - gCamPos.x;
const float _ldy = mLocalToWorld._42 - gCamPos.y;
const float _ldz = mLocalToWorld._43 - gCamPos.z;
const float lodDist = sqrtf(_ldx*_ldx + _ldy*_ldy + _ldz*_ldz);
for (int iOp = 0; iOp < mDrawOpCount; iOp++)
{
L4DRAWOP *drawOp = &mDrawOps[iOp];
if (drawOp->alphaTest != (pass == PASS_ALPHABLEND))
continue;
if (drawOp->drawAsDecal != (pass == PASS_DECAL))
continue;
if (drawOp->drawAsSky != (pass == PASS_SKY))
continue;
if (drawOp->lodFar > 0.0f && drawOp->lodFar < 1.0e8f)
{
const bool bandVis = !(lodDist < drawOp->lodNear || lodDist >= drawOp->lodFar);
// DIAG (BT_LOD_LOG): log every band-gate visibility FLIP with the model
// name + band + distance -- identifies exactly which piece blinks while
// the mech moves (the user's "objects appear/disappear with position").
static int s_lodLog = -1;
if (s_lodLog < 0) { const char *lv = getenv("BT_LOD_LOG"); s_lodLog = (lv != 0 && lv[0] == '1') ? 1 : 0; }
if (s_lodLog)
{
static int s_firstEval = 0;
if (s_firstEval < 5)
{
++s_firstEval;
DEBUG_STREAM << "[lodgate] eval " << (mDbgName[0] ? mDbgName : "?")
<< " op" << iOp << " d=" << lodDist << " far=" << drawOp->lodFar << "\n" << std::flush;
}
const int nowVis = bandVis ? 1 : 2;
if (drawOp->lastBandVisible != 0 && drawOp->lastBandVisible != nowVis)
{
static int s_flips = 0;
if (s_flips < 400)
{
++s_flips;
DEBUG_STREAM << "[lodflip] " << (mDbgName[0] ? mDbgName : "?")
<< " op" << iOp << (bandVis ? " IN " : " OUT ")
<< "band=[" << drawOp->lodNear << ".." << drawOp->lodFar
<< ") d=" << lodDist << "\n" << std::flush;
}
}
drawOp->lastBandVisible = nowVis;
}
if (!bandVis)
continue; // outside this LOD's viewing band
}
mDevice->SetMaterial(&drawOp->material);
#ifndef RP3_EMULATE
SetTextureScrolling(&(drawOp->texture), targetRenderFrame);
SetTexture(drawOp->texture.texture);
#endif
// PUNCH (dpl_Punchize): cutout op -- alpha-TEST the hole texels (alpha 0,
// keyed at load) in the opaque pass, z-write preserved. States restored
// right after the draw so ordinary ops are untouched.
DWORD sAT = 0, sARef = 0, sAFunc = 0;
if (drawOp->punchThrough)
{
mDevice->GetRenderState(D3DRS_ALPHATESTENABLE, &sAT);
mDevice->GetRenderState(D3DRS_ALPHAREF, &sARef);
mDevice->GetRenderState(D3DRS_ALPHAFUNC, &sAFunc);
mDevice->SetRenderState(D3DRS_ALPHATESTENABLE, TRUE);
mDevice->SetRenderState(D3DRS_ALPHAREF, 0x80);
mDevice->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL);
}
// LAYER DEPTH BIAS (additive-LOD coplanar resolution; see l4d3d.h):
// finer layers pulled a few depth-buffer steps toward the camera so
// flush duplicated surfaces resolve deterministically to the detail
// layer (the board's submission-order rule) instead of z-fighting.
DWORD sDB = 0;
if (drawOp->lodDepthBias != 0.0f)
{
mDevice->GetRenderState(D3DRS_DEPTHBIAS, &sDB);
mDevice->SetRenderState(D3DRS_DEPTHBIAS, *(const DWORD *)&drawOp->lodDepthBias);
}
gNumBatches++;
if (drawOp->bgfPrimCount > 0 && mBgfVB != NULL)
{
// direct-draw: known contiguous range, no attribute-table scan.
// (DrawSubset used to set the vertex decl itself -- assert the FVF here
// since beam/HUD passes change it mid-frame; runtime filters redundancy.)
mDevice->SetFVF(L4VERTEX_FVF);
mDevice->SetStreamSource(0, mBgfVB, 0, mBgfStride);
mDevice->SetIndices(mBgfIB);
mDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, mBgfNumVerts,
drawOp->bgfStartIndex, drawOp->bgfPrimCount);
}
else
mMesh->DrawSubset(iOp);
if (drawOp->punchThrough)
{
mDevice->SetRenderState(D3DRS_ALPHATESTENABLE, sAT);
mDevice->SetRenderState(D3DRS_ALPHAREF, sARef);
mDevice->SetRenderState(D3DRS_ALPHAFUNC, sAFunc);
}
if (drawOp->lodDepthBias != 0.0f)
mDevice->SetRenderState(D3DRS_DEPTHBIAS, sDB);
}
}
void d3d_OBJECT::DrawSpheres(int pass, const D3DXMATRIX *viewTransform)
{
mDevice->SetFVF(L4POINTVERTEX_FVF);
mDevice->SetStreamSource(0, mVB, 0, sizeof(L4POINTVERTEX));
mDevice->SetMaterial(&mDrawOps[0].material);
SetTexture(NULL);
mDevice->DrawPrimitive(D3DPT_POINTLIST, 0, mVertCount);
}
void d3d_OBJECT::SetTextureScrolling(const L4TEXOP *texture, Time targetRenderFrame)
{
if (d3d_OBJECT::mLastTextureScrollState != texture->doScroll)
{
// the texture scrolling state has changed
if (texture->doScroll)
{
mDevice->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);
SetTextureAddressing(texture->wrap_u, texture->wrap_v);
}
else
{
mDevice->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE);
SetTextureAddressing(L4TEXOP::WrapType::REPEAT, L4TEXOP::WrapType::REPEAT);
}
d3d_OBJECT::mLastTextureScrollState = texture->doScroll;
}
// if the texture is supposed to be scrolled then
// we have to set the transforms regardless of state
if (texture->doScroll)
{
Scalar time = (Scalar)targetRenderFrame;
D3DXMATRIX translate;
D3DXMatrixIdentity(&translate);
// 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);
}
}
void d3d_OBJECT::SetTextureAddressing(L4TEXOP::WrapType wrap_u, L4TEXOP::WrapType wrap_v)
{
if (wrap_u != d3d_OBJECT::mLastWrapU)
{
d3d_OBJECT::mLastWrapU = wrap_u;
if (wrap_u == L4TEXOP::REPEAT)
mDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP);
else if (wrap_u == L4TEXOP::CLAMP)
mDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP);
else if (wrap_u == L4TEXOP::SELECT)
mDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_BORDER);
}
if (wrap_v != d3d_OBJECT::mLastWrapV)
{
d3d_OBJECT::mLastWrapV = wrap_v;
if (wrap_v == L4TEXOP::REPEAT)
mDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);
else if (wrap_v == L4TEXOP::CLAMP)
mDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP);
else if (wrap_v == L4TEXOP::SELECT)
mDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_BORDER);
}
}
void d3d_OBJECT::SetTexture(LPDIRECT3DTEXTURE9 texture)
{
bool texturingOn = (texture != NULL);
if (texturingOn != d3d_OBJECT::mLastTexturingState)
{
d3d_OBJECT::mLastTexturingState = texturingOn;
if (texturingOn)
{
mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
mDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
}
else
mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_DISABLE);
}
if (texture != NULL)
{
mDevice->SetTexture(0, texture);
}
}