Merge gfx/sky into integration

# Conflicts:
#	LIGHTING.md
#	THIRD-PARTY.md
#	crates/benilla-app/assets/ui/OptionsFrame.xml
#	crates/benilla-app/src/capture/scenarios.rs
#	crates/benilla-app/src/cvars.rs
#	crates/benilla-app/src/game_plugins.rs
#	crates/benilla-app/src/lib.rs
#	crates/benilla-app/src/video.rs
This commit is contained in:
pkuzic
2026-09-25 11:45:19 +01:00
20 changed files with 808 additions and 9 deletions
+2
View File
@@ -26,6 +26,7 @@ behind each constant.
| 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 dithering | a faint screen-space dither in the FFXGlow combine so smooth sky and fog gradients do not band (MONKEY p0; was env-only) | Advanced Graphics → Sky Dithering, cvar `skyDither` 0/1 (default 0, Graphics preset High = 1), env `WOW_DITHER=1` still forces it on; bridge in `benilla-app/src/monkey_gfx.rs` |
| HDR emission and bloom | lit WMO windows, additive models/particles and magma can exceed display white; a soft-capped fullscreen halo is added before the faithful FFX clamp | Advanced Graphics → Bloom (Off/Low/High), cvar `bloom`; Off is the unchanged reference image, suggested High preset value 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 /
@@ -54,6 +55,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 <Data> wmolights <wmo> [--verts <group>]`, `wmolamps`, `m2firescan`
print the inputs the system works from (groups, batch classes, portals, claims, flame emitters).
+5 -1
View File
@@ -33,6 +33,10 @@ where they stand. Do not remove either.
| `crates/benilla-world/src/water_fx/bob.rs` | `wxl-experimental-water` | `world/Ride.cpp` | Visual-only wave riding for swimmers; the authoritative position never moves (principle, implemented independently) |
| `crates/benilla-app/src/post/grading.rs` + `crates/benilla-app/src/post/grading.wgsl` | `wxl-retail-grading` | `Grading.*`, `shaders/Grading.ps.hlsl` | 32³ LUT convention, strength control and strip lookup mathematics, with the strip uploaded as a native 3D texture (ported from the GPL-3.0-or-later source implementation, Copyright (C) 2026 WarcraftXL) |
| `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) |
Planned (not yet in the tree): breaker index from `sea/Shore.hpp`, `shaders/Wave.hlsli`; grass wind from `wxl-experimental-wind`
(`field/Wind.*`, `grass/GrassWind.*`); cloud sheets from `wxl-retail-clouds` (`Clouds.*`).
(`field/Wind.*`, `grass/GrassWind.*`).
The lane that ports one adds its row here.
+39 -3
View File
@@ -498,6 +498,8 @@
bloom = "Bloom",
sunShafts = "Sun Shafts",
colorGrading = "Colour Grading",
-- MONKEY (sky): the sky tier row.
skyQuality = "Sky Quality",
fogOff = "Off",
waterQuality = "Water Quality",
waterClassic = "Classic",
@@ -547,6 +549,12 @@
"Applies a subtle zone-specific colour palette to the 3D world and blends between its day and night looks. The interface is never graded.",
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 "
@@ -639,6 +647,8 @@
colorGrading = "Цветокоррекция",
-- MONKEY (volumetric fog): Russian atmospheric quality row.
volumetricFog = "Объёмный туман",
-- MONKEY (sky): the sky tier row.
skyQuality = "Качество неба",
fogOff = "Выкл",
waterQuality = "Качество воды",
waterClassic = "Классическое",
@@ -690,6 +700,12 @@
-- MONKEY (volumetric fog): localised effect and cost explanation.
VOLUMETRIC_FOG =
"Выкл отключает объёмный туман. Низкое добавляет дымку вблизи и лучи солнца или луны с меньшим числом шагов; Высокое использует больше шагов для более плавных лучей. Туман гуще на рассвете и в непогоду, а в помещениях слабее. Для лучей нужны тени; без теней остаётся только дымка.",
-- MONKEY (sky): what each sky tier adds.
SKY_QUALITY =
"«Классическое» — небо оригинального клиента. «Улучшенное» сглаживает "
.. "цветовой переход неба, добавляет мягкое сияние вокруг солнца и звёздное "
.. "ночное небо с Млечным Путём. «Высокое» также добавляет облакам мелкие "
.. "детали и освещённые солнцем края. Почти не влияет на производительность.",
WATER_QUALITY =
"«Классическое» — вода оригинального клиента. «Улучшенное» добавляет плавные переходы у берегов, "
.. "анимированную пену у берегов и вокруг объектов, подвижные блики на волнах, отражение неба "
@@ -830,7 +846,11 @@
-- three of eighteen shadow settings and no hint that the other fifteen existed.
AdvancedGraphics = {
-- MONKEY (volumetric fog): include in refresh, defaults and search.
"RowLightingQuality", "RowWaterQuality", "RowVolumetricFog", "RowBloom", "RowSunShafts", "RowColorGrading",
"RowLightingQuality", "RowWaterQuality", "RowVolumetricFog",
-- MONKEY (sky): the sky tier row.
"RowSkyQuality",
-- MONKEY (post): the world-only screen effects.
"RowBloom", "RowSunShafts", "RowColorGrading",
"RowCharacterShadows", "RowWorldShadows", "RowShadowDistance",
"RowShadowResolution", "RowMoonShadows",
"RowInteriorLight", "RowInteriorShadows", "RowExteriorShadows", "RowTorchSoft",
@@ -2639,12 +2659,28 @@
})
</OnLoad></Scripts>
</Frame>
<!-- MONKEY (post): live HDR/bloom tier; Off is the baseline. -->
<Frame name="$parentRowBloom" inherits="OptionsDropdownRowTemplate">
<!-- MONKEY (sky): the sky tier, Classic / Enhanced / High. -->
<Frame name="$parentRowSkyQuality" inherits="OptionsDropdownRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowVolumetricFog" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowVolumetricFog" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
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" },
})
</OnLoad></Scripts>
</Frame>
<!-- MONKEY (post): live HDR/bloom tier; Off is the baseline. -->
<Frame name="$parentRowBloom" inherits="OptionsDropdownRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowSkyQuality" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowSkyQuality" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
OptionsRow_OnLoad(self, "bloom", BENILLA_ADVGFX.bloom,
"BENILLA_TOOLTIP_BLOOM")
@@ -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
@@ -1475,6 +1477,50 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
name: "ironforge-forge", map: Some(MAP_AZEROTH),
eye: GFX_FORGE_EYE, look: GFX_FORGE_LOOK, minute: 720, ui: None,
},
// 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: SKY_HIGH_EYE,
look: [-8770.0, 50.0, 232.0],
minute: 1200,
ui: None,
},
Scenario {
name: "sky-elwynn-night",
map: Some(MAP_AZEROTH),
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_HIGH_EYE,
look: [-9017.0, -197.0, 485.0],
minute: 60,
ui: None,
},
Scenario {
name: "sky-overcast",
map: Some(MAP_AZEROTH),
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,
},
];
/// MONKEY (p0 baseline): the Elwynn programme view, above the canopy east of Goldshire looking
+6
View File
@@ -932,6 +932,12 @@ pub(crate) const REGISTERED: &[Registered] = &[
ours("bloom", "2", "benilla's own: HDR emissive bloom, 0 Off / 1 Low / 2 High"),
ours("sunShafts", "1", "benilla's own: depth-occluded screen-space sun shafts"),
ours("colorGrading", "1", "benilla's own: zone and day/night 32-cube colour grade"),
// 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",
+2
View File
@@ -249,6 +249,8 @@ impl PluginGroup for GamePlugins {
.add(crate::monkey_gfx::MonkeyGfxPlugin)
// MONKEY (post): world-only HDR effects, before the legacy FFX clamp/UI composite.
.add(crate::post::PostPlugin)
// 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)
+2
View File
@@ -79,6 +79,8 @@ mod volumetric_fog;
mod monkey_gfx;
// MONKEY (post): optional world-only HDR post-processing.
mod post;
// MONKEY (sky): the skyQuality cvar bridge.
mod sky_quality;
mod entities;
mod fishing_line;
mod footprints;
+49
View File
@@ -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::<SkyQuality>().add_systems(Update, bridge);
}
}
fn bridge(video: Res<VideoConfig>, mut out: ResMut<SkyQuality>, mut pinned: Local<Option<Option<u8>>>) {
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::<VideoConfig>().add_plugins(SkyQualityPlugin);
app.update();
assert_eq!(app.world().resource::<SkyQuality>().0, VideoConfig::default().sky_quality);
for tier in [2u8, 1, 0] {
app.world_mut().resource_mut::<VideoConfig>().sky_quality = tier;
app.update();
assert_eq!(app.world().resource::<SkyQuality>().0, tier);
app.update();
assert!(!app.world().resource_ref::<SkyQuality>().is_changed());
}
}
}
@@ -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"),
+6
View File
@@ -228,6 +228,8 @@ pub(crate) struct VideoConfig {
pub(crate) sun_shafts: bool,
// MONKEY (post): zone/day-night LUT grading at the world-to-UI boundary.
pub(crate) color_grading: bool,
// 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,
@@ -478,6 +480,8 @@ impl Default for VideoConfig {
// MONKEY (post): part of the shipped High graphics preset.
sun_shafts: true,
color_grading: true,
// 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() {
@@ -563,6 +567,8 @@ pub(crate) fn on_cvar(
// MONKEY (post): the shafts lane is binary.
"sunshafts" => cfg.sun_shafts = v != 0.0,
"colorgrading" => cfg.color_grading = v != 0.0,
// 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(),
+73 -1
View File
@@ -29,11 +29,32 @@ pub type CloudMaterial = ExtendedMaterial<StandardMaterial, CloudExt>;
/// The colored cloud texture. The color math is CPU-side as in the reference, so the texels are
/// raw gamma bytes in a non-sRGB texture.
#[derive(Asset, AsBindGroup, Clone, TypePath)]
#[bind_group_data(CloudFxKey)]
pub struct CloudExt {
/// The colored tile, alpha the coverage, re-uploaded each regen (`0x58ac70`).
#[texture(100)]
#[sampler(101)]
pub(crate) texels: Handle<Image>,
/// 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,
}
/// MONKEY (sky): the pipeline key: High compiles `SKY_FX_CLOUDS` into `cloud.wgsl`; below High
/// the reference's code runs with nothing added.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct CloudFxKey {
high: bool,
}
impl From<&CloudExt> for CloudFxKey {
fn from(e: &CloudExt) -> Self {
Self { high: e.fx.x >= 1.5 }
}
}
impl MaterialExtension for CloudExt {
@@ -50,8 +71,14 @@ impl MaterialExtension for CloudExt {
_pipeline: &MaterialExtensionPipeline,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
_key: MaterialExtensionKey<Self>,
key: MaterialExtensionKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
// MONKEY (sky): the High cloud branch.
if key.bind_group_data.high {
if let Some(fragment) = descriptor.fragment.as_mut() {
fragment.shader_defs.push("SKY_FX_CLOUDS".into());
}
}
sky_pipeline_state(descriptor);
Ok(())
}
@@ -156,6 +183,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 +243,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<crate::lighting::WowLighting>,
quality: Res<crate::sky_fx::SkyQuality>,
clock: Res<crate::sky_fx::SkyClock>,
layer: Option<Res<CloudLayer>>,
mut materials: ResMut<Assets<CloudMaterial>>,
) {
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::*;
+2
View File
@@ -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.
+2
View File
@@ -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;
+4 -1
View File
@@ -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");
}
}
+53 -1
View File
@@ -2,19 +2,71 @@
// 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
#ifdef SKY_FX_CLOUDS
#import benilla_world::sky_fx
#endif
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var cloud_tex: texture_2d<f32>;
@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<f32>,
lit: vec4<f32>,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(102) var<uniform> cfx: CloudFx;
#ifdef SKY_FX_CLOUDS
// MONKEY (sky): coverage with detail at a sheet point; `oct` detail octaves.
fn detailed_cover(uv: vec2<f32>, 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));
}
#endif
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let texel = textureSample(cloud_tex, cloud_samp, in.uv);
var a = texel.a;
var rgb = texel.rgb;
// MONKEY (sky): High only (the `SKY_FX_CLOUDS` pipeline); Classic and Enhanced keep the
// kernel's texels as they are.
#ifdef SKY_FX_CLOUDS
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 stp = 0.011;
var occl = detailed_cover(in.uv + sd * stp, 2);
occl += detailed_cover(in.uv + sd * stp * 2.0, 2) * 0.75;
occl += detailed_cover(in.uv + sd * stp * 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 * stp * 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<f32>(1.0));
}
}
#endif
#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<f32>(texel.rgb * a, a);
return vec4<f32>(rgb * a, a);
}
+49
View File
@@ -5,6 +5,11 @@
forward_io::VertexOutput,
mesh_view_bindings::view,
}
// MONKEY (sky): the Enhanced/High library (smooth gradient, sun glow, night sky), compiled only
// into the `SKY_FX` pipeline so Classic keeps the reference's exact code.
#ifdef SKY_FX
#import benilla_world::sky_fx
#endif
struct SkyColors {
sky0: vec4<f32>, // zenith (90°)
@@ -14,6 +19,13 @@ 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 (sky): x = skyQuality (0 Classic), y = sky clock (s), z = night-sky alpha, w = glow
// strength.
fx: vec4<f32>,
// MONKEY (sky): xyz = camera to the visible sun, w unused.
sun: vec4<f32>,
// MONKEY (sky): rgb = glow colour (gamma), w unused.
glow: vec4<f32>,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var<uniform> sky: SkyColors;
@@ -66,6 +78,43 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
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.
#ifdef SKY_FX
if (sky.fx.x >= 0.5) {
let stops = array<vec3<f32>, 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<f32>(sky_fx::dither_tri(in.position.xy));
return vec4<f32>(clamp(out, vec3<f32>(0.0), vec3<f32>(1.0)), 1.0);
}
#endif
// Elevation gradient, linear between rings like the reference's Gouraud-shaded dome.
var col: vec3<f32>;
if (elev <= 0.0) {
@@ -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<f32>) -> vec3<f32> {
let lo = c / 12.92;
let hi = pow((max(c, vec3<f32>(0.0)) + 0.055) / 1.055, vec3<f32>(2.4));
return select(hi, lo, c <= vec3<f32>(0.04045));
}
fn linear_to_srgb(c: vec3<f32>) -> vec3<f32> {
let lo = c * 12.92;
let hi = 1.055 * pow(max(c, vec3<f32>(0.0)), vec3<f32>(1.0 / 2.4)) - 0.055;
return select(hi, lo, c <= vec3<f32>(0.0031308));
}
// ---- hashes and noise -------------------------------------------------------------------------
// pcg3d (Jarzynski & Olano 2020): three well-mixed u32 from three.
fn pcg3(v_in: vec3<u32>) -> vec3<u32> {
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<u32>(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<i32>) -> vec3<f32> {
return vec3<f32>(pcg3(bitcast<vec3<u32>>(p)) >> vec3<u32>(8u)) * (1.0 / 16777216.0);
}
fn hash_cell2(p: vec2<i32>, salt: u32) -> f32 {
return f32(pcg3(vec3<u32>(bitcast<vec2<u32>>(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<f32>, salt: u32) -> f32 {
let i = vec2<i32>(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<i32>(1, 0), salt);
let c = hash_cell2(i + vec2<i32>(0, 1), salt);
let d = hash_cell2(i + vec2<i32>(1, 1), salt);
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
fn value_noise3(p: vec3<f32>) -> f32 {
let i = vec3<i32>(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<i32>(1, 0, 0)).x;
let n010 = hash33(i + vec3<i32>(0, 1, 0)).x;
let n110 = hash33(i + vec3<i32>(1, 1, 0)).x;
let n001 = hash33(i + vec3<i32>(0, 0, 1)).x;
let n101 = hash33(i + vec3<i32>(1, 0, 1)).x;
let n011 = hash33(i + vec3<i32>(0, 1, 1)).x;
let n111 = hash33(i + vec3<i32>(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>) -> 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<f32>(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<f32, 6>(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<f32>, d1: vec3<f32>, h0: f32, h1: f32) -> vec3<f32> {
let w1 = 2.0 * h1 + h0;
let w2 = h1 + 2.0 * h0;
let safe0 = select(d0, vec3<f32>(1.0), d0 == vec3<f32>(0.0));
let safe1 = select(d1, vec3<f32>(1.0), d1 == vec3<f32>(0.0));
let m = (w1 + w2) / (w1 / safe0 + w2 / safe1);
return select(vec3<f32>(0.0), m, d0 * d1 > vec3<f32>(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<vec3<f32>, 6>) -> vec3<f32> {
if (elev <= 0.0) {
return y[0];
}
if (elev >= 90.0) {
return y[5];
}
var yy = y;
var se = STOP_ELEV;
var d: array<vec3<f32>, 5>;
var h: array<f32, 5>;
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<f32>(0.0);
var m1 = vec3<f32>(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>) -> f32 {
let h = hash33(vec3<i32>(vec2<i32>(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<f32>, sun: vec3<f32>) -> 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<f32>, t: f32, px: f32, band: f32) -> vec3<f32> {
let p = dir * STAR_GRID;
let base = floor(p);
let f = p - base;
let o = select(vec3<f32>(-1.0), vec3<f32>(1.0), f > vec3<f32>(0.5));
let density = 0.022 * (1.0 + 2.5 * band);
var acc = vec3<f32>(0.0);
for (var i = 0u; i < 8u; i++) {
let off = vec3<f32>(f32(i & 1u), f32((i >> 1u) & 1u), f32((i >> 2u) & 1u)) * o;
let cell = vec3<i32>(base + off);
let h = hash33(cell);
if (h.x > density) {
continue;
}
let j = hash33(cell + vec3<i32>(7919, 104729, 1299709));
let centre = normalize(vec3<f32>(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<f32>(0.78, 0.86, 1.0), vec3<f32>(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<f32> = vec3<f32>(0.40, 0.52, -0.754);
const GALAXY_CORE: vec3<f32> = vec3<f32>(-0.62, 0.36, -0.08);
// Band weight in [0, 1] toward `dir`.
fn galaxy_band(dir: vec3<f32>) -> 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<f32>, band: f32) -> vec3<f32> {
if (band < 0.01) {
return vec3<f32>(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<f32>(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<f32>, 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<f32>(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<f32>, t: f32, octaves: i32) -> f32 {
let p = uv * 36.0 + vec2<f32>(0.011, 0.0043) * t;
let w = vec2<f32>(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);
}
+78 -2
View File
@@ -32,6 +32,7 @@ pub type SkyMaterial = ExtendedMaterial<StandardMaterial, SkyExt>;
/// The five sky stops, zenith to horizon, and the fog colour on one binding (100); `sky.wgsl`'s
/// struct must match this field order.
#[derive(Asset, AsBindGroup, Clone, TypePath)]
#[bind_group_data(SkyFxKey)]
pub struct SkyExt {
/// `SkyColor0`: zenith (90°).
#[uniform(100)]
@@ -55,6 +56,29 @@ 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,
}
/// MONKEY (sky): the pipeline key: Enhanced/High compile `SKY_FX` into `sky.wgsl`; Classic keeps
/// the reference's code with nothing added.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct SkyFxKey {
fx: bool,
}
impl From<&SkyExt> for SkyFxKey {
fn from(e: &SkyExt) -> Self {
Self { fx: e.fx.x >= 0.5 }
}
}
impl MaterialExtension for SkyExt {
@@ -73,8 +97,14 @@ impl MaterialExtension for SkyExt {
_pipeline: &MaterialExtensionPipeline,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
_key: MaterialExtensionKey<Self>,
key: MaterialExtensionKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
// MONKEY (sky): the Enhanced/High branch.
if key.bind_group_data.fx {
if let Some(fragment) = descriptor.fragment.as_mut() {
fragment.shader_defs.push("SKY_FX".into());
}
}
if let Some(depth) = descriptor.depth_stencil.as_mut() {
depth.depth_write_enabled = false;
}
@@ -183,6 +213,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 +276,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<WowLighting>,
// MONKEY (sky): the tier, the sky clock and the cloud cover over the sun.
quality: Res<crate::sky_fx::SkyQuality>,
clock: Res<crate::sky_fx::SkyClock>,
clouds: Res<crate::clouds::CloudCoverage>,
dome: Query<&MeshMaterial3d<SkyMaterial>, With<Sky>>,
mut materials: ResMut<Assets<SkyMaterial>>,
) {
@@ -270,11 +309,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 +334,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),
)
}
+117
View File
@@ -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<u8> {
std::env::var("WOW_SKY_QUALITY")
.ok()
.and_then(|v| v.trim().parse::<u8>().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<f32>,
}
/// 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.22;
/// 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<Shader> =
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::<f32>().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<Time>, mut clock: ResMut<SkyClock>) {
if let Some(t) = clock.frozen {
if clock.secs != t {
clock.secs = t;
}
return;
}
clock.secs = (clock.secs + time.delta_secs()) % 3600.0;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn glow_fades_out_below_the_horizon_and_under_cloud() {
assert_eq!(glow_strength(-0.2, 0.0, 0.0), 0.0);
assert!((glow_strength(0.5, 0.0, 0.0) - GLOW_GAIN).abs() < 1e-6);
assert!(glow_strength(0.5, 1.0, 0.0) < 0.25 * GLOW_GAIN);
assert!(glow_strength(0.5, 0.0, 1.0) < 0.2 * GLOW_GAIN);
// Monotone across dusk.
let mut prev = 0.0;
for i in 0..40 {
let y = -0.15 + i as f32 * 0.01;
let g = glow_day_fade(y);
assert!(g >= prev);
prev = g;
}
}
}
@@ -80,6 +80,8 @@ impl PluginGroup for WorldPlugins {
.add(crate::world_map::WorldMapPlugin)
.add(crate::lighting::LightingPlugin)
// Sky dome: the Light.dbc gradient backdrop (camera-centred), driven by the lighting.
// MONKEY (sky): before the sky, whose dome imports its WGSL library.
.add(crate::sky_fx::SkyFxPlugin)
.add(crate::sky::SkyPlugin)
// The WMO skybox, after `SkyPlugin`, whose dome it stands down.
.add(crate::skybox::SkyboxPlugin)