diff --git a/client/src/lib.rs b/client/src/lib.rs index 6dad7944a1..23ad15aa00 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -49,7 +49,7 @@ use common::{ trade::{PendingTrade, SitePrices, TradeAction, TradeId, TradeResult}, uid::{IdMaps, Uid}, vol::RectVolSize, - weather::{Weather, WeatherGrid}, + weather::{CompressedWeather, SharedWeatherGrid, Weather, WeatherGrid}, }; #[cfg(feature = "tracy")] use common_base::plot; use common_base::{prof_span, span}; @@ -178,12 +178,32 @@ pub struct SiteInfoRich { } struct WeatherLerp { - old: (WeatherGrid, Instant), - new: (WeatherGrid, Instant), + old: (SharedWeatherGrid, Instant), + new: (SharedWeatherGrid, Instant), + old_local_wind: (Vec2, Instant), + new_local_wind: (Vec2, Instant), + local_wind: Vec2, } impl WeatherLerp { - fn weather_update(&mut self, weather: WeatherGrid) { + fn local_wind_update(&mut self, wind: Vec2) { + self.old_local_wind = mem::replace(&mut self.new_local_wind, (wind, Instant::now())); + } + + fn update_local_wind(&mut self) { + // Assumes updates are regular + let t = (self.new_local_wind.1.elapsed().as_secs_f32() + / self + .new_local_wind + .1 + .duration_since(self.old_local_wind.1) + .as_secs_f32()) + .clamp(0.0, 1.0); + + self.local_wind = Vec2::lerp_unclamped(self.old_local_wind.0, self.new_local_wind.0, t); + } + + fn weather_update(&mut self, weather: SharedWeatherGrid) { self.old = mem::replace(&mut self.new, (weather, Instant::now())); } @@ -191,13 +211,14 @@ impl WeatherLerp { // that updates come at regular intervals. fn update(&mut self, to_update: &mut WeatherGrid) { prof_span!("WeatherLerp::update"); + self.update_local_wind(); let old = &self.old.0; let new = &self.new.0; if new.size() == Vec2::zero() { return; } if to_update.size() != new.size() { - *to_update = new.clone(); + *to_update = WeatherGrid::from(new); } if old.size() == new.size() { // Assumes updates are regular @@ -209,7 +230,7 @@ impl WeatherLerp { .iter_mut() .zip(old.iter().zip(new.iter())) .for_each(|((_, current), ((_, old), (_, new)))| { - *current = Weather::lerp_unclamped(old, new, t); + *current = CompressedWeather::lerp_unclamped(old, new, t); }); } } @@ -217,9 +238,14 @@ impl WeatherLerp { impl Default for WeatherLerp { fn default() -> Self { + let old = Instant::now(); + let new = Instant::now(); Self { - old: (WeatherGrid::new(Vec2::zero()), Instant::now()), - new: (WeatherGrid::new(Vec2::zero()), Instant::now()), + old: (SharedWeatherGrid::new(Vec2::zero()), old), + new: (SharedWeatherGrid::new(Vec2::zero()), new), + old_local_wind: (Vec2::zero(), old), + new_local_wind: (Vec2::zero(), new), + local_wind: Vec2::zero(), } } } @@ -1716,7 +1742,11 @@ impl Client { /// Returns Weather::default if no player position exists. pub fn weather_at_player(&self) -> Weather { self.position() - .map(|wpos| self.state.weather_at(wpos.xy())) + .map(|p| { + let mut weather = self.state.weather_at(p.xy()); + weather.wind = self.weather.local_wind; + weather + }) .unwrap_or_default() } @@ -2539,6 +2569,9 @@ impl Client { ServerGeneral::WeatherUpdate(weather) => { self.weather.weather_update(weather); }, + ServerGeneral::LocalWindUpdate(wind) => { + self.weather.local_wind_update(wind); + }, ServerGeneral::SpectatePosition(pos) => { frontend_events.push(Event::SpectatePosition(pos)); }, diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index 89c79d76fa..2f7ecf8ad9 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -17,7 +17,7 @@ use common::{ trade::{PendingTrade, SitePrices, TradeId, TradeResult}, uid::Uid, uuid::Uuid, - weather::WeatherGrid, + weather::SharedWeatherGrid, }; use hashbrown::HashMap; use serde::{Deserialize, Serialize}; @@ -214,7 +214,8 @@ pub enum ServerGeneral { /// Economic information about sites SiteEconomy(EconomyInfo), MapMarker(comp::MapMarkerUpdate), - WeatherUpdate(WeatherGrid), + WeatherUpdate(SharedWeatherGrid), + LocalWindUpdate(Vec2), /// Suggest the client to spectate a position. Called after client has /// requested teleport etc. SpectatePosition(Vec3), @@ -339,6 +340,7 @@ impl ServerMsg { | ServerGeneral::SiteEconomy(_) | ServerGeneral::MapMarker(_) | ServerGeneral::WeatherUpdate(_) + | ServerGeneral::LocalWindUpdate(_) | ServerGeneral::SpectatePosition(_) => { c_type == ClientType::Game && presence.is_some() }, diff --git a/common/src/weather.rs b/common/src/weather.rs index 2ee3a61a40..8d2e78cd22 100644 --- a/common/src/weather.rs +++ b/common/src/weather.rs @@ -32,11 +32,11 @@ impl Weather { } } - pub fn lerp_unclamped(from: &Self, to: &Self, t: f32) -> Self { + pub fn lerp_unclamped(&self, to: &Self, t: f32) -> Self { Self { - cloud: f32::lerp_unclamped(from.cloud, to.cloud, t), - rain: f32::lerp_unclamped(from.rain, to.rain, t), - wind: Vec2::::lerp_unclamped(from.wind, to.wind, t), + cloud: f32::lerp_unclamped(self.cloud, to.cloud, t), + rain: f32::lerp_unclamped(self.rain, to.rain, t), + wind: Vec2::::lerp_unclamped(self.wind, to.wind, t), } } @@ -75,11 +75,105 @@ pub const CHUNKS_PER_CELL: u32 = 16; pub const CELL_SIZE: u32 = CHUNKS_PER_CELL * TerrainChunkSize::RECT_SIZE.x; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct WeatherGrid { weather: Grid, } +/// Weather that's compressed in order to send it to the client. +#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)] +pub struct CompressedWeather { + cloud: u8, + rain: u8, +} + +impl CompressedWeather { + pub fn lerp_unclamped(&self, to: &CompressedWeather, t: f32) -> Weather { + Weather { + cloud: f32::lerp_unclamped(self.cloud as f32, to.cloud as f32, t) / 255.0, + rain: f32::lerp_unclamped(self.rain as f32, to.rain as f32, t) / 255.0, + wind: Vec2::zero(), + } + } +} + +impl From for CompressedWeather { + fn from(weather: Weather) -> Self { + Self { + cloud: (weather.cloud * 255.0).round() as u8, + rain: (weather.rain * 255.0).round() as u8, + } + } +} + +impl From for Weather { + fn from(weather: CompressedWeather) -> Self { + Self { + cloud: weather.cloud as f32 / 255.0, + rain: weather.rain as f32 / 255.0, + wind: Vec2::zero(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SharedWeatherGrid { + weather: Grid, +} + +impl From<&WeatherGrid> for SharedWeatherGrid { + fn from(value: &WeatherGrid) -> Self { + Self { + weather: Grid::from_raw( + value.weather.size(), + value + .weather + .raw() + .iter() + .copied() + .map(CompressedWeather::from) + .collect::>(), + ), + } + } +} + +impl From<&SharedWeatherGrid> for WeatherGrid { + fn from(value: &SharedWeatherGrid) -> Self { + Self { + weather: Grid::from_raw( + value.weather.size(), + value + .weather + .raw() + .iter() + .copied() + .map(Weather::from) + .collect::>(), + ), + } + } +} + +impl SharedWeatherGrid { + pub fn new(size: Vec2) -> Self { + size.map(|e| debug_assert!(i32::try_from(e).is_ok())); + Self { + weather: Grid::new(size.as_(), CompressedWeather::default()), + } + } + + pub fn iter(&self) -> impl Iterator, &CompressedWeather)> { + self.weather.iter() + } + + pub fn iter_mut(&mut self) -> impl Iterator, &mut CompressedWeather)> { + self.weather.iter_mut() + } + + pub fn size(&self) -> Vec2 { self.weather.size().as_() } +} + /// Transforms a world position to cell coordinates. Where (0.0, 0.0) in cell /// coordinates is the center of the weather cell located at (0, 0) in the grid. fn to_cell_pos(wpos: Vec2) -> Vec2 { wpos / CELL_SIZE as f32 - 0.5 } @@ -113,6 +207,13 @@ impl WeatherGrid { pub fn size(&self) -> Vec2 { self.weather.size().as_() } + pub fn get(&self, cell_pos: Vec2) -> Weather { + self.weather + .get(cell_pos.as_()) + .copied() + .unwrap_or_default() + } + /// Get the weather at a given world position by doing bilinear /// interpolation between four cells. pub fn get_interpolated(&self, wpos: Vec2) -> Weather { diff --git a/server/src/client.rs b/server/src/client.rs index 8b785ff663..a19bf45011 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -192,6 +192,7 @@ impl Client { | ServerGeneral::FinishedTrade(_) | ServerGeneral::MapMarker(_) | ServerGeneral::WeatherUpdate(_) + | ServerGeneral::LocalWindUpdate(_) | ServerGeneral::SpectatePosition(_) => { PreparedMsg::new(2, &g, &self.in_game_stream_params) }, diff --git a/server/src/cmd.rs b/server/src/cmd.rs index fbe69ac378..b8a1f90e68 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -10,7 +10,7 @@ use crate::{ SettingError, WhitelistInfo, WhitelistRecord, }, sys::terrain::NpcData, - weather::WeatherSim, + weather::WeatherJob, wiring, wiring::OutputFormula, Server, Settings, StateExt, @@ -4474,11 +4474,14 @@ fn handle_weather_zone( let mut add_zone = |weather: weather::Weather| { if let Ok(pos) = position(server, client, "player") { let pos = pos.0.xy() / weather::CELL_SIZE as f32; - server + if let Some(weather_job) = server .state .ecs_mut() - .write_resource::() - .add_zone(weather, pos, radius, time); + .write_resource::>() + .as_mut() + { + weather_job.queue_zone(weather, pos, radius, time); + } } }; match name.as_str() { diff --git a/server/src/lib.rs b/server/src/lib.rs index 24bd2877ab..e25b298b5d 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -371,6 +371,7 @@ impl Server { pool.configure("CHUNK_GENERATOR", |n| n / 2 + n / 4); pool.configure("CHUNK_SERIALIZER", |n| n / 2); pool.configure("RTSIM_SAVE", |_| 1); + pool.configure("WEATHER", |_| 1); } state .ecs_mut() @@ -588,7 +589,7 @@ impl Server { return Err(Error::RtsimError(err)); }, } - weather::init(&mut state, &world); + weather::init(&mut state); } let server_constants = ServerConstants { diff --git a/server/src/weather/mod.rs b/server/src/weather/mod.rs index 3d36d15d59..710a9cc8fa 100644 --- a/server/src/weather/mod.rs +++ b/server/src/weather/mod.rs @@ -1,41 +1,23 @@ -use common::weather::CHUNKS_PER_CELL; -use common_ecs::{dispatch, System}; +use common_ecs::dispatch; use common_state::State; use specs::DispatcherBuilder; -use std::time::Duration; - -use crate::sys::SysScheduler; mod sim; -mod sync; mod tick; -pub use sim::WeatherSim; +pub use tick::WeatherJob; /// How often the weather is updated, in seconds const WEATHER_DT: f32 = 5.0; pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { dispatch::(dispatch_builder, &[]); - dispatch::(dispatch_builder, &[&tick::Sys::sys_name()]); } #[cfg(feature = "worldgen")] -pub fn init(state: &mut State, world: &world::World) { - let weather_size = world.sim().get_size() / CHUNKS_PER_CELL; - let sim = WeatherSim::new(weather_size, world); - state.ecs_mut().insert(sim); +pub fn init(state: &mut State) { + use crate::weather::sim::LightningCells; - // NOTE: If weather computations get too heavy, this should not block the main - // thread. - state - .ecs_mut() - .insert(SysScheduler::::every(Duration::from_secs_f32( - WEATHER_DT, - ))); - state - .ecs_mut() - .insert(SysScheduler::::every(Duration::from_secs_f32( - WEATHER_DT, - ))); + state.ecs_mut().insert(None::); + state.ecs_mut().insert(LightningCells::default()); } diff --git a/server/src/weather/sim.rs b/server/src/weather/sim.rs index 5310df541a..753b2c6002 100644 --- a/server/src/weather/sim.rs +++ b/server/src/weather/sim.rs @@ -1,12 +1,9 @@ use common::{ - event::EventBus, grid::Grid, - outcome::Outcome, resources::TimeOfDay, weather::{Weather, WeatherGrid, CELL_SIZE, CHUNKS_PER_CELL}, }; use noise::{NoiseFn, SuperSimplex, Turbulence}; -use rand::prelude::*; use vek::*; use world::World; @@ -31,6 +28,12 @@ pub struct WeatherSim { zones: Grid>, } +/// A list of weather cells where lightning has a chance to strike. +#[derive(Default)] +pub struct LightningCells { + pub cells: Vec>, +} + impl WeatherSim { pub fn new(size: Vec2, world: &World) -> Self { Self { @@ -85,14 +88,8 @@ impl WeatherSim { } } - // Time step is cell size / maximum wind speed - pub fn tick( - &mut self, - time_of_day: &TimeOfDay, - outcomes: &EventBus, - out: &mut WeatherGrid, - world: &World, - ) { + // Time step is cell size / maximum wind speed. + pub fn tick(&mut self, time_of_day: TimeOfDay, out: &mut WeatherGrid) -> LightningCells { let time = time_of_day.0; let base_nz = Turbulence::new( @@ -105,6 +102,7 @@ impl WeatherSim { let rain_nz = SuperSimplex::new(); + let mut lightning_cells = Vec::new(); for (point, cell) in out.iter_mut() { if let Some(zone) = &mut self.zones[point] { *cell = zone.weather; @@ -147,16 +145,14 @@ impl WeatherSim { rain_nz.get((spos + 1.0).into_array()).powi(3) as f32, ) * 200.0 * (1.0 - pressure); - - if cell.rain > 0.2 && cell.cloud > 0.15 && thread_rng().gen_bool(0.01) { - let wpos = wpos.map(|e| { - e as f32 + thread_rng().gen_range(-1.0..1.0) * CELL_SIZE as f32 * 0.5 - }); - outcomes.emit_now(Outcome::Lightning { - pos: wpos.with_z(world.sim().get_alt_approx(wpos.as_()).unwrap_or(0.0)), - }); - } } + + if cell.rain > 0.2 && cell.cloud > 0.15 { + lightning_cells.push(point); + } + } + LightningCells { + cells: lightning_cells, } } diff --git a/server/src/weather/sync.rs b/server/src/weather/sync.rs deleted file mode 100644 index b5d58c22ed..0000000000 --- a/server/src/weather/sync.rs +++ /dev/null @@ -1,37 +0,0 @@ -use common::weather::WeatherGrid; -use common_ecs::{Origin, Phase, System}; -use common_net::msg::ServerGeneral; -use specs::{Join, ReadExpect, ReadStorage, Write}; - -use crate::{client::Client, sys::SysScheduler}; - -#[derive(Default)] -pub struct Sys; - -impl<'a> System<'a> for Sys { - type SystemData = ( - ReadExpect<'a, WeatherGrid>, - Write<'a, SysScheduler>, - ReadStorage<'a, Client>, - ); - - const NAME: &'static str = "weather::sync"; - const ORIGIN: Origin = Origin::Server; - const PHASE: Phase = Phase::Create; - - fn run( - _job: &mut common_ecs::Job, - (weather_grid, mut scheduler, clients): Self::SystemData, - ) { - if scheduler.should_run() { - let mut lazy_msg = None; - for client in clients.join() { - if lazy_msg.is_none() { - lazy_msg = - Some(client.prepare(ServerGeneral::WeatherUpdate(weather_grid.clone()))); - } - lazy_msg.as_ref().map(|msg| client.send_prepared(msg)); - } - } - } -} diff --git a/server/src/weather/tick.rs b/server/src/weather/tick.rs index c52bf0c99a..acf213ee8a 100644 --- a/server/src/weather/tick.rs +++ b/server/src/weather/tick.rs @@ -1,24 +1,64 @@ -use common::{event::EventBus, outcome::Outcome, resources::TimeOfDay, weather::WeatherGrid}; +use common::{ + comp, + event::EventBus, + outcome::Outcome, + resources::{DeltaTime, ProgramTime, TimeOfDay}, + slowjob::{SlowJob, SlowJobPool}, + weather::{SharedWeatherGrid, Weather, WeatherGrid}, +}; use common_ecs::{Origin, Phase, System}; -use specs::{Read, ReadExpect, Write, WriteExpect}; -use std::sync::Arc; +use common_net::msg::ServerGeneral; +use rand::{seq::SliceRandom, thread_rng, Rng}; +use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteExpect}; +use std::{mem, sync::Arc}; +use vek::Vec2; use world::World; -use crate::sys::SysScheduler; +use crate::{client::Client, Tick}; -use super::sim::WeatherSim; +use super::{ + sim::{LightningCells, WeatherSim}, + WEATHER_DT, +}; + +enum WeatherJobState { + Working(SlowJob), + Idle(WeatherSim), + None, +} + +pub struct WeatherJob { + last_update: ProgramTime, + weather_tx: crossbeam_channel::Sender<(WeatherGrid, LightningCells, WeatherSim)>, + weather_rx: crossbeam_channel::Receiver<(WeatherGrid, LightningCells, WeatherSim)>, + state: WeatherJobState, + qeued_zones: Vec<(Weather, Vec2, f32, f32)>, +} + +impl WeatherJob { + pub fn queue_zone(&mut self, weather: Weather, pos: Vec2, radius: f32, time: f32) { + self.qeued_zones.push((weather, pos, radius, time)) + } +} #[derive(Default)] pub struct Sys; impl<'a> System<'a> for Sys { type SystemData = ( + Entities<'a>, Read<'a, TimeOfDay>, - WriteExpect<'a, WeatherSim>, + Read<'a, ProgramTime>, + Read<'a, Tick>, + Read<'a, DeltaTime>, + Write<'a, LightningCells>, + Write<'a, Option>, WriteExpect<'a, WeatherGrid>, - Write<'a, SysScheduler>, + WriteExpect<'a, SlowJobPool>, ReadExpect<'a, EventBus>, ReadExpect<'a, Arc>, + ReadStorage<'a, Client>, + ReadStorage<'a, comp::Pos>, ); const NAME: &'static str = "weather::tick"; @@ -27,13 +67,110 @@ impl<'a> System<'a> for Sys { fn run( _job: &mut common_ecs::Job, - (game_time, mut sim, mut grid, mut scheduler, outcomes, world): Self::SystemData, + ( + entities, + game_time, + program_time, + tick, + delta_time, + mut lightning_cells, + mut weather_job, + mut grid, + slow_job_pool, + outcomes, + world, + clients, + positions, + ): Self::SystemData, ) { - if scheduler.should_run() { - if grid.size() != sim.size() { + let to_update = match &mut *weather_job { + Some(weather_job) => (program_time.0 - weather_job.last_update.0 >= WEATHER_DT as f64) + .then_some(weather_job), + None => { + let (weather_tx, weather_rx) = crossbeam_channel::bounded(1); + + let weather_size = world.sim().get_size() / common::weather::CHUNKS_PER_CELL; + let mut sim = WeatherSim::new(weather_size, &world); *grid = WeatherGrid::new(sim.size()); + *lightning_cells = sim.tick(*game_time, &mut grid); + + *weather_job = Some(WeatherJob { + last_update: *program_time, + weather_tx, + weather_rx, + state: WeatherJobState::Idle(sim), + qeued_zones: Vec::new(), + }); + + None + }, + }; + + if let Some(weather_job) = to_update { + if matches!(weather_job.state, WeatherJobState::Working(_)) + && let Ok((new_grid, new_lightning_cells, sim)) = weather_job.weather_rx.try_recv() { + *grid = new_grid; + *lightning_cells = new_lightning_cells; + let mut lazy_msg = None; + for client in clients.join() { + if lazy_msg.is_none() { + lazy_msg = Some(client.prepare(ServerGeneral::WeatherUpdate( + SharedWeatherGrid::from(&*grid), + ))); + } + lazy_msg.as_ref().map(|msg| client.send_prepared(msg)); + } + weather_job.state = WeatherJobState::Idle(sim); + + } + + if matches!(weather_job.state, WeatherJobState::Idle(_)) { + weather_job.last_update = *program_time; + let old_state = mem::replace(&mut weather_job.state, WeatherJobState::None); + + let WeatherJobState::Idle(mut sim) = old_state else { + unreachable!() + }; + + let weather_tx = weather_job.weather_tx.clone(); + let game_time = *game_time; + for (weather, pos, radius, time) in weather_job.qeued_zones.drain(..) { + sim.add_zone(weather, pos, radius, time) + } + let job = slow_job_pool.spawn("WEATHER", move || { + let mut grid = WeatherGrid::new(sim.size()); + let lightning_cells = sim.tick(game_time, &mut grid); + let _ = weather_tx.send((grid, lightning_cells, sim)); + }); + + weather_job.state = WeatherJobState::Working(job); + } + } + + // Chance to emit lightning every frame from one or more of the cells that + // currently has the correct weather conditions. + let mut outcome_emitter = outcomes.emitter(); + let mut rng = thread_rng(); + let num_cells = lightning_cells.cells.len() as f64 * 0.0015 * delta_time.0 as f64; + let num_cells = num_cells.floor() as u32 + rng.gen_bool(num_cells.fract()) as u32; + + for _ in 0..num_cells { + let cell_pos = lightning_cells.cells.choose(&mut rng).expect( + "This is non-empty, since we multiply with its len for the chance to do a \ + lightning strike.", + ); + let wpos = cell_pos + .map(|e| (e as f32 + rng.gen_range(0.0..1.0)) * common::weather::CELL_SIZE as f32); + outcome_emitter.emit(Outcome::Lightning { + pos: wpos.with_z(world.sim().get_alt_approx(wpos.as_()).unwrap_or(0.0)), + }); + } + + for (entity, client, pos) in (&entities, &clients, &positions).join() { + if entity.id() as u64 % 30 == tick.0 % 30 { + let weather = grid.get_interpolated(pos.0.xy()); + client.send_fallible(ServerGeneral::LocalWindUpdate(weather.wind)); } - sim.tick(&game_time, &outcomes, &mut grid, &world); } } } diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 5060cf6bcc..26a2094eff 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -780,7 +780,7 @@ impl FigureMgr { // Are shadows enabled at all? let can_shadow_sun = renderer.pipeline_modes().shadow.is_map() && is_daylight; - let weather = scene_data.state.weather_at(cam_pos.xy()); + let weather = scene_data.client.weather_at_player(); let cam_pos = math::Vec3::from(cam_pos); diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 167913f647..a813459769 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -1198,7 +1198,7 @@ impl Scene { .max_weather_near(focus_off.xy() + cam_pos.xy()); self.wind_vel = weather.wind_vel(); if weather.rain > RAIN_THRESHOLD { - let weather = client.state().weather_at(focus_off.xy() + cam_pos.xy()); + let weather = client.weather_at_player(); let rain_vel = weather.rain_vel(); let rain_view_mat = math::Mat4::look_at_rh(look_at, look_at + rain_vel, up); diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index 52f595eac0..1a09c74752 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -1580,7 +1580,7 @@ impl Terrain { min: visible_bounding_box.min.as_::(), max: visible_bounding_box.max.as_::(), }; - let weather = scene_data.state.weather_at(focus_off.xy() + cam_pos.xy()); + let weather = scene_data.client.weather_at_player(); let ray_direction = math::Vec3::::from(weather.rain_vel().normalized()); // NOTE: We use proj_mat_treeculler here because