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:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user