From 111742f83e894941fd769dc3f1a623bf961f0ede Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 19:03:50 +0000 Subject: [PATCH] MONKEY (polish): the High cloud detail loops over the sky clock's day wrap The detail drift was `(0.011, 0.0043) * t`, so the field jumped to a new pattern when the sky clock wrapped at 86400 s. `value_noise_plane` now takes a lattice period and hashes cell indices modulo it; the detail plane tiles every CLOUD_TILE = 64 cells (octave o every 64 << o, the half-rate domain warp every 32), and the drift is whole tiles per wrap, CLOUD_DRIFT_TILES = (15, 6), i.e. 0.01111 and 0.00444 cells/s (+1% and +3% over WarcraftXL's rates). The shader takes the drift as `fract(tiles * t / SKY_WRAP) * tile`, so the field at the wrap equals the field at 0, and the drift term stays below 64 instead of growing to ~950 in f32. One tile spans 1.78 of the sheet's UV, wider than the dome shows, so no spatial repeat is visible. A test reads `sky_fx.wgsl` and pins SKY_WRAP to SKY_CLOCK_WRAP_S, an even tile, whole tiles of drift and the rates within 5% of the originals. Verified: cargo test -p benilla-world --lib (644 passed, 8 ignored); cloud.wgsl (SKY_FX_CLOUDS) and sky.wgsl (SKY_FX) compose and validate under naga_oil 0.20 / naga 27 offline. 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/shaders/sky_fx.wgsl | 35 +++++++++++++----- crates/benilla-world/src/sky_fx.rs | 37 ++++++++++++++++++-- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/crates/benilla-world/src/shaders/sky_fx.wgsl b/crates/benilla-world/src/shaders/sky_fx.wgsl index 251fcb04..e9f8f16c 100644 --- a/crates/benilla-world/src/shaders/sky_fx.wgsl +++ b/crates/benilla-world/src/shaders/sky_fx.wgsl @@ -54,14 +54,18 @@ fn hash_cell(p: vec2, salt: u32) -> f32 { // 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_noise_plane(p: vec2, salt: u32) -> f32 { +// MONKEY (polish): the lattice repeats every `period` cells, so a shift by `period` is no change. +fn value_noise_plane(p: vec2, salt: u32, period: i32) -> f32 { let i = vec2(floor(p)); let f = fract(p); let u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); - let a = hash_cell(i, salt); - let b = hash_cell(i + vec2(1, 0), salt); - let c = hash_cell(i + vec2(0, 1), salt); - let d = hash_cell(i + vec2(1, 1), salt); + let per = vec2(period); + let lo = ((i % per) + per) % per; + let hi = (lo + vec2(1)) % per; + let a = hash_cell(lo, salt); + let b = hash_cell(vec2(hi.x, lo.y), salt); + let c = hash_cell(vec2(lo.x, hi.y), salt); + let d = hash_cell(hi, salt); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); } @@ -244,6 +248,15 @@ fn milky_way(dir: vec3, band: f32) -> vec3 { // ---- S3: cloud detail ------------------------------------------------------------------------- +// MONKEY (polish): the detail field loops over the sky clock's wrap. It tiles every `CLOUD_TILE` +// cells of the detail plane (octave o every `CLOUD_TILE << o` of its own lattice; the half-rate +// warp every `CLOUD_TILE / 2`), and the drift covers whole tiles per wrap, so the field at +// `SKY_WRAP` is the field at 0. One tile is 64 / 36 of the sheet, wider than the dome's UV. +const CLOUD_TILE: i32 = 64; +// Tiles of drift per wrap on u and v: 0.01111 and 0.00444 cells/s, WarcraftXL's 0.011 and 0.0043 +// rounded to whole tiles. +const CLOUD_DRIFT_TILES: vec2 = vec2(15.0, 6.0); + // 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; @@ -256,7 +269,7 @@ fn cloud_billow(p: vec2, octaves: i32) -> f32 { var tot = 0.0; var q = p; for (var o = 0; o < octaves; o++) { - s += a * billow(value_noise_plane(q, u32(o) * 57u)); + s += a * billow(value_noise_plane(q, u32(o) * 57u, CLOUD_TILE << u32(o))); tot += a; a *= 0.55; q = q * 2.0 + vec2(3.7, 1.9); @@ -267,8 +280,14 @@ fn cloud_billow(p: vec2, octaves: i32) -> f32 { // The detail field in [0, 1] on the sheet: a broad warp twists the domain before the billowed fbm // is read, which turns thresholded blobs into cauliflower edges. fn cloud_detail(uv: vec2, t: f32, octaves: i32) -> f32 { - let p = uv * 36.0 + vec2(0.011, 0.0043) * t; - let w = vec2(value_noise_plane(p * 0.5, 173u), value_noise_plane(p * 0.5 + 5.3, 219u)) - 0.5; + // The drift modulo one tile: seamless at the wrap, and small enough to keep f32 precision. + let drift = fract(CLOUD_DRIFT_TILES * (t / SKY_WRAP)) * f32(CLOUD_TILE); + let p = uv * 36.0 + drift; + let half_tile = CLOUD_TILE / 2; + let w = vec2( + value_noise_plane(p * 0.5, 173u, half_tile), + value_noise_plane(p * 0.5 + 5.3, 219u, half_tile), + ) - 0.5; return cloud_billow(p + w * 2.6, octaves); } diff --git a/crates/benilla-world/src/sky_fx.rs b/crates/benilla-world/src/sky_fx.rs index 2c6accc4..4aead18a 100644 --- a/crates/benilla-world/src/sky_fx.rs +++ b/crates/benilla-world/src/sky_fx.rs @@ -47,8 +47,8 @@ impl SkyQuality { /// /// MONKEY (fix-sky): it wrapped hourly in f32, and every star's twinkle jumped at the wrap. Now it /// accumulates in f64 and wraps at [`SKY_CLOCK_WRAP_S`]; the shader's twinkle rates are whole -/// cycles per wrap (seamless). The High cloud-detail drift still re-patterns at the wrap, once -/// per 24 h of continuous play. +/// cycles per wrap (seamless). MONKEY (polish): the High cloud detail tiles and drifts whole +/// tiles per wrap (`CLOUD_TILE`, `CLOUD_DRIFT_TILES`), so it is seamless there too. #[derive(Resource, Default)] pub struct SkyClock { pub secs: f32, @@ -175,6 +175,39 @@ fn tick_sky_clock(time: Res