From 083b148bb00b9c34b63219becb5e87ab80da3107 Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 13 Jul 2026 21:24:06 -0500 Subject: [PATCH] Render: FOG restored -- authentic per-map/time/weather fog now renders (task #63) User: "aren't some levels supposed to have fog?" -- they were, and none showed. BTDPL.INI (the DPL env INI, L4DPLCFG) authors fog=near far r g b (+ often nosearchlightfog=) on EVERY map/time/WEATHER leaf; weather is a fixed egg field (clear/fog/soup). Shipped eggs pin cavern/night/clear -> page dsnitclear (near 90, far 1100, dark blue). The env pipeline resolved + pushed it to D3D, but it rendered INVISIBLE: D3DRS_FOGTABLEMODE=D3DFOG_LINEAR (table fog) derives its factor from the perspective-NONLINEAR z-buffer, so without WFOG the 90..1100 range collapsed to fog-factor ~1 (no fog everywhere). Fix (L4VIDEO.cpp world pass): auto-detect D3DPRASTERCAPS_WFOG -> per-pixel W-fog (smooth, == the arcade dpl_fog_type_pixel_lin); else VERTEX fog (eye-space but per-vertex -> splotchy on coarse terrain tris, so only a fallback). FOGENABLE + the fog mode are re-asserted each world-pass frame (the old one-time set got clobbered per-pass, which is why plain table fog showed nothing). Verified on this GPU: WFOG present -> per-pixel table fog, user-confirmed smooth. Also corrected task #20's false "shipped maps define no fog" comment (it checked .MAP/.RES, not BTDPL.INI). Env hooks: BT_FOGMODE=table|vertex|off, BT_WEATHER= clear|fog|soup, BT_FOG="near far r g b" (now honored on the authored branch too); [fog] resolved/model diagnostics. KB: rendering.md fog section added. Co-Authored-By: Claude Opus 4.8 (1M context) --- context/rendering.md | 15 ++++++++ engine/MUNGA_L4/L4VIDEO.cpp | 71 ++++++++++++++++++++++++++++++++----- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/context/rendering.md b/context/rendering.md index 1f44c7f..fa87321 100644 --- a/context/rendering.md +++ b/context/rendering.md @@ -52,6 +52,21 @@ DECLOUDS must stay in the sky pass). Authored TEXTURE **SCROLL** (BMF TEXTURE ta `L4DPLCFG`): resolves map+time → clip/fog/ambient + a directional sun. Bring-up stubs (EnsureValidProjection / per-frame ambient / uninit cloud colours) had clobbered it — now they RE-ASSERT the env values. Debug: copy BTDPL.INI → *DBG.INI with `debug=True`. [T2] +- **Fog — per map/time/WEATHER (task #63).** `BTDPL.INI` authors `fog= ` (+ often + `nosearchlightfog=`) on EVERY map/time/weather leaf. WEATHER is a fixed authored egg field + (`clear`/`fog`/`soup`, `mission->GetMissionWeather()`; missing = fatal in the arcade); the env pipeline + branches location→time→weather to the leaf. Shipped eggs pin `cavern/night/clear` → page `dsnitclear` + (near 90, far 1100, dark blue 0.1,0.1,0.12); `fog`→`dsnitfog` (500), `soup`→`dsnitsoup` (300). The + arcade fog model is per-PIXEL linear (`dpl_fog_type_pixel_lin`, dpl.h:265). **It rendered INVISIBLE in + the port:** `D3DRS_FOGTABLEMODE=D3DFOG_LINEAR` (table fog) reads the perspective-NONLINEAR z-buffer, so + without WFOG the authored 90..1100 range collapsed to fog-factor≈1 (no fog). FIX (L4VIDEO.cpp world + pass ~8194): auto-detect `D3DPRASTERCAPS_WFOG` → per-pixel W-fog (smooth, == the arcade); else VERTEX + fog (eye-space but per-vertex → splotchy on coarse terrain tris); FOGENABLE + mode re-asserted each + world-pass frame (the old one-time set got clobbered per-pass). **Task #20's "shipped maps define no + fog" was WRONG** — it checked `.MAP`/`.RES`, but fog lives in BTDPL.INI (the DPL env INI), as in the + arcade. Night fog is dark BY DESIGN (distance→murk, not light haze); day pages author light haze. Env: + `BT_FOGMODE=table|vertex|off`, `BT_WEATHER=clear|fog|soup`, `BT_FOG="near far r g b"` (all branches); + `[fog] resolved`/`[fog] model` log lines. [T1 pipeline / T0 arcade model / T2 fix] - **Projection / stencil (task #55):** the BTDPL.INI `viewangle=60` is the **HORIZONTAL** FOV — authentic 60×47 frustum at 4:3; fovY derived via `BTFovYFromHorizontal` (L4VIDEO.cpp), applied at all 5 projection sites (it was mis-applied as vertical). The device depth-stencil is now diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 855a376..a122a8d 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -4697,6 +4697,11 @@ void if(master_notation_file->GetEntry(starting_page_name, "fog" ,&TempStringPtr)) { sscanf(TempStringPtr, "%f %f %f %f %f", &fogNear, &fogFar, &fogRed, &fogGreen, &fogBlue); + // BT (task #63): BT_FOG="near far r g b" overrides the AUTHORED fog for + // tuning/demo (works on the main branch now, not just the no-fog fallback). + if (const char *bf = getenv("BT_FOG")) + if (!(bf[0]=='0' && bf[1]==0)) + sscanf(bf, "%f %f %f %f %f", &fogNear, &fogFar, &fogRed, &fogGreen, &fogBlue); searchLightFogRed = fogRed; searchLightFogGreen = fogGreen; searchLightFogBlue = fogBlue; @@ -4709,6 +4714,9 @@ void noSearchLightFogFar = fogFar; currentFogNear = fogNear; currentFogFar = fogFar; + DEBUG_STREAM << "[fog] resolved page='" << starting_page_name << "' near=" << fogNear + << " far=" << fogFar << " rgb=(" << fogRed << "," << fogGreen << "," + << fogBlue << ")\n" << std::flush; // Force a 0-0 black fog on startup // dpl_SetViewFog(dplMainView, dpl_fog_type_pixel_lin, 0.0, 0.0, 0.0, 0.01, 0.05); //TODO: for fog testing, just set the values here @@ -4722,13 +4730,15 @@ void else { // - // BT port (task #20): the SHIPPED maps define no fog entry at all (verified: - // no 'fog' key in any .MAP source nor in BTL4.RES strings) -- so the world - // edge and the void beyond are fully visible, which no pod player ever saw - // (cockpit FOV + mission design kept views inward). Provide a period- - // plausible default curtain: color matched to the horizon so geometry fades - // into the void seamlessly. env BT_FOG="near far r g b" overrides; - // BT_FOG=0 disables. + // CORRECTION (task #63): task #20's claim "the SHIPPED maps define no fog" + // was WRONG -- it looked in .MAP/.RES, but the arcade sourced fog from the + // DPL env INI (L4DPLCFG = content/BTDPL.INI), which authors a fog= (usually + // nosearchlightfog= too) key on EVERY map/time/weather leaf page. The main + // (if) branch above handles the real authored fog; this else branch is only + // reached by a page that genuinely lacks a fog= key (e.g. a *_default page + // whose weather compare is commented out AND has no own fog). Keep a period- + // plausible fallback curtain there so such a page still fades to the horizon; + // BT_FOG="near far r g b" overrides, BT_FOG=0 disables. // float fN = 150.0f, fF = 520.0f, fR = 0.44f, fG = 0.44f, fB = 0.62f; int fogOn = 1; @@ -5167,7 +5177,11 @@ void } else if(strcmp(target_string,"weather") == 0) { - compare_strings[token_count] = mission->GetMissionWeather(); + // BT (task #63): BT_WEATHER overrides the egg's authored weather so + // clear/fog/soup fog curtains can be A/B'd without re-authoring eggs + // (the shipped eggs pin weather=clear). Authentic default = the egg. + const char *wov = getenv("BT_WEATHER"); + compare_strings[token_count] = (wov && *wov) ? wov : mission->GetMissionWeather(); // std::cout<SetRenderState(D3DRS_FOGSTART, *((DWORD*)(¤tFogNear))); mDevice->SetRenderState(D3DRS_FOGEND, *((DWORD*)(¤tFogFar))); + // BT (task #63): the arcade fog was EYE-SPACE linear (dpl_fog_type_pixel_lin). + // The port left D3DRS_FOGTABLEMODE = D3DFOG_LINEAR (table/PIXEL fog), whose fog + // factor comes from the perspective-NONLINEAR z-buffer -- and without a WFOG cap + // FOGSTART/END are z-buffer [0,1] units, so the authored 90..1100 range gave a + // fog factor of ~1 (NO fog) across the entire scene: fog was wired but invisible. + // Use VERTEX fog: it is computed per-vertex in EYE SPACE, so FOGSTART/END are the + // authored world near/far and the haze curtain renders across the real range. + // This is the world-geometry pass (SHOULD be fogged; sky/cockpit passes manage + // their own). BT_FOGMODE=table = the old z-based mode (A/B); BT_FOGMODE=off. + { + static const char *s_fm = getenv("BT_FOGMODE"); + static const int s_fOff = (s_fm && strcmp(s_fm, "off") == 0) ? 1 : 0; + // Fog MODEL. The arcade used per-PIXEL linear fog (dpl_fog_type_pixel_lin) -- + // smooth. D3D fixed-function per-pixel (TABLE) fog is only eye-space (correct + // over the authored near/far) when the GPU exposes D3DPRASTERCAPS_WFOG; without + // it, table fog reads the NONLINEAR z-buffer and the 90..1100 range collapses + // to no fog. Auto-detect: WFOG -> table (smooth, faithful); else VERTEX fog + // (eye-space but per-vertex -> splotchy on coarse terrain tris). s_mode: + // 1=table, 0=vertex. BT_FOGMODE=table|vertex|off forces. + static int s_mode = -1; + if (s_mode < 0) + { + if (s_fm && strcmp(s_fm, "table") == 0) s_mode = 1; + else if (s_fm && strcmp(s_fm, "vertex") == 0) s_mode = 0; + else + { + D3DCAPS9 caps; memset(&caps, 0, sizeof(caps)); + s_mode = (SUCCEEDED(mDevice->GetDeviceCaps(&caps)) + && (caps.RasterCaps & D3DPRASTERCAPS_WFOG)) ? 1 : 0; + } + DEBUG_STREAM << "[fog] model=" << (s_mode ? "table/WFOG (per-pixel, smooth)" + : "vertex (per-vertex; splotchy on coarse tris)") + << " (BT_FOGMODE=table|vertex|off overrides)\n" << std::flush; + } + mDevice->SetRenderState(D3DRS_FOGENABLE, s_fOff ? FALSE : TRUE); + if (!s_fOff) + { + mDevice->SetRenderState(D3DRS_FOGVERTEXMODE, s_mode ? D3DFOG_NONE : D3DFOG_LINEAR); + mDevice->SetRenderState(D3DRS_FOGTABLEMODE, s_mode ? D3DFOG_LINEAR : D3DFOG_NONE); + } + } mDevice->SetRenderState(D3DRS_ZWRITEENABLE, true); mDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false); mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);