Implement fog: source-derived depth formula, not a guess
BTDPL.INI's fog values for this mission (testarn.egg = arena1/day/clear,
[ardayclear] leaf: fog=200.0 1250.0 0.32 0.3 0.5, clip=0.25 1300.0) are exact,
verified ground truth. The depth term took two tries: `tz` (texz) looked like
the right candidate (BT411 says fog is linear on "W") but measured live it
DECREASES with distance -- the wrong direction/shape. Re-derived from the
actual firmware source instead of guessing again: AS860/XFPROJ.SS's own
comment on the projection code ("pz=wz*invz, where pz=1 at near clip, 0 at
infinity") establishes the z-buffer value as a normalized hither/distance
quantity, giving distance = hither * 2^20 / raw_zbuf_value.
gpu_raster.py: added a 10th shader output channel carrying the raw z-buffer
float (previously only a filled/empty flag existed), and the fog blend using
this formula + BTDPL.INI's exact color. Verified offline: distance climbs
smoothly (394->1051) across a receding ceiling, landing inside the authored
200-1250 range with no further fudging; visually the horizon shows the
authored blue-purple haze, correctly absent near the camera.
IG-SHADING-MODEL.md: documented the false start (tz) and the source-grounded
fix, with the honest caveats that remain (2^20 exact scale, hither as a fixed
per-mission constant vs. per-view from the wire).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -204,16 +204,42 @@ Verified live from the actual wire + firmware source (`sda4/DPL3/VRENDER/PXPL5SU
|
||||
rule) against its r24/g24/b24 value to see if unlit polys show the
|
||||
material ramp's actual endpoint colors.
|
||||
|
||||
### 6b. Fog — NOT YET IMPLEMENTED in this renderer
|
||||
### 6b. Fog — IMPLEMENTED (2026-07-20), depth formula source-derived
|
||||
BT411 (`rendering.md` §Fog) has the complete, verified model: per-map/time/
|
||||
weather leaf in `content/BTDPL.INI` (`fog=<near> <far> <r> <g> <b>`), per-pixel
|
||||
LINEAR fog on perspective W (not the nonlinear z-buffer). This renderer already
|
||||
computes the per-pixel `tz` (perspective W-ish denominator) for every polygon
|
||||
(the M5 divide), so the mechanism to blend by depth already exists — what's
|
||||
missing is (a) reading the CURRENT mission's resolved BTDPL.INI fog leaf
|
||||
(map+time+weather → near/far/rgb) and (b) the blend itself
|
||||
(`out = mix(surface_color, fog_rgb, clamp((depth-near)/(far-near), 0, 1))`
|
||||
applied after texture/flat-color resolution). Not yet attempted.
|
||||
LINEAR fog on distance. For THIS mission (testarn.egg = arena1/day/clear) the
|
||||
leaf is `[ardayclear]`: `clip=0.25 1300.0`, `fog=200.0 1250.0 0.32 0.3 0.5` —
|
||||
verified against the live game tree, exact match.
|
||||
|
||||
**The depth term was initially attempted with `tz` (texz) directly, which is
|
||||
WRONG** — measured live (a vertical scan up a receding ceiling, row 0 to row
|
||||
160): `tz` DECREASES with distance (324 -> 121), i.e. it behaves as an
|
||||
inverse-depth quantity, not the "linear on W" BT411 describes. Re-derived from
|
||||
actual firmware SOURCE instead of guessing again: `sda4/DPL3/VRENDER/AS860/
|
||||
XFPROJ.SS`'s own comment on the projection code says `invz=eye_d/rz` and
|
||||
`pz=wz*invz (where pz=1 at near clip, 0 at infinity)` — i.e. the z-buffer value
|
||||
(`dvpx_zbuf`, 20-bit) is a NORMALIZED `hither/distance` quantity. So:
|
||||
|
||||
```
|
||||
distance = hither * 2^20 / raw_zbuf_value
|
||||
```
|
||||
|
||||
Implemented in `gpu_raster.py` (`FOG_HITHER/FOG_ZSCALE/FOG_NEAR/FOG_FAR/FOG_RGB`
|
||||
+ the shader's new 10th output channel carrying the raw z-buffer float). Sanity
|
||||
check (frame 11, netdeath capture, a receding ceiling): distance climbs
|
||||
394 -> 1051 smoothly from the near edge to the horizon vanishing point,
|
||||
landing squarely inside the authored 200-1250 fog range with no re-guessing
|
||||
needed after the tz correction. Visually: the horizon now shows the authored
|
||||
blue-purple haze (0.32,0.3,0.5), fading correctly toward the near floor.
|
||||
|
||||
**Not yet independently cross-validated:** the exact 2^20 scale (vs. 2^20-1 or
|
||||
a slightly different fixed-point convention) and treating `hither` as a fixed
|
||||
per-mission constant (0.25) rather than reading it per-view from the wire's
|
||||
view-flush payload (the OTHER renderer already parses `hither +44, yon +48` —
|
||||
see [[dpl3-revive-integration]] memory) are reasonable, source-grounded
|
||||
choices, not exact proof. Good enough to trust qualitatively; treat exact
|
||||
near/far crossover distances as approximate until cross-checked against a
|
||||
reference screenshot or the view-flush hither/yon.
|
||||
|
||||
### 6c. HUD — not rendering at all, cause NOT YET INVESTIGATED
|
||||
No wire-level investigation done yet. BT411's `gauges-hud.md` /
|
||||
|
||||
@@ -19,6 +19,23 @@ W, H = 832, 512
|
||||
PROG_LO, PROG_HI = 0x08158000, 0x08170000
|
||||
EDGE = {0x42, 0x0d, 0x2c}
|
||||
|
||||
# --- Fog (IG-SHADING-MODEL.md sec 6b) ---
|
||||
# BTDPL.INI arena_day_default -> ardayclear leaf (THIS mission: testarn.egg =
|
||||
# arena1/day/clear): fog=200.0 1250.0 0.32 0.3 0.5, clip=0.25 1300.0. Exact
|
||||
# match, verified against the live game tree.
|
||||
#
|
||||
# Depth term derived from source, not guessed: AS860/XFPROJ.SS's own comment
|
||||
# on the projection code -- "pz=wz*invz (where pz=1 at near clip, 0 at
|
||||
# infinity)" -- i.e. the z-buffer value is a normalized hither/distance
|
||||
# quantity, quantized to the 20-bit dvpx_zbuf field. So distance =
|
||||
# hither * 2^20 / raw_z. The 2^20 scale and treating hither as a fixed
|
||||
# per-mission constant (vs. reading it per-view from the wire) are NOT yet
|
||||
# independently cross-validated -- see IG-SHADING-MODEL.md sec 6b.
|
||||
FOG_HITHER = 0.25
|
||||
FOG_ZSCALE = float(1 << 20)
|
||||
FOG_NEAR, FOG_FAR = 200.0, 1250.0
|
||||
FOG_RGB = np.array([0.32, 0.3, 0.5]) * 255.0
|
||||
|
||||
FX_SHADER = """
|
||||
#version 430
|
||||
#extension GL_ARB_gpu_shader_fp64 : enable
|
||||
@@ -61,15 +78,20 @@ void main() {
|
||||
litR = float(prims[b+40]); litG = float(prims[b+41]); litB = float(prims[b+42]);
|
||||
}
|
||||
int pix = y*width + x;
|
||||
outbuf[pix*9+0] = (zbest > -1e29lf) ? 0x3f800000u : 0xff800000u;
|
||||
outbuf[pix*9+1] = floatBitsToUint(float(uu));
|
||||
outbuf[pix*9+2] = floatBitsToUint(float(vv));
|
||||
outbuf[pix*9+3] = floatBitsToUint(float(tz));
|
||||
outbuf[pix*9+4] = floatBitsToUint(tid);
|
||||
outbuf[pix*9+5] = floatBitsToUint(hasuv);
|
||||
outbuf[pix*9+6] = floatBitsToUint(litR);
|
||||
outbuf[pix*9+7] = floatBitsToUint(litG);
|
||||
outbuf[pix*9+8] = floatBitsToUint(litB);
|
||||
outbuf[pix*10+0] = (zbest > -1e29lf) ? 0x3f800000u : 0xff800000u;
|
||||
outbuf[pix*10+1] = floatBitsToUint(float(uu));
|
||||
outbuf[pix*10+2] = floatBitsToUint(float(vv));
|
||||
outbuf[pix*10+3] = floatBitsToUint(float(tz));
|
||||
outbuf[pix*10+4] = floatBitsToUint(tid);
|
||||
outbuf[pix*10+5] = floatBitsToUint(hasuv);
|
||||
outbuf[pix*10+6] = floatBitsToUint(litR);
|
||||
outbuf[pix*10+7] = floatBitsToUint(litG);
|
||||
outbuf[pix*10+8] = floatBitsToUint(litB);
|
||||
// raw z-buffer plane value (dvpx_zbuf): per AS860/XFPROJ.SS's own comment,
|
||||
// "pz=wz*invz (where pz=1 at near clip, 0 at infinity)" -- a normalized
|
||||
// hither/distance quantity, quantized to the 20-bit zbuf field. Used for
|
||||
// fog depth (see Renderer.frame): distance = hither*2^20 / raw_z.
|
||||
outbuf[pix*10+9] = floatBitsToUint(float(zbest > -1e29lf ? zbest : 0.0lf));
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -161,13 +183,13 @@ class Renderer:
|
||||
return None
|
||||
n_prims = len(prims) // 11
|
||||
b0 = self.ctx.buffer(prims.tobytes())
|
||||
out = self.ctx.buffer(reserve=W * H * 9 * 4)
|
||||
out = self.ctx.buffer(reserve=W * H * 10 * 4)
|
||||
b0.bind_to_storage_buffer(0)
|
||||
out.bind_to_storage_buffer(1)
|
||||
self.prog_gl['n_prims'] = n_prims
|
||||
self.prog_gl['width'] = W
|
||||
self.prog_gl.run(group_x=(W + 63) // 64, group_y=H)
|
||||
raw = np.frombuffer(out.read(), dtype=np.uint32).reshape(H, W, 9)
|
||||
raw = np.frombuffer(out.read(), dtype=np.uint32).reshape(H, W, 10)
|
||||
b0.release(); out.release()
|
||||
zb = raw[..., 0].view(np.float32)
|
||||
uu = raw[..., 1].view(np.float32).astype(np.float64)
|
||||
@@ -175,6 +197,7 @@ class Renderer:
|
||||
tz = raw[..., 3].view(np.float32).astype(np.float64)
|
||||
tid = raw[..., 4].view(np.float32).astype(np.int64)
|
||||
hasuv = raw[..., 5].view(np.float32)
|
||||
rawz = raw[..., 9].view(np.float32).astype(np.float64)
|
||||
# lit color (dvpx_r24/g24/b24 -> TREEclmpintoMEM): the polygon's
|
||||
# computed lighting, clamped to [0,255] -- see build_prims. Used ONLY
|
||||
# for flat (untextured) polys. NOT applied to textured polys: per
|
||||
@@ -209,4 +232,10 @@ class Renderer:
|
||||
ui = wrap_index((ucoord[sel] * tu) >> 8, tu, wrap_u)
|
||||
vi = wrap_index((vcoord[sel] * tv) >> 8, tv, wrap_v)
|
||||
composite_masked(img, sel, arr[vi, ui], cut_mode)
|
||||
# per-pixel linear fog on world distance (see FOG_* constants above)
|
||||
with np.errstate(divide='ignore', invalid='ignore'):
|
||||
dist = np.where(rawz > 0, FOG_HITHER * FOG_ZSCALE / rawz, np.inf)
|
||||
fog_t = np.clip((dist - FOG_NEAR) / (FOG_FAR - FOG_NEAR), 0.0, 1.0)
|
||||
img[filled] = (img[filled].astype(np.float64) * (1 - fog_t[filled, None])
|
||||
+ FOG_RGB * fog_t[filled, None]).astype(np.uint8)
|
||||
return img
|
||||
|
||||
Reference in New Issue
Block a user