#include "mungal4.h" #pragma hdrstop #include "l4d3d.h" #include "L4VIDEO.h" #include "bgfload.h" #include "image.h" #include #include 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 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::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; object->mDrawOps[i].copRole = 0; 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 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].copRole = data.batches[i].copRole; // punch stencil-cut kit (task #55) object->mDrawOps[i].texture.texture = NULL; object->mDrawOps[i].texture.wrap_u = L4TEXOP::REPEAT; 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 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 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), ¢er, &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(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, ¢er, &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=): 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]; // COCKPIT PUNCH KIT (task #55): aperture-MASK ops never draw colour -- // they are consumed by the FOLLOWING hull op's stencil-cut sequence. if (drawOp->copRole == 1) continue; 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++; // COCKPIT PUNCH STENCIL-CUT (task #55, the i860 'damageize' mechanism): // a role-2 HULL op is preceded by its role-1 aperture-MASK op. Sequence: // 1. draw the mask stencil-only (no colour, no z-write, z-TESTED) -> ref 1 // 2. draw the hull with stencil-reject where ref==1 (the window cutouts) // 3. re-draw the mask writing stencil 0 (clean up for other punch groups) // The stencil never touches z, so entities drawn later still show through // the apertures correctly. const bool copCut = (drawOp->copRole == 2 && iOp > 0 && mDrawOps[iOp - 1].copRole == 1 && mDrawOps[iOp - 1].bgfPrimCount > 0 && drawOp->bgfPrimCount > 0 && mBgfVB != NULL); if (copCut) { L4DRAWOP *maskOp = &mDrawOps[iOp - 1]; mDevice->SetFVF(L4VERTEX_FVF); mDevice->SetStreamSource(0, mBgfVB, 0, mBgfStride); mDevice->SetIndices(mBgfIB); // 1. mask -> stencil ref 1 (colour + z-write off) mDevice->SetRenderState(D3DRS_COLORWRITEENABLE, 0); mDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE); mDevice->SetRenderState(D3DRS_STENCILENABLE, TRUE); mDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS); mDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE); mDevice->SetRenderState(D3DRS_STENCILREF, 1); mDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, mBgfNumVerts, maskOp->bgfStartIndex, maskOp->bgfPrimCount); // 2. hull, stencil-rejected under the mask mDevice->SetRenderState(D3DRS_COLORWRITEENABLE, D3DCOLORWRITEENABLE_RED | D3DCOLORWRITEENABLE_GREEN | D3DCOLORWRITEENABLE_BLUE | D3DCOLORWRITEENABLE_ALPHA); mDevice->SetRenderState(D3DRS_ZWRITEENABLE, TRUE); mDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_NOTEQUAL); mDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_KEEP); mDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, mBgfNumVerts, drawOp->bgfStartIndex, drawOp->bgfPrimCount); // 3. mask again -> stencil ref 0 (clean) mDevice->SetRenderState(D3DRS_COLORWRITEENABLE, 0); mDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE); mDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS); mDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE); mDevice->SetRenderState(D3DRS_STENCILREF, 0); mDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, mBgfNumVerts, maskOp->bgfStartIndex, maskOp->bgfPrimCount); // restore mDevice->SetRenderState(D3DRS_STENCILENABLE, FALSE); mDevice->SetRenderState(D3DRS_COLORWRITEENABLE, D3DCOLORWRITEENABLE_RED | D3DCOLORWRITEENABLE_GREEN | D3DCOLORWRITEENABLE_BLUE | D3DCOLORWRITEENABLE_ALPHA); mDevice->SetRenderState(D3DRS_ZWRITEENABLE, TRUE); } else 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); } }