From fc99f2c858be168d4a6ff06311498359d0f27ac3 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 25 Sep 2026 01:33:52 +0100 Subject: [PATCH] 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 --- LIGHTING.md | 4 ++ crates/benilla-assets/src/materials.rs | 2 + .../src/shaders/enhanced_water.wgsl | 7 ++- .../benilla-assets/src/shaders/fog_hook.wgsl | 63 +++++++++++++++++++ crates/benilla-assets/src/shaders/liquid.wgsl | 8 ++- .../benilla-assets/src/shaders/terrain.wgsl | 8 ++- crates/benilla-assets/src/shaders/wdl.wgsl | 6 ++ .../benilla-assets/src/shaders/wow_model.wgsl | 8 ++- .../benilla-world/src/shaders/static_gx.wgsl | 8 ++- .../benilla-world/src/shaders/wow_effect.wgsl | 16 +++-- 10 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 crates/benilla-assets/src/shaders/fog_hook.wgsl diff --git a/LIGHTING.md b/LIGHTING.md index 59e0f2f1..27a804c4 100644 --- a/LIGHTING.md +++ b/LIGHTING.md @@ -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. diff --git a/crates/benilla-assets/src/materials.rs b/crates/benilla-assets/src/materials.rs index 08878c86..a2254aa8 100644 --- a/crates/benilla-assets/src/materials.rs +++ b/crates/benilla-assets/src/materials.rs @@ -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"); } diff --git a/crates/benilla-assets/src/shaders/enhanced_water.wgsl b/crates/benilla-assets/src/shaders/enhanced_water.wgsl index ac4a27c2..95eb1183 100644 --- a/crates/benilla-assets/src/shaders/enhanced_water.wgsl +++ b/crates/benilla-assets/src/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; @group(#{MATERIAL_BIND_GROUP}) @binding(106) var scene_colour: texture_2d; @@ -86,8 +88,9 @@ fn water_fog(world_pos: vec3, room_fog: u32) -> vec4 { } if colour.w <= 0.5 { return vec4(colour.rgb, 1.0); } let eye_z = -(view.view_from_world * vec4(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(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 diff --git a/crates/benilla-assets/src/shaders/fog_hook.wgsl b/crates/benilla-assets/src/shaders/fog_hook.wgsl new file mode 100644 index 00000000..e931ccf9 --- /dev/null +++ b/crates/benilla-assets/src/shaders/fog_hook.wgsl @@ -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 { + 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, + span: vec2, + eye_z: f32, + world_pos: vec3, + camera_pos: vec3, + scene_colour: bool, + mf: MonkeyFrame, +) -> vec4 { + let classic = vec4(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, + fog_rgb: vec3, + span: vec2, + eye_z: f32, + world_pos: vec3, + camera_pos: vec3, + scene_colour: bool, + mf: MonkeyFrame, +) -> vec3 { + let f = fog_sample(fog_rgb, span, eye_z, world_pos, camera_pos, scene_colour, mf); + return mix(f.rgb, rgb, f.w); +} diff --git a/crates/benilla-assets/src/shaders/liquid.wgsl b/crates/benilla-assets/src/shaders/liquid.wgsl index 293cd96f..56bfd3e7 100644 --- a/crates/benilla-assets/src/shaders/liquid.wgsl +++ b/crates/benilla-assets/src/shaders/liquid.wgsl @@ -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; @group(#{MATERIAL_BIND_GROUP}) @binding(101) var frames_samp: sampler; @@ -132,9 +134,9 @@ fn apply_fog(rgb: vec3, world_pos: vec3, room_fog: u32) -> vec3 { return rgb; } let eye_z = -(view.view_from_world * vec4(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 diff --git a/crates/benilla-assets/src/shaders/terrain.wgsl b/crates/benilla-assets/src/shaders/terrain.wgsl index c5394a0b..d4345ae6 100644 --- a/crates/benilla-assets/src/shaders/terrain.wgsl +++ b/crates/benilla-assets/src/shaders/terrain.wgsl @@ -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; @group(#{MATERIAL_BIND_GROUP}) @binding(104) var alpha_array: texture_2d_array; @@ -780,9 +782,9 @@ fn fragment(in: TerrainVsOut) -> @location(0) vec4 { // 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(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; diff --git a/crates/benilla-assets/src/shaders/wdl.wgsl b/crates/benilla-assets/src/shaders/wdl.wgsl index ea85aa3e..d7a72666 100644 --- a/crates/benilla-assets/src/shaders/wdl.wgsl +++ b/crates/benilla-assets/src/shaders/wdl.wgsl @@ -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(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(1.0), rgb, vec2(0.0, 1.0), eye_z, + in.world_position.xyz, view.world_position, true, w.monkey); } var out: WdlFsOut; diff --git a/crates/benilla-assets/src/shaders/wow_model.wgsl b/crates/benilla-assets/src/shaders/wow_model.wgsl index 0f211ca6..c7c9dcb4 100644 --- a/crates/benilla-assets/src/shaders/wow_model.wgsl +++ b/crates/benilla-assets/src/shaders/wow_model.wgsl @@ -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(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(0.0); } else if (fog_policy == 2u) { fog_rgb = vec3(1.0); } else if (fog_policy == 3u) { fog_rgb = vec3(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); } diff --git a/crates/benilla-world/src/shaders/static_gx.wgsl b/crates/benilla-world/src/shaders/static_gx.wgsl index 0850dc75..a626e709 100644 --- a/crates/benilla-world/src/shaders/static_gx.wgsl +++ b/crates/benilla-world/src/shaders/static_gx.wgsl @@ -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 { 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(rgb, 1.0); diff --git a/crates/benilla-world/src/shaders/wow_effect.wgsl b/crates/benilla-world/src/shaders/wow_effect.wgsl index e3921b8f..ed16930b 100644 --- a/crates/benilla-world/src/shaders/wow_effect.wgsl +++ b/crates/benilla-world/src/shaders/wow_effect.wgsl @@ -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, // 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, }; @vertex @@ -85,6 +89,7 @@ fn vertex(v: Vertex) -> VertexOutput { out.clip_position = view.clip_from_world * vec4(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(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(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 { // 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(0.0); } else if (wow_params.fog.x > 2.5 && wow_params.fog.x < 3.5) { fog_rgb = vec3(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