diff --git a/common/src/terrain/mod.rs b/common/src/terrain/mod.rs index a4a647512f..79645b4d30 100644 --- a/common/src/terrain/mod.rs +++ b/common/src/terrain/mod.rs @@ -88,6 +88,7 @@ pub struct TerrainChunkMeta { contains_river: bool, river_velocity: Vec3, temp: f32, + humidity: f32, contains_settlement: bool, contains_dungeon: bool, } @@ -102,6 +103,7 @@ impl TerrainChunkMeta { contains_river: bool, river_velocity: Vec3, temp: f32, + humidity: f32, contains_settlement: bool, contains_dungeon: bool, ) -> Self { @@ -114,6 +116,7 @@ impl TerrainChunkMeta { contains_river, river_velocity, temp, + humidity, contains_settlement, contains_dungeon, } @@ -129,6 +132,7 @@ impl TerrainChunkMeta { contains_river: false, river_velocity: Vec3::zero(), temp: 0.0, + humidity: 0.0, contains_settlement: false, contains_dungeon: false, } @@ -153,6 +157,8 @@ impl TerrainChunkMeta { pub fn contains_dungeon(&self) -> bool { self.contains_dungeon } pub fn temp(&self) -> f32 { self.temp } + + pub fn humidity(&self) -> f32 { self.humidity } } // Terrain type aliases diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 4e740f8558..9041035479 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -5,6 +5,7 @@ pub mod lod; pub mod math; pub mod particle; pub mod simple; +pub mod smoke_cycle; pub mod terrain; pub mod trail; diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 96113ccb46..2362561686 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -24,6 +24,7 @@ use common::{ use common_base::span; use hashbrown::HashMap; use rand::prelude::*; +//use rand_chacha::ChaCha8Rng; use specs::{saveload::MarkerAllocator, Join, WorldExt}; use std::{ f32::consts::{PI, TAU}, @@ -1249,9 +1250,35 @@ impl ParticleMgr { } // smoke is more complex as it comes with varying rate and color { + // fn create_smoke( + // position: Vec3, + // temperature: f32, + // humidity: f32, + // time_of_day: f32, + // ) -> FireplaceProperties { + // let mut rng2 = ChaCha8Rng::from_seed(seed_from_pos(pos)); + // let strength_mod = (0.5_f32 - temperature).max(0.0); // -0.5 (desert) to + // 1.5 (ice) let strength = + // rng2.gen_range((5.0 * strength_mod)..(100.0 * strength_mod).max(1.0)) + // as u8; let dryness = (biome_dryness(chunk.meta().biome()) + + // rng2.gen_range(-20..20)) .min(255) + // .max(0) as u8; + // // tracing::trace!(?pos, ?strength, ?dryness); + // FireplaceProperties::new(pos, dryness, strength) + // } + struct FirePlaceProperties { + position: Vec3, + strength: f32, + dry_chance: f32, + } + let range = 8_usize; let rate = 3.0 / 128.0; let lifetime = 40.0; + let time_of_day = scene_data + .state + .get_time_of_day() + .rem_euclid(24.0 * 60.0 * 60.0) as f32; // mode: ParticleMode::CampfireSmoke, for offset in Spiral2d::new().take((range * 2 + 1).pow(2)) { @@ -1259,32 +1286,49 @@ impl ParticleMgr { terrain.get(chunk_pos).map(|chunk_data| { let blocks = &chunk_data.blocks_of_interest.smokers; - let sum = blocks - .iter() - .fold(0u32, |sum, smoker| sum + smoker.strength as u32); - let avg_particles = dt * sum as f32 * rate; + let mut smoke_properties: Vec = Vec::new(); let block_pos = Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32)); + let mut sum = 0.0_f32; + for smoker in blocks.iter() { + let position = block_pos + smoker.position; + let prop = crate::scene::smoke_cycle::smoke_at_time( + position, + smoker.temperature, + smoker.humidity, + time_of_day, + ); + sum += prop.0; + smoke_properties.push(FirePlaceProperties { + position, + strength: prop.0, + dry_chance: 0.5, + }); + } + // let sum = blocks + // .iter() + // .fold(0u32, |sum, smoker| sum + smoker.strength as u32); + let avg_particles = dt * sum as f32 * rate; let particle_count = avg_particles.trunc() as usize + (rng.gen::() < avg_particles.fract()) as usize; - let chosen = - blocks.choose_multiple_weighted(&mut rng, particle_count, |smoker| { - smoker.strength as u32 - }); + let chosen = smoke_properties.choose_multiple_weighted( + &mut rng, + particle_count, + |smoker| smoker.strength, + ); if let Ok(chosen) = chosen { let mut smoke_particles: Vec = chosen .map(|smoker| { Particle::new( Duration::from_secs_f32(lifetime), time, - if rng.gen::() > smoker.dryness { + if rng.gen::() > smoker.dry_chance { ParticleMode::BlackSmoke } else { ParticleMode::CampfireSmoke }, - (block_pos + smoker.position) - .map(|e: i32| e as f32 + rng.gen::()), + smoker.position.map(|e: i32| e as f32 + rng.gen::()), ) }) .collect(); diff --git a/voxygen/src/scene/smoke_cycle.rs b/voxygen/src/scene/smoke_cycle.rs new file mode 100644 index 0000000000..0c883808ca --- /dev/null +++ b/voxygen/src/scene/smoke_cycle.rs @@ -0,0 +1,183 @@ +use rand::prelude::*; +use rand_chacha::ChaCha8Rng; +use vek::*; + +// create pseudorandom from position +fn seed_from_pos(pos: Vec3) -> [u8; 32] { + [ + pos.x as u8, + (pos.x >> 8) as u8, + (pos.x >> 16) as u8, + (pos.x >> 24) as u8, + 0, + 0, + 0, + 0, + pos.y as u8, + (pos.y >> 8) as u8, + (pos.y >> 16) as u8, + (pos.y >> 24) as u8, + 0, + 0, + 0, + 0, + pos.z as u8, + (pos.z >> 8) as u8, + (pos.z >> 16) as u8, + (pos.z >> 24) as u8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] +} + +struct FireplaceTiming { + // all this assumes sunrise at 6am, sunset at 6pm + breakfast: f32, // 5am to 7am + dinner: f32, // 5pm to 7pm + daily_cycle: f32, // 30min to 2hours +} + +const SMOKE_BREAKFAST_STRENGTH: f32 = 96.0; +const SMOKE_BREAKFAST_HALF_DURATION: f32 = 45.0 * 60.0; +const SMOKE_BREAKFAST_START: f32 = 5.0 * 60.0 * 60.0; +const SMOKE_BREAKFAST_RANGE: f32 = 2.0 * 60.0 * 60.0; +const SMOKE_DINNER_STRENGTH: f32 = 128.0; +const SMOKE_DINNER_HALF_DURATION: f32 = 60.0 * 60.0; +const SMOKE_DINNER_START: f32 = 17.0 * 60.0 * 60.0; +const SMOKE_DINNER_RANGE: f32 = 2.0 * 60.0 * 60.0; +const SMOKE_DAILY_CYCLE_MIN: f32 = 30.0 * 60.0; +const SMOKE_DAILY_CYCLE_MAX: f32 = 120.0 * 60.0; +const SMOKE_MAX_TEMPERATURE: f32 = 0.0; // temperature for nominal smoke (0..daily_var) +const SMOKE_MAX_TEMP_VALUE: f32 = 1.0; +const SMOKE_TEMP_MULTIPLIER: f32 = 96.0; +const SMOKE_DAILY_VARIATION: f32 = 32.0; + +struct FireplaceClimate { + daily_strength: f32, // can be negative (offset) + day_start: f32, // seconds since breakfast for daily cycle + day_end: f32, // seconds before dinner on daily cycle +} + +fn create_timing(rng: &mut ChaCha8Rng) -> FireplaceTiming { + let breakfast: f32 = SMOKE_BREAKFAST_START + rng.gen::() * SMOKE_BREAKFAST_RANGE; + let dinner: f32 = SMOKE_DINNER_START + rng.gen::() * SMOKE_DINNER_RANGE; + let daily_cycle: f32 = + SMOKE_DAILY_CYCLE_MIN + rng.gen::() * (SMOKE_DAILY_CYCLE_MAX - SMOKE_DAILY_CYCLE_MIN); + FireplaceTiming { + breakfast, + dinner, + daily_cycle, + } +} + +fn create_climate(temperature: f32, _humidity: f32) -> FireplaceClimate { + // temp -1…1, humidity 0…1 + let daily_strength = + (SMOKE_MAX_TEMPERATURE - temperature).min(SMOKE_MAX_TEMP_VALUE) * SMOKE_TEMP_MULTIPLIER; + // when is breakfast down to daily strength + // daily_strength == + // SMOKE_BREAKFAST_STRENGTH*(1.0-(t-breakfast)/SMOKE_BREAKFAST_HALF_DURATION) + // (t-breakfast) = (1.0 - + // daily_strength/SMOKE_BREAKFAST_STRENGTH)*SMOKE_BREAKFAST_HALF_DURATION + let day_start = (SMOKE_BREAKFAST_STRENGTH - daily_strength.max(0.0)) + * (SMOKE_BREAKFAST_HALF_DURATION / SMOKE_BREAKFAST_STRENGTH); + let day_end = (SMOKE_DINNER_STRENGTH - daily_strength.max(0.0)) + * (SMOKE_DINNER_HALF_DURATION / SMOKE_DINNER_STRENGTH); + FireplaceClimate { + daily_strength, + day_start, + day_end, + } +} + +type Increasing = bool; + +pub fn smoke_at_time( + position: Vec3, + temperature: f32, + humidity: f32, + time_of_day: f32, +) -> (f32, Increasing) { + let mut pseudorandom = ChaCha8Rng::from_seed(seed_from_pos(position)); + let timing = create_timing(&mut pseudorandom); + let climate = create_climate(temperature, humidity); + let after_breakfast = time_of_day - timing.breakfast; + //let after_dinner = time_of_day-timing.dinner; + if after_breakfast < -SMOKE_BREAKFAST_HALF_DURATION { + /* night */ + (0.0, false) + } else if after_breakfast < 0.0 { + /* cooking breakfast */ + ( + (SMOKE_BREAKFAST_HALF_DURATION + after_breakfast) + * (SMOKE_BREAKFAST_STRENGTH / SMOKE_BREAKFAST_HALF_DURATION), + true, + ) + } else if time_of_day < climate.day_start { + /* cooling */ + ( + (SMOKE_BREAKFAST_HALF_DURATION - after_breakfast) + * (SMOKE_BREAKFAST_STRENGTH / SMOKE_BREAKFAST_HALF_DURATION), + false, + ) + } else if time_of_day < climate.day_end { + /* day cycle */ + let day_phase = ((time_of_day - climate.day_start) / timing.daily_cycle).fract(); + if day_phase < 0.5 { + ( + (climate.daily_strength + day_phase * (2.0 * SMOKE_DAILY_VARIATION)).max(0.0), + true, + ) + } else { + ( + (climate.daily_strength + (1.0 - day_phase) * (2.0 * SMOKE_DAILY_VARIATION)) + .max(0.0), + false, + ) + } + } else if time_of_day < timing.dinner { + /* cooking dinner */ + ( + (SMOKE_BREAKFAST_HALF_DURATION + time_of_day - timing.dinner) + * (SMOKE_BREAKFAST_STRENGTH / SMOKE_BREAKFAST_HALF_DURATION), + true, + ) + } else { + /* cooling + night */ + ( + (SMOKE_BREAKFAST_HALF_DURATION - time_of_day + timing.dinner).max(0.0) + * (SMOKE_BREAKFAST_STRENGTH / SMOKE_BREAKFAST_HALF_DURATION), + false, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_smoke() { + let position = Vec3::new(22_i32, 11, 33); + let temperature = -0.5; + let humidity = 0.5; + for i in (0..24 * 4) { + let time_of_day = 15.0 * 60.0 * (i as f32); + println!( + "{:.2} {}", + time_of_day / (60.0 * 60.0), + smoke_at_time(position, temperature, humidity, time_of_day) + ); + } + } +} diff --git a/voxygen/src/scene/terrain/watcher.rs b/voxygen/src/scene/terrain/watcher.rs index 2166c77e34..2374b71e31 100644 --- a/voxygen/src/scene/terrain/watcher.rs +++ b/voxygen/src/scene/terrain/watcher.rs @@ -1,5 +1,5 @@ use crate::hud::CraftingTab; -use common::terrain::{BiomeKind, BlockKind, SpriteKind, TerrainChunk}; +use common::terrain::{BlockKind, SpriteKind, TerrainChunk}; use common_base::span; use rand::prelude::*; use rand_chacha::ChaCha8Rng; @@ -12,18 +12,18 @@ pub enum Interaction { Mine, } -pub struct SmokeProperties { +pub struct FireplaceProperties { pub position: Vec3, - pub dryness: u8, // 0 = black smoke, 255 = white - pub strength: u8, // 0 = thin, 128 = normal, 255 = very strong + pub humidity: f32, + pub temperature: f32, } -impl SmokeProperties { - fn new(position: Vec3, dryness: u8, strength: u8) -> Self { +impl FireplaceProperties { + fn new(position: Vec3, humidity: f32, temperature: f32) -> Self { Self { position, - dryness, - strength, + humidity, + temperature, } } } @@ -36,7 +36,7 @@ pub struct BlocksOfInterest { pub slow_river: Vec>, pub fast_river: Vec>, pub fires: Vec>, - pub smokers: Vec, + pub smokers: Vec, pub beehives: Vec>, pub reeds: Vec>, pub fireflies: Vec>, @@ -54,60 +54,6 @@ pub struct BlocksOfInterest { pub lights: Vec<(Vec3, u8)>, } -fn biome_dryness(biome: BiomeKind) -> i32 { - match biome { - BiomeKind::Void => 0, - BiomeKind::Lake => 0, - BiomeKind::Ocean => 0, - BiomeKind::Swamp => 10, - BiomeKind::Jungle => 60, - BiomeKind::Snowland => 60, - BiomeKind::Desert => 100, // dry but dung - BiomeKind::Mountain => 160, - BiomeKind::Forest => 180, - BiomeKind::Taiga => 180, - BiomeKind::Grassland => 200, - BiomeKind::Savannah => 240, - } -} - -fn seed_from_pos(pos: Vec3) -> [u8; 32] { - [ - pos.x as u8, - (pos.x >> 8) as u8, - (pos.x >> 16) as u8, - (pos.x >> 24) as u8, - 0, - 0, - 0, - 0, - pos.y as u8, - (pos.y >> 8) as u8, - (pos.y >> 16) as u8, - (pos.y >> 24) as u8, - 0, - 0, - 0, - 0, - pos.z as u8, - (pos.z >> 8) as u8, - (pos.z >> 16) as u8, - (pos.z >> 24) as u8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ] -} - impl BlocksOfInterest { pub fn from_chunk(chunk: &TerrainChunk) -> Self { span!(_guard, "from_chunk", "BlocksOfInterest::from_chunk"); @@ -158,18 +104,12 @@ impl BlocksOfInterest { BlockKind::Snow | BlockKind::Ice if rng.gen_range(0..16) == 0 => snow.push(pos), _ => match block.get_sprite() { Some(SpriteKind::Ember) => { - let mut rng2 = ChaCha8Rng::from_seed(seed_from_pos(pos)); - let strength_mod = (0.5_f32 - chunk.meta().temp()).max(0.0); // -0.5 (desert) to 1.5 (ice) - let strength = rng2 - .gen_range((5.0 * strength_mod)..(100.0 * strength_mod).max(1.0)) - as u8; - let dryness = (biome_dryness(chunk.meta().biome()) - + rng2.gen_range(-20..20)) - .min(255) - .max(0) as u8; - // tracing::trace!(?pos, ?strength, ?dryness); fires.push(pos); - smokers.push(SmokeProperties::new(pos, dryness, strength)); + smokers.push(FireplaceProperties::new( + pos, + chunk.meta().humidity(), + chunk.meta().temp(), + )); }, // Offset positions to account for block height. // TODO: Is this a good idea? @@ -197,7 +137,7 @@ impl BlocksOfInterest { interactables.push((pos, Interaction::Craft(CraftingTab::All))) }, Some(SpriteKind::SmokeDummy) => { - smokers.push(SmokeProperties::new(pos, 255, 128)); + smokers.push(FireplaceProperties::new(pos, 0.0, -1.0)); }, Some(SpriteKind::Forge) => interactables .push((pos, Interaction::Craft(CraftingTab::ProcessedMaterial))), diff --git a/world/src/lib.rs b/world/src/lib.rs index 8a02605cb8..86f9f21e4f 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -294,6 +294,7 @@ impl World { sim_chunk.river.is_river(), sim_chunk.river.velocity, sim_chunk.temp, + sim_chunk.humidity, sim_chunk .sites .iter()