diff --git a/crates/benilla-assets/src/shaders/fog_hook.wgsl b/crates/benilla-assets/src/shaders/fog_hook.wgsl index 6730fbd8..c9d66bac 100644 --- a/crates/benilla-assets/src/shaders/fog_hook.wgsl +++ b/crates/benilla-assets/src/shaders/fog_hook.wgsl @@ -15,6 +15,21 @@ // // Extension point for the FOG lane: `fog_sample` branches on `mf.fog_d.x` (`MonkeyFrame.fog_model`, // 0 classic / 1 modern). The modern arm currently returns the classic result. +// +// MONKEY (fog): the Modern model (cvar `fogModel` 1; CPU half `lighting/fog_model.rs`). `fog_d.x` +// holds the scene fog end when Modern is on (0 = Classic, every path below is then untouched): +// - survival: radial distance, exponential from the zone's start, fitted so 60 % survives at the +// middle of the 1.12 span, closed linearly over the last stretch (from max(start, 0.3·end), the +// modern client's 1/0.7 end fade) so the fog still ends where the zone authored it; optional +// height fog below `fog_a.y` (Bevy Y); `fog_a.w` blends back toward the 1.12 linear ramp. +// - colour (scene colour only, never the black/white/grey blend policies): shifts to the end-fog +// colour `fog_c` by (d / end)³, then leans to the sun-fog colour `fog_b` inside a cubic lobe around +// the sun (cosine threshold `fog_d.y`, direction octahedral in `fog_d.zw`; strength already faded +// for night on the CPU). +// - only spans ending at the scene fog end take it: an interior WMO fog pair stays classic. +// - the WDL hull's own (0, 1) pair asks for the horizon: fully fogged, in the far colour. +// The sky dome (`fog_sky_horizon`) and the volumetric haze (`fog_modern_colour`) call the same +// colour function, so the fogged world, the far hull and the sky's horizon meet in one colour. #import benilla::monkey_frame::MonkeyFrame @@ -24,6 +39,85 @@ fn fog_linear(eye_z: f32, span: vec2) -> f32 { return clamp((span.y - eye_z) / denom, 0.0, 1.0); } +// MONKEY (fog): exp survival at the span midpoint = 0.6, i.e. k·(end − start) = −2·ln 0.6. +const FOG_EXP_MID: f32 = 1.0216512; + +// MONKEY (fog): the unit vector packed by `fog_model::oct_encode`. +fn fog_oct_decode(e: vec2) -> vec3 { + var v = vec3(e.x, 1.0 - abs(e.x) - abs(e.y), e.y); + let t = max(-v.y, 0.0); + v.x += select(t, -t, v.x >= 0.0); + v.z += select(t, -t, v.z >= 0.0); + return normalize(v); +} + +// MONKEY (fog): whether the Modern model is on for this frame. +fn fog_is_modern(sel_row: vec4) -> bool { + return sel_row.x > 0.5; +} + +// MONKEY (fog): the Modern fog colour seen along unit `dir` at radial distance `d` (yd), from the +// scene fog colour `base`: end-fog shift, then the sun lobe. `d` may be huge (the sky). +fn fog_modern_colour( + base: vec3, + dir: vec3, + d: f32, + sun_row: vec4, + end_row: vec4, + sel_row: vec4, +) -> vec3 { + var c = base; + if (end_row.w > 0.0) { + let t = clamp(d / end_row.w, 0.0, 1.0); + c = mix(c, end_row.rgb, t * t * t); + } + if (sun_row.w > 0.0) { + let sun = fog_oct_decode(sel_row.zw); + let a = sel_row.y; + let s = clamp((dot(dir, sun) - a) / max(1.0 - a, 0.001), 0.0, 1.0); + c = mix(c, sun_row.rgb, sun_row.w * s * s * s); + } + return c; +} + +// MONKEY (fog): the Modern survival (1 = no fog) at radial distance `d` for the pair `span`. +fn fog_modern_survival(span: vec2, d: f32, world_y: f32, height_row: vec4) -> f32 { + let len = max(span.y - span.x, 0.001); + let k = FOG_EXP_MID / len; + let x = max(d - span.x, 0.0); + var expo = exp(-k * x); + if (height_row.x > 0.0) { + // Extra density at and below the plane, fading out `1 / falloff` yd above it. + let hf = clamp(1.0 - (world_y - height_row.y) * height_row.z, 0.0, 1.0); + expo = mix(expo, exp(-k * (1.0 + height_row.x) * x), hf); + } + let fade_from = max(span.x, 0.3 * span.y); + let end_fade = clamp((span.y - d) / max(span.y - fade_from, 0.001), 0.0, 1.0); + let linear = clamp((span.y - d) / len, 0.0, 1.0); + return mix(min(expo, end_fade), linear, clamp(height_row.w, 0.0, 1.0)); +} + +// MONKEY (fog): the sky dome's low band under Modern. `col` is the dome's own colour at `dir`, +// `horizon_rgb` the colour it uses at 0° (the scene fog colour); the far fog colour along `dir` +// replaces the horizon and eases into the dome's gradient over the first 12° of elevation. +// Classic returns `col` untouched. +fn fog_sky_horizon( + col: vec3, + horizon_rgb: vec3, + dir: vec3, + sun_row: vec4, + end_row: vec4, + sel_row: vec4, +) -> vec3 { + if (!fog_is_modern(sel_row)) { + return col; + } + let far = fog_modern_colour(horizon_rgb, dir, 1.0e9, sun_row, end_row, sel_row); + let elev = asin(clamp(dir.y, -1.0, 1.0)); + let w = 1.0 - smoothstep(0.0, 0.20943951, elev); + return clamp(col + (far - horizon_rgb) * w, vec3(0.0), vec3(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 @@ -40,9 +134,23 @@ fn fog_sample( ) -> vec4 { let classic = vec4(fog_rgb, fog_linear(eye_z, span)); if (mf.fog_d.x > 0.5) { - // FOG lane: the modern model (height / sun / end-colour / curve fog) goes here, reading - // mf.fog_a..fog_d. Until it lands, the modern arm is the classic law. - return classic; + // MONKEY (fog): the Modern model (header). + let delta = world_pos - camera_pos; + let d = length(delta); + let dir = delta / max(d, 0.0001); + if (span.x == 0.0 && span.y == 1.0) { + // The WDL hull's own pair: the horizon, fully fogged in the far colour. + return vec4(fog_modern_colour(fog_rgb, dir, d, mf.fog_b, mf.fog_c, mf.fog_d), 0.0); + } + if (abs(span.y - mf.fog_d.x) > 0.01) { + // Not the scene fog (an interior WMO pair): classic. + return classic; + } + var rgb = fog_rgb; + if (scene_colour) { + rgb = fog_modern_colour(fog_rgb, dir, d, mf.fog_b, mf.fog_c, mf.fog_d); + } + return vec4(rgb, fog_modern_survival(span, d, world_pos.y, mf.fog_a)); } return classic; } diff --git a/crates/benilla-assets/src/shaders/monkey_frame.wgsl b/crates/benilla-assets/src/shaders/monkey_frame.wgsl index 97cbeba8..8d0ca4ce 100644 --- a/crates/benilla-assets/src/shaders/monkey_frame.wgsl +++ b/crates/benilla-assets/src/shaders/monkey_frame.wgsl @@ -14,7 +14,7 @@ struct MonkeyFrame { fog_a: vec4, // height_fog_density, height_fog_height (world Y, yd), height_fog_falloff, curve_blend fog_b: vec4, // sun_fog_rgb (gamma 0..1), sun_fog_strength fog_c: vec4, // end_fog_rgb (gamma 0..1), end_fog_distance (yd) - fog_d: vec4, // fog_model (0 classic, 1 modern), sun_fog_angle (cos), 0, 0 + fog_d: vec4, // fog_model (0 classic; modern = scene fog end yd >= 1), sun_fog_angle (cos), sun dir (octahedral xy) wind_a: vec4, // dir_x, dir_y (unit, world XZ: .x = world x, .y = world z), speed (yd/s), gust 0..1 wind_b: vec4, // time_s, sway_strength, grass_strength, tree_strength wet_a: vec4, // rain_rate 0..1, wetness 0..1, ripple_time_s, snow 0..1 diff --git a/crates/benilla-world/src/lighting/fog_model.rs b/crates/benilla-world/src/lighting/fog_model.rs new file mode 100644 index 00000000..a700116f --- /dev/null +++ b/crates/benilla-world/src/lighting/fog_model.rs @@ -0,0 +1,310 @@ +//! MONKEY (fog): the Modern fog model's CPU half — fills the FOG rows of [`MonkeyFrame`] every +//! frame. The law itself lives in `benilla-assets/src/shaders/fog_hook.wgsl`. +//! +//! Setting: cvar `fogModel` (0 Classic = the 1.12 linear fog, byte-identical; 1 Modern), bridged by +//! `benilla-app/src/monkey_gfx.rs` into [`FogModelSetting`]; `WOW_FOGMODEL=0|1` overrides it for a +//! session (captures). +//! +//! Modern, per frame: +//! - **Distances** stay the zone's: the shader's exponential curve is fitted to the 1.12 start/end +//! pair it already receives. Only past the reference's farclip ceiling does the end grow with the +//! view distance ([`modern_fog_end`], called from `resolve.rs`), so a larger `farclip` shows more. +//! - **Colours** without `LightFogBand.dbc` rows are derived from the 1.12 bands +//! ([`derived_band`]): the sun-fog lobe leans the fog toward the sun colour (IntBand 9) around the +//! sun and fades out at night; the far fog shifts toward the sky's lowest ring (1.8°) so the +//! fogged world, the WDL hull and the sky dome's horizon meet in one colour. Height fog is off. +//! - With `LightFogBand.dbc` rows, those override the derivation per `LightParams`, blended over +//! the same area-light chain as the stock bands. +//! +//! Packed rows (see `monkey_frame.rs`): `fog_a` height density / height (Bevy Y) / falloff / curve +//! blend; `fog_b` sun rgb / strength × day; `fog_c` end rgb / end distance; `fog_d` the scene fog end +//! (> 0 = Modern; the shader applies the model only to spans ending there, so an interior WMO fog +//! stays classic) / sun-fog cosine / the sun direction, octahedral-encoded. + +use bevy::prelude::*; + +use benilla_assets::{LockRecover, WorldAssets}; +use benilla_formats::{FogBand, FogBandCatalog}; + +use super::{FogModel, LightSampler, MonkeyFrame, WowLighting}; +use crate::terrain_stream::SPAWN_XY; +use crate::view::WorldCamera; +use crate::world_map::CurrentMap; +use benilla_assets::coords::bevy_to_wow; + +/// The live fog-model choice (cvar `fogModel`). Default Classic; `WOW_FOGMODEL` overrides. +#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)] +pub struct FogModelSetting(pub FogModel); + +impl Default for FogModelSetting { + fn default() -> Self { + FogModelSetting(env_override().unwrap_or(FogModel::Classic)) + } +} + +/// `WOW_FOGMODEL=0|1`, read once. +fn env_override() -> Option { + static ENV: std::sync::OnceLock> = std::sync::OnceLock::new(); + *ENV.get_or_init(|| { + std::env::var("WOW_FOGMODEL").ok().and_then(|v| match v.trim() { + "0" => Some(FogModel::Classic), + "1" => Some(FogModel::Modern), + _ => None, + }) + }) +} + +impl FogModelSetting { + /// Apply a cvar write; the env override, when set, wins for the session. + pub fn set_from_cvar(&mut self, v: f32) { + let want = env_override().unwrap_or(if v >= 0.5 { + FogModel::Modern + } else { + FogModel::Classic + }); + if self.0 != want { + self.0 = want; + } + } + + pub fn modern(&self) -> bool { + self.0 == FogModel::Modern + } +} + +/// `LightFogBand.dbc`, loaded once; empty without the file. +#[derive(Resource, Default)] +pub struct FogBandTable(pub FogBandCatalog); + +/// The reference client's farclip ceiling (`FARCLIP_RANGE` end). Up to it Modern keeps the 1.12 +/// fog pair exactly; beyond it the fog end scales with the view distance. +pub const FOG_REF_FARCLIP: f32 = 777.0; + +/// The Modern scene fog pair: the zone's end, stretched by `farclip / 777` once the view distance +/// passes the reference ceiling, and never past `farclip` (the wall). Classic is +/// `(frac·min(end, farclip), min(end, farclip))`; at `farclip ≤ 777` the two are identical. +pub fn modern_fog_end(fog_end_raw: f32, fog_start_frac: f32, farclip: f32) -> (f32, f32) { + let stretch = (farclip / FOG_REF_FARCLIP).max(1.0); + let end = (fog_end_raw * stretch).min(farclip); + (fog_start_frac * end, end) +} + +/// Sun-fog lobe defaults (no `LightFogBand` row): strength and the cosine the lobe starts at. +pub const DERIVED_SUN_FOG_STRENGTH: f32 = 0.5; +pub const DERIVED_SUN_FOG_ANGLE: f32 = 0.25; +/// How far the sun-fog colour leans from the fog colour toward the sun colour (IntBand 9). +const DERIVED_SUN_FOG_LEAN: f32 = 0.6; +/// How far the end-fog colour leans from the fog colour toward the 1.8° sky ring. +const DERIVED_END_FOG_LEAN: f32 = 0.5; + +fn mix3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] { + [ + a[0] + (b[0] - a[0]) * t, + a[1] + (b[1] - a[1]) * t, + a[2] + (b[2] - a[2]) * t, + ] +} + +/// The Modern fields from 1.12 data alone: no height fog, the exponential curve, a sun-fog lobe in +/// the sun's colour and an end colour leaning to the sky's lowest ring, ending at the scene fog end. +pub fn derived_band(l: &WowLighting) -> FogBand { + FogBand { + height_density: 0.0, + height: 0.0, + height_falloff: 0.0, + curve_blend: 0.0, + sun_rgb: mix3(l.fog_color, l.spec, DERIVED_SUN_FOG_LEAN), + sun_strength: DERIVED_SUN_FOG_STRENGTH, + sun_angle: DERIVED_SUN_FOG_ANGLE, + end_rgb: mix3(l.fog_color, l.sky[4], DERIVED_END_FOG_LEAN), + end_distance: 0.0, + } +} + +/// The sun-fog day factor from the visible sun's height (`sin(elevation)`): full from ~3° up, +/// fading through the horizon to 0 by ~6° below it (the modern client's SunAngleBlend ramp). +pub fn sun_fog_day(sun_y: f32) -> f32 { + let t = ((sun_y + 0.1) / 0.15).clamp(0.0, 1.0); + t * t * (3.0 - 2.0 * t) +} + +/// Octahedral encoding of a unit vector into `[-1, 1]²` (the shader's `oct_decode`). +pub fn oct_encode(v: Vec3) -> [f32; 2] { + let v = v.normalize_or(Vec3::Y); + let n = v / (v.x.abs() + v.y.abs() + v.z.abs()); + if n.y >= 0.0 { + [n.x, n.z] + } else { + let sx = if n.x >= 0.0 { 1.0 } else { -1.0 }; + let sz = if n.z >= 0.0 { 1.0 } else { -1.0 }; + [(1.0 - n.z.abs()) * sx, (1.0 - n.x.abs()) * sz] + } +} + +/// Write the FOG rows of `frame` for `band` under the resolved light. Classic zeroes them. +pub fn fill_frame(frame: &mut MonkeyFrame, model: FogModel, l: &WowLighting, band: &FogBand) { + if model == FogModel::Classic { + frame.fog_model = FogModel::Classic; + frame.fog_scene_end = 0.0; + frame.height_fog_density = 0.0; + frame.height_fog_height = 0.0; + frame.height_fog_falloff = 0.0; + frame.fog_curve_blend = 0.0; + frame.sun_fog_rgb = [0.0; 3]; + frame.sun_fog_strength = 0.0; + frame.sun_fog_angle = 0.0; + frame.sun_fog_dir = [0.0; 2]; + frame.end_fog_rgb = [0.0; 3]; + frame.end_fog_distance = 0.0; + return; + } + let sun = l.celestial_dir; + frame.fog_model = FogModel::Modern; + frame.fog_scene_end = l.fog_end; + frame.height_fog_density = band.height_density.max(0.0); + // WoW Z is Bevy Y. + frame.height_fog_height = band.height; + frame.height_fog_falloff = band.height_falloff.max(0.0); + frame.fog_curve_blend = band.curve_blend.clamp(0.0, 1.0); + frame.sun_fog_rgb = band.sun_rgb; + frame.sun_fog_strength = band.sun_strength.clamp(0.0, 1.0) * sun_fog_day(sun.y); + frame.sun_fog_angle = band.sun_angle.clamp(-1.0, 0.999); + frame.sun_fog_dir = oct_encode(sun); + frame.end_fog_rgb = band.end_rgb; + frame.end_fog_distance = if band.end_distance > 0.0 { + band.end_distance + } else { + l.fog_end + }; +} + +/// Startup: read `LightFogBand.dbc` (absent → empty). +pub(super) fn load_fog_bands(mut commands: Commands, world_assets: Option>) { + let Some(world_assets) = world_assets else { + return; + }; + let mut chain = world_assets.chain.lock_recover(); + let cat = FogBandCatalog::load(&mut chain); + if !cat.is_empty() { + info!("LightFogBand.dbc loaded: Modern fog uses its authored fields"); + } + commands.insert_resource(FogBandTable(cat)); +} + +/// Per frame: the Modern fog fields into [`MonkeyFrame`] (after the light resolve). +#[allow(clippy::too_many_arguments)] +pub(super) fn update_fog_model( + setting: Res, + lighting: Res, + table: Option>, + sampler: Option>, + clock: Res, + cam: Query<&GlobalTransform, With>, + current_map: Option>, + eye_liquid: crate::liquid::EyeLiquid, + viewer: Res, + weather: Option>, + mut frame: ResMut, +) { + let derived = derived_band(&lighting); + let band = match (&table, &sampler, setting.modern()) { + (Some(t), Some(s), true) if !t.0.is_empty() => { + let pos = match cam.single() { + Ok(t) => bevy_to_wow(t.translation()), + Err(_) => [SPAWN_XY.0, SPAWN_XY.1, 83.5], + }; + let map = current_map.as_ref().map_or(0, |m| m.0); + let sub = eye_liquid.submersion(); + let time = clock.minute * 2; + let storm = weather + .as_ref() + .map_or(0.0, |w| crate::weather::storm_blend(w.sky_density)); + let chain = |stormy| s.0.blend_chain(map, pos, stormy, sub, viewer.ghost); + let clear = t.0.sample_chain(&chain(false), time, &derived); + let stormy = (storm > 0.0) + .then(|| t.0.sample_chain(&chain(true), time, &derived)) + .flatten(); + match (clear, stormy) { + (None, None) => derived, + (c, s) => c.unwrap_or(derived).lerp(&s.unwrap_or(derived), storm), + } + } + _ => derived, + }; + let mut next = *frame; + fill_frame(&mut next, setting.0, &lighting, &band); + if *frame != next { + *frame = next; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn modern_end_matches_classic_up_to_the_reference_farclip() { + for far in [177.0, 350.0, 500.0, 777.0] { + for end in [120.0, 444.0, 900.0] { + let classic_end: f32 = f32::min(end, far); + let (s, e) = modern_fog_end(end, 0.25, far); + assert_eq!(e, classic_end); + assert_eq!(s, 0.25 * classic_end); + } + } + } + + #[test] + fn modern_end_grows_past_the_reference_farclip() { + let (_, e) = modern_fog_end(444.0, 0.25, 1554.0); + assert!((e - 888.0).abs() < 1e-3); + // Never past the wall. + let (_, e) = modern_fog_end(900.0, 0.25, 1000.0); + assert_eq!(e, 1000.0); + } + + #[test] + fn classic_leaves_the_fog_rows_zero() { + let mut f = MonkeyFrame::default(); + let l = WowLighting::default(); + fill_frame(&mut f, FogModel::Classic, &l, &derived_band(&l)); + assert_eq!(f, MonkeyFrame::default()); + assert!(f.pack(0.0, 0.0)[..4].iter().flatten().all(|v| *v == 0.0)); + } + + #[test] + fn modern_marks_the_scene_end_and_fades_the_sun_at_night() { + let mut l = WowLighting::default(); + l.fog_end = 444.0; + l.celestial_dir = Vec3::new(0.0, 0.8, 0.6); + let mut f = MonkeyFrame::default(); + fill_frame(&mut f, FogModel::Modern, &l, &derived_band(&l)); + assert_eq!(f.fog_scene_end, 444.0); + assert_eq!(f.end_fog_distance, 444.0); + assert!((f.sun_fog_strength - DERIVED_SUN_FOG_STRENGTH).abs() < 1e-6); + l.celestial_dir = Vec3::new(0.0, -0.5, 0.86); + fill_frame(&mut f, FogModel::Modern, &l, &derived_band(&l)); + assert_eq!(f.sun_fog_strength, 0.0); + } + + #[test] + fn oct_encoding_round_trips() { + fn decode(e: [f32; 2]) -> Vec3 { + let mut v = Vec3::new(e[0], 1.0 - e[0].abs() - e[1].abs(), e[1]); + let t = (-v.y).max(0.0); + v.x += if v.x >= 0.0 { -t } else { t }; + v.z += if v.z >= 0.0 { -t } else { t }; + v.normalize() + } + for d in [ + Vec3::Y, + -Vec3::Y, + Vec3::new(0.3, 0.2, -0.9), + Vec3::new(-0.7, -0.1, 0.7), + Vec3::new(0.1, -0.9, -0.4), + ] { + let d = d.normalize(); + assert!(decode(oct_encode(d)).dot(d) > 0.9999, "{d:?}"); + } + } +} diff --git a/crates/benilla-world/src/lighting/mod.rs b/crates/benilla-world/src/lighting/mod.rs index 81f80ad3..9fe4d4bd 100644 --- a/crates/benilla-world/src/lighting/mod.rs +++ b/crates/benilla-world/src/lighting/mod.rs @@ -13,6 +13,8 @@ mod flicker; // MONKEY (flame flicker): the per-light fire wobble folded in at p mod global_light; // the one shared global-light storage buffer (replaces the per-material push) mod monkey_frame; // MONKEY (p0 MonkeyFrame): the programme's per-frame block after the point table pub use monkey_frame::{FogModel, MonkeyFrame, MAX_BENDERS, MONKEY_FRAME_ROWS}; +pub mod fog_model; // MONKEY (fog): the Modern fog model's CPU half (MonkeyFrame fog rows) +pub use fog_model::FogModelSetting; mod lava_light; // MONKEY (lava light): magma surface fixtures and their independent gain pub use lava_light::{LavaLight, LavaLightGain}; mod prop_probes; // the per-instance interior-prop SH probe table (slot ↔ MeshTag payload) @@ -318,6 +320,16 @@ impl Plugin for LightingPlugin { // This frame's submersion verdict, which the sky-pass suppression also reads. .after(crate::liquid::SubmersionVerdict), ); + // MONKEY (fog): the fog-model setting, LightFogBand.dbc and the per-frame fog rows. + app.init_resource::() + .init_resource::() + .add_systems(Startup, fog_model::load_fog_bands.after(AssetSet::Open)) + .add_systems( + Update, + fog_model::update_fog_model + .after(update_time_lighting) + .in_set(LightingResolveSet), + ); // The shared light buffer, packed after the resolve and uploaded in the render world. global_light::register(app); // MONKEY (daylight fixtures): the per-frame re-aim, ordered before the packer's own set. diff --git a/crates/benilla-world/src/lighting/monkey_frame.rs b/crates/benilla-world/src/lighting/monkey_frame.rs index c583ec98..363058ef 100644 --- a/crates/benilla-world/src/lighting/monkey_frame.rs +++ b/crates/benilla-world/src/lighting/monkey_frame.rs @@ -17,7 +17,7 @@ //! | 0 `fog_a` | height_fog_density, height_fog_height, height_fog_falloff | curve_blend | //! | 1 `fog_b` | sun_fog_rgb | sun_fog_strength | //! | 2 `fog_c` | end_fog_rgb | end_fog_distance | -//! | 3 `fog_d` | fog_model (0 classic, 1 modern), sun_fog_angle, 0 | 0 | +//! | 3 `fog_d` | fog_model (0 classic; Modern = the scene fog end, yd, ≥ 1), sun_fog_angle, sun dir oct.x | sun dir oct.y | //! | 4 `wind_a` | dir_x, dir_y, speed | gust | //! | 5 `wind_b` | time_s, sway_strength, grass_strength | tree_strength | //! | 6 `wet_a` | rain_rate, wetness, ripple_time_s | snow | @@ -64,6 +64,11 @@ pub struct MonkeyFrame { pub sun_fog_strength: f32, /// Cosine threshold of the sun-fog lobe. pub sun_fog_angle: f32, + /// MONKEY (fog): the sun direction (Bevy space), octahedral-encoded (`fog_model::oct_encode`). + pub sun_fog_dir: [f32; 2], + /// MONKEY (fog): the scene fog end (yd) the Modern law applies to; packed as `fog_d.x`, so an + /// interior WMO span (a different end) stays classic. + pub fog_scene_end: f32, /// Gamma 0..1. pub end_fog_rgb: [f32; 3], pub end_fog_distance: f32, @@ -94,9 +99,10 @@ impl MonkeyFrame { /// The 16 packed rows. `time_of_day` (0..1) and `night` (0..1) are the packer's own inputs. pub fn pack(&self, time_of_day: f32, night: f32) -> [[f32; 4]; MONKEY_FRAME_ROWS] { let mut rows = [[0.0f32; 4]; MONKEY_FRAME_ROWS]; + // MONKEY (fog): Modern packs the scene fog end (never below 1) as its flag. let model = match self.fog_model { FogModel::Classic => 0.0, - FogModel::Modern => 1.0, + FogModel::Modern => self.fog_scene_end.max(1.0), }; rows[0] = [ self.height_fog_density, @@ -108,7 +114,7 @@ impl MonkeyFrame { rows[1] = [s[0], s[1], s[2], self.sun_fog_strength]; let e = self.end_fog_rgb; rows[2] = [e[0], e[1], e[2], self.end_fog_distance]; - rows[3] = [model, self.sun_fog_angle, 0.0, 0.0]; + rows[3] = [model, self.sun_fog_angle, self.sun_fog_dir[0], self.sun_fog_dir[1]]; rows[4] = [self.wind_dir[0], self.wind_dir[1], self.wind_speed, self.wind_gust]; rows[5] = [self.wind_time_s, self.sway_strength, self.grass_strength, self.tree_strength]; rows[6] = [self.rain_rate, self.wetness, self.ripple_time_s, self.snow]; @@ -144,6 +150,8 @@ mod tests { end_fog_rgb: [9.0, 10.0, 11.0], end_fog_distance: 12.0, sun_fog_angle: 13.0, + sun_fog_dir: [0.5, -0.5], + fog_scene_end: 444.0, wind_dir: [14.0, 15.0], wind_speed: 16.0, wind_gust: 17.0, @@ -162,7 +170,7 @@ mod tests { assert_eq!(r[0], [1.0, 2.0, 3.0, 4.0]); assert_eq!(r[1], [5.0, 6.0, 7.0, 8.0]); assert_eq!(r[2], [9.0, 10.0, 11.0, 12.0]); - assert_eq!(r[3], [1.0, 13.0, 0.0, 0.0]); + assert_eq!(r[3], [444.0, 13.0, 0.5, -0.5]); assert_eq!(r[4], [14.0, 15.0, 16.0, 17.0]); assert_eq!(r[5], [18.0, 19.0, 20.0, 21.0]); assert_eq!(r[6], [22.0, 23.0, 24.0, 25.0]); diff --git a/crates/benilla-world/src/lighting/resolve.rs b/crates/benilla-world/src/lighting/resolve.rs index 72e95217..e1749d54 100644 --- a/crates/benilla-world/src/lighting/resolve.rs +++ b/crates/benilla-world/src/lighting/resolve.rs @@ -177,7 +177,9 @@ pub(super) fn update_time_lighting( eye_liquid: crate::liquid::EyeLiquid, viewer: Res, weather: Option>, - view: Res, + // MONKEY (fog): paired with the fog-model setting (Modern decouples the fog end from farclip + // past the reference ceiling); a tuple keeps the system under Bevy's 16-parameter limit. + (view, fog_setting): (Res, Res), time: Res