MONKEY (p0): fog_hook.wgsl, one distance-fog law for every receiver

`benilla::fog_hook` (fog_linear / fog_sample / apply_fog) replaces the seven
hand-rolled copies of the linear planar-eye-Z fog: terrain, wow_model,
static_gx, liquid, enhanced_water (factor form), wow_effect (scene fog; the
rain streak fade uses fog_linear) and wdl (the hull's own 0..1 yd pair).
Pure functions, values passed as arguments, same expressions in the same
order, so the classic result is bit-identical. `fog_sample` branches on
MonkeyFrame.fog_model (fog3.x) as the FOG lane's extension point, currently
returning the classic result. wow_effect gains a world_pos varying for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pkuzic
2026-09-25 01:33:52 +01:00
co-authored by Claude Opus 5.5
parent 3415538908
commit fc99f2c858
10 changed files with 111 additions and 19 deletions
+4
View File
@@ -35,6 +35,10 @@ is its own module, documented in `WATER.md`.
## Where the code lives
- **Shaders**: `crates/benilla-assets/src/shaders/shadow_hook.wgsl` (shared shadow sampling),
`fog_hook.wgsl` (MONKEY p0: the ONE distance-fog law — `fog_linear`, `fog_sample`, `apply_fog`;
terrain, wow_model, static_gx, liquid, enhanced_water, wow_effect and wdl all call it; classic is
bit-identical to the old per-shader copies, and `MonkeyFrame.fog_model` = 1 is the FOG lane's
extension point), `monkey_frame.wgsl` (the MonkeyFrame struct),
`crates/benilla-world/src/shaders/torch_depth.wgsl`, and the lighting lanes inside
`static_gx.wgsl`, `wow_model.wgsl` and `terrain.wgsl`. The three receivers mirror each other; the
comments say where.
+2
View File
@@ -32,6 +32,8 @@ pub fn register_shaders(app: &mut App) {
// MONKEY (p0 MonkeyFrame): `benilla::monkey_frame`, the struct every `WowLight` mirror declares
// after its point table (LIGHTING.md "Invariants").
load_shader_library!(app, "shaders/monkey_frame.wgsl");
// MONKEY (p0 fog hook): `benilla::fog_hook`, the one distance-fog law all seven receivers call.
load_shader_library!(app, "shaders/fog_hook.wgsl");
// MONKEY (enhanced water): the optional water module, `benilla::enhanced_water` (WATER.md).
load_shader_library!(app, "shaders/enhanced_water.wgsl");
}
@@ -22,6 +22,8 @@
#import bevy_pbr::mesh_view_bindings::{view, globals}
// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
#import benilla::monkey_frame
// MONKEY (p0 fog hook): the one distance-fog law every receiver calls.
#import benilla::fog_hook
@group(#{MATERIAL_BIND_GROUP}) @binding(103) var scene_depth: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(106) var scene_colour: texture_2d<f32>;
@@ -86,8 +88,9 @@ fn water_fog(world_pos: vec3<f32>, room_fog: u32) -> vec4<f32> {
}
if colour.w <= 0.5 { return vec4<f32>(colour.rgb, 1.0); }
let eye_z = -(view.view_from_world * vec4<f32>(world_pos, 1.0)).z;
let factor = clamp((span.y - eye_z) / max(span.y - span.x, 0.001), 0.0, 1.0);
return vec4<f32>(colour.rgb, factor);
// MONKEY (p0 fog hook): the shared fog law (fog_hook.wgsl), as a factor; classic is bit-identical.
return fog_hook::fog_sample(colour.rgb, span, eye_z, world_pos, view.world_position, true,
water_light.monkey);
}
// True where this surface takes the Enhanced path: water (not magma/slime), a tier above
@@ -0,0 +1,63 @@
#define_import_path benilla::fog_hook
// MONKEY (p0 fog hook): the ONE place the distance fog is computed.
//
// Every receiver — terrain, wow_model, static_gx, liquid, enhanced_water, wow_effect, wdl — calls
// in here instead of hand-rolling the fog law, so a new fog model touches this file only (the
// `shadow_hook.wgsl` pattern). Pure functions: the receivers bind the shared light buffer at
// different slots (material binding 90, group 2 binding 0, group 1 binding 2, binding 105), so
// every value arrives as an argument and this file owns no bindings.
//
// The classic law is the reference's GL_LINEAR fog on PLANAR eye-Z in gamma space (terrain.wgsl
// has the full note): `factor = clamp((end - eye_z) / max(end - start, 0.001), 0, 1)`, then
// `mix(fog_rgb, rgb, factor)`. The expressions below are the ones the receivers carried, in the
// same order, so the classic result is bit-identical.
//
// Extension point for the FOG lane: `fog_sample` branches on `mf.fog3.x` (`MonkeyFrame.fog_model`,
// 0 classic / 1 modern). The modern arm currently returns the classic result.
#import benilla::monkey_frame::MonkeyFrame
// The classic survival factor: 1 = no fog, 0 = fully fogged.
fn fog_linear(eye_z: f32, span: vec2<f32>) -> f32 {
let denom = max(span.y - span.x, 0.001);
return clamp((span.y - eye_z) / denom, 0.0, 1.0);
}
// The fog a surface stands in: rgb = the fog colour, w = how much of the surface survives (1 = no
// fog). `fog_rgb` is the colour the caller's policy picked; `scene_colour` is true when that is the
// scene's own fog colour (false for the forced black/white/grey policies of additive, Mod and Mod2x
// batches, which a new model may attenuate but must not recolour). `span` = (start, end) yd,
// `eye_z` = planar eye depth (yd), `world_pos` / `camera_pos` in Bevy world space.
fn fog_sample(
fog_rgb: vec3<f32>,
span: vec2<f32>,
eye_z: f32,
world_pos: vec3<f32>,
camera_pos: vec3<f32>,
scene_colour: bool,
mf: MonkeyFrame,
) -> vec4<f32> {
let classic = vec4<f32>(fog_rgb, fog_linear(eye_z, span));
if (mf.fog3.x > 0.5) {
// FOG lane: the modern model (height / sun / end-colour / curve fog) goes here, reading
// mf.fog0..fog3. Until it lands, the modern arm is the classic law.
return classic;
}
return classic;
}
// Fog `rgb` in place: `mix(fog colour, rgb, survival)`. Same arguments as `fog_sample`.
fn apply_fog(
rgb: vec3<f32>,
fog_rgb: vec3<f32>,
span: vec2<f32>,
eye_z: f32,
world_pos: vec3<f32>,
camera_pos: vec3<f32>,
scene_colour: bool,
mf: MonkeyFrame,
) -> vec3<f32> {
let f = fog_sample(fog_rgb, span, eye_z, world_pos, camera_pos, scene_colour, mf);
return mix(f.rgb, rgb, f.w);
}
@@ -32,6 +32,8 @@
#import benilla::enhanced_water::{water_active, water_swell, enhanced_water, WaterFragment}
// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
#import benilla::monkey_frame
// MONKEY (p0 fog hook): the one distance-fog law every receiver calls.
#import benilla::fog_hook
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var frames: texture_2d_array<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(101) var frames_samp: sampler;
@@ -132,9 +134,9 @@ fn apply_fog(rgb: vec3<f32>, world_pos: vec3<f32>, room_fog: u32) -> vec3<f32> {
return rgb;
}
let eye_z = -(view.view_from_world * vec4<f32>(world_pos, 1.0)).z;
let denom = max(fog_span.y - fog_span.x, 0.001);
let factor = clamp((fog_span.y - eye_z) / denom, 0.0, 1.0);
return mix(fog_color.xyz, rgb, factor);
// MONKEY (p0 fog hook): the shared fog law (fog_hook.wgsl); classic is bit-identical.
return fog_hook::apply_fog(rgb, fog_color.xyz, fog_span, eye_z, world_pos, view.world_position,
true, wow_light.monkey);
}
@vertex
@@ -20,6 +20,8 @@
#import benilla::shadow_hook
// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
#import benilla::monkey_frame
// MONKEY (p0 fog hook): the one distance-fog law every receiver calls.
#import benilla::fog_hook
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var layer_array: texture_2d_array<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(104) var alpha_array: texture_2d_array<f32>;
@@ -780,9 +782,9 @@ fn fragment(in: TerrainVsOut) -> @location(0) vec4<f32> {
// the per-pixel form is what survives Bevy's mesh interpolators without a custom slot.
if (wow_light.fog_color.w > 0.5) {
let eye_z = -(view.view_from_world * vec4<f32>(in.world_position.xyz, 1.0)).z;
let denom = max(wow_light.fog_params.y - wow_light.fog_params.x, 0.001);
let factor = clamp((wow_light.fog_params.y - eye_z) / denom, 0.0, 1.0);
tuned = mix(wow_light.fog_color.xyz, tuned, factor);
// MONKEY (p0 fog hook): the shared fog law (fog_hook.wgsl); classic is bit-identical.
tuned = fog_hook::apply_fog(tuned, wow_light.fog_color.xyz, wow_light.fog_params.xy, eye_z,
in.world_position.xyz, view.world_position, true, wow_light.monkey);
}
// Raw gamma out: the framebuffer holds gamma bytes and blends in gamma like the reference's;
@@ -19,6 +19,8 @@
// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
#import benilla::monkey_frame
// MONKEY (p0 fog hook): the one distance-fog law every receiver calls.
#import benilla::fog_hook
/// How far the band reaches inside the far-clip wall: the reference's far-band near plane
/// `farclip - 33.0` (`[0x8101b0]`, about one WDL outer cell).
@@ -81,6 +83,10 @@ fn fragment(in: WdlVsOut) -> WdlFsOut {
var rgb = vec3<f32>(1.0);
if (w.fog_color.w > 0.5) {
rgb = w.fog_color.xyz;
// MONKEY (p0 fog hook): the hull's own pair (start 0, end 1 yd) through the shared law, so
// a new fog model colours the horizon the way it colours the world; classic = the line above.
rgb = fog_hook::apply_fog(vec3<f32>(1.0), rgb, vec2<f32>(0.0, 1.0), eye_z,
in.world_position.xyz, view.world_position, true, w.monkey);
}
var out: WdlFsOut;
@@ -22,6 +22,8 @@
#import benilla::shadow_hook
// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
#import benilla::monkey_frame
// MONKEY (p0 fog hook): the one distance-fog law every receiver calls.
#import benilla::fog_hook
// bevy_pbr 0.18.1's `forward_io::FragmentOutput`. No depth output: a fragment depth write costs
// the pipeline early-Z, so the sky lane pins its depth in the vertex stage.
@@ -1720,13 +1722,13 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
let fog_policy = (u32(m.clutter_fade.z) >> 4u) & 7u;
if (fog_color.w > 0.5 && fog_policy != 4u) {
let eye_z = -(view.view_from_world * vec4<f32>(in.world_position.xyz, 1.0)).z;
let denom = max(fog_span.y - fog_span.x, 0.001);
let factor = clamp((fog_span.y - eye_z) / denom, 0.0, 1.0);
var fog_rgb = fog_color.xyz;
if (fog_policy == 1u) { fog_rgb = vec3<f32>(0.0); }
else if (fog_policy == 2u) { fog_rgb = vec3<f32>(1.0); }
else if (fog_policy == 3u) { fog_rgb = vec3<f32>(0.50196078); }
rgb = mix(fog_rgb, rgb, factor);
// MONKEY (p0 fog hook): the shared fog law (fog_hook.wgsl); classic is bit-identical.
rgb = fog_hook::apply_fog(rgb, fog_rgb, fog_span, eye_z, in.world_position.xyz,
view.world_position, fog_policy == 0u, wow_light.monkey);
}
@@ -12,6 +12,8 @@
#import benilla::shadow_hook
// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
#import benilla::monkey_frame
// MONKEY (p0 fog hook): the one distance-fog law every receiver calls.
#import benilla::fog_hook
// Group 0 is Bevy's standard mesh-view bind group (view matrices, directional-light records and
// the shadow textures the retained pass reads).
@@ -1980,9 +1982,9 @@ fn fragment(in: GxVsOut) -> @location(0) vec4<f32> {
fog_span = wow_light.wmo_fog_params.xy;
}
if (fog_color.w > 0.5 && (in.word & WORD_FOG_OFF) == 0u) {
let denom = max(fog_span.y - fog_span.x, 0.001);
let factor = clamp((fog_span.y - eye_z) / denom, 0.0, 1.0);
rgb = mix(fog_color.xyz, rgb, factor);
// MONKEY (p0 fog hook): the shared fog law (fog_hook.wgsl); classic is bit-identical.
rgb = fog_hook::apply_fog(rgb, fog_color.xyz, fog_span, eye_z, in.world_position.xyz,
view.world_position, true, wow_light.monkey);
}
// Gamma-space output; alpha pinned 1.0, every draw here is opaque.
return vec4<f32>(rgb, 1.0);
@@ -14,6 +14,8 @@
#import bevy_render::view::View
// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
#import benilla::monkey_frame
// MONKEY (p0 fog hook): the one distance-fog law every receiver calls.
#import benilla::fog_hook
// Prefix of `lighting::global_light`'s buffer; keep in sync with wow_model.wgsl's copy.
struct WowLight {
@@ -73,6 +75,8 @@ struct VertexOutput {
@location(1) color: vec4<f32>,
// Planar eye-Z for fog and the farclip wall, positive in front of the camera.
@location(2) view_z: f32,
// MONKEY (p0 fog hook): world position (Bevy space) for the fog hook's modern arm.
@location(3) world_pos: vec3<f32>,
};
@vertex
@@ -85,6 +89,7 @@ fn vertex(v: Vertex) -> VertexOutput {
out.clip_position = view.clip_from_world * vec4<f32>(v.position, 1.0);
// Eye-Z via the full affine transform; its rounding is yard-scale and harmless.
out.view_z = -(view.view_from_world * vec4<f32>(v.position, 1.0)).z;
out.world_pos = v.position; // MONKEY (p0 fog hook)
#else
// Cam-relative verts: view_from_world is [R | −R·cam] and the rebase already subtracted cam,
// so only the rotation applies.
@@ -95,6 +100,7 @@ fn vertex(v: Vertex) -> VertexOutput {
) * v.position;
out.clip_position = view.clip_from_view * vec4<f32>(view_pos, 1.0);
out.view_z = -view_pos.z;
out.world_pos = v.position + view.world_position; // MONKEY (p0 fog hook): undo the rebase
#endif
out.uv = v.uv;
out.color = v.color;
@@ -150,19 +156,19 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// Scene fog: the world's linear fog (same start and end, planar eye-Z) in gamma space before
// the blend; only its colour follows the per-blend policy in `wow_params.fog.x`.
if (wow_light.fog_color.w > 0.5 && wow_params.fog.x > 0.5) {
let denom = max(wow_light.fog_params.y - wow_light.fog_params.x, 0.001);
let factor = clamp((wow_light.fog_params.y - in.view_z) / denom, 0.0, 1.0);
var fog_rgb = wow_light.fog_color.xyz;
if (wow_params.fog.x > 1.5 && wow_params.fog.x < 2.5) { fog_rgb = vec3<f32>(0.0); }
else if (wow_params.fog.x > 2.5 && wow_params.fog.x < 3.5) { fog_rgb = vec3<f32>(1.0); }
else if (wow_params.fog.x > 3.5) { fog_rgb = RAIN_FOG_GREY; }
rgb = mix(fog_rgb, rgb, factor);
// MONKEY (p0 fog hook): the shared fog law (fog_hook.wgsl); classic is bit-identical.
rgb = fog_hook::apply_fog(rgb, fog_rgb, wow_light.fog_params.xy, in.view_z, in.world_pos,
view.world_position, wow_params.fog.x < 1.5, wow_light.monkey);
}
// Rain's forced fog: grey over the draw's own start/end, whatever the scene fog; grey is
// neutral under Mod2x, so this is the streaks' distance fade.
if (wow_params.fog.y > 0.5) {
let denom = max(wow_params.fog.w - wow_params.fog.z, 0.001);
let factor = clamp((wow_params.fog.w - in.view_z) / denom, 0.0, 1.0);
// MONKEY (p0 fog hook): the draw's own distance fade, always the classic linear law.
let factor = fog_hook::fog_linear(in.view_z, wow_params.fog.zw);
rgb = mix(RAIN_FOG_GREY, rgb, factor);
}
#ifdef BLEND_ADD