use bevy::math::{Vec2, Vec3};
use super::mirror::{self, Surface};
use super::model;
use super::{ElementsGlobals, SNOW_ALBEDO, SNOW_ROUGHNESS};
use crate::climate::Weather;
fn close(a: f32, b: f32, tolerance: f32, what: &str) {
assert!((a - b).abs() <= tolerance, "{what}: {a} vs {b}");
}
fn surface(base: Vec3, roughness: f32, metallic: f32, wet: f32, puddle: f32, snow: f32) -> Surface {
mirror::weather_surface(
base,
roughness,
metallic,
Vec3::splat(0.5),
Vec3::Y,
wet,
puddle,
snow,
Vec2::ZERO,
)
}
#[test]
fn snow_on_meshes_follows_the_flora_curve() {
for (ny, expected) in [
(1.0, 0.5),
(0.9, 0.5),
(0.8, 0.5),
(0.7, 0.422),
(0.6, 0.078),
(0.5, 0.0),
] {
close(
mirror::mesh_snow_cover(0.5, ny),
expected,
1e-3,
&format!("snow 0.5 at n_y {ny}"),
);
}
close(
mirror::mesh_snow_cover(1.0, 0.45),
1.0,
1e-6,
"snow 1 at n_y 0.45",
);
for ny in [-1.0, 0.0, 0.5, 1.0] {
assert_eq!(mirror::mesh_snow_cover(0.0, ny), 0.0, "snow 0 at {ny}");
}
assert_eq!(
(SNOW_ALBEDO.red, SNOW_ALBEDO.green, SNOW_ALBEDO.blue),
(0.80, 0.83, 0.88)
);
assert_eq!(mirror::SNOW_ALBEDO, Vec3::new(0.80, 0.83, 0.88));
assert_eq!(SNOW_ROUGHNESS, 0.9);
assert_eq!(mirror::SNOW_ROUGHNESS, SNOW_ROUGHNESS);
}
#[test]
fn the_shading_numbers_are_the_notes() {
let base = Vec3::new(0.6, 0.4, 0.2);
close(mirror::porosity(0.5, 0.0), 0.0, 1e-6, "φ(0.5)");
close(mirror::porosity(0.7, 0.0), 0.5, 1e-6, "φ(0.7)");
close(mirror::porosity(0.9, 0.0), 1.0, 1e-6, "φ(0.9)");
assert_eq!(mirror::porosity(0.9, 1.0), 0.0);
let s = surface(base, 0.9, 0.0, 1.0, 0.0, 0.0);
for (got, want) in [
(s.base_color.x, 0.3),
(s.base_color.y, 0.2),
(s.base_color.z, 0.1),
] {
close(got, want, 1e-6, "albedo × 0.5");
}
close(s.roughness, 0.1, 1e-6, "wet roughness");
close(
surface(base, 0.05, 0.0, 1.0, 0.0, 0.0).roughness,
0.05,
1e-6,
"never rougher",
);
let s = surface(base, 0.7, 0.0, 0.5, 0.0, 0.0);
close(s.base_color.x / base.x, 0.875, 1e-6, "× 0.875");
let s = surface(base, 0.5, 0.0, 1.0, 0.0, 0.0);
assert_eq!(s.base_color, base);
close(s.roughness, 0.1, 1e-6, "glossy even when not porous");
let s = surface(base, 0.9, 0.0, 0.0, 1.0, 0.0);
close(
s.roughness,
mirror::PUDDLE_ROUGHNESS,
1e-6,
"puddle roughness",
);
close(s.reflectance.x, 0.354, 1e-3, "puddle reflectance");
close(
0.16 * s.reflectance.x * s.reflectance.x,
0.02,
1e-4,
"puddle F0",
);
close(s.base_color.x, 0.3, 1e-6, "puddle albedo");
assert_eq!(s.normal, Vec3::Y, "a level, unrippled puddle");
for wet in [0.0, 0.5, 1.0] {
for puddle in [0.0, 1.0] {
let s = surface(base, 0.3, 1.0, wet, puddle, 1.0);
assert_eq!(
s.base_color,
mirror::SNOW_ALBEDO,
"wet {wet} puddle {puddle}"
);
assert_eq!(s.roughness, 0.9);
assert_eq!(s.metallic, 0.0, "snow covers metal");
}
}
let reflectance = Vec3::new(0.5, 0.5, 0.5);
let n = Vec3::new(0.3, 0.9, -0.1).normalize();
let s = mirror::weather_surface(
base,
0.37,
0.2,
reflectance,
n,
0.0,
0.0,
0.0,
Vec2::new(0.2, 0.1),
);
assert_eq!(
s,
Surface {
base_color: base,
roughness: 0.37,
metallic: 0.2,
reflectance,
normal: n,
}
);
assert_eq!(mirror::cloud_shadow_ratio(1.0, 1.0), 1.0);
}
#[test]
fn the_film_model_table_in_the_note_is_reproduced() {
let (re, n) = (0.0659_f32, 1.33_f32);
let ri = 1.0 - (1.0 - re) / (n * n);
close(ri, 0.4720, 5e-4, "r_i");
for (rho, ratio) in [
(0.05, 0.505),
(0.1, 0.518),
(0.2, 0.545),
(0.3, 0.575),
(0.5, 0.646),
(0.8, 0.792),
] {
let wet = (1.0 - re) * (1.0 - ri) * rho / (1.0 - ri * rho);
close(wet / rho, ratio, 1.5e-3, &format!("ρ = {rho}"));
}
}
#[test]
fn ripples_follow_lagardes_layers() {
for (f, expected) in [
(0.2, [0.8, 0.0, 0.0, 0.0]),
(0.5, [1.0, 1.0, 0.0, 0.0]),
(1.0, [1.0, 1.0, 1.0, 1.0]),
(0.0, [0.0; 4]),
] {
let w = mirror::ripple_weights(f);
for k in 0..4 {
close(w[k], expected[k], 1e-6, &format!("f {f} layer {k}"));
}
}
assert_eq!(
mirror::ripple_layer(Vec2::new(0.1, 0.1), 0, 0.3, 0.0),
Vec2::ZERO
);
let mut peak = 0.0_f32;
for k in 0..4u32 {
for phase in (0..20).map(|i| i as f32 / 20.0) {
for ix in 0..120 {
for iz in 0..120 {
let xz = Vec2::new(ix as f32, iz as f32) * 0.0173;
peak = peak.max(mirror::ripple_layer(xz, k, phase, 1.0).length());
}
}
}
}
assert!(peak <= mirror::RIPPLE_STRENGTH + 1e-6, "peak {peak}");
assert!(peak > 0.15, "a ring is visible: peak {peak}");
println!("peak ripple normal offset of one layer: {peak:.4} (strength 0.35)");
let (phases, weights) = ([0.1, 0.4, 0.7, 0.9], mirror::ripple_weights(1.0));
let xz = Vec2::new(0.13, 0.29);
let sum: Vec2 = (0..4)
.map(|k| mirror::ripple_layer(xz, k as u32, phases[k], weights[k]))
.sum();
assert_eq!(mirror::ripple_offset(xz, phases, weights), sum);
for t0 in [3599.99, 86_399.99, 1.0e6] {
let (a, b) = (model::ripple_phases(t0), model::ripple_phases(t0 + 0.02));
for k in 0..4 {
let step = (b[k] - a[k]).rem_euclid(1.0);
let expected = (0.02 * model::RIPPLE_TIME_MUL[k] * model::RIPPLE_RATE) as f32;
close(step, expected, 2e-3, &format!("t {t0} layer {k}"));
}
}
close(
model::ripple_phases(0.0)[1],
(0.2 * 1.6_f32).fract(),
1e-6,
"TimeAdd",
);
}
#[test]
fn rain_streaks_run_down_walls_only_while_raining() {
let wall = Vec3::X;
let mut peak = 0.0_f32;
let mut lit = 0;
for ix in 0..200 {
for iy in 0..200 {
let p = Vec3::new(3.0, iy as f32 * 0.05, ix as f32 * 0.011);
let s = mirror::rain_streak(p, wall, 1.0, 12.5);
peak = peak.max(s);
lit += usize::from(s > 0.05);
close(
mirror::rain_streak(p, wall, 0.1, 12.5),
0.5 * s,
1e-6,
"linear in f below 0.2",
);
assert_eq!(
mirror::rain_streak(p, wall, 0.2, 12.5),
s,
"saturated at 0.2"
);
assert_eq!(mirror::rain_streak(p, wall, 0.0, 12.5), 0.0);
assert_eq!(mirror::rain_streak(p, Vec3::Y, 1.0, 12.5), 0.0);
}
}
println!("streak coverage s>0.05: {} of 40000, peak {peak}", lit);
assert!(peak <= 0.4 + 1e-6, "peak {peak}");
assert!(peak > 0.15, "a rivulet is visible: peak {peak}");
assert!(
lit > 0 && lit < 200 * 200 / 2,
"streaks cover part of a wall: {lit}"
);
for i in 0..50 {
let p = Vec3::new(1.0 + 0.37 * i as f32, 0.9 + 0.11 * i as f32, 2.0);
let before = mirror::rain_streak(p, wall, 1.0, 8191.9);
let after = mirror::rain_streak(p, wall, 1.0, 8191.9 - model::STREAK_WRAP_S as f32);
close(before, after, 6e-3, "streak wrap");
}
close(
model::streak_phase(8192.0 + 5.0),
5.0,
1e-4,
"streak phase wraps",
);
}
#[test]
fn rain_intensity_is_the_liquid_fraction() {
let weather = |mm_h, t| Weather {
precipitation_mm_h: mm_h,
temperature_c: t,
..Weather::default()
};
close(
model::rain_intensity(&weather(4.0, 15.0)),
0.2,
1e-6,
"Rain",
);
close(
model::rain_intensity(&weather(25.0, 15.0)),
1.0,
1e-6,
"Storm",
);
assert_eq!(model::rain_intensity(&weather(1.5, -5.0)), 0.0, "Snow");
close(
model::rain_intensity(&weather(10.0, 0.5)),
0.25,
1e-6,
"sleet",
);
}
#[test]
fn the_globals_pack_as_the_shader_struct() {
let globals = ElementsGlobals {
wetness: 0.1,
snow_cover: 0.2,
rain_intensity: 0.3,
camera_transmittance: 0.4,
shadow_origin: Vec2::new(5.0, 6.0),
shadow_extent: 7.0,
shadow_enabled: true,
ripple_phase: [8.0, 9.0, 10.0, 11.0],
wind: Vec2::new(12.0, 13.0),
streak_phase: 14.0,
light_direction: Vec3::new(15.0, 16.0, 17.0),
};
assert_eq!(
globals.pack(),
[
[0.1, 0.2, 0.3, 0.4],
[5.0, 6.0, 7.0, 1.0],
[8.0, 9.0, 10.0, 11.0],
[12.0, 13.0, 14.0, 0.0],
[15.0, 16.0, 17.0, 0.0],
]
);
let source = include_str!("../surface_cover.wesl");
let body = &source[source.find("struct ElementsGlobals").expect("the struct")..];
let body = &body[..body.find("\n}").expect("its end")];
assert_eq!(body.matches("vec4<f32>,").count(), 5, "five vec4 fields");
let none = ElementsGlobals::gather(None, &Weather::default(), None, 0.0);
assert_eq!(none.camera_transmittance, 1.0);
assert!(!none.shadow_enabled);
}
#[test]
fn the_extension_binds_one_fragment_storage_buffer_at_101() {
let source = include_str!("mod.rs");
let body = &source[source
.find("pub struct WeatheredExtension")
.expect("the struct")..];
let body = &body[..body.find("\n}").expect("the end of the struct")];
assert_eq!(body.matches("#[storage(").count(), 1);
assert!(
body.contains("#[storage(101, read_only, visibility(fragment))]"),
"{body}"
);
assert!(body.contains("#[uniform(100)]"));
}
#[test]
fn a_drenched_surface_is_darker_than_a_wet_one() {
let base = Vec3::new(0.6, 0.4, 0.2);
let s = surface(base, 0.9, 0.0, mirror::DRENCHED_MAX, 0.0, 0.0);
close(s.base_color.x / base.x, 0.3, 1e-6, "drenched albedo ×0.3");
close(s.roughness, 0.1, 1e-6, "drenched roughness");
assert_eq!(surface(base, 0.9, 0.0, 3.0, 0.0, 0.0), s);
}