veloren_common_systems/phys/
weather.rs

1use common::{
2    comp::Pos,
3    resources::TimeOfDay,
4    terrain::{CoordinateConversions, NEIGHBOR_DELTA, SiteKindMeta, TerrainGrid},
5    weather::WeatherGrid,
6};
7use common_base::{self, prof_span};
8use itertools::Itertools;
9use vek::*;
10
11/// Simulates winds based on weather and terrain data for specific position
12// TODO: Consider exporting it if one wants to build nice visuals
13pub(super) fn simulated_wind_vel(
14    pos: &Pos,
15    weather: &WeatherGrid,
16    terrain: &TerrainGrid,
17    time_of_day: &TimeOfDay,
18) -> Result<Vec3<f32>, ()> {
19    prof_span!(guard, "Apply Weather INIT");
20
21    let pos_2d = pos.0.as_().xy();
22    let chunk_pos: Vec2<i32> = pos_2d.wpos_to_cpos();
23    let Some(current_chunk) = terrain.get_key(chunk_pos) else {
24        return Err(());
25    };
26
27    let meta = current_chunk.meta();
28
29    let interp_weather = weather.get_interpolated(pos.0.xy());
30    // Weather sim wind
31    let interp_alt = terrain
32        .get_interpolated(pos_2d, |c| c.meta().alt())
33        .unwrap_or(0.);
34    let interp_tree_density = terrain
35        .get_interpolated(pos_2d, |c| c.meta().tree_density())
36        .unwrap_or(0.);
37    let interp_town = terrain
38        .get_interpolated(pos_2d, |c| match c.meta().site() {
39            Some(SiteKindMeta::Settlement(_)) => 2.7,
40            _ => 1.0,
41        })
42        .unwrap_or(0.);
43    let normal = terrain
44        .get_interpolated(pos_2d, |c| {
45            c.meta()
46                .approx_chunk_terrain_normal()
47                .unwrap_or(Vec3::unit_z())
48        })
49        .unwrap_or(Vec3::unit_z());
50    let above_ground = pos.0.z - interp_alt;
51    let wind_velocity = interp_weather.wind_vel();
52
53    let surrounding_chunks_metas = NEIGHBOR_DELTA
54        .iter()
55        .map(move |&(x, y)| chunk_pos + Vec2::new(x, y))
56        .filter_map(|cpos| terrain.get_key(cpos).map(|c| c.meta()))
57        .collect::<Vec<_>>();
58
59    drop(guard);
60
61    prof_span!(guard, "thermals");
62
63    // === THERMALS ===
64
65    // Sun angle of incidence.
66    //
67    // 0.0..1.0, 0.25 morning, 0.45 midday, 0.66 evening, 0.79 night, 0.0/1.0
68    // midnight
69    let sun_dir = time_of_day.get_sun_dir().normalized();
70    let mut lift = ((sun_dir - normal.normalized()).magnitude() - 0.5).max(0.2) * 2.3;
71
72    // TODO: potential source of harsh edges in wind speed.
73    let temperatures = surrounding_chunks_metas.iter().map(|m| m.temp()).minmax();
74
75    // More thermals if hot chunks border cold chunks
76    lift *= match temperatures {
77        itertools::MinMaxResult::NoElements | itertools::MinMaxResult::OneElement(_) => 1.0,
78        itertools::MinMaxResult::MinMax(a, b) => 0.8 + ((a - b).abs() * 1.1),
79    }
80    .min(2.0);
81
82    // TODO: potential source of harsh edges in wind speed.
83    //
84    // Way more thermals in strong rain as its often caused by strong thermals.
85    // Less in weak rain or cloudy ..
86    lift *= if interp_weather.rain.is_between(0.5, 1.0) && interp_weather.cloud.is_between(0.6, 1.0)
87    {
88        1.5
89    } else if interp_weather.rain.is_between(0.2, 0.5) && interp_weather.cloud.is_between(0.3, 0.6)
90    {
91        0.8
92    } else {
93        1.0
94    };
95
96    // The first 15 blocks are weaker. Starting from the ground should be difficult.
97    lift *= (above_ground / 15.).min(1.);
98    lift *= (220. - above_ground / 20.).clamp(0.0, 1.0);
99
100    // TODO: Smooth this, and increase height some more (500 isnt that much higher
101    // than the spires)
102    if interp_alt > 500.0 {
103        lift *= 0.8;
104    }
105
106    // More thermals above towns, the materials tend to heat up more.
107    lift *= interp_town;
108
109    // Bodies of water cool the air, causing less thermals.
110    lift *= terrain
111        .get_interpolated(pos_2d, |c| 1. - c.meta().near_water() as i32 as f32)
112        .unwrap_or(1.);
113
114    drop(guard);
115
116    // === Ridge/Wave lift ===
117
118    let mut ridge_lift = {
119        const RIDGE_LIFT_COEFF: f32 = 1.0;
120
121        let steepness = normal.angle_between(Vec3::unit_z());
122
123        // angle between normal and wind
124        let mut angle = wind_velocity.angle_between(normal.xy()); // 1.4 radians of zero
125
126        // a deadzone of +-1.5 radians if wind is blowing away from
127        // the mountainside.
128        angle = (angle - 1.3).max(0.0);
129
130        // the ridge lift is based on the angle and the velocity of the wind
131        angle * steepness * wind_velocity.magnitude() * RIDGE_LIFT_COEFF
132    };
133
134    // Cliffs mean more lift
135    // 44 seems to be max, according to a lerp in WorldSim::generate_cliffs
136    ridge_lift *= 0.9 + (meta.cliff_height() / 44.0) * 1.2;
137
138    // Height based fall-off (https://www.desmos.com/calculator/jijqfunchg)
139    ridge_lift *= 1. / (1. + (1.3f32.powf(0.1 * above_ground - 15.)));
140
141    // More flat wind above ground (https://www.desmos.com/calculator/jryiyqsdnx)
142    let wind_factor = 1. / (0.25 + (0.96f32.powf(0.1 * above_ground - 15.)));
143
144    let mut wind_vel = (wind_velocity * wind_factor).with_z(lift + ridge_lift);
145
146    // probably 0. to 1. src: SiteKind::is_suitable_loc comparisons
147    wind_vel *= (1.0 - interp_tree_density).max(0.7);
148
149    // Clamp magnitude, we never want to throw players around way too fast.
150    let magn = wind_vel.magnitude_squared().max(0.0001);
151
152    // 600 here is compared to squared ~ 25. this limits the magnitude of the wind.
153    wind_vel *= magn.min(600.) / magn;
154
155    Ok(wind_vel)
156}