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) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-13 21:24:06 -05:00
co-authored by Claude Opus 4.8
parent a9c0d2c854
commit 083b148bb0
2 changed files with 78 additions and 8 deletions
+15
View File
@@ -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 `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 (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] 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=<near> <far> <r> <g> <b>` (+ 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 — - **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 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 all 5 projection sites (it was mis-applied as vertical). The device depth-stencil is now
+63 -8
View File
@@ -4697,6 +4697,11 @@ void
if(master_notation_file->GetEntry(starting_page_name, "fog" ,&TempStringPtr)) if(master_notation_file->GetEntry(starting_page_name, "fog" ,&TempStringPtr))
{ {
sscanf(TempStringPtr, "%f %f %f %f %f", &fogNear, &fogFar, &fogRed, &fogGreen, &fogBlue); 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; searchLightFogRed = fogRed;
searchLightFogGreen = fogGreen; searchLightFogGreen = fogGreen;
searchLightFogBlue = fogBlue; searchLightFogBlue = fogBlue;
@@ -4709,6 +4714,9 @@ void
noSearchLightFogFar = fogFar; noSearchLightFogFar = fogFar;
currentFogNear = fogNear; currentFogNear = fogNear;
currentFogFar = fogFar; 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 // 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); // 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 //TODO: for fog testing, just set the values here
@@ -4722,13 +4730,15 @@ void
else else
{ {
// //
// BT port (task #20): the SHIPPED maps define no fog entry at all (verified: // CORRECTION (task #63): task #20's claim "the SHIPPED maps define no fog"
// no 'fog' key in any .MAP source nor in BTL4.RES strings) -- so the world // was WRONG -- it looked in .MAP/.RES, but the arcade sourced fog from the
// edge and the void beyond are fully visible, which no pod player ever saw // DPL env INI (L4DPLCFG = content/BTDPL.INI), which authors a fog= (usually
// (cockpit FOV + mission design kept views inward). Provide a period- // nosearchlightfog= too) key on EVERY map/time/weather leaf page. The main
// plausible default curtain: color matched to the horizon so geometry fades // (if) branch above handles the real authored fog; this else branch is only
// into the void seamlessly. env BT_FOG="near far r g b" overrides; // reached by a page that genuinely lacks a fog= key (e.g. a *_default page
// BT_FOG=0 disables. // 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; float fN = 150.0f, fF = 520.0f, fR = 0.44f, fG = 0.44f, fB = 0.62f;
int fogOn = 1; int fogOn = 1;
@@ -5167,7 +5177,11 @@ void
} }
else if(strcmp(target_string,"weather") == 0) 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<<target_string<<" = "<<compare_strings[token_count]<<"\n"; // std::cout<<target_string<<" = "<<compare_strings[token_count]<<"\n";
} }
else if(strcmp(target_string,"scenario") == 0) else if(strcmp(target_string,"scenario") == 0)
@@ -8191,6 +8205,47 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
} }
mDevice->SetRenderState(D3DRS_FOGSTART, *((DWORD*)(&currentFogNear))); mDevice->SetRenderState(D3DRS_FOGSTART, *((DWORD*)(&currentFogNear)));
mDevice->SetRenderState(D3DRS_FOGEND, *((DWORD*)(&currentFogFar))); mDevice->SetRenderState(D3DRS_FOGEND, *((DWORD*)(&currentFogFar)));
// 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_ZWRITEENABLE, true);
mDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false); mDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false);
mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);