Merge branch 'laundmo/glide_weather_wind' into 'master'

Add Lift to gliders (thermals, ridge lift)

See merge request veloren/veloren!3977
This commit is contained in:
Illia Denysenko 2024-02-08 16:34:46 +00:00
commit 2a9e03caf2
17 changed files with 597 additions and 58 deletions

View File

@ -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"

View File

@ -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);

View File

@ -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<f32> {
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<f32> {
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) }

View File

@ -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,
}

View File

@ -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<f32>,
approx_chunk_terrain_normal: Option<Vec3<f32>>,
rockiness: f32,
cliff_height: f32,
temp: f32,
humidity: f32,
site: Option<SiteKindMeta>,
@ -158,10 +164,14 @@ impl TerrainChunkMeta {
tree_density: f32,
contains_cave: bool,
contains_river: bool,
near_water: bool,
river_velocity: Vec3<f32>,
temp: f32,
humidity: f32,
site: Option<SiteKindMeta>,
approx_chunk_terrain_normal: Option<Vec3<f32>>,
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<f32> { self.river_velocity }
pub fn site(&self) -> Option<SiteKindMeta> { 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<i32>, sprite_cfg: SpriteCfg) {
self.sprite_cfgs.insert(rpos, sprite_cfg);
}
pub fn approx_chunk_terrain_normal(&self) -> Option<Vec3<f32>> {
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<T, F>(&self, pos: Vec2<i32>, mut f: F) -> Option<T>
where
T: Copy + Default + Add<Output = T> + Mul<f32, Output = T>,
F: FnMut(&TerrainChunk) -> T,
{
let pos = pos.as_::<f64>().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 {

View File

@ -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<Vec3<f32>, ()> {
prof_span!(guard, "Apply Weather INIT");
let pos_2d = pos.0.as_().xy();
let chunk_pos: Vec2<i32> = 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::<Vec<_>>();
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<Rider>>,
is_volume_ridings: ReadStorage<'a, Is<VolumeRider>>,
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<Read<'a, WeatherGrid>>,
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<i32> = 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
(

View File

@ -96,6 +96,7 @@ pub struct EguiInnerState {
selected_entity_cylinder_height: f32,
frame_times: Vec<f32>,
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() });
});

View File

@ -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<comp::Fluid>) -> 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.
///

View File

@ -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<f32> {
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<f32> {
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 {

View File

@ -110,6 +110,7 @@ pub enum ParticleMode {
PhoenixBeam = 56,
PhoenixBuildUpAim = 57,
ClayShrapnel = 58,
Airflow = 59,
}
impl ParticleMode {

View File

@ -9,7 +9,8 @@ use vek::*;
#[derive(Debug, PartialEq)]
pub enum DebugShape {
Line([Vec3<f32>; 2]),
/// [Start, End], width
Line([Vec3<f32>; 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,
);

View File

@ -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<f64>,
last_lightning: Option<(Vec3<f32>, 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<f32> {
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<f32> {
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::<comp::Vel>();
let Some(vel) = vels.get(client.entity()) else {
return;
};
let phys_states = &ecs.read_component::<comp::PhysicsState>();
let Some(phys) = phys_states.get(client.entity()) else {
return;
};
let positions = &ecs.read_component::<comp::Pos>();
let Some(pos) = positions.get(client.entity()) else {
return;
};
let weather = ecs.read_resource::<WeatherGrid>();
// 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]);
}
}
}
}

View File

@ -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::<Interpolated>(),
ecs.read_storage::<Vel>().maybe(),
&ecs.read_storage::<CharacterState>(),
&ecs.read_storage::<Body>(),
&ecs.read_storage::<Ori>(),
&ecs.read_storage::<CharacterActivity>(),
)
.join()
for (entity, interpolated, vel, character_state, body, ori, character_activity, physics) in
(
&ecs.entities(),
&ecs.read_storage::<Interpolated>(),
ecs.read_storage::<Vel>().maybe(),
&ecs.read_storage::<CharacterState>(),
&ecs.read_storage::<Body>(),
&ecs.read_storage::<Ori>(),
&ecs.read_storage::<CharacterActivity>(),
&ecs.read_storage::<PhysicsState>(),
)
.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,
)
});
}
},
_ => {},
}
}

View File

@ -89,6 +89,14 @@ enum TickAction {
Disconnect,
}
#[derive(Default)]
pub struct PlayerDebugLines {
pub chunk_normal: Option<DebugShapeId>,
pub wind: Option<DebugShapeId>,
pub fluid_vel: Option<DebugShapeId>,
pub vel: Option<DebugShapeId>,
}
pub struct SessionState {
scene: Scene,
pub(crate) client: Rc<RefCell<Client>>,
@ -113,6 +121,7 @@ pub struct SessionState {
#[cfg(not(target_os = "macos"))]
mumble_link: SharedLink,
hitboxes: HashMap<specs::Entity, DebugShapeId>,
lines: PlayerDebugLines,
tracks: HashMap<Vec2<i32>, Vec<DebugShapeId>>,
}
@ -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

View File

@ -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))))

View File

@ -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);

View File

@ -1636,6 +1636,26 @@ impl WorldSim {
TerrainChunk::water(CONFIG.sea_level as i32)
}
pub fn approx_chunk_terrain_normal(&self, chunk_pos: Vec2<i32>) -> Option<Vec3<f32>> {
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 {