From cb6150ff4a2f026a949d6efe89d213675c74a731 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 25 Sep 2026 02:53:02 +0100 Subject: [PATCH 1/4] MONKEY (sky): sky-* capture scenarios (dusk, night, zenith night, overcast) Co-Authored-By: Claude Opus 5.5 --- crates/benilla-app/src/capture/scenarios.rs | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/benilla-app/src/capture/scenarios.rs b/crates/benilla-app/src/capture/scenarios.rs index d970a9fd..7da0b3fa 100644 --- a/crates/benilla-app/src/capture/scenarios.rs +++ b/crates/benilla-app/src/capture/scenarios.rs @@ -1379,6 +1379,41 @@ pub(super) const ON_DEMAND: &[Scenario] = &[ minute: 0, ui: None, }, + // MONKEY (sky): the sky lane's fixtures. Dusk looks from the Goldshire lake toward the low sun + // (az 45°, elev ≈5° at 20:00); night looks away from the moon at 01:00 (stars at full curve); + // zenith is the high dome at night. Overcast is `sky-overcast` under `WOW_WEATHER=rain,0.6`. + Scenario { + name: "sky-elwynn-dusk", + map: Some(MAP_AZEROTH), + eye: WATER_EYE, + look: [-9315.0, -98.6, 102.3], + minute: 1200, + ui: None, + }, + Scenario { + name: "sky-elwynn-night", + map: Some(MAP_AZEROTH), + eye: WATER_EYE, + look: [-9739.0, -522.6, 210.8], + minute: 60, + ui: None, + }, + Scenario { + name: "sky-zenith-night", + map: Some(MAP_AZEROTH), + eye: SKY_EYE, + look: [-9050.7, -230.7, 387.0], + minute: 60, + ui: None, + }, + Scenario { + name: "sky-overcast", + map: Some(MAP_AZEROTH), + eye: SKY_EYE, + look: [-8797.0, 23.0, 264.0], + minute: 1000, + ui: None, + }, ]; /// The owner's reported vantage for the lava-glow report: `(-7048.8, -1000.6, 242.0)` facing From f863e4640fb1c38766df7c3b4476c5e6498b53c5 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 25 Sep 2026 04:09:34 +0100 Subject: [PATCH 2/4] =?UTF-8?q?MONKEY=20(sky):=20skyQuality=20tier=20?= =?UTF-8?q?=E2=80=94=20smooth=20gradient,=20sun=20glow,=20night=20sky,=20H?= =?UTF-8?q?igh=20cloud=20detail=20+=20sun=20march?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit skyQuality 0 Classic (unchanged), 1 Enhanced, 2 High; Advanced Graphics row (EN+RU), cvar bridge, sky-* scenarios above the canopy. Cloud technique ported from WarcraftXL wxl-retail-clouds (THIRD-PARTY.md). Co-Authored-By: Claude Opus 5.5 --- LIGHTING.md | 2 + THIRD-PARTY.md | 13 + crates/benilla-app/assets/ui/OptionsFrame.xml | 36 ++- crates/benilla-app/src/capture/scenarios.rs | 33 ++- crates/benilla-app/src/cvars.rs | 6 + crates/benilla-app/src/game_plugins.rs | 2 + crates/benilla-app/src/lib.rs | 2 + crates/benilla-app/src/sky_quality.rs | 49 ++++ .../src/ui_script/options_tests.rs | 2 + crates/benilla-app/src/video.rs | 6 + crates/benilla-world/src/clouds/layer.rs | 52 ++++ crates/benilla-world/src/clouds/mod.rs | 2 + crates/benilla-world/src/lib.rs | 2 + crates/benilla-world/src/shaders.rs | 5 +- crates/benilla-world/src/shaders/cloud.wgsl | 47 ++- crates/benilla-world/src/shaders/sky.wgsl | 44 +++ crates/benilla-world/src/shaders/sky_fx.wgsl | 269 ++++++++++++++++++ crates/benilla-world/src/sky.rs | 58 +++- crates/benilla-world/src/sky_fx.rs | 117 ++++++++ crates/benilla-world/src/world_plugins.rs | 2 + 20 files changed, 734 insertions(+), 15 deletions(-) create mode 100644 THIRD-PARTY.md create mode 100644 crates/benilla-app/src/sky_quality.rs create mode 100644 crates/benilla-world/src/shaders/sky_fx.wgsl create mode 100644 crates/benilla-world/src/sky_fx.rs diff --git a/LIGHTING.md b/LIGHTING.md index 2122d3df..d0a9c7cd 100644 --- a/LIGHTING.md +++ b/LIGHTING.md @@ -24,6 +24,7 @@ behind each constant. | Moon shadows | at night the same shadow rig re-aims at the moon and casts a faint shadow; dims only the night sky term, never point lights | `moonShadowStrength` | | Ground-effect spells | Flamestrike, Rain of Fire, Consecration, Flare and fire traps light the ground for their duration; frost and nature areas stay dark | `spellLightGain` | | Volumetric fog | near-field haze that converges on the zone fog colour (clear within 10 yd, full by 150 yd; mistier at dawn and in bad weather, faint indoors) and sun/moon light shafts through gaps, sampled from the shadow map; own fullscreen pass after the main pass (`benilla-app/src/volumetric_fog.rs`) | Advanced Graphics → Volumetric Fog (Off/Low/High), cvar `volumetricFog`, env `WOW_VOLFOG=0\|1\|2` | +| Sky quality | Enhanced: the five Light.dbc sky stops through a smooth monotone curve in linear light (no bands at the rings), a soft sun glow tinted by sun and fog colour (fades at night and under cloud), a procedural star field with twinkle and a faint Milky Way over the stock `Stars.m2`. High adds domain-warped cloud detail and sun-lit clouds (self-shadow, silver lining; technique from WarcraftXL, see `THIRD-PARTY.md`). Classic is the reference sky unchanged | Advanced Graphics → Sky Quality, cvar `skyQuality` 0 Classic / 1 Enhanced / 2 High, env `WOW_SKY_QUALITY` | | Night and interior level | global dimming of the night sky term and of interior ambient | `nightGain`, `interiorGain`, `interiorBakeFloor` | Players reach all of it from **Options -> Advanced Graphics** (a Lighting Quality preset Off / Low / Medium / High plus the individual rows; Off is the original client look). The dev build has a panel for all of it: **Ctrl+Shift+D → Lighting & shadows**, with Dim / Default / @@ -47,6 +48,7 @@ is its own module, documented in `WATER.md`. `character_shadow.rs`, `world_shadow.rs`, `blob_shadow.rs`, `entities/carried_light.rs`, `entities/spell_fx/lifecycle.rs`, `dynamic_interior.rs`, `debug_panel/lighting_controls.rs`, and the cvars in `cvars.rs` / `video.rs`. +- **Sky** (`crates/benilla-world/src`): `sky_fx.rs` (tier, clock, glow inputs), `shaders/sky_fx.wgsl` (gradient curve, glow, stars, cloud noise), the tier branches in `shaders/sky.wgsl` and `shaders/cloud.wgsl`, `clouds/layer.rs` (`update_cloud_fx`); the cvar bridge is `benilla-app/src/sky_quality.rs`. - **Tools**: `benilla-extract wmolights [--verts ]`, `wmolamps`, `m2firescan` print the inputs the system works from (groups, batch classes, portals, claims, flame emitters). diff --git a/THIRD-PARTY.md b/THIRD-PARTY.md new file mode 100644 index 00000000..d28892d1 --- /dev/null +++ b/THIRD-PARTY.md @@ -0,0 +1,13 @@ +# Third-party code and techniques + +## WarcraftXL + +[WarcraftXL](https://github.com/WarcraftXL) by **iThorgrim** is a 3.3.5 client extension. The files +below port code or a technique from it; each carries the header line +`Ported from WarcraftXL (https://github.com/WarcraftXL) by iThorgrim — module , .` + +| File | WarcraftXL module | Source files | What was taken | +|---|---|---|---| +| `crates/benilla-world/src/shaders/sky_fx.wgsl` | wxl-retail-clouds | `Clouds.cpp`, `Clouds.hpp` | Domain-warped billow fbm (quintic value noise, 0.6 cotton blend, 0.55 octave gain) for the cloud detail | +| `crates/benilla-world/src/shaders/cloud.wgsl` | wxl-retail-clouds | `Clouds.cpp` | The 3-tap march toward the sun (self-shadow, ambient floor) and the one-tap silver lining | +| `crates/benilla-world/src/clouds/layer.rs` (`update_cloud_fx`) | wxl-retail-clouds | `Clouds.cpp` | March direction = the sun projected on the sheet, strength folded by its flatness (noon clouds evenly lit) | diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index 71e19e82..b1d74e0b 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -494,6 +494,8 @@ lightingQuality = "Lighting Quality", -- MONKEY (volumetric fog): localised atmospheric quality row. volumetricFog = "Volumetric Fog", + -- MONKEY (sky): the sky tier row. + skyQuality = "Sky Quality", fogOff = "Off", waterQuality = "Water Quality", waterClassic = "Classic", @@ -534,6 +536,12 @@ -- MONKEY (volumetric fog): explain cost and the shadow-map dependency. VOLUMETRIC_FOG = "Off disables volumetric fog. Low adds nearby haze and sun or moon shafts with fewer steps; High uses more steps for smoother shafts. Mist is heavier at dawn and in bad weather, and subtle indoors. Shafts require shadows; with shadows off only haze remains.", + -- MONKEY (sky): what each sky tier adds. + SKY_QUALITY = + "Classic is the original client's sky. Enhanced smooths the sky's colour " + .. "gradient, adds a soft glow around the sun and a starry night sky with the " + .. "Milky Way. High also gives the clouds finer detail and sunlit edges. " + .. "Costs very little.", WATER_QUALITY = "Classic is the original client's water. Enhanced adds soft shorelines, " .. "animated foam around shores and objects, moving wave highlights, sky reflection " @@ -618,6 +626,8 @@ lightingQuality = "Качество освещения", -- MONKEY (volumetric fog): Russian atmospheric quality row. volumetricFog = "Объёмный туман", + -- MONKEY (sky): the sky tier row. + skyQuality = "Качество неба", fogOff = "Выкл", waterQuality = "Качество воды", waterClassic = "Классическое", @@ -660,6 +670,12 @@ -- MONKEY (volumetric fog): localised effect and cost explanation. VOLUMETRIC_FOG = "Выкл отключает объёмный туман. Низкое добавляет дымку вблизи и лучи солнца или луны с меньшим числом шагов; Высокое использует больше шагов для более плавных лучей. Туман гуще на рассвете и в непогоду, а в помещениях слабее. Для лучей нужны тени; без теней остаётся только дымка.", + -- MONKEY (sky): what each sky tier adds. + SKY_QUALITY = + "«Классическое» — небо оригинального клиента. «Улучшенное» сглаживает " + .. "цветовой переход неба, добавляет мягкое сияние вокруг солнца и звёздное " + .. "ночное небо с Млечным Путём. «Высокое» также добавляет облакам мелкие " + .. "детали и освещённые солнцем края. Почти не влияет на производительность.", WATER_QUALITY = "«Классическое» — вода оригинального клиента. «Улучшенное» добавляет плавные переходы у берегов, " .. "анимированную пену у берегов и вокруг объектов, подвижные блики на волнах, отражение неба " @@ -796,6 +812,8 @@ AdvancedGraphics = { -- MONKEY (volumetric fog): include in refresh, defaults and search. "RowLightingQuality", "RowWaterQuality", "RowVolumetricFog", + -- MONKEY (sky): the sky tier row. + "RowSkyQuality", "RowCharacterShadows", "RowWorldShadows", "RowShadowDistance", "RowShadowResolution", "RowMoonShadows", "RowInteriorLight", "RowInteriorShadows", "RowExteriorShadows", "RowTorchSoft", @@ -2602,11 +2620,27 @@ }) - + + + + OptionsRow_OnLoad(self, "skyQuality", BENILLA_ADVGFX.skyQuality, + "BENILLA_TOOLTIP_SKY_QUALITY") + OptionsDropdownRow_Setup(self, { + { text = BENILLA_ADVGFX.waterClassic, value = "0" }, + { text = BENILLA_ADVGFX.waterEnhanced, value = "1" }, + { text = BENILLA_ADVGFX.waterHigh, value = "2" }, + }) + + + + + + + OptionsRow_OnLoad(self, "characterShadows", BENILLA_ADVGFX.characterShadows, diff --git a/crates/benilla-app/src/capture/scenarios.rs b/crates/benilla-app/src/capture/scenarios.rs index 7da0b3fa..2e1ef189 100644 --- a/crates/benilla-app/src/capture/scenarios.rs +++ b/crates/benilla-app/src/capture/scenarios.rs @@ -253,6 +253,8 @@ pub(super) enum SubjectKind { pub(super) const GROUND_EYE: [f32; 3] = [-8980.0, -160.0, 110.0]; pub(super) const GROUND_LOOK: [f32; 3] = [-8949.95, -132.49, 84.0]; pub(super) const SKY_EYE: [f32; 3] = [-8980.0, -160.0, 112.0]; +/// MONKEY (sky): 80 yd over the Northshire sky eye, above every canopy. +pub(super) const SKY_HIGH_EYE: [f32; 3] = [-8980.0, -160.0, 190.0]; pub(super) const SKY_LOOK: [f32; 3] = [-8740.0, 80.0, 168.0]; // up + out: horizon in the lower third, dome above // Farmhouse viewpoints (decision 0071): compass looks from the human-start login spot. Kept @@ -1379,41 +1381,50 @@ pub(super) const ON_DEMAND: &[Scenario] = &[ minute: 0, ui: None, }, - // MONKEY (sky): the sky lane's fixtures. Dusk looks from the Goldshire lake toward the low sun - // (az 45°, elev ≈5° at 20:00); night looks away from the moon at 01:00 (stars at full curve); - // zenith is the high dome at night. Overcast is `sky-overcast` under `WOW_WEATHER=rain,0.6`. + // MONKEY (sky): the sky lane's fixtures, from 80 yd above Northshire (clear of the canopy). + // Dusk faces the low sun (az 45°, elev ≈5° at 20:00); night faces away from the moon at 01:00 + // (full star curve); zenith looks nearly straight up; overcast is meant for + // `WOW_WEATHER=1,0.5`; noon faces away from the sun. Scenario { name: "sky-elwynn-dusk", map: Some(MAP_AZEROTH), - eye: WATER_EYE, - look: [-9315.0, -98.6, 102.3], + eye: SKY_HIGH_EYE, + look: [-8770.0, 50.0, 232.0], minute: 1200, ui: None, }, Scenario { name: "sky-elwynn-night", map: Some(MAP_AZEROTH), - eye: WATER_EYE, - look: [-9739.0, -522.6, 210.8], + eye: SKY_HIGH_EYE, + look: [-9164.0, -344.0, 340.0], minute: 60, ui: None, }, Scenario { name: "sky-zenith-night", map: Some(MAP_AZEROTH), - eye: SKY_EYE, - look: [-9050.7, -230.7, 387.0], + eye: SKY_HIGH_EYE, + look: [-9017.0, -197.0, 485.0], minute: 60, ui: None, }, Scenario { name: "sky-overcast", map: Some(MAP_AZEROTH), - eye: SKY_EYE, - look: [-8797.0, 23.0, 264.0], + eye: SKY_HIGH_EYE, + look: [-9185.0, 45.0, 268.0], minute: 1000, ui: None, }, + Scenario { + name: "sky-noon", + map: Some(MAP_AZEROTH), + eye: SKY_HIGH_EYE, + look: [-9185.0, -365.0, 268.0], + minute: 720, + ui: None, + }, ]; /// The owner's reported vantage for the lava-glow report: `(-7048.8, -1000.6, 242.0)` facing diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs index 31a7eced..a72f81f4 100644 --- a/crates/benilla-app/src/cvars.rs +++ b/crates/benilla-app/src/cvars.rs @@ -921,6 +921,12 @@ pub(crate) const REGISTERED: &[Registered] = &[ ), // MONKEY (volumetric fog): saved live tier; capture override stays session-only. ours("volumetricFog", "1", "benilla's own: near-field volumetric fog, 0 Off / 1 Low / 2 High"), + // MONKEY (sky): the sky tier; the High graphics preset sets 2. + ours( + "skyQuality", + "0", + "benilla's own: sky quality, 0 Classic / 1 Enhanced (smooth gradient, sun glow, stars) / \n 2 High (+ detailed sun-lit clouds)", + ), ours( "waterQuality", "1", diff --git a/crates/benilla-app/src/game_plugins.rs b/crates/benilla-app/src/game_plugins.rs index adda66ec..2e3fe53b 100644 --- a/crates/benilla-app/src/game_plugins.rs +++ b/crates/benilla-app/src/game_plugins.rs @@ -245,6 +245,8 @@ impl PluginGroup for GamePlugins { .add(crate::dynamic_interior::DynamicInteriorPlugin) // MONKEY (volumetric fog): bridge the live setting to Bevy. .add(crate::volumetric_fog::VolumetricFogPlugin) + // MONKEY (sky): the sky tier bridge. + .add(crate::sky_quality::SkyQualityPlugin) // The realmlist (decision 1667) — the logon address the login screen edits. Same reason as // VideoPlugin above: it is a CVar knob, so its resource has to exist before `load_config`. .add(crate::realmlist::RealmlistPlugin) diff --git a/crates/benilla-app/src/lib.rs b/crates/benilla-app/src/lib.rs index a7f9ac23..2f38edc0 100644 --- a/crates/benilla-app/src/lib.rs +++ b/crates/benilla-app/src/lib.rs @@ -75,6 +75,8 @@ mod doodad_events; // MONKEY (volumetric fog): optional near-field atmosphere plugin. mod volumetric_fog; mod dynamic_interior; +// MONKEY (sky): the skyQuality cvar bridge. +mod sky_quality; mod entities; mod fishing_line; mod footprints; diff --git a/crates/benilla-app/src/sky_quality.rs b/crates/benilla-app/src/sky_quality.rs new file mode 100644 index 00000000..e56503e7 --- /dev/null +++ b/crates/benilla-app/src/sky_quality.rs @@ -0,0 +1,49 @@ +//! MONKEY (sky): the `skyQuality` cvar → [`benilla_world::sky_fx::SkyQuality`] bridge. +//! +//! 0 Classic (the reference's sky, unchanged), 1 Enhanced (smooth gradient, sun glow, night sky), +//! 2 High (+ detailed, sun-lit clouds). `$WOW_SKY_QUALITY` pins the tier for the session, so +//! captures stay independent of `config.toml`. + +use bevy::prelude::*; +use benilla_world::sky_fx::SkyQuality; + +use crate::video::VideoConfig; + +pub(crate) struct SkyQualityPlugin; + +impl Plugin for SkyQualityPlugin { + fn build(&self, app: &mut App) { + app.init_resource::().add_systems(Update, bridge); + } +} + +fn bridge(video: Res, mut out: ResMut, mut pinned: Local>>) { + let pin = *pinned.get_or_insert_with(SkyQuality::env_override); + let want = SkyQuality(pin.unwrap_or(video.sky_quality).min(SkyQuality::MAX)); + if *out != want { + *out = want; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bridge_publishes_the_tier_without_dirtying_idle_frames() { + if SkyQuality::env_override().is_some() { + return; // pinned by the environment; nothing to observe + } + let mut app = App::new(); + app.init_resource::().add_plugins(SkyQualityPlugin); + app.update(); + assert_eq!(app.world().resource::().0, VideoConfig::default().sky_quality); + for tier in [2u8, 1, 0] { + app.world_mut().resource_mut::().sky_quality = tier; + app.update(); + assert_eq!(app.world().resource::().0, tier); + app.update(); + assert!(!app.world().resource_ref::().is_changed()); + } + } +} diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs index 43d3f917..812352c0 100644 --- a/crates/benilla-app/src/ui_script/options_tests.rs +++ b/crates/benilla-app/src/ui_script/options_tests.rs @@ -3000,6 +3000,8 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() { const BENILLA_OWNED: &[(&str, &str)] = &[ // MONKEY (volumetric fog): the row owns a translated tooltip too. ("BENILLA_TOOLTIP_VOLUMETRIC_FOG", "AdvancedGraphicsRowVolumetricFog"), + // MONKEY (sky): the sky tier row owns its translated tooltip. + ("BENILLA_TOOLTIP_SKY_QUALITY", "AdvancedGraphicsRowSkyQuality"), ("BENILLA_TOOLTIP_WATER_QUALITY", "AdvancedGraphicsRowWaterQuality"), ("BENILLA_TOOLTIP_LAVA_GLOW", "AdvancedGraphicsRowLavaGlow"), ("BENILLA_TOOLTIP_RENDER_SCALE", "GraphicsRowRenderScale"), diff --git a/crates/benilla-app/src/video.rs b/crates/benilla-app/src/video.rs index 07c17824..0fcc5c1e 100644 --- a/crates/benilla-app/src/video.rs +++ b/crates/benilla-app/src/video.rs @@ -222,6 +222,8 @@ pub(crate) struct VideoConfig { pub(crate) water_quality: u8, // MONKEY (volumetric fog): live camera raymarch tier: 0 Off, 1 Low, 2 High. pub(crate) volumetric_fog: u8, + // MONKEY (sky): the sky tier: 0 Classic, 1 Enhanced, 2 High (`sky_quality::bridge`). + pub(crate) sky_quality: u8, /// Brightness of lava lighting its surroundings, 0..4; 0 disables the glow. /// Published to `benilla_world::lighting::LavaLightGain` by `dynamic_interior::bridge`. pub(crate) lava_light_gain: f32, @@ -467,6 +469,8 @@ impl Default for VideoConfig { water_quality: 1, // MONKEY (volumetric fog): default to the inexpensive atmosphere. volumetric_fog: 1, + // MONKEY (sky): Classic until a preset or the player picks a tier. + sky_quality: 0, lava_light_gain: 1.0, fire_flicker: 1.0, display: if windowed_env() { @@ -547,6 +551,8 @@ pub(crate) fn on_cvar( "waterquality" => cfg.water_quality = v.clamp(0.0, 2.0) as u8, // MONKEY (volumetric fog): constrain UI/console writes to supported tiers. "volumetricfog" => cfg.volumetric_fog = v.clamp(0.0, 2.0) as u8, + // MONKEY (sky): the sky tier, clamped to Classic..High. + "skyquality" => cfg.sky_quality = v.clamp(0.0, 2.0) as u8, "lavalightgain" => cfg.lava_light_gain = v.clamp(0.0, 4.0), "worldshadows" => cfg.world_shadows = ev.flag(), "charactershadows" => cfg.character_shadows = ev.flag(), diff --git a/crates/benilla-world/src/clouds/layer.rs b/crates/benilla-world/src/clouds/layer.rs index c1abfe8c..b76e8024 100644 --- a/crates/benilla-world/src/clouds/layer.rs +++ b/crates/benilla-world/src/clouds/layer.rs @@ -34,6 +34,13 @@ pub struct CloudExt { #[texture(100)] #[sampler(101)] pub(crate) texels: Handle, + /// MONKEY (sky): `x` the sky tier (detail at 2), `y` the sky clock, `zw` the unit direction + /// toward the glow body on the sheet ([`update_cloud_fx`]). + #[uniform(102)] + pub(crate) fx: Vec4, + /// MONKEY (sky): the Light.dbc cloud sun colour (`rgb`, gamma) and the march strength (`w`). + #[uniform(102)] + pub(crate) lit: Vec4, } impl MaterialExtension for CloudExt { @@ -156,6 +163,9 @@ pub(super) fn setup_cloud_layer( }, extension: CloudExt { texels: image.clone(), + // MONKEY (sky): Classic until `update_cloud_fx` reads the tier. + fx: Vec4::ZERO, + lit: Vec4::ZERO, }, }); commands.spawn(( @@ -213,6 +223,48 @@ pub(super) fn follow_cloud_dome( *gt = GlobalTransform::from(*tf); } +/// MONKEY (sky): feeds the High cloud shading. +/// Ported from WarcraftXL (https://github.com/WarcraftXL) by iThorgrim — module wxl-retail-clouds, Clouds.cpp. +/// The march walks the sheet toward the glow body +/// (sun by day, moon by night), scaled by how far the body sits off the zenith (a noon sun has no +/// direction on the sheet worth marching, as in WarcraftXL) and by the kernel's own glow envelope. +pub(super) fn update_cloud_fx( + light: Res, + quality: Res, + clock: Res, + layer: Option>, + mut materials: ResMut>, +) { + let Some(layer) = layer else { + return; + }; + let (fx, lit) = if quality.high() { + let body = light.cloud_glow_dir; + let flat = Vec2::new(body.x, body.z); + let len = flat.length(); + let d = if len > 1e-3 { flat / len } else { Vec2::ZERO }; + let strength = + (len * 1.6).min(1.0) * light.cloud_glow_track * (1.0 - 0.75 * light.storm_bcc); + let q = |v: f32| benilla_assets::quantize(v, 4096.0); + let sun = light.cloud_colors[0]; + ( + Vec4::new(f32::from(quality.0), clock.secs, q(d.x), q(d.y)), + Vec4::new(q(sun[0]), q(sun[1]), q(sun[2]), q(strength.clamp(0.0, 1.0))), + ) + } else { + (Vec4::ZERO, Vec4::ZERO) + }; + benilla_assets::write_gated( + &mut materials, + &layer.material, + |m| m.extension.fx != fx || m.extension.lit != lit, + |m| { + m.extension.fx = fx; + m.extension.lit = lit; + }, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/benilla-world/src/clouds/mod.rs b/crates/benilla-world/src/clouds/mod.rs index aca2e21e..8464004e 100644 --- a/crates/benilla-world/src/clouds/mod.rs +++ b/crates/benilla-world/src/clouds/mod.rs @@ -69,6 +69,8 @@ impl Plugin for CloudsPlugin { layer::apply_cloud_visibility .after(crate::skybox::SkyboxResolve) .after(crate::liquid::SubmersionVerdict), + // MONKEY (sky): the High cloud shading's per-frame inputs. + layer::update_cloud_fx.in_set(crate::lighting::LightingConsumeSet), ), ) // Camera-anchored after transform propagation, like the sky dome. diff --git a/crates/benilla-world/src/lib.rs b/crates/benilla-world/src/lib.rs index 02aef8e8..0fb8964a 100644 --- a/crates/benilla-world/src/lib.rs +++ b/crates/benilla-world/src/lib.rs @@ -144,6 +144,8 @@ pub mod rig_rider; pub mod schedule; mod shaders; pub mod sky; +// MONKEY (sky): the sky quality tier and the Enhanced/High inputs. +pub mod sky_fx; pub mod sky_order; pub mod skybox; pub mod static_gx; diff --git a/crates/benilla-world/src/shaders.rs b/crates/benilla-world/src/shaders.rs index 52183c43..058ced84 100644 --- a/crates/benilla-world/src/shaders.rs +++ b/crates/benilla-world/src/shaders.rs @@ -11,6 +11,8 @@ use bevy::prelude::*; pub(crate) fn plugin(app: &mut App) { bevy::asset::embedded_asset!(app, "shaders/sky_vertex.wgsl"); bevy::asset::embedded_asset!(app, "shaders/sky.wgsl"); + // MONKEY (sky): the Enhanced/High library `sky.wgsl` and `cloud.wgsl` import. + bevy::asset::embedded_asset!(app, "shaders/sky_fx.wgsl"); bevy::asset::embedded_asset!(app, "shaders/star.wgsl"); bevy::asset::embedded_asset!(app, "shaders/cloud.wgsl"); bevy::asset::embedded_asset!(app, "shaders/celestial.wgsl"); @@ -59,6 +61,7 @@ mod tests { // `wow_model.wgsl`); 7 since 1429 added `static_gx.wgsl` (the B1 retained pass); // 8 since 2016 added `sky_vertex.wgsl` (the sky's shared far-depth vertex stage); // 9 since the torch-shadow Phase 1 added `torch_depth.wgsl` (the interior depth map). - assert_eq!(found, 9, "the engine's shader set changed size"); + // MONKEY (sky): 10 with `sky_fx.wgsl`, the sky's Enhanced/High library. + assert_eq!(found, 10, "the engine's shader set changed size"); } } diff --git a/crates/benilla-world/src/shaders/cloud.wgsl b/crates/benilla-world/src/shaders/cloud.wgsl index 084b54b8..91b18485 100644 --- a/crates/benilla-world/src/shaders/cloud.wgsl +++ b/crates/benilla-world/src/shaders/cloud.wgsl @@ -2,19 +2,64 @@ // the image the reference binds as its texture (`0x58ac70`). This stage applies the dome's rim // fade (`0x6d0530`) and blends the raw gamma texels premultiplied. Depth is the far pin in // `sky_vertex.wgsl`. +// +// MONKEY (sky): at `skyQuality` 2 the texels get GPU detail and a short sun march. +// Ported from WarcraftXL (https://github.com/WarcraftXL) by iThorgrim — module wxl-retail-clouds, Clouds.cpp. #import bevy_pbr::forward_io::VertexOutput +#import benilla_world::sky_fx @group(#{MATERIAL_BIND_GROUP}) @binding(100) var cloud_tex: texture_2d; @group(#{MATERIAL_BIND_GROUP}) @binding(101) var cloud_samp: sampler; +// MONKEY (sky): `fx.x` the sky tier, `.y` the sky clock (s), `.zw` the unit direction toward the +// glow body on the sheet (u = world x, v = world z); `lit.rgb` the Light.dbc cloud sun colour +// (gamma), `.w` the march strength (the body's flatness × the glow envelope). +struct CloudFx { + fx: vec4, + lit: vec4, +}; +@group(#{MATERIAL_BIND_GROUP}) @binding(102) var cfx: CloudFx; + +// MONKEY (sky): coverage with detail at a sheet point; `oct` detail octaves. +fn detailed_cover(uv: vec2, oct: i32) -> f32 { + let a = textureSampleLevel(cloud_tex, cloud_samp, uv, 0.0).a; + if (a <= 0.0) { + return 0.0; + } + return sky_fx::cloud_erode(a, sky_fx::cloud_detail(uv, cfx.fx.y, oct)); +} + @fragment fn fragment(in: VertexOutput) -> @location(0) vec4 { let texel = textureSample(cloud_tex, cloud_samp, in.uv); var a = texel.a; + var rgb = texel.rgb; + // MONKEY (sky): High only; Classic and Enhanced keep the kernel's texels as they are. + if (cfx.fx.x >= 1.5) { + let n = sky_fx::cloud_detail(in.uv, cfx.fx.y, 4); + a = sky_fx::cloud_erode(a, n); + // A painterly tone inside the mass from the same detail. + rgb = rgb * (0.93 + 0.14 * n); + let strength = cfx.lit.w; + if (a > 0.003 && strength > 0.0) { + let sd = cfx.fx.zw; + let step = 0.011; + var occl = detailed_cover(in.uv + sd * step, 2); + occl += detailed_cover(in.uv + sd * step * 2.0, 2) * 0.75; + occl += detailed_cover(in.uv + sd * step * 3.5, 2) * 0.5; + let transmit = exp(-occl * 1.44 * strength); + // WarcraftXL's ambient floor 0.55, softened toward the painted texel. + let light = 0.62 + 0.38 * transmit; + let ahead = detailed_cover(in.uv + sd * step * 1.5, 2); + let rim = max(ahead - a, 0.0) * (1.0 - a) * transmit * strength; + rgb = rgb * mix(1.0, light, strength) + cfx.lit.rgb * rim * 0.9; + rgb = min(rgb, vec3(1.0)); + } + } #ifdef VERTEX_COLORS a *= in.color.a; // the dome's rim fade (ring alphas) #endif // Premultiplied gamma blend; the RGB is already the reference's byte math. - return vec4(texel.rgb * a, a); + return vec4(rgb * a, a); } diff --git a/crates/benilla-world/src/shaders/sky.wgsl b/crates/benilla-world/src/shaders/sky.wgsl index e25ae0fa..c1f371f6 100644 --- a/crates/benilla-world/src/shaders/sky.wgsl +++ b/crates/benilla-world/src/shaders/sky.wgsl @@ -5,6 +5,8 @@ forward_io::VertexOutput, mesh_view_bindings::view, } +// MONKEY (sky): the Enhanced/High library (smooth gradient, sun glow, night sky). +#import benilla_world::sky_fx struct SkyColors { sky0: vec4, // zenith (90°) @@ -14,6 +16,13 @@ struct SkyColors { sky4: vec4, // 1.8° fog: vec4, // horizon (0°) and below: LightIntBand row 7 warp: vec4, // x = dawn/dusk warp strength S (0 = off), y = sun azimuth (rad), zw reserved + // MONKEY (sky): x = skyQuality (0 Classic), y = sky clock (s), z = night-sky alpha, w = glow + // strength. + fx: vec4, + // MONKEY (sky): xyz = camera to the visible sun, w unused. + sun: vec4, + // MONKEY (sky): rgb = glow colour (gamma), w unused. + glow: vec4, }; @group(#{MATERIAL_BIND_GROUP}) @binding(100) var sky: SkyColors; @@ -66,6 +75,41 @@ fn fragment(in: VertexOutput) -> @location(0) vec4 { s4 = mix(warp_one(sky.sky4.rgb, g0, warp_s), warp_one(sky.sky4.rgb, g1, warp_s), f); } + // MONKEY (sky): Enhanced/High. The same stops through a monotone cubic in linear light, then + // the sun glow and the night sky added in linear light, back to gamma, dithered. + if (sky.fx.x >= 0.5) { + let stops = array, 6>( + sky_fx::srgb_to_linear(sky.fog.rgb), + sky_fx::srgb_to_linear(s4), + sky_fx::srgb_to_linear(s3), + sky_fx::srgb_to_linear(s2c), + sky_fx::srgb_to_linear(s1), + sky_fx::srgb_to_linear(sky.sky0.rgb), + ); + var lin = sky_fx::smooth_gradient(elev, stops); + // Sun glow: a halo tinted by the sun and fog colours, faded at night and under cloud on the + // CPU; below the horizon it thins out over the fog band. + if (sky.fx.w > 0.0) { + let below = smoothstep(-0.12, 0.02, dir.y); + lin += sky_fx::srgb_to_linear(sky.glow.rgb) + * (sky.fx.w * below * sky_fx::sun_glow(dir, normalize(sky.sun.xyz))); + } + // Night sky: stars and the Milky Way, fixed to world directions, thinned toward the + // horizon. The cloud dome draws over this, so clouds hide it where they stand. + let night = sky.fx.z; + // The pixel's size in star-grid units; derivatives stay in uniform control flow. + let px = length(fwidth(dir)) * sky_fx::STAR_GRID * 0.5; + if (night > 0.0 && dir.y > 0.0) { + let band = sky_fx::galaxy_band(dir); + let stars = sky_fx::star_field(dir, sky.fx.y, max(px, 1e-4), band) + * smoothstep(0.0, 0.2, dir.y); + let milky = sky_fx::milky_way(dir, band) * 0.05 * smoothstep(0.03, 0.35, dir.y); + lin += (stars * 0.85 + milky) * night; + } + let out = sky_fx::linear_to_srgb(lin) + vec3(sky_fx::dither_tri(in.position.xy)); + return vec4(clamp(out, vec3(0.0), vec3(1.0)), 1.0); + } + // Elevation gradient, linear between rings like the reference's Gouraud-shaded dome. var col: vec3; if (elev <= 0.0) { diff --git a/crates/benilla-world/src/shaders/sky_fx.wgsl b/crates/benilla-world/src/shaders/sky_fx.wgsl new file mode 100644 index 00000000..ed74f870 --- /dev/null +++ b/crates/benilla-world/src/shaders/sky_fx.wgsl @@ -0,0 +1,269 @@ +// MONKEY (sky): the Enhanced/High sky library (`skyQuality` ≥ 1), imported by `sky.wgsl` and +// `cloud.wgsl`. Classic never calls into it. Colour math is in linear light; the callers convert +// the gamma stops in and the result back out. +// +// The cloud detail (`cloud_billow`, `cloud_detail`): +// Ported from WarcraftXL (https://github.com/WarcraftXL) by iThorgrim — module wxl-retail-clouds, Clouds.cpp, Clouds.hpp. +#define_import_path benilla_world::sky_fx + +const TAU: f32 = 6.2831853; + +fn srgb_to_linear(c: vec3) -> vec3 { + let lo = c / 12.92; + let hi = pow((max(c, vec3(0.0)) + 0.055) / 1.055, vec3(2.4)); + return select(hi, lo, c <= vec3(0.04045)); +} + +fn linear_to_srgb(c: vec3) -> vec3 { + let lo = c * 12.92; + let hi = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055; + return select(hi, lo, c <= vec3(0.0031308)); +} + +// ---- hashes and noise ------------------------------------------------------------------------- + +// pcg3d (Jarzynski & Olano 2020): three well-mixed u32 from three. +fn pcg3(v_in: vec3) -> vec3 { + var v = v_in * 1664525u + 1013904223u; + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + v = v ^ (v >> vec3(16u)); + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + return v; +} + +fn hash33(p: vec3) -> vec3 { + return vec3(pcg3(bitcast>(p)) >> vec3(8u)) * (1.0 / 16777216.0); +} + +fn hash_cell2(p: vec2, salt: u32) -> f32 { + return f32(pcg3(vec3(bitcast>(p), salt)).x >> 8u) * (1.0 / 16777216.0); +} + +// Value noise with the quintic fade: the cubic's second-derivative seam shows as square plateaus +// on a slowly drifting sky (the WarcraftXL note). +fn value_noise2(p: vec2, salt: u32) -> f32 { + let i = vec2(floor(p)); + let f = fract(p); + let u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); + let a = hash_cell2(i, salt); + let b = hash_cell2(i + vec2(1, 0), salt); + let c = hash_cell2(i + vec2(0, 1), salt); + let d = hash_cell2(i + vec2(1, 1), salt); + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + +fn value_noise3(p: vec3) -> f32 { + let i = vec3(floor(p)); + let f = fract(p); + let u = f * f * (3.0 - 2.0 * f); + let n000 = hash33(i).x; + let n100 = hash33(i + vec3(1, 0, 0)).x; + let n010 = hash33(i + vec3(0, 1, 0)).x; + let n110 = hash33(i + vec3(1, 1, 0)).x; + let n001 = hash33(i + vec3(0, 0, 1)).x; + let n101 = hash33(i + vec3(1, 0, 1)).x; + let n011 = hash33(i + vec3(0, 1, 1)).x; + let n111 = hash33(i + vec3(1, 1, 1)).x; + let x00 = mix(n000, n100, u.x); + let x10 = mix(n010, n110, u.x); + let x01 = mix(n001, n101, u.x); + let x11 = mix(n011, n111, u.x); + return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z); +} + +fn fbm3(p: vec3) -> f32 { + var s = 0.0; + var a = 0.5; + var q = p; + for (var o = 0; o < 3; o++) { + s += a * value_noise3(q); + q = q * 2.03 + vec3(1.7, 9.2, 3.4); + a *= 0.5; + } + return s / 0.875; +} + +// ---- S1: the smooth gradient ------------------------------------------------------------------ + +// The reference's ring elevations (degrees), horizon to zenith. +const STOP_ELEV = array(0.0, 1.8, 3.7, 9.8, 16.8, 90.0); + +// Fritsch–Carlson tangent at an interior stop: 0 at a local extremum, else the weighted harmonic +// mean of the two secants, so the curve never overshoots a stop (no halos, no new colours). +fn pchip_tangent(d0: vec3, d1: vec3, h0: f32, h1: f32) -> vec3 { + let w1 = 2.0 * h1 + h0; + let w2 = h1 + 2.0 * h0; + let safe0 = select(d0, vec3(1.0), d0 == vec3(0.0)); + let safe1 = select(d1, vec3(1.0), d1 == vec3(0.0)); + let m = (w1 + w2) / (w1 / safe0 + w2 / safe1); + return select(vec3(0.0), m, d0 * d1 > vec3(0.0)); +} + +// Monotone cubic through the six stops (`y[0]` = fog at 0°, `y[5]` = zenith), linear light. The +// end tangents are 0: flat into the fog band below the horizon and flat over the zenith, so the +// curve is C1 everywhere and the rings leave no Mach bands. +fn smooth_gradient(elev: f32, y: array, 6>) -> vec3 { + if (elev <= 0.0) { + return y[0]; + } + if (elev >= 90.0) { + return y[5]; + } + var yy = y; + var se = STOP_ELEV; + var d: array, 5>; + var h: array; + for (var i = 0; i < 5; i++) { + h[i] = se[i + 1] - se[i]; + d[i] = (yy[i + 1] - yy[i]) / h[i]; + } + var k = 0; + for (var i = 1; i < 5; i++) { + if (elev >= se[i]) { + k = i; + } + } + var m0 = vec3(0.0); + var m1 = vec3(0.0); + if (k > 0) { + m0 = pchip_tangent(d[k - 1], d[k], h[k - 1], h[k]); + } + if (k < 4) { + m1 = pchip_tangent(d[k], d[k + 1], h[k], h[k + 1]); + } + let hk = h[k]; + let t = (elev - se[k]) / hk; + let t2 = t * t; + let t3 = t2 * t; + let h00 = 2.0 * t3 - 3.0 * t2 + 1.0; + let h10 = t3 - 2.0 * t2 + t; + let h01 = -2.0 * t3 + 3.0 * t2; + let h11 = t3 - t2; + return h00 * yy[k] + h10 * hk * m0 + h01 * yy[k + 1] + h11 * hk * m1; +} + +// Triangular dither of ±1 LSB at 8 bits, fixed per pixel (no temporal shimmer). +fn dither_tri(frag: vec2) -> f32 { + let h = hash33(vec3(vec2(frag), 7)); + return (h.x + h.y - 1.0) / 255.0; +} + +// ---- S4: the sun glow ------------------------------------------------------------------------- + +// Henyey–Greenstein phase normalised to 1 toward the sun. +fn hg_norm(c: f32, g: f32) -> f32 { + let g2 = g * g; + let a = 1.0 + g2 - 2.0 * g * c; + let a1 = (1.0 - g) * (1.0 - g); + return pow(a1 / a, 1.5); +} + +// A two-lobe Mie-like halo: a broad aureole plus a tighter core round the disc. +fn sun_glow(dir: vec3, sun: vec3) -> f32 { + let c = dot(dir, sun); + return 0.55 * hg_norm(c, 0.70) + 0.45 * hg_norm(c, 0.93); +} + +// ---- S5: the night sky ------------------------------------------------------------------------ + +// Grid cells per unit radius: one cell spans ~0.3°, so a candidate star every few pixels. +const STAR_GRID: f32 = 180.0; + +// A procedural star field fixed to world directions. Each grid cell holds at most one star (a +// density draw), jittered inside the cell; the eight cells round the sample are tested so no star +// is cut at a cell face. Brightness is a steep power law (few bright, many faint), the footprint +// is at least a pixel wide so a star does not vanish between pixels, and a slow twinkle is +// stronger low in the sky. `px` is the pixel's size in grid units. +fn star_field(dir: vec3, t: f32, px: f32, band: f32) -> vec3 { + let p = dir * STAR_GRID; + let base = floor(p); + let f = p - base; + let o = select(vec3(-1.0), vec3(1.0), f > vec3(0.5)); + let density = 0.035 * (1.0 + 2.5 * band); + var acc = vec3(0.0); + for (var i = 0u; i < 8u; i++) { + let off = vec3(f32(i & 1u), f32((i >> 1u) & 1u), f32((i >> 2u) & 1u)) * o; + let cell = vec3(base + off); + let h = hash33(cell); + if (h.x > density) { + continue; + } + let j = hash33(cell + vec3(7919, 104729, 1299709)); + let centre = normalize(vec3(cell) + 0.5 + (j - 0.5) * 0.9) * STAR_GRID; + let d = length(p - centre); + let mag = pow(h.y, 22.0); + let b = 0.035 + 0.965 * mag; + let sigma = px * (0.55 + 0.7 * mag); + let spot = exp(-d * d / (2.0 * sigma * sigma)); + let tw_amp = 0.18 + 0.3 * (1.0 - clamp(dir.y, 0.0, 1.0)); + let tw = 1.0 + tw_amp * sin(t * (1.3 + 3.1 * j.x) + TAU * j.y) * sin(t * (0.6 + 1.7 * j.z) + TAU * h.z); + // Star colour: mostly white, a few warm and a few blue-white. + let tint = mix(vec3(0.78, 0.86, 1.0), vec3(1.0, 0.88, 0.72), h.z); + acc += tint * (b * spot * tw); + } + return acc; +} + +// The Milky Way's plane in Bevy space (y up): tilted so the band arcs across the dome. +const GALAXY_N: vec3 = vec3(0.40, 0.52, -0.754); +const GALAXY_CORE: vec3 = vec3(-0.62, 0.36, -0.08); + +// Band weight in [0, 1] toward `dir`. +fn galaxy_band(dir: vec3) -> f32 { + let b = dot(dir, normalize(GALAXY_N)); + return exp(-b * b / (2.0 * 0.15 * 0.15)); +} + +// A faint, mottled band with a dark dust lane down its middle and a brighter core. +fn milky_way(dir: vec3, band: f32) -> vec3 { + if (band < 0.01) { + return vec3(0.0); + } + let b = dot(dir, normalize(GALAXY_N)); + let cloud = fbm3(dir * 5.0); + let lane = exp(-(b - 0.02) * (b - 0.02) / (2.0 * 0.03 * 0.03)) * smoothstep(0.35, 0.7, fbm3(dir * 9.0 + 11.0)); + let core = 1.0 + 1.2 * pow(max(dot(dir, normalize(GALAXY_CORE)), 0.0), 4.0); + let v = band * (0.3 + 0.7 * cloud) * (1.0 - 0.75 * lane) * core; + return vec3(0.72, 0.8, 1.0) * v; +} + +// ---- S3: cloud detail ------------------------------------------------------------------------- + +// WarcraftXL's billow blend: fold each octave toward rounded puffs by `cotton` (0.6). +fn billow(v: f32) -> f32 { + return v + (1.0 - abs(2.0 * v - 1.0) - v) * 0.6; +} + +// Billowed fbm, amplitude 0.55 per octave as in WarcraftXL. +fn cloud_billow(p: vec2, octaves: i32) -> f32 { + var s = 0.0; + var a = 1.0; + var tot = 0.0; + var q = p; + for (var o = 0; o < octaves; o++) { + s += a * billow(value_noise2(q, u32(o) * 57u)); + tot += a; + a *= 0.55; + q = q * 2.0 + vec2(3.7, 1.9); + } + return s / tot; +} + +// The detail field in [0, 1] on the sheet: a broad warp twists the domain before the billowed fbm +// is read, which turns thresholded blobs into cauliflower edges. +fn cloud_detail(uv: vec2, t: f32, octaves: i32) -> f32 { + let p = uv * 36.0 + vec2(0.011, 0.0043) * t; + let w = vec2(value_noise2(p * 0.5, 173u), value_noise2(p * 0.5 + 5.3, 219u)) - 0.5; + return cloud_billow(p + w * 2.6, octaves); +} + +// Apply the detail to a base coverage: the edges (mid coverage) are pushed in and out by the +// detail, solid cores and clear sky stay put, so the CPU tile still says where cloud is. +fn cloud_erode(a: f32, n: f32) -> f32 { + let edge = 4.0 * a * (1.0 - a); + return clamp(a + (n - 0.5) * 1.2 * edge, 0.0, 1.0); +} diff --git a/crates/benilla-world/src/sky.rs b/crates/benilla-world/src/sky.rs index 265fd9db..661cca7d 100644 --- a/crates/benilla-world/src/sky.rs +++ b/crates/benilla-world/src/sky.rs @@ -55,6 +55,16 @@ pub struct SkyExt { /// none), `y` the sun azimuth `atan2(sun.z, sun.x)` in radians, `zw` reserved. #[uniform(100)] pub(crate) warp: Vec4, + /// MONKEY (sky): `x` the [`crate::sky_fx::SkyQuality`] tier (0 = Classic, the fields below + /// unused), `y` the sky clock in seconds, `z` the night-sky alpha, `w` the sun-glow strength. + #[uniform(100)] + pub(crate) fx: Vec4, + /// MONKEY (sky): camera to the visible sun (`xyz`). + #[uniform(100)] + pub(crate) sun: Vec4, + /// MONKEY (sky): the glow colour, gamma (`rgb`). + #[uniform(100)] + pub(crate) glow: Vec4, } impl MaterialExtension for SkyExt { @@ -183,6 +193,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 (sky): Classic until update_sky_colors reads the tier. + fx: Vec4::ZERO, + sun: Vec4::Y, + glow: Vec4::ZERO, }, }); commands.spawn(( @@ -242,8 +256,13 @@ fn apply_sky_visibility( } /// Push the time-of-day sky stops and fog colour (`WowLighting`) into the dome material. +#[allow(clippy::too_many_arguments)] fn update_sky_colors( light: Res, + // MONKEY (sky): the tier, the sky clock and the cloud cover over the sun. + quality: Res, + clock: Res, + clouds: Res, dome: Query<&MeshMaterial3d, With>, mut materials: ResMut>, ) { @@ -270,11 +289,16 @@ fn update_sky_colors( 0.0, 0.0, ); + // MONKEY (sky): the Enhanced/High inputs; all zero at Classic so the write gate stays quiet. + let (fx, sun_v, glow) = sky_fx_inputs(&light, *quality, clock.secs, &clouds); benilla_assets::write_gated( &mut materials, &handle.0, |m| { - m.extension.sky0 != sky[0] + m.extension.fx != fx + || m.extension.sun != sun_v + || m.extension.glow != glow + || m.extension.sky0 != sky[0] || m.extension.sky1 != sky[1] || m.extension.sky2 != sky[2] || m.extension.sky3 != sky[3] @@ -290,6 +314,38 @@ fn update_sky_colors( m.extension.sky4 = sky[4]; m.extension.fog = fog; m.extension.warp = warp; + m.extension.fx = fx; + m.extension.sun = sun_v; + m.extension.glow = glow; }, ); } + +/// MONKEY (sky): the dome's Enhanced/High uniforms, `(fx, sun, glow)`; zeros at Classic. +fn sky_fx_inputs( + light: &WowLighting, + quality: crate::sky_fx::SkyQuality, + secs: f32, + clouds: &crate::clouds::CloudCoverage, +) -> (Vec4, Vec4, Vec4) { + if !quality.enhanced() { + return (Vec4::ZERO, Vec4::Y, Vec4::ZERO); + } + let sun = light.celestial_dir.normalize_or(Vec3::Y); + // The cover at the glare point, the same sample the flare's occlusion reads. + let cover = clouds.coverage(sun * 12.0); + let strength = crate::sky_fx::glow_strength(sun.y, cover, light.storm_bcc); + // Warm toward the sun colour, grounded in the fog colour so dusk glows orange, noon pale. + let tint = Vec3::from(light.celestial_tint); + let fog = Vec3::from(light.fog_color); + let glow = fog.lerp(tint, 0.6); + // The night sky follows the star curve, thinned by storm weather. + let night = (light.star_alpha * (1.0 - light.storm_bcc)).clamp(0.0, 1.0); + // Quantized like every other lane here, so an idle frame writes nothing. + let q = |v: f32| benilla_assets::quantize(v, 4096.0); + ( + Vec4::new(f32::from(quality.0), secs, q(night), q(strength)), + Vec4::new(q(sun.x), q(sun.y), q(sun.z), 0.0), + Vec4::new(q(glow.x), q(glow.y), q(glow.z), 0.0), + ) +} diff --git a/crates/benilla-world/src/sky_fx.rs b/crates/benilla-world/src/sky_fx.rs new file mode 100644 index 00000000..42321f58 --- /dev/null +++ b/crates/benilla-world/src/sky_fx.rs @@ -0,0 +1,117 @@ +//! MONKEY (sky): the sky quality tier and what the Enhanced/High sky reads each frame. +//! +//! `skyQuality` 0 is Classic, the reference's image byte for byte: the dome's piecewise-linear +//! gradient and the kernel's cloud texels, untouched. 1 (Enhanced) turns on the smooth gradient, +//! the sun glow and the procedural night sky in `sky.wgsl`; 2 (High) adds the cloud detail and the +//! sun-lit cloud shading in `cloud.wgsl`. The WGSL lives in `shaders/sky_fx.wgsl`. +//! +//! The High cloud detail and sun march (`sky_fx.wgsl`, `cloud.wgsl`, `clouds/layer.rs`): +//! Ported from WarcraftXL (https://github.com/WarcraftXL) by iThorgrim — module wxl-retail-clouds, Clouds.cpp, Clouds.hpp. + +use bevy::prelude::*; + +/// The sky tier: 0 Classic, 1 Enhanced, 2 High. Written by the app from the `skyQuality` cvar; +/// `$WOW_SKY_QUALITY` pins it for the session (captures). +#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Default)] +pub struct SkyQuality(pub u8); + +impl SkyQuality { + /// The highest tier. + pub const MAX: u8 = 2; + + /// The session override, `$WOW_SKY_QUALITY` clamped to the tiers. + pub fn env_override() -> Option { + std::env::var("WOW_SKY_QUALITY") + .ok() + .and_then(|v| v.trim().parse::().ok()) + .map(|v| v.min(Self::MAX)) + } + + pub fn enhanced(self) -> bool { + self.0 >= 1 + } + + pub fn high(self) -> bool { + self.0 >= 2 + } +} + +/// The sky's own animation clock (star twinkle, cloud detail drift), seconds wrapped to an hour so +/// the shader's `sin` keeps its precision. Frozen in capture mode, at `$WOW_CAPTURE_SKY_T` or 0. +#[derive(Resource, Default)] +pub struct SkyClock { + pub secs: f32, + frozen: Option, +} + +/// How strongly the glow shows: a broad halo at `GLOW_GAIN` × the sun colour at the sun itself. +pub(crate) const GLOW_GAIN: f32 = 0.32; + +/// The glow's day fade: full once the sun clears the horizon, gone a few degrees under it. +pub(crate) fn glow_day_fade(sun_y: f32) -> f32 { + let t = ((sun_y + 0.10) / 0.16).clamp(0.0, 1.0); + t * t * (3.0 - 2.0 * t) +} + +/// The glow strength: the day fade, dimmed by the cloud cover over the sun (the same coverage the +/// glare's occlusion reads) and by storm weather. +pub(crate) fn glow_strength(sun_y: f32, cover_at_sun: f32, bcc: f32) -> f32 { + GLOW_GAIN * glow_day_fade(sun_y) * (1.0 - 0.8 * cover_at_sun.clamp(0.0, 1.0)) + * (1.0 - 0.85 * bcc.clamp(0.0, 1.0)) +} + +pub(crate) struct SkyFxPlugin; + +impl Plugin for SkyFxPlugin { + fn build(&self, app: &mut App) { + // The library `sky.wgsl` and `cloud.wgsl` import; its embedded path is served by + // `shaders::plugin`, this keeps it loaded so the import resolves. + let lib: Handle = + bevy::asset::load_embedded_asset!(app, "shaders/sky_fx.wgsl"); + std::mem::forget(lib); + let frozen = std::env::var_os("WOW_CAPTURE").map(|_| { + std::env::var("WOW_CAPTURE_SKY_T") + .ok() + .and_then(|v| v.trim().parse::().ok()) + .unwrap_or(0.0) + }); + app.insert_resource(SkyClock { + secs: frozen.unwrap_or(0.0), + frozen, + }); + let quality = SkyQuality(SkyQuality::env_override().unwrap_or(0)); + app.insert_resource(quality) + .add_systems(Update, tick_sky_clock); + } +} + +fn tick_sky_clock(time: Res