use bevy::prelude::*;
#[derive(Resource, Clone, Copy, Debug, PartialEq, Reflect)]
pub struct PrecipitationLook {
pub rain_fall_scale: f32,
pub rain_wind_scale: f32,
pub rain_tint: LinearRgba,
pub flake_size_spread: f32,
pub flake_min_half_width_px: f32,
}
impl Default for PrecipitationLook {
fn default() -> Self {
Self {
rain_fall_scale: 1.0,
rain_wind_scale: 1.0,
rain_tint: LinearRgba::WHITE,
flake_size_spread: 0.0,
flake_min_half_width_px: 1.0,
}
}
}
impl PrecipitationLook {
pub fn readable() -> Self {
Self {
rain_fall_scale: 4.0,
rain_wind_scale: 0.5,
rain_tint: LinearRgba::new(0.85, 0.9, 1.0, 0.7),
flake_size_spread: 0.5,
flake_min_half_width_px: 1.5,
}
}
pub(crate) fn gpu(&self) -> (Vec4, Vec4) {
let sane = |v: f32, fallback: f32| if v.is_finite() { v.max(0.0) } else { fallback };
let tint = self.rain_tint;
(
Vec4::new(
sane(self.rain_fall_scale, 1.0),
sane(self.rain_wind_scale, 1.0),
sane(self.flake_size_spread, 0.0),
sane(self.flake_min_half_width_px, 1.0),
),
Vec4::new(
sane(tint.red, 1.0),
sane(tint.green, 1.0),
sane(tint.blue, 1.0),
sane(tint.alpha, 1.0),
),
)
}
}
#[cfg(test)]
pub(crate) fn styled_streak(streak: Vec3, fall: f32, wind: f32) -> Vec3 {
Vec3::new(streak.x * wind, streak.y * fall, streak.z * wind)
}
#[cfg(test)]
pub(crate) fn flake_size_factor(u1: f32, u2: f32, sigma: f32) -> f32 {
let z = ((-2.0 * u1.ln()).sqrt() * (std::f32::consts::TAU * u2).cos()).clamp(-2.0, 2.0);
(sigma * z - 0.5 * sigma * sigma).exp()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::precipitation::model::{drop_fall_speed, marshall_palmer_lambda};
struct Uniforms(u64);
impl Uniforms {
fn next(&mut self) -> f32 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
((self.0 >> 40) as f32 + 0.5) / 16_777_216.0
}
}
fn mean_streak_angle(pitch_deg: f32, fall: f32, wind: f32) -> f32 {
let camera = Transform::default().looking_to(
Vec3::new(
0.0,
-pitch_deg.to_radians().sin(),
-pitch_deg.to_radians().cos(),
),
Vec3::Y,
);
let (right, up) = (*camera.right(), *camera.up());
let lambda = marshall_palmer_lambda(4.0);
let mut u = Uniforms(7);
let n = 10_000;
let mut sum = 0.0;
for _ in 0..n {
let d_mm = -(u.next() * u.next() * u.next() * u.next()).ln() / lambda;
let velocity = Vec3::new(7.0, -drop_fall_speed(d_mm), 0.0);
let s = styled_streak(velocity / 60.0, fall, wind);
let (x, y) = (s.dot(right), s.dot(up));
sum += x.abs().atan2(y.abs()).to_degrees();
}
sum / n as f32
}
#[test]
fn a_readable_rain_stands_upright_under_an_orthographic_camera() {
let styled = mean_streak_angle(35.0, 4.0, 0.5);
let physical = mean_streak_angle(35.0, 1.0, 1.0);
println!(
"RESULT rain streak from vertical: physical {physical:.1}°, readable {styled:.1}°"
);
assert!(styled < 25.0, "styled rain leans {styled}°");
assert!(physical > 45.0, "physical rain leans {physical}°");
let readable = PrecipitationLook::readable();
assert_eq!(
mean_streak_angle(35.0, readable.rain_fall_scale, readable.rain_wind_scale),
styled
);
}
#[test]
fn a_size_spread_varies_the_flakes_about_their_own_size() {
let mut u = Uniforms(11);
let factors: Vec<f32> = (0..10_000)
.map(|_| flake_size_factor(u.next(), u.next(), 0.5))
.collect();
let n = factors.len() as f32;
let mean = factors.iter().sum::<f32>() / n;
let sd = (factors.iter().map(|f| (f - mean).powi(2)).sum::<f32>() / n).sqrt();
assert!(sd / mean >= 0.3, "coefficient of variation {}", sd / mean);
assert!((mean - 1.0).abs() < 0.05, "mean factor {mean}");
let mut u = Uniforms(13);
for _ in 0..1000 {
assert_eq!(flake_size_factor(u.next(), u.next(), 0.0), 1.0);
}
}
#[test]
fn the_default_look_is_the_identity() {
let (look, tint) = PrecipitationLook::default().gpu();
assert_eq!(look, Vec4::new(1.0, 1.0, 0.0, 1.0));
assert_eq!(tint, Vec4::ONE);
let bad = PrecipitationLook {
rain_fall_scale: f32::NAN,
flake_size_spread: -1.0,
..default()
};
assert_eq!(bad.gpu().0, Vec4::new(1.0, 1.0, 0.0, 1.0));
}
}