From 1f094e78808ef31f8bbe923355aa9051411faf0e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 19:02:03 +0000 Subject: [PATCH] MONKEY (polish): the sky clock leaves the sky and cloud materials At skyQuality >= 1 the dome and cloud materials were rewritten every frame only because `fx.y` carried the sky clock, so each frame re-prepared both material bind groups. The clock now lives in `SkyClockBuffer`, a 16-byte STORAGE | COPY_DST buffer created beside the shared light buffer (AssetSet::Open) and bound read-only by both materials (sky binding 101, cloud binding 103), the same shape as the materials' shared `light_buf`. The render world writes `SkyClock::secs` into it in PrepareResources each frame. `fx.y` is now reserved (0), so `write_gated` only touches a material when a real input moves. The clock is still our own f64 accumulator with the seamless 86400 s wrap (Bevy's globals.time wraps hourly); capture mode still freezes it. A headless build with no RenderDevice gets no buffer and spawns no sky or cloud dome. Verified: cargo check -p benilla; cargo test -p benilla-world --lib (643 passed, 8 ignored); sky.wgsl (Classic and SKY_FX) and cloud.wgsl (plain and SKY_FX_CLOUDS + VERTEX_COLORS) compose and validate under naga_oil 0.20 / naga 27 offline against stubbed bevy_pbr imports and the real fog_hook, monkey_frame and sky_fx modules. No GPU or WoW data here; built on Linux only. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6yUWgPosSakvL23gtgvpH --- crates/benilla-world/src/clouds/layer.rs | 19 ++++-- crates/benilla-world/src/shaders/cloud.wgsl | 8 ++- crates/benilla-world/src/shaders/sky.wgsl | 6 +- crates/benilla-world/src/sky.rs | 22 ++++--- crates/benilla-world/src/sky_fx.rs | 67 ++++++++++++++++++++- 5 files changed, 104 insertions(+), 18 deletions(-) diff --git a/crates/benilla-world/src/clouds/layer.rs b/crates/benilla-world/src/clouds/layer.rs index fc495faf..cfffa46e 100644 --- a/crates/benilla-world/src/clouds/layer.rs +++ b/crates/benilla-world/src/clouds/layer.rs @@ -12,7 +12,7 @@ use bevy::pbr::{ }; use bevy::prelude::*; use bevy::render::render_resource::{ - AsBindGroup, Extent3d, RenderPipelineDescriptor, SpecializedMeshPipelineError, + AsBindGroup, Buffer, Extent3d, RenderPipelineDescriptor, SpecializedMeshPipelineError, TextureDimension, TextureFormat, }; use bevy::shader::ShaderRef; @@ -35,13 +35,18 @@ pub struct CloudExt { #[texture(100)] #[sampler(101)] pub(crate) texels: Handle, - /// MONKEY (sky): `x` the sky tier (detail at 2), `y` the sky clock, `zw` the unit direction - /// toward the glow body on the sheet ([`update_cloud_fx`]). + /// MONKEY (sky): `x` the sky tier (detail at 2), `y` reserved (0; the clock is + /// [`Self::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 (polish): the shared sky clock ([`crate::sky_fx::SkyClockBuffer`]), outside the + /// uniform so the drift never rewrites the material. + #[storage(103, read_only, buffer)] + pub(crate) clock: Buffer, } /// MONKEY (sky): the pipeline key: High compiles `SKY_FX_CLOUDS` into `cloud.wgsl`; below High @@ -159,7 +164,11 @@ pub(super) fn setup_cloud_layer( mut meshes: ResMut>, mut materials: ResMut>, mut images: ResMut>, + clock: Option>, ) { + let Some(clock) = clock else { + return; + }; let image = images.add(Image::new( Extent3d { width: COLS as u32, @@ -186,6 +195,7 @@ pub(super) fn setup_cloud_layer( // MONKEY (sky): Classic until `update_cloud_fx` reads the tier. fx: Vec4::ZERO, lit: Vec4::ZERO, + clock: clock.0.clone(), }, }); commands.spawn(( @@ -251,7 +261,6 @@ pub(super) fn follow_cloud_dome( pub(super) fn update_cloud_fx( light: Res, quality: Res, - clock: Res, layer: Option>, mut materials: ResMut>, ) { @@ -268,7 +277,7 @@ pub(super) fn update_cloud_fx( 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(f32::from(quality.0), 0.0, 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 { diff --git a/crates/benilla-world/src/shaders/cloud.wgsl b/crates/benilla-world/src/shaders/cloud.wgsl index d2413618..36167bea 100644 --- a/crates/benilla-world/src/shaders/cloud.wgsl +++ b/crates/benilla-world/src/shaders/cloud.wgsl @@ -14,7 +14,7 @@ @group(#{MATERIAL_BIND_GROUP}) @binding(100) var cloud_tex: texture_2d; @group(#{MATERIAL_BIND_GROUP}) @binding(101) var cloud_samp: sampler; -// MONKEY (sky): `fx.x` the sky tier, `.y` the sky clock (s), `.zw` the unit direction toward the +// MONKEY (sky): `fx.x` the sky tier, `.y` reserved, `.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 { @@ -22,6 +22,8 @@ struct CloudFx { lit: vec4, }; @group(#{MATERIAL_BIND_GROUP}) @binding(102) var cfx: CloudFx; +// MONKEY (polish): x = the sky clock (s, wrapped at a day), written per frame outside the material. +@group(#{MATERIAL_BIND_GROUP}) @binding(103) var sky_clock: vec4; #ifdef SKY_FX_CLOUDS // MONKEY (sky): coverage with detail at a sheet point; `oct` detail octaves. @@ -30,7 +32,7 @@ fn detailed_cover(uv: vec2, oct: i32) -> f32 { if (a <= 0.0) { return 0.0; } - return sky_fx::cloud_erode(a, sky_fx::cloud_detail(uv, cfx.fx.y, oct)); + return sky_fx::cloud_erode(a, sky_fx::cloud_detail(uv, sky_clock.x, oct)); } #endif @@ -43,7 +45,7 @@ fn fragment(in: VertexOutput) -> @location(0) vec4 { // 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); + let n = sky_fx::cloud_detail(in.uv, sky_clock.x, 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); diff --git a/crates/benilla-world/src/shaders/sky.wgsl b/crates/benilla-world/src/shaders/sky.wgsl index bf732117..5301a4a2 100644 --- a/crates/benilla-world/src/shaders/sky.wgsl +++ b/crates/benilla-world/src/shaders/sky.wgsl @@ -21,7 +21,7 @@ struct SkyColors { sky4: vec4, // 1.8° fog: vec4, // horizon (0°) and below: LightIntBand row 7 warp: vec4, // x = dawn/dusk warp strength S (0 = off), y = sun azimuth (rad), zw reserved - // MONKEY (sky): x = skyQuality (0 Classic), y = sky clock (s), z = night-sky alpha, w = glow + // MONKEY (sky): x = skyQuality (0 Classic), y = reserved, z = night-sky alpha, w = glow // strength. fx: vec4, // MONKEY (sky): xyz = camera to the visible sun, w unused. @@ -34,6 +34,8 @@ struct SkyColors { mf_fog_d: vec4, }; @group(#{MATERIAL_BIND_GROUP}) @binding(100) var sky: SkyColors; +// MONKEY (polish): x = the sky clock (s, wrapped at a day), written per frame outside the material. +@group(#{MATERIAL_BIND_GROUP}) @binding(101) var sky_clock: vec4; // Glow `g` for a sun-relative azimuth phase, sampled linearly like `0x6d0f50` from the reference's // six-keyframe wrap-around table at `0xce9af8` (written by `0x6ce210`); 0.125 is the sun bearing. @@ -137,7 +139,7 @@ fn fragment(in: VertexOutput) -> @location(0) vec4 { 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) + let stars = sky_fx::star_field(dir, sky_clock.x, 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; diff --git a/crates/benilla-world/src/sky.rs b/crates/benilla-world/src/sky.rs index 90552c67..a1bcec7e 100644 --- a/crates/benilla-world/src/sky.rs +++ b/crates/benilla-world/src/sky.rs @@ -17,7 +17,7 @@ use bevy::pbr::{ }; use bevy::prelude::*; use bevy::render::render_resource::{ - AsBindGroup, RenderPipelineDescriptor, SpecializedMeshPipelineError, + AsBindGroup, Buffer, RenderPipelineDescriptor, SpecializedMeshPipelineError, }; use bevy::shader::ShaderRef; @@ -57,7 +57,8 @@ pub struct SkyExt { #[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. + /// unused), `y` reserved (0; the clock is [`Self::clock`]), `z` the night-sky alpha, `w` the + /// sun-glow strength. #[uniform(100)] pub(crate) fx: Vec4, /// MONKEY (sky): camera to the visible sun (`xyz`). @@ -73,6 +74,10 @@ pub struct SkyExt { pub(crate) mf_fog_c: Vec4, #[uniform(100)] pub(crate) mf_fog_d: Vec4, + /// MONKEY (polish): the shared sky clock ([`crate::sky_fx::SkyClockBuffer`]), outside the + /// uniform so the clock never rewrites the material. + #[storage(101, read_only, buffer)] + pub(crate) clock: Buffer, } /// MONKEY (sky): the pipeline key: Enhanced/High compile `SKY_FX` into `sky.wgsl`; Classic keeps @@ -206,7 +211,11 @@ fn setup_sky( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, + clock: Option>, ) { + let Some(clock) = clock else { + return; + }; let mesh = meshes.add(dome_mesh()); // Seeded with the neutral-day fallback; `update_sky_colors` overwrites from Light.dbc each frame. let material = materials.add(SkyMaterial { @@ -231,6 +240,7 @@ fn setup_sky( mf_fog_b: Vec4::ZERO, mf_fog_c: Vec4::ZERO, mf_fog_d: Vec4::ZERO, + clock: clock.0.clone(), }, }); commands.spawn(( @@ -293,9 +303,8 @@ fn apply_sky_visibility( #[allow(clippy::too_many_arguments)] fn update_sky_colors( light: Res, - // MONKEY (sky): the tier, the sky clock and the cloud cover over the sun. + // MONKEY (sky): the tier and the cloud cover over the sun. quality: Res, - clock: Res, clouds: Res, // MONKEY (fog): the Modern fog rows for the shared horizon colour. monkey: Res, @@ -326,7 +335,7 @@ fn update_sky_colors( 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); + let (fx, sun_v, glow) = sky_fx_inputs(&light, *quality, &clouds); // MONKEY (fog): rows B-D as packed for the light buffer; clock lanes are not read here. let rows = monkey.pack(0.0, 0.0); let mf_fog = [ @@ -374,7 +383,6 @@ fn update_sky_colors( fn sky_fx_inputs( light: &WowLighting, quality: crate::sky_fx::SkyQuality, - secs: f32, clouds: &crate::clouds::CloudCoverage, ) -> (Vec4, Vec4, Vec4) { if !quality.enhanced() { @@ -393,7 +401,7 @@ fn sky_fx_inputs( // 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(f32::from(quality.0), 0.0, q(night), q(strength)), Vec4::new(q(sun.x), q(sun.y), q(sun.z), 0.0), Vec4::new(q(glow.x), q(glow.y), q(glow.z), 0.0), ) diff --git a/crates/benilla-world/src/sky_fx.rs b/crates/benilla-world/src/sky_fx.rs index e8462579..2c6accc4 100644 --- a/crates/benilla-world/src/sky_fx.rs +++ b/crates/benilla-world/src/sky_fx.rs @@ -9,6 +9,12 @@ //! Ported from WarcraftXL (https://github.com/WarcraftXL) by iThorgrim — module wxl-retail-clouds, Clouds.cpp, Clouds.hpp. use bevy::prelude::*; +use bevy::render::{ + extract_resource::{ExtractResource, ExtractResourcePlugin}, + render_resource::{Buffer, BufferDescriptor, BufferUsages}, + renderer::{RenderDevice, RenderQueue}, + Render, RenderApp, RenderSystems, +}; /// 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). @@ -53,6 +59,25 @@ pub struct SkyClock { /// The sky clock's wrap in seconds; `sky_fx.wgsl`'s `SKY_WRAP` must match. pub const SKY_CLOCK_WRAP_S: f64 = 86_400.0; +/// MONKEY (polish): the sky clock on the GPU, one `vec4` (`x` = [`SkyClock::secs`]) that the sky +/// and cloud materials bind read-only. It is rewritten in place each frame in the render world, +/// so the materials re-prepare only when a real input moves, not to advance the clock. Bevy's +/// `globals.time` is not used: it wraps hourly, and the twinkle rates are whole cycles per day. +#[derive(Resource, Clone, ExtractResource)] +pub struct SkyClockBuffer(pub Buffer); + +/// The clock value the render world uploads. +#[derive(Resource, Clone, Copy)] +struct SkyClockSecs(f32); + +impl ExtractResource for SkyClockSecs { + type Source = SkyClock; + + fn extract_resource(source: &SkyClock) -> Self { + Self(source.secs) + } +} + /// 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; @@ -91,10 +116,50 @@ impl Plugin for SkyFxPlugin { }); let quality = SkyQuality(SkyQuality::env_override().unwrap_or(0)); app.insert_resource(quality) - .add_systems(Update, tick_sky_clock); + .add_systems(Update, tick_sky_clock) + // Beside the shared light buffer, ahead of the sky and cloud materials built after it. + .add_systems( + Startup, + init_sky_clock_buffer.in_set(benilla_assets::AssetSet::Open), + ) + .add_plugins(( + ExtractResourcePlugin::::default(), + ExtractResourcePlugin::::default(), + )); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems( + Render, + upload_sky_clock.in_set(RenderSystems::PrepareResources), + ); + } } } +/// A headless build has no device, and then no sky or cloud dome either. +fn init_sky_clock_buffer(mut commands: Commands, device: Option>) { + let Some(device) = device else { + return; + }; + commands.insert_resource(SkyClockBuffer(device.create_buffer(&BufferDescriptor { + label: Some("sky_clock"), + size: 16, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + mapped_at_creation: false, + }))); +} + +/// Render-world: the clock into the shared buffer before any sky draw reads it. +fn upload_sky_clock( + queue: Res, + buffer: Option>, + secs: Option>, +) { + let (Some(buffer), Some(secs)) = (buffer, secs) else { + return; + }; + queue.write_buffer(&buffer.0, 0, bytemuck::cast_slice(&[secs.0, 0.0, 0.0, 0.0])); +} + fn tick_sky_clock(time: Res