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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6yUWgPosSakvL23gtgvpH
This commit is contained in:
Claude
2026-09-25 19:02:03 +00:00
parent f633864976
commit 1f094e7880
5 changed files with 104 additions and 18 deletions
+14 -5
View File
@@ -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<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`]).
/// 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<Assets<Mesh>>,
mut materials: ResMut<Assets<CloudMaterial>>,
mut images: ResMut<Assets<Image>>,
clock: Option<Res<crate::sky_fx::SkyClockBuffer>>,
) {
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<crate::lighting::WowLighting>,
quality: Res<crate::sky_fx::SkyQuality>,
clock: Res<crate::sky_fx::SkyClock>,
layer: Option<Res<CloudLayer>>,
mut materials: ResMut<Assets<CloudMaterial>>,
) {
@@ -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 {
+5 -3
View File
@@ -14,7 +14,7 @@
@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
// 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<f32>,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(102) var<uniform> 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<storage, read> sky_clock: vec4<f32>;
#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<f32>, 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<f32> {
// 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);
+4 -2
View File
@@ -21,7 +21,7 @@ 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
// MONKEY (sky): x = skyQuality (0 Classic), y = reserved, z = night-sky alpha, w = glow
// strength.
fx: vec4<f32>,
// MONKEY (sky): xyz = camera to the visible sun, w unused.
@@ -34,6 +34,8 @@ struct SkyColors {
mf_fog_d: vec4<f32>,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var<uniform> 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<storage, read> sky_clock: vec4<f32>;
// 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<f32> {
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;
+15 -7
View File
@@ -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<Assets<Mesh>>,
mut materials: ResMut<Assets<SkyMaterial>>,
clock: Option<Res<crate::sky_fx::SkyClockBuffer>>,
) {
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<WowLighting>,
// 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<crate::sky_fx::SkyQuality>,
clock: Res<crate::sky_fx::SkyClock>,
clouds: Res<crate::clouds::CloudCoverage>,
// MONKEY (fog): the Modern fog rows for the shared horizon colour.
monkey: Res<crate::lighting::MonkeyFrame>,
@@ -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),
)
+66 -1
View File
@@ -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::<SkyClockBuffer>::default(),
ExtractResourcePlugin::<SkyClockSecs>::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<Res<RenderDevice>>) {
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<RenderQueue>,
buffer: Option<Res<SkyClockBuffer>>,
secs: Option<Res<SkyClockSecs>>,
) {
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<Time>, mut clock: ResMut<SkyClock>) {
if let Some(t) = clock.frozen {
if clock.secs != t {