diff --git a/.cargo/config b/.cargo/config index 87d684cbbf..5dbe6321db 100644 --- a/.cargo/config +++ b/.cargo/config @@ -31,4 +31,4 @@ dbg-voxygen = "run --bin veloren-voxygen --profile debuginfo" # misc swarm = "run --bin swarm --features client/bin_bot,client/tick_network --" ci-clippy = "clippy --all-targets --locked --features=bin_cmd_doc_gen,bin_compression,bin_csv,bin_graphviz,bin_bot,bin_asset_migrate,asset_tweak,bin,stat" -ci-clippy2 = "clippy -p veloren-voxygen --locked --no-default-features --features=default-publish" +ci-clippy2 = "clippy -p veloren-voxygen --locked --no-default-features --features=default-publish" \ No newline at end of file diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index b59f41a2ec..1eb5b25819 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -95,6 +95,7 @@ const int ENERGY_PHOENIX = 55; const int PHOENIX_BEAM = 56; const int PHOENIX_BUILD_UP_AIM = 57; const int CLAY_SHRAPNEL = 58; +const int AIRFLOW = 59; // meters per second squared (acceleration) const float earth_gravity = 9.807; @@ -163,10 +164,24 @@ mat4 spin_in_axis(vec3 axis, float angle) float c = cos(angle); float oc = 1.0 - c; - return mat4(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0, - oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0, - oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0, - 0, 0, 0, 1); + return mat4( + oc * axis.x * axis.x + c, + oc * axis.x * axis.y - axis.z * s, + oc * axis.z * axis.x + axis.y * s, + 0, + + oc * axis.x * axis.y + axis.z * s, + oc * axis.y * axis.y + c, + oc * axis.y * axis.z - axis.x * s, + 0, + + oc * axis.z * axis.x - axis.y * s, + oc * axis.y * axis.z + axis.x * s, + oc * axis.z * axis.z + c, + 0, + + 0, 0, 0, 1 + ); } mat4 identity() { @@ -984,6 +999,23 @@ void main() { spin_in_axis(vec3(1,0,0),0) ); break; + case AIRFLOW: + perp_axis = normalize(cross(inst_dir, vec3(1.0, 0.0, 0.0))); + attr = Attr( + // offsets + inst_dir * 0.2 * length(inst_dir) * percent() + inst_dir * percent() * 0.08, + // scale + vec3( + 0.3 * length(inst_dir), + 0.3 * length(inst_dir), + 3.0 * length(inst_dir) * percent() * (1 - percent()) + ), + // color + vec4(1.1, 1.1, 1.1, 0.3), + // rotation + spin_in_axis(perp_axis, asin(inst_dir.z / length(inst_dir)) + PI / 2.0) + ); + break; default: attr = Attr( linear_motion( @@ -1008,7 +1040,14 @@ void main() { // First 3 normals are negative, next 3 are positive // TODO: Make particle normals match orientation - vec4 normals[6] = vec4[](vec4(-1,0,0,0), vec4(1,0,0,0), vec4(0,-1,0,0), vec4(0,1,0,0), vec4(0,0,-1,0), vec4(0,0,1,0)); + vec4 normals[6] = vec4[]( + vec4(-1,0,0,0), + vec4(1,0,0,0), + vec4(0,-1,0,0), + vec4(0,1,0,0), + vec4(0,0,-1,0), + vec4(0,0,1,0) + ); f_norm = // inst_pos * normalize(((normals[(v_norm_ao >> 0) & 0x7u]) * attr.rot).xyz); diff --git a/common/src/resources.rs b/common/src/resources.rs index 007f34d0bd..acac63151d 100644 --- a/common/src/resources.rs +++ b/common/src/resources.rs @@ -2,10 +2,31 @@ use crate::comp::Pos; use serde::{Deserialize, Serialize}; use specs::Entity; use std::ops::{Mul, MulAssign}; +use vek::Vec3; /// A resource that stores the time of day. #[derive(Copy, Clone, Debug, Serialize, Deserialize, Default)] pub struct TimeOfDay(pub f64); +impl TimeOfDay { + pub fn new(t: f64) -> Self { TimeOfDay(t) } + + fn get_angle_rad(self) -> f32 { + const TIME_FACTOR: f64 = (std::f64::consts::PI * 2.0) / (3600.0 * 24.0); + ((self.0 * TIME_FACTOR) % (std::f64::consts::PI * 2.0)) as f32 + } + + /// Computes the direction of light from the sun based on the time of day. + pub fn get_sun_dir(self) -> Vec3 { + let angle_rad = self.get_angle_rad(); + Vec3::new(-angle_rad.sin(), 0.0, angle_rad.cos()) + } + + /// Computes the direction of light from the moon based on the time of day. + pub fn get_moon_dir(self) -> Vec3 { + let angle_rad = self.get_angle_rad(); + -Vec3::new(-angle_rad.sin(), 0.0, angle_rad.cos() - 0.5).normalized() + } +} impl TimeOfDay { pub fn day(&self) -> f64 { self.0.rem_euclid(24.0 * 3600.0) } diff --git a/common/src/states/glide_wield.rs b/common/src/states/glide_wield.rs index a47c151084..0671421250 100644 --- a/common/src/states/glide_wield.rs +++ b/common/src/states/glide_wield.rs @@ -26,10 +26,30 @@ impl From<&JoinData<'_>> for Data { Self { // Aspect ratio is what really matters for lift/drag ratio // and the aerodynamics model works for ARs up to 25. + // // The inflated dimensions are hopefully only a temporary // bandaid for the poor glide ratio experienced under 2.5G. + // + // The formula is: + // s: span_length_modifier + // c: chord_length_modifier + // h: height (this is a hack to balance different races) + // + // p_a = Pi/4 * c * h * s * h + // AR + // = (s * h)^2 / p_a + // = (s * h)^2 / (Pi / 4 * (c * h) * (s * h)) + // = (s * h) / (c * h) / (Pi / 4) + // = s / c / Pi/4 + // + // or if c is 1, + // = s / Pi/4 + // + // In other words, the bigger `span_length` the better. + // // A span/chord ratio of 4.5 gives an AR of ~5.73. - span_length: scale * 4.5, + // A span/chord ratio of 3.0 gives an ARI of ~3.82. + span_length: scale * 3.0, chord_length: scale, ori: *data.ori, } diff --git a/common/src/terrain/mod.rs b/common/src/terrain/mod.rs index 5e6e0fd8e6..fa01cf3e53 100644 --- a/common/src/terrain/mod.rs +++ b/common/src/terrain/mod.rs @@ -6,6 +6,8 @@ pub mod site; pub mod sprite; pub mod structure; +use std::ops::{Add, Mul}; + // Reexports pub use self::{ biome::BiomeKind, @@ -140,7 +142,11 @@ pub struct TerrainChunkMeta { tree_density: f32, contains_cave: bool, contains_river: bool, + near_water: bool, river_velocity: Vec3, + approx_chunk_terrain_normal: Option>, + rockiness: f32, + cliff_height: f32, temp: f32, humidity: f32, site: Option, @@ -158,10 +164,14 @@ impl TerrainChunkMeta { tree_density: f32, contains_cave: bool, contains_river: bool, + near_water: bool, river_velocity: Vec3, temp: f32, humidity: f32, site: Option, + approx_chunk_terrain_normal: Option>, + rockiness: f32, + cliff_height: f32, ) -> Self { Self { name, @@ -170,6 +180,7 @@ impl TerrainChunkMeta { tree_density, contains_cave, contains_river, + near_water, river_velocity, temp, humidity, @@ -178,6 +189,9 @@ impl TerrainChunkMeta { debug_points: Vec::new(), debug_lines: Vec::new(), sprite_cfgs: HashMap::default(), + approx_chunk_terrain_normal, + rockiness, + cliff_height, } } @@ -189,6 +203,7 @@ impl TerrainChunkMeta { tree_density: 0.0, contains_cave: false, contains_river: false, + near_water: false, river_velocity: Vec3::zero(), temp: 0.0, humidity: 0.0, @@ -197,6 +212,9 @@ impl TerrainChunkMeta { debug_points: Vec::new(), debug_lines: Vec::new(), sprite_cfgs: HashMap::default(), + approx_chunk_terrain_normal: None, + rockiness: 0.0, + cliff_height: 0.0, } } @@ -204,6 +222,7 @@ impl TerrainChunkMeta { pub fn biome(&self) -> BiomeKind { self.biome } + /// Altitude in blocks pub fn alt(&self) -> f32 { self.alt } pub fn tree_density(&self) -> f32 { self.tree_density } @@ -212,10 +231,13 @@ impl TerrainChunkMeta { pub fn contains_river(&self) -> bool { self.contains_river } + pub fn near_water(&self) -> bool { self.near_water } + pub fn river_velocity(&self) -> Vec3 { self.river_velocity } pub fn site(&self) -> Option { self.site } + /// Temperature from 0 to 1 (possibly -1 to 1) pub fn temp(&self) -> f32 { self.temp } pub fn humidity(&self) -> f32 { self.humidity } @@ -239,6 +261,14 @@ impl TerrainChunkMeta { pub fn set_sprite_cfg_at(&mut self, rpos: Vec3, sprite_cfg: SpriteCfg) { self.sprite_cfgs.insert(rpos, sprite_cfg); } + + pub fn approx_chunk_terrain_normal(&self) -> Option> { + self.approx_chunk_terrain_normal + } + + pub fn rockiness(&self) -> f32 { self.rockiness } + + pub fn cliff_height(&self) -> f32 { self.cliff_height } } // Terrain type aliases @@ -279,6 +309,39 @@ impl TerrainGrid { && self.is_space(*pos) }) } + + pub fn get_interpolated(&self, pos: Vec2, mut f: F) -> Option + where + T: Copy + Default + Add + Mul, + F: FnMut(&TerrainChunk) -> T, + { + let pos = pos.as_::().wpos_to_cpos(); + + let cubic = |a: T, b: T, c: T, d: T, x: f32| -> T { + let x2 = x * x; + + // Catmull-Rom splines + let co0 = a * -0.5 + b * 1.5 + c * -1.5 + d * 0.5; + let co1 = a + b * -2.5 + c * 2.0 + d * -0.5; + let co2 = a * -0.5 + c * 0.5; + let co3 = b; + + co0 * x2 * x + co1 * x2 + co2 * x + co3 + }; + + let mut x = [T::default(); 4]; + + for (x_idx, j) in (-1..3).enumerate() { + let y0 = f(self.get_key(pos.map2(Vec2::new(j, -1), |e, q| e.max(0.0) as i32 + q))?); + let y1 = f(self.get_key(pos.map2(Vec2::new(j, 0), |e, q| e.max(0.0) as i32 + q))?); + let y2 = f(self.get_key(pos.map2(Vec2::new(j, 1), |e, q| e.max(0.0) as i32 + q))?); + let y3 = f(self.get_key(pos.map2(Vec2::new(j, 2), |e, q| e.max(0.0) as i32 + q))?); + + x[x_idx] = cubic(y0, y1, y2, y3, pos.y.fract() as f32); + } + + Some(cubic(x[0], x[1], x[2], x[3], pos.x.fract() as f32)) + } } impl TerrainChunk { diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 89377b3f6a..b987081c65 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -12,15 +12,17 @@ use common::{ link::Is, mounting::{Rider, VolumeRider}, outcome::Outcome, - resources::{DeltaTime, GameMode}, + resources::{DeltaTime, GameMode, TimeOfDay}, states, - terrain::{Block, BlockKind, TerrainGrid}, + terrain::{Block, BlockKind, CoordinateConversions, SiteKindMeta, TerrainGrid, NEIGHBOR_DELTA}, uid::Uid, util::{Projection, SpatialGrid}, vol::{BaseVol, ReadVol}, + weather::WeatherGrid, }; use common_base::{prof_span, span}; use common_ecs::{Job, Origin, ParMode, Phase, PhysicsMetrics, System}; +use itertools::Itertools; use rayon::iter::ParallelIterator; use specs::{ shred, Entities, Entity, Join, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData, @@ -115,6 +117,151 @@ fn integrate_forces( vel } +/// Simulates winds based on weather and terrain data for specific position +// TODO: Consider exporting it if one wants to build nice visuals +fn simulated_wind_vel( + pos: &Pos, + weather: &WeatherGrid, + terrain: &TerrainGrid, + time_of_day: &TimeOfDay, +) -> Result, ()> { + prof_span!(guard, "Apply Weather INIT"); + + let pos_2d = pos.0.as_().xy(); + let chunk_pos: Vec2 = pos_2d.wpos_to_cpos(); + let Some(current_chunk) = terrain.get_key(chunk_pos) else { + return Err(()); + }; + + let meta = current_chunk.meta(); + + let interp_weather = weather.get_interpolated(pos.0.xy()); + // Weather sim wind + let interp_alt = terrain + .get_interpolated(pos_2d, |c| c.meta().alt()) + .unwrap_or(0.); + let interp_tree_density = terrain + .get_interpolated(pos_2d, |c| c.meta().tree_density()) + .unwrap_or(0.); + let interp_town = terrain + .get_interpolated(pos_2d, |c| match c.meta().site() { + Some(SiteKindMeta::Settlement(_)) => 3.5, + _ => 1.0, + }) + .unwrap_or(0.); + let normal = terrain + .get_interpolated(pos_2d, |c| { + c.meta() + .approx_chunk_terrain_normal() + .unwrap_or(Vec3::unit_z()) + }) + .unwrap_or(Vec3::unit_z()); + let above_ground = pos.0.z - interp_alt; + let wind_velocity = interp_weather.wind_vel(); + + let surrounding_chunks_metas = NEIGHBOR_DELTA + .iter() + .map(move |&(x, y)| chunk_pos + Vec2::new(x, y)) + .filter_map(|cpos| terrain.get_key(cpos).map(|c| c.meta())) + .collect::>(); + + drop(guard); + + prof_span!(guard, "thermals"); + + // === THERMALS === + + // Sun angle of incidence. + // + // 0.0..1.0, 0.25 morning, 0.45 midday, 0.66 evening, 0.79 night, 0.0/1.0 + // midnight + let sun_dir = time_of_day.get_sun_dir().normalized(); + let mut lift = ((sun_dir - normal.normalized()).magnitude() - 0.5).max(0.2) * 3.; + + // TODO: potential source of harsh edges in wind speed. + let temperatures = surrounding_chunks_metas.iter().map(|m| m.temp()).minmax(); + + // More thermals if hot chunks border cold chunks + lift *= match temperatures { + itertools::MinMaxResult::NoElements | itertools::MinMaxResult::OneElement(_) => 1.0, + itertools::MinMaxResult::MinMax(a, b) => 0.8 + ((a - b).abs() * 1.1), + } + .min(2.0); + + // TODO: potential source of harsh edges in wind speed. + // + // Way more thermals in strong rain as its often caused by strong thermals. + // Less in weak rain or cloudy .. + lift *= if interp_weather.rain.is_between(0.5, 1.0) && interp_weather.cloud.is_between(0.6, 1.0) + { + 1.5 + } else if interp_weather.rain.is_between(0.2, 0.5) && interp_weather.cloud.is_between(0.3, 0.6) + { + 0.8 + } else { + 1.0 + }; + + // The first 15 blocks are weaker. Starting from the ground should be difficult. + lift *= (above_ground / 15.).min(1.); + lift *= (220. - above_ground / 20.).clamp(0.0, 1.0); + + // TODO: Smooth this, and increase height some more (500 isnt that much higher + // than the spires) + if interp_alt > 500.0 { + lift *= 0.8; + } + + // More thermals above towns, the materials tend to heat up more. + lift *= interp_town; + + // Bodies of water cool the air, causing less thermals. + lift *= terrain + .get_interpolated(pos_2d, |c| 1. - c.meta().near_water() as i32 as f32) + .unwrap_or(1.); + + drop(guard); + + // === Ridge/Wave lift === + + let mut ridge_lift = { + let steepness = normal.angle_between(normal.with_z(0.)).max(0.5); + + // angle between normal and wind + let mut angle = wind_velocity.angle_between(normal.xy()); // 1.4 radians of zero + + // a deadzone of +-1.5 radians if wind is blowing away from + // the mountainside. + angle = (angle - 1.3).max(0.0); + + // the ridge lift is based on the angle and the velocity of the wind + angle * steepness * wind_velocity.magnitude() * 2.5 + }; + + // Cliffs mean more lift + // 44 seems to be max, according to a lerp in WorldSim::generate_cliffs + ridge_lift *= 0.9 + (meta.cliff_height() / 44.0) * 1.2; + + // Height based fall-off (https://www.desmos.com/calculator/jijqfunchg) + ridge_lift *= 1. / (1. + (1.3f32.powf(0.1 * above_ground - 15.))); + + // More flat wind above ground (https://www.desmos.com/calculator/jryiyqsdnx) + let wind_factor = 1. / (0.25 + (0.96f32.powf(0.1 * above_ground - 15.))); + + let mut wind_vel = (wind_velocity * wind_factor).with_z(lift + ridge_lift); + + // probably 0. to 1. src: SiteKind::is_suitable_loc comparisons + wind_vel *= (1.0 - interp_tree_density).max(0.7); + + // Clamp magnitude, we never want to throw players around way too fast. + let magn = wind_vel.magnitude_squared().max(0.0001); + + // 600 here is compared to squared ~ 25. this limits the magnitude of the wind. + wind_vel *= magn.min(600.) / magn; + + Ok(wind_vel) +} + fn calc_z_limit(char_state_maybe: Option<&CharacterState>, collider: &Collider) -> (f32, f32) { let modifier = if char_state_maybe.map_or(false, |c_s| c_s.is_dodge() || c_s.is_glide()) { 0.5 @@ -150,11 +297,12 @@ pub struct PhysicsRead<'a> { is_ridings: ReadStorage<'a, Is>, is_volume_ridings: ReadStorage<'a, Is>, projectiles: ReadStorage<'a, Projectile>, - char_states: ReadStorage<'a, CharacterState>, - bodies: ReadStorage<'a, Body>, character_states: ReadStorage<'a, CharacterState>, + bodies: ReadStorage<'a, Body>, densities: ReadStorage<'a, Density>, stats: ReadStorage<'a, Stats>, + weather: Option>, + time_of_day: Read<'a, TimeOfDay>, } #[derive(SystemData)] @@ -236,7 +384,7 @@ impl<'a> PhysicsData<'a> { &mut self.write.previous_phys_cache, &self.read.colliders, self.read.scales.maybe(), - self.read.char_states.maybe(), + self.read.character_states.maybe(), ) .join() { @@ -369,7 +517,7 @@ impl<'a> PhysicsData<'a> { // moving whether it should interact into the collider component // or into a separate component. read.projectiles.maybe(), - read.char_states.maybe(), + read.character_states.maybe(), ) .par_join() .map_init( @@ -438,7 +586,7 @@ impl<'a> PhysicsData<'a> { previous_cache, mass, collider, - read.char_states.get(entity), + read.character_states.get(entity), read.is_ridings.get(entity), )) }) @@ -601,6 +749,54 @@ impl<'a> PhysicsData<'a> { ref mut write, } = self; + prof_span!(guard, "Apply Weather"); + if let Some(weather) = &read.weather { + for (_, state, pos, phys) in ( + &read.entities, + &read.character_states, + &write.positions, + &mut write.physics_states, + ) + .join() + { + // Don't simulate for non-gliding, for now + if !state.is_glide() { + continue; + } + + let pos_2d = pos.0.as_().xy(); + let chunk_pos: Vec2 = pos_2d.wpos_to_cpos(); + let Some(current_chunk) = &read.terrain.get_key(chunk_pos) else { + // oopsie + continue; + }; + + let meta = current_chunk.meta(); + + // Skip simulating for entites deeply under the ground + if pos.0.z < meta.alt() - 25.0 { + continue; + } + + // If couldn't simulate wind for some reason, skip + let Ok(wind_vel) = + simulated_wind_vel(pos, weather, &read.terrain, &read.time_of_day) + else { + continue; + }; + + phys.in_fluid = phys.in_fluid.map(|f| match f { + Fluid::Air { elevation, .. } => Fluid::Air { + vel: Vel(wind_vel), + elevation, + }, + fluid => fluid, + }); + } + } + + drop(guard); + prof_span!(guard, "insert PosVelOriDefer"); // NOTE: keep in sync with join below ( diff --git a/voxygen/egui/src/lib.rs b/voxygen/egui/src/lib.rs index 9ac3d7642b..330d1c2647 100644 --- a/voxygen/egui/src/lib.rs +++ b/voxygen/egui/src/lib.rs @@ -96,6 +96,7 @@ pub struct EguiInnerState { selected_entity_cylinder_height: f32, frame_times: Vec, windows: EguiWindows, + debug_vectors_enabled: bool, } #[derive(Clone, Default)] @@ -118,6 +119,7 @@ impl Default for EguiInnerState { selected_entity_cylinder_height: 10.0, frame_times: Vec::new(), windows: EguiWindows::default(), + debug_vectors_enabled: false, } } } @@ -142,6 +144,7 @@ pub enum EguiAction { }, DebugShape(EguiDebugShapeAction), SetExperimentalShader(String, bool), + SetShowDebugVector(bool), } #[derive(Default)] @@ -225,6 +228,7 @@ pub fn maintain_egui_inner( let mut max_entity_distance = egui_state.max_entity_distance; let mut selected_entity_cylinder_height = egui_state.selected_entity_cylinder_height; let mut windows = egui_state.windows.clone(); + let mut debug_vectors_enabled_mut = egui_state.debug_vectors_enabled; // If a debug cylinder was added in the last frame, store it against the // selected entity @@ -261,6 +265,7 @@ pub fn maintain_egui_inner( ui.checkbox(&mut windows.ecs_entities, "ECS Entities"); ui.checkbox(&mut windows.frame_time, "Frame Time"); ui.checkbox(&mut windows.experimental_shaders, "Experimental Shaders"); + ui.checkbox(&mut debug_vectors_enabled_mut, "Show Debug Vectors"); }); }); @@ -510,6 +515,12 @@ pub fn maintain_egui_inner( } } }; + if debug_vectors_enabled_mut != egui_state.debug_vectors_enabled { + egui_actions + .actions + .push(EguiAction::SetShowDebugVector(debug_vectors_enabled_mut)); + egui_state.debug_vectors_enabled = debug_vectors_enabled_mut; + } egui_state.max_entity_distance = max_entity_distance; egui_state.selected_entity_cylinder_height = selected_entity_cylinder_height; @@ -754,8 +765,7 @@ fn selected_entity_window( two_col_row(ui, "On Wall", physics_state.on_wall.map_or("-".to_owned(), |x| format!("{:.1},{:.1},{:.1}", x.x, x.y, x.z ))); two_col_row(ui, "Touching Entities", physics_state.touch_entities.len().to_string()); two_col_row(ui, "In Fluid", match physics_state.in_fluid { - - Some(Fluid::Air { elevation, .. }) => format!("Air (Elevation: {:.1})", elevation), + Some(Fluid::Air { elevation, vel, .. }) => format!("Air (Elevation: {:.1}), vel: ({:.1},{:.1},{:.1}) ({:.1} u/s)", elevation, vel.0.x, vel.0.y, vel.0.z, vel.0.magnitude()), Some(Fluid::Liquid { depth, kind, .. }) => format!("{:?} (Depth: {:.1})", kind, depth), _ => "None".to_owned() }); }); diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index ef0d35c5f5..40a55e958f 100755 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -270,6 +270,7 @@ widget_ids! { velocity, glide_ratio, glide_aoe, + air_vel, orientation, look_direction, loaded_distance, @@ -2735,6 +2736,7 @@ impl Hud { .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .set(self.ids.glide_ratio, ui_widgets); + // Glide Angle of Attack let glide_angle_text = angle_of_attack_text( debug_info.in_fluid, debug_info.velocity, @@ -2746,6 +2748,14 @@ impl Hud { .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .set(self.ids.glide_aoe, ui_widgets); + // Air velocity + let air_vel_text = air_velocity(debug_info.in_fluid); + Text::new(&air_vel_text) + .color(TEXT_COLOR) + .down_from(self.ids.glide_aoe, V_PAD) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(14)) + .set(self.ids.air_vel, ui_widgets); // Player's orientation vector let orientation_text = match debug_info.ori { Some(ori) => { @@ -2759,7 +2769,7 @@ impl Hud { }; Text::new(&orientation_text) .color(TEXT_COLOR) - .down_from(self.ids.glide_aoe, V_PAD) + .down_from(self.ids.air_vel, V_PAD) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .set(self.ids.orientation, ui_widgets); @@ -5387,6 +5397,17 @@ pub fn angle_of_attack_text( } } +fn air_velocity(fluid: Option) -> String { + if let Some(comp::Fluid::Air { vel: air_vel, .. }) = fluid { + format!( + "Air Velocity: ({:.1}, {:.1}, {:.1})", + air_vel.0.x, air_vel.0.y, air_vel.0.z + ) + } else { + "Air Velocity: Not in Air".to_owned() + } +} + /// Converts multiplier to percentage. /// NOTE: floats are not the most precise type. /// diff --git a/voxygen/src/render/pipelines/mod.rs b/voxygen/src/render/pipelines/mod.rs index fc87440c25..da20cd7d0c 100644 --- a/voxygen/src/render/pipelines/mod.rs +++ b/voxygen/src/render/pipelines/mod.rs @@ -20,7 +20,7 @@ pub mod ui; use super::{Consts, Renderer, Texture}; use crate::scene::camera::CameraMode; use bytemuck::{Pod, Zeroable}; -use common::{terrain::BlockKind, util::srgb_to_linear}; +use common::{resources::TimeOfDay, terrain::BlockKind, util::srgb_to_linear}; use std::marker::PhantomData; use vek::*; @@ -144,8 +144,8 @@ impl Globals { 0.0, 0.0, ], - sun_dir: Vec4::from_direction(Self::get_sun_dir(time_of_day)).into_array(), - moon_dir: Vec4::from_direction(Self::get_moon_dir(time_of_day)).into_array(), + sun_dir: Vec4::from_direction(TimeOfDay::new(time_of_day).get_sun_dir()).into_array(), + moon_dir: Vec4::from_direction(TimeOfDay::new(time_of_day).get_moon_dir()).into_array(), tick: [ (tick % TIME_OVERFLOW) as f32, (tick / TIME_OVERFLOW).floor() as f32, @@ -195,23 +195,6 @@ impl Globals { globals_dummy: [0.0; 3], } } - - fn get_angle_rad(time_of_day: f64) -> f32 { - const TIME_FACTOR: f64 = (std::f64::consts::PI * 2.0) / (3600.0 * 24.0); - ((time_of_day * TIME_FACTOR) % (std::f64::consts::PI * 2.0)) as f32 - } - - /// Computes the direction of light from the sun based on the time of day. - pub fn get_sun_dir(time_of_day: f64) -> Vec3 { - let angle_rad = Self::get_angle_rad(time_of_day); - Vec3::new(-angle_rad.sin(), 0.0, angle_rad.cos()) - } - - /// Computes the direction of light from the moon based on the time of day. - pub fn get_moon_dir(time_of_day: f64) -> Vec3 { - let angle_rad = Self::get_angle_rad(time_of_day); - -Vec3::new(-angle_rad.sin(), 0.0, angle_rad.cos() - 0.5).normalized() - } } impl Default for Globals { diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index 34a156c755..9fd14f365f 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -110,6 +110,7 @@ pub enum ParticleMode { PhoenixBeam = 56, PhoenixBuildUpAim = 57, ClayShrapnel = 58, + Airflow = 59, } impl ParticleMode { diff --git a/voxygen/src/scene/debug.rs b/voxygen/src/scene/debug.rs index bfb94015bb..412ef5e48b 100644 --- a/voxygen/src/scene/debug.rs +++ b/voxygen/src/scene/debug.rs @@ -9,7 +9,8 @@ use vek::*; #[derive(Debug, PartialEq)] pub enum DebugShape { - Line([Vec3; 2]), + /// [Start, End], width + Line([Vec3; 2], f32), Cylinder { radius: f32, height: f32, @@ -96,13 +97,13 @@ impl DebugShape { }; match self { - DebugShape::Line([a, b]) => { + DebugShape::Line([a, b], width) => { //let h = Vec3::new(0.0, 1.0, 0.0); //mesh.push_quad(quad(*a, a + h, b + h, *b)); box_along_line( LineSegment3 { start: *a, end: *b }, - 0.1, - 0.1, + *width, + *width, [1.0; 4], &mut mesh, ); diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index c87821d971..6db5454bd3 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -27,6 +27,7 @@ use crate::{ GlobalsBindGroup, Light, Model, PointLightMatrix, PostProcessLocals, RainOcclusionLocals, Renderer, Shadow, ShadowLocals, SkyboxVertex, }, + session::PlayerDebugLines, settings::Settings, window::{AnalogGameInput, Event}, }; @@ -38,9 +39,10 @@ use common::{ tool::ToolKind, }, outcome::Outcome, - resources::{DeltaTime, TimeScale}, + resources::{DeltaTime, TimeOfDay, TimeScale}, terrain::{BlockKind, TerrainChunk, TerrainGrid}, vol::ReadVol, + weather::WeatherGrid, }; use common_base::{prof_span, span}; use common_state::State; @@ -121,6 +123,8 @@ pub struct Scene { pub interpolated_time_of_day: Option, last_lightning: Option<(Vec3, f64)>, local_time: f64, + + pub debug_vectors_enabled: bool, } pub struct SceneData<'a> { @@ -148,11 +152,11 @@ pub struct SceneData<'a> { impl<'a> SceneData<'a> { pub fn get_sun_dir(&self) -> Vec3 { - Globals::get_sun_dir(self.interpolated_time_of_day.unwrap_or(0.0)) + TimeOfDay::new(self.interpolated_time_of_day.unwrap_or(0.0)).get_sun_dir() } pub fn get_moon_dir(&self) -> Vec3 { - Globals::get_moon_dir(self.interpolated_time_of_day.unwrap_or(0.0)) + TimeOfDay::new(self.interpolated_time_of_day.unwrap_or(0.0)).get_moon_dir() } } @@ -360,6 +364,7 @@ impl Scene { interpolated_time_of_day: None, last_lightning: None, local_time: 0.0, + debug_vectors_enabled: false, } } @@ -1541,7 +1546,7 @@ impl Scene { for line in chunk.meta().debug_lines().iter() { let shape_id = self .debug - .add_shape(DebugShape::Line([line.start, line.end])); + .add_shape(DebugShape::Line([line.start, line.end], 0.1)); ret.push(shape_id); self.debug .set_context(shape_id, [0.0; 4], [1.0; 4], [0.0, 0.0, 0.0, 1.0]); @@ -1638,4 +1643,87 @@ impl Scene { keep }); } + + pub fn maintain_debug_vectors(&mut self, client: &Client, lines: &mut PlayerDebugLines) { + lines + .chunk_normal + .take() + .map(|id| self.debug.remove_shape(id)); + lines.fluid_vel.take().map(|id| self.debug.remove_shape(id)); + lines.wind.take().map(|id| self.debug.remove_shape(id)); + lines.vel.take().map(|id| self.debug.remove_shape(id)); + if self.debug_vectors_enabled { + let ecs = client.state().ecs(); + + let vels = &ecs.read_component::(); + let Some(vel) = vels.get(client.entity()) else { + return; + }; + + let phys_states = &ecs.read_component::(); + let Some(phys) = phys_states.get(client.entity()) else { + return; + }; + + let positions = &ecs.read_component::(); + let Some(pos) = positions.get(client.entity()) else { + return; + }; + + let weather = ecs.read_resource::(); + // take id and remove to delete the previous lines. + + const LINE_WIDTH: f32 = 0.05; + // Fluid Velocity + { + let Some(fluid) = phys.in_fluid else { + return; + }; + let shape = DebugShape::Line([pos.0, pos.0 + fluid.flow_vel().0 / 2.], LINE_WIDTH); + let id = self.debug.add_shape(shape); + lines.fluid_vel = Some(id); + self.debug + .set_context(id, [0.0; 4], [0.18, 0.72, 0.87, 0.8], [0.0, 0.0, 0.0, 1.0]); + } + // Chunk Terrain Normal Vector + { + let Some(chunk) = client.current_chunk() else { + return; + }; + let shape = DebugShape::Line( + [ + pos.0, + pos.0 + + chunk + .meta() + .approx_chunk_terrain_normal() + .unwrap_or(Vec3::unit_z()) + * 2.5, + ], + LINE_WIDTH, + ); + let id = self.debug.add_shape(shape); + lines.chunk_normal = Some(id); + self.debug + .set_context(id, [0.0; 4], [0.22, 0.63, 0.1, 0.8], [0.0, 0.0, 0.0, 1.0]); + } + // Wind + { + let wind = weather.get_interpolated(pos.0.xy()).wind_vel(); + let shape = DebugShape::Line([pos.0, pos.0 + wind * 5.0], LINE_WIDTH); + let id = self.debug.add_shape(shape); + lines.wind = Some(id); + self.debug + .set_context(id, [0.0; 4], [0.76, 0.76, 0.76, 0.8], [0.0, 0.0, 0.0, 1.0]); + } + // Player Vel + { + let shape = DebugShape::Line([pos.0, pos.0 + vel.0 / 2.0], LINE_WIDTH); + let id = self.debug.add_shape(shape); + lines.vel = Some(id); + self.debug + .set_context(id, [0.0; 4], [0.98, 0.76, 0.01, 0.8], [0.0, 0.0, 0.0, 1.0]); + } + } + } } diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 631786e043..eb9177b9ad 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -15,7 +15,8 @@ use common::{ item::Reagent, object, shockwave::{self, ShockwaveDodgeable}, - Beam, Body, CharacterActivity, CharacterState, Ori, Pos, Scale, Shockwave, Vel, + Beam, Body, CharacterActivity, CharacterState, Fluid, Ori, PhysicsState, Pos, Scale, + Shockwave, Vel, }, figure::Segment, outcome::Outcome, @@ -916,16 +917,18 @@ impl ParticleMgr { let dt = scene_data.state.get_delta_time(); let mut rng = thread_rng(); - for (entity, interpolated, vel, character_state, body, ori, character_activity) in ( - &ecs.entities(), - &ecs.read_storage::(), - ecs.read_storage::().maybe(), - &ecs.read_storage::(), - &ecs.read_storage::(), - &ecs.read_storage::(), - &ecs.read_storage::(), - ) - .join() + for (entity, interpolated, vel, character_state, body, ori, character_activity, physics) in + ( + &ecs.entities(), + &ecs.read_storage::(), + ecs.read_storage::().maybe(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ) + .join() { match character_state { CharacterState::Boost(_) => { @@ -1317,6 +1320,61 @@ impl ParticleMgr { ); } }, + CharacterState::Glide(_) => { + if let Some(Fluid::Air { + vel: air_vel, + elevation: _, + }) = physics.in_fluid + { + // Empirical observation is that air_vel is somewhere + // between 0.0 and 13.0, but we are extending to be sure + const MAX_AIR_VEL: f32 = 15.0; + const MIN_AIR_VEL: f32 = -2.0; + + let minmax_norm = |val, min, max| (val - min) / (max - min); + + let wind_speed = air_vel.0.magnitude(); + + // Less means more frequent particles + let heartbeat = 200 + - Lerp::lerp( + 50u64, + 150, + minmax_norm(wind_speed, MIN_AIR_VEL, MAX_AIR_VEL), + ); + + let new_count = self.particles.len() + + usize::from( + self.scheduler.heartbeats(Duration::from_millis(heartbeat)), + ); + + // More number, longer particles + let duration = Lerp::lerp( + 0u64, + 1000, + minmax_norm(wind_speed, MIN_AIR_VEL, MAX_AIR_VEL), + ); + let duration = Duration::from_millis(duration); + + self.particles.resize_with(new_count, || { + let start_pos = interpolated.pos + + Vec3::new( + body.max_radius(), + body.max_radius(), + body.height() / 2.0, + ) + .map(|d| d * rng.gen_range(-10.0..10.0)); + + Particle::new_directed( + duration, + time, + ParticleMode::Airflow, + start_pos, + start_pos + air_vel.0, + ) + }); + } + }, _ => {}, } } diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 8b1295e86d..b8d3c01d39 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -89,6 +89,14 @@ enum TickAction { Disconnect, } +#[derive(Default)] +pub struct PlayerDebugLines { + pub chunk_normal: Option, + pub wind: Option, + pub fluid_vel: Option, + pub vel: Option, +} + pub struct SessionState { scene: Scene, pub(crate) client: Rc>, @@ -113,6 +121,7 @@ pub struct SessionState { #[cfg(not(target_os = "macos"))] mumble_link: SharedLink, hitboxes: HashMap, + lines: PlayerDebugLines, tracks: HashMap, Vec>, } @@ -186,6 +195,7 @@ impl SessionState { hitboxes: HashMap::new(), metadata, tracks: HashMap::new(), + lines: Default::default(), } } @@ -237,6 +247,7 @@ impl SessionState { &mut self.hitboxes, &mut self.tracks, ); + self.scene.maintain_debug_vectors(&client, &mut self.lines); // All this camera code is just to determine if it's underwater for the sfx // filter diff --git a/voxygen/src/ui/egui/mod.rs b/voxygen/src/ui/egui/mod.rs index efdd9b18f4..7c44be44b5 100644 --- a/voxygen/src/ui/egui/mod.rs +++ b/voxygen/src/ui/egui/mod.rs @@ -102,6 +102,9 @@ impl EguiState { } } }, + EguiAction::SetShowDebugVector(enabled) => { + scene.debug_vectors_enabled = enabled; + }, }); new_render_mode.map(|rm| SettingsChange::Graphics(Graphics::ChangeRenderMode(Box::new(rm)))) diff --git a/world/src/lib.rs b/world/src/lib.rs index 95a7fb9b3f..429e03bdae 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -36,6 +36,7 @@ pub use block::BlockGen; use civ::WorldCivStage; pub use column::ColumnSample; pub use common::terrain::site::{DungeonKindMeta, SettlementKindMeta}; +use common::terrain::CoordinateConversions; pub use index::{IndexOwned, IndexRef}; use sim::WorldSimStage; @@ -54,8 +55,7 @@ use common::{ resources::TimeOfDay, rtsim::ChunkResource, terrain::{ - Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunk, TerrainChunkMeta, - TerrainChunkSize, TerrainGrid, + Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize, TerrainGrid, }, vol::{ReadVol, RectVolSize, WriteVol}, }; @@ -373,6 +373,7 @@ impl World { sim_chunk.tree_density, sim_chunk.cave.1.alt != 0.0, sim_chunk.river.is_river(), + sim_chunk.river.near_water(), sim_chunk.river.velocity, sim_chunk.temp, sim_chunk.humidity, @@ -391,6 +392,9 @@ impl World { .distance_squared(chunk_center_wpos2d) }) .map(|id| index.sites[*id].kind.convert_to_meta().unwrap_or_default()), + self.sim.approx_chunk_terrain_normal(chunk_pos), + sim_chunk.rockiness, + sim_chunk.cliff_height, ); let mut chunk = TerrainChunk::new(base_z, stone, air, meta); diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index d8bdc52a12..ff2acb27b9 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -1636,6 +1636,26 @@ impl WorldSim { TerrainChunk::water(CONFIG.sea_level as i32) } + pub fn approx_chunk_terrain_normal(&self, chunk_pos: Vec2) -> Option> { + let curr_chunk = self.get(chunk_pos)?; + let downhill_chunk_pos = curr_chunk.downhill?.wpos_to_cpos(); + let downhill_chunk = self.get(downhill_chunk_pos)?; + // special case if chunks are flat + if (curr_chunk.alt - downhill_chunk.alt) == 0. { + return Some(Vec3::unit_z()); + } + let curr = chunk_pos.cpos_to_wpos_center().as_().with_z(curr_chunk.alt); + let down = downhill_chunk_pos + .cpos_to_wpos_center() + .as_() + .with_z(downhill_chunk.alt); + let downwards = curr - down; + let flat = downwards.with_z(down.z); + let mut res = downwards.cross(flat).cross(downwards); + res.normalize(); + Some(res) + } + /// Draw a map of the world based on chunk information. Returns a buffer of /// u32s. pub fn get_map(&self, index: IndexRef, calendar: Option<&Calendar>) -> WorldMapMsg {