Files
BT412/engine/MUNGA_L4/L4D3D.cpp
T
arcattackandClaude Fable 5 b6fe686b22 Shadow: authentic terrain tilt + decal draw order (feet layer correctly)
User report: the ground shadow painted OVER the mech's feet.  Root cause: the
flat quad + big depth-bias (-0.004 ~= 2-4 world units) workaround for slope
burial -- a bias big enough to beat the TERRAIN also beat the FEET in the
depth test, and the shadow drew in the late alpha-blend pass (after the mech),
so z-arbitration was the only layering control.

Fix, four co-dependent parts (user-verified live: feet on top, opaque,
survives inclines/declines):

- TILT (mech.cpp UpdateShadowJoint): apply the SKL's own contract for
  jointshadow ("apply terrain angle to pitch and roll") -- quad up aligned to
  the surface normal via an orthonormal basis fed to the ENGINE's own
  LinearMatrix->EulerAngles conversion.  Hand-derived Euler signs are exactly
  how the prior tilt attempts "dug into the hillside"; the engine conversion
  is convention-proof.  ~35 deg cliff-guard cap.
- SAMPLER (mech4.cpp): surface gradient from the collision probes PLUS
  BTVisualGroundLift per probe -- the quad hugs the VISIBLE terrain, whose
  lift varies across a slope; world->local rotation by the TRUE yaw from
  localToWorld, not the drift-prone gDriveHeading mirror (the task-#48 bug
  class).  BT_SHADOW_LOG traces the normals.
- DRAW ORDER (new PASS_SHADOW, l4d3d.h/L4VIDRND/L4VIDEO): shadows draw
  between the STATIC terrain and the DYNAMIC opaque bodies -- the classic
  decal order.  The mech, drawn after, z-passes over the shadow: the bias
  can never paint the shadow over the feet, structurally.
- BIAS pairing (L4D3D.cpp): tilt on (default) -> decal epsilon -0.0008;
  BT_SHADOW_TILT=0 flat fallback -> the old -0.004.  BT_SHADOW_BIAS overrides.

KB (locomotion.md): the four-part arrangement recorded as co-dependent, with
the gait-desync-symptom diagnostic hint retained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:59:48 -05:00

1220 lines
45 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;
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 (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;
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 = (0.299f*((p>>16)&0xFF) + 0.587f*((p>>8)&0xFF) + 0.114f*(p&0xFF)) / 255.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);
translate._31 = -texture->scrollUDelta * time;
translate._32 = texture->scrollVDelta * time;
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);
}
}