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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6yUWgPosSakvL23gtgvpH
This commit is contained in:
@@ -54,14 +54,18 @@ fn hash_cell(p: vec2<i32>, 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<f32>, 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<f32>, salt: u32, period: i32) -> 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_cell(i, salt);
|
||||
let b = hash_cell(i + vec2<i32>(1, 0), salt);
|
||||
let c = hash_cell(i + vec2<i32>(0, 1), salt);
|
||||
let d = hash_cell(i + vec2<i32>(1, 1), salt);
|
||||
let per = vec2<i32>(period);
|
||||
let lo = ((i % per) + per) % per;
|
||||
let hi = (lo + vec2<i32>(1)) % per;
|
||||
let a = hash_cell(lo, salt);
|
||||
let b = hash_cell(vec2<i32>(hi.x, lo.y), salt);
|
||||
let c = hash_cell(vec2<i32>(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<f32>, band: f32) -> vec3<f32> {
|
||||
|
||||
// ---- 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<f32> = vec2<f32>(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<f32>, 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<f32>(3.7, 1.9);
|
||||
@@ -267,8 +280,14 @@ fn cloud_billow(p: vec2<f32>, 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<f32>, t: f32, octaves: i32) -> f32 {
|
||||
let p = uv * 36.0 + vec2<f32>(0.011, 0.0043) * t;
|
||||
let w = vec2<f32>(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<f32>(
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Time>, mut clock: ResMut<SkyClock>) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A `const NAME ... = <value>;` line of `sky_fx.wgsl`, as text.
|
||||
fn wgsl_const(name: &str) -> String {
|
||||
let src = include_str!("shaders/sky_fx.wgsl");
|
||||
let line = src
|
||||
.lines()
|
||||
.find(|l| l.starts_with(&format!("const {name}:")))
|
||||
.unwrap_or_else(|| panic!("no const {name}"));
|
||||
let value = line.split('=').nth(1).unwrap().trim();
|
||||
value.trim_end_matches(';').trim().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shader_wrap_and_the_cloud_loop_match_the_clock() {
|
||||
let wrap: f64 = wgsl_const("SKY_WRAP").parse().unwrap();
|
||||
assert_eq!(wrap, SKY_CLOCK_WRAP_S);
|
||||
// The loop needs an even tile (the half-rate warp tiles at half of it) and whole tiles of
|
||||
// drift per wrap; the rates stay within 5% of WarcraftXL's 0.011 and 0.0043 cells/s.
|
||||
let tile: i32 = wgsl_const("CLOUD_TILE").parse().unwrap();
|
||||
assert!(tile > 0 && tile % 2 == 0);
|
||||
let drift = wgsl_const("CLOUD_DRIFT_TILES");
|
||||
let tiles: Vec<f64> = drift
|
||||
.trim_start_matches("vec2<f32>(")
|
||||
.trim_end_matches(')')
|
||||
.split(',')
|
||||
.map(|v| v.trim().parse().unwrap())
|
||||
.collect();
|
||||
for (n, rate) in tiles.iter().zip([0.011, 0.0043]) {
|
||||
assert_eq!(n.fract(), 0.0, "whole tiles per wrap");
|
||||
let actual = n * f64::from(tile) / SKY_CLOCK_WRAP_S;
|
||||
assert!((actual / rate - 1.0).abs() < 0.05, "drift {actual} vs {rate}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glow_fades_out_below_the_horizon_and_under_cloud() {
|
||||
assert_eq!(glow_strength(-0.2, 0.0, 0.0), 0.0);
|
||||
|
||||
Reference in New Issue
Block a user