MONKEY (fog): Modern fog model (fogModel 1) in fog_hook + fog_model.rs

fog_hook.wgsl: radial exponential fog fitted to the zone's 1.12 start/end
(60 % survival mid-span, closed over the last stretch so the zone end
holds), optional height fog, end-fog colour shift, sun-fog lobe; interior
WMO spans stay classic; the WDL hull pair takes the far colour.
lighting/fog_model.rs fills the MonkeyFrame fog rows each frame (derived
from the 1.12 bands, or LightFogBand.dbc rows); fog_d.x carries the scene
fog end as the Modern flag, fog_d.zw the octahedral sun direction.
resolve.rs: Modern stretches the fog end with farclip past 777 only.
sky.wgsl/sky.rs: the dome's low band eases into the same far colour.
Classic packs zeros and every new path branches out.

Includes the cherry-picked p0 fix 05d6b1ad (MonkeyFrame rows fog_a..).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pkuzic
2026-09-25 09:20:59 +01:00
co-authored by Claude Opus 5.5
parent ee13878f8a
commit bbea017484
8 changed files with 492 additions and 12 deletions
+111 -3
View File
@@ -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>) -> 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<f32>) -> vec3<f32> {
var v = vec3<f32>(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<f32>) -> 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<f32>,
dir: vec3<f32>,
d: f32,
sun_row: vec4<f32>,
end_row: vec4<f32>,
sel_row: vec4<f32>,
) -> vec3<f32> {
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<f32>, d: f32, world_y: f32, height_row: vec4<f32>) -> 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<f32>,
horizon_rgb: vec3<f32>,
dir: vec3<f32>,
sun_row: vec4<f32>,
end_row: vec4<f32>,
sel_row: vec4<f32>,
) -> vec3<f32> {
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<f32>(0.0), vec3<f32>(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<f32> {
let classic = vec4<f32>(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<f32>(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<f32>(rgb, fog_modern_survival(span, d, world_pos.y, mf.fog_a));
}
return classic;
}
@@ -14,7 +14,7 @@ struct MonkeyFrame {
fog_a: vec4<f32>, // height_fog_density, height_fog_height (world Y, yd), height_fog_falloff, curve_blend
fog_b: vec4<f32>, // sun_fog_rgb (gamma 0..1), sun_fog_strength
fog_c: vec4<f32>, // end_fog_rgb (gamma 0..1), end_fog_distance (yd)
fog_d: vec4<f32>, // fog_model (0 classic, 1 modern), sun_fog_angle (cos), 0, 0
fog_d: vec4<f32>, // fog_model (0 classic; modern = scene fog end yd >= 1), sun_fog_angle (cos), sun dir (octahedral xy)
wind_a: vec4<f32>, // dir_x, dir_y (unit, world XZ: .x = world x, .y = world z), speed (yd/s), gust 0..1
wind_b: vec4<f32>, // time_s, sway_strength, grass_strength, tree_strength
wet_a: vec4<f32>, // rain_rate 0..1, wetness 0..1, ripple_time_s, snow 0..1
@@ -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<FogModel> {
static ENV: std::sync::OnceLock<Option<FogModel>> = 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<ResMut<WorldAssets>>) {
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<FogModelSetting>,
lighting: Res<WowLighting>,
table: Option<Res<FogBandTable>>,
sampler: Option<Res<LightSampler>>,
clock: Res<super::GameClock>,
cam: Query<&GlobalTransform, With<WorldCamera>>,
current_map: Option<Res<CurrentMap>>,
eye_liquid: crate::liquid::EyeLiquid,
viewer: Res<crate::view::Viewer>,
weather: Option<Res<crate::weather::WeatherState>>,
mut frame: ResMut<MonkeyFrame>,
) {
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:?}");
}
}
}
+12
View File
@@ -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::<fog_model::FogModelSetting>()
.init_resource::<fog_model::FogBandTable>()
.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.
@@ -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]);
+9 -2
View File
@@ -177,7 +177,9 @@ pub(super) fn update_time_lighting(
eye_liquid: crate::liquid::EyeLiquid,
viewer: Res<crate::view::Viewer>,
weather: Option<Res<crate::weather::WeatherState>>,
view: Res<crate::view::ViewDistance>,
// 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<crate::view::ViewDistance>, Res<super::FogModelSetting>),
time: Res<Time>,
wmo_fog: Res<crate::wmo_portal::CameraWmoFog>,
mut wmo_ramp: ResMut<WmoCrossfade>,
@@ -250,7 +252,12 @@ pub(super) fn update_time_lighting(
// Elwynn commits 125/500 clear and −139/278 at full storm (`0x6cee30`): the negative start is
// the near veil, and the short storm end whites out the middle distance.
let (fog_start, fog_end) = scene_fog(atmo.fog_end, atmo.fog_start_frac, view.farclip);
let (fog_start, fog_end) = if fog_setting.modern() {
// MONKEY (fog): identical up to farclip 777, then the end grows with the view distance.
super::fog_model::modern_fog_end(atmo.fog_end, atmo.fog_start_frac, view.farclip)
} else {
scene_fog(atmo.fog_end, atmo.fog_start_frac, view.farclip)
};
// The interior crossfade is its own triple; the scene fog stays untouched. Submerged, the
// underwater param owns the fog and the ramp is bypassed, as the reference snaps it for magma
// and slime inside an MFOG interior (`0x6cef6f`); the reference's MFOG underwater block
+8 -1
View File
@@ -5,6 +5,8 @@
forward_io::VertexOutput,
mesh_view_bindings::view,
}
// MONKEY (fog): the Modern fog colour at the horizon.
#import benilla::fog_hook
struct SkyColors {
sky0: vec4<f32>, // zenith (90°)
@@ -14,6 +16,10 @@ struct SkyColors {
sky4: vec4<f32>, // 1.8°
fog: vec4<f32>, // horizon (0°) and below: LightIntBand row 7
warp: vec4<f32>, // x = dawn/dusk warp strength S (0 = off), y = sun azimuth (rad), zw reserved
// MONKEY (fog): MonkeyFrame fog1..fog3 (all zero = Classic).
mf_fog1: vec4<f32>,
mf_fog2: vec4<f32>,
mf_fog3: vec4<f32>,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var<uniform> sky: SkyColors;
@@ -83,6 +89,7 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
}
// Raw gamma out: the reference draws the sky as raw DBC bytes, sRGB off (`0x6d4940`).
let rgb = col;
// MONKEY (fog): Modern eases the low band into the world's far fog colour; Classic = `col`.
let rgb = fog_hook::fog_sky_horizon(col, sky.fog.rgb, dir, sky.mf_fog1, sky.mf_fog2, sky.mf_fog3);
return vec4<f32>(rgb, 1.0);
}
+29 -1
View File
@@ -55,6 +55,15 @@ pub struct SkyExt {
/// none), `y` the sun azimuth `atan2(sun.z, sun.x)` in radians, `zw` reserved.
#[uniform(100)]
pub(crate) warp: Vec4,
/// MONKEY (fog): `MonkeyFrame` fog rows 1-3 (sun fog, end fog, model/sun dir), so the dome's
/// horizon takes the same Modern fog colour as the world (`fog_hook::fog_sky_horizon`). All
/// zero (Classic) leaves the dome untouched.
#[uniform(100)]
pub(crate) mf_fog1: Vec4,
#[uniform(100)]
pub(crate) mf_fog2: Vec4,
#[uniform(100)]
pub(crate) mf_fog3: Vec4,
}
impl MaterialExtension for SkyExt {
@@ -183,6 +192,10 @@ fn setup_sky(
sky4: col([0.78, 0.86, 0.95]),
fog: col([0.55, 0.72, 0.92]),
warp: Vec4::ZERO, // update_sky_colors fills S + sun azimuth each frame
// MONKEY (fog)
mf_fog1: Vec4::ZERO,
mf_fog2: Vec4::ZERO,
mf_fog3: Vec4::ZERO,
},
});
commands.spawn((
@@ -244,6 +257,8 @@ fn apply_sky_visibility(
/// Push the time-of-day sky stops and fog colour (`WowLighting`) into the dome material.
fn update_sky_colors(
light: Res<WowLighting>,
// MONKEY (fog): the Modern fog rows for the horizon.
monkey: Res<crate::lighting::MonkeyFrame>,
dome: Query<&MeshMaterial3d<SkyMaterial>, With<Sky>>,
mut materials: ResMut<Assets<SkyMaterial>>,
) {
@@ -270,11 +285,21 @@ fn update_sky_colors(
0.0,
0.0,
);
// MONKEY (fog): rows 1-3 as packed for the light buffer (the clock lanes are not read here).
let rows = monkey.pack(0.0, 0.0);
let mf = [
Vec4::from_array(rows[1]),
Vec4::from_array(rows[2]),
Vec4::from_array(rows[3]),
];
benilla_assets::write_gated(
&mut materials,
&handle.0,
|m| {
m.extension.sky0 != sky[0]
m.extension.mf_fog1 != mf[0]
|| m.extension.mf_fog2 != mf[1]
|| m.extension.mf_fog3 != mf[2]
|| m.extension.sky0 != sky[0]
|| m.extension.sky1 != sky[1]
|| m.extension.sky2 != sky[2]
|| m.extension.sky3 != sky[3]
@@ -290,6 +315,9 @@ fn update_sky_colors(
m.extension.sky4 = sky[4];
m.extension.fog = fog;
m.extension.warp = warp;
m.extension.mf_fog1 = mf[0];
m.extension.mf_fog2 = mf[1];
m.extension.mf_fog3 = mf[2];
},
);
}