mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'christof/smoke_variations' into 'master'
Smoke variations See merge request veloren/veloren!3348
This commit is contained in:
commit
ec2b78c533
@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Added loot ownership for NPC drops
|
||||
- Bamboo collectibles now spawn near rivers
|
||||
- Chest sprites can longer be exploded
|
||||
- Smoke varies by temperature, humidity, time of day and house
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -75,6 +75,7 @@ const int TORNADO = 33;
|
||||
const int DEATH = 34;
|
||||
const int ENERGY_BUFFING = 35;
|
||||
const int WEB_STRAND = 36;
|
||||
const int BLACK_SMOKE = 37;
|
||||
|
||||
// meters per second squared (acceleration)
|
||||
const float earth_gravity = 9.807;
|
||||
@ -191,7 +192,18 @@ void main() {
|
||||
vec3(rand2 * 0.02, rand3 * 0.02, 1.0 + rand4 * 0.1)
|
||||
),
|
||||
vec3(linear_scale(0.5)),
|
||||
vec4(vec3(0.8, 0.8, 1) * 0.5, start_end(1.0, 0.0)),
|
||||
vec4(vec3(0.8, 0.8, 1) * 0.125 * (3.8 + rand0), start_end(1.0, 0.0)),
|
||||
spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3 + lifetime * 0.5)
|
||||
);
|
||||
break;
|
||||
case BLACK_SMOKE:
|
||||
attr = Attr(
|
||||
linear_motion(
|
||||
vec3(0),
|
||||
vec3(rand2 * 0.02, rand3 * 0.02, 1.0 + rand4 * 0.1)
|
||||
),
|
||||
vec3(linear_scale(0.5)),
|
||||
vec4(vec3(0.8, 0.8, 1) * 0.125 * (1.8 + rand0), start_end(1.0, 0.0)),
|
||||
spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3 + lifetime * 0.5)
|
||||
);
|
||||
break;
|
||||
|
@ -88,6 +88,7 @@ pub struct TerrainChunkMeta {
|
||||
contains_river: bool,
|
||||
river_velocity: Vec3<f32>,
|
||||
temp: f32,
|
||||
humidity: f32,
|
||||
contains_settlement: bool,
|
||||
contains_dungeon: bool,
|
||||
}
|
||||
@ -102,6 +103,7 @@ impl TerrainChunkMeta {
|
||||
contains_river: bool,
|
||||
river_velocity: Vec3<f32>,
|
||||
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
|
||||
|
@ -87,6 +87,7 @@ pub enum ParticleMode {
|
||||
Death = 34,
|
||||
EnergyBuffing = 35,
|
||||
WebStrand = 36,
|
||||
BlackSmoke = 37,
|
||||
}
|
||||
|
||||
impl ParticleMode {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -5,6 +5,7 @@ use crate::{
|
||||
pipelines::particle::ParticleMode, Instances, Light, Model, ParticleDrawer,
|
||||
ParticleInstance, ParticleVertex, Renderer,
|
||||
},
|
||||
scene::terrain::FireplaceType,
|
||||
};
|
||||
use common::{
|
||||
assets::{AssetExt, DotVoxAsset},
|
||||
@ -1181,14 +1182,6 @@ impl ParticleMgr {
|
||||
mode: ParticleMode::FireBowl,
|
||||
cond: |_| true,
|
||||
},
|
||||
BlockParticles {
|
||||
blocks: |boi| &boi.smokers,
|
||||
range: 8,
|
||||
rate: 3.0,
|
||||
lifetime: 40.0,
|
||||
mode: ParticleMode::CampfireSmoke,
|
||||
cond: |_| true,
|
||||
},
|
||||
BlockParticles {
|
||||
blocks: |boi| &boi.fireflies,
|
||||
range: 6,
|
||||
@ -1255,6 +1248,88 @@ impl ParticleMgr {
|
||||
});
|
||||
}
|
||||
}
|
||||
// smoke is more complex as it comes with varying rate and color
|
||||
{
|
||||
struct SmokeProperties {
|
||||
position: Vec3<i32>,
|
||||
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;
|
||||
|
||||
for offset in Spiral2d::new().take((range * 2 + 1).pow(2)) {
|
||||
let chunk_pos = player_chunk + offset;
|
||||
|
||||
terrain.get(chunk_pos).map(|chunk_data| {
|
||||
let blocks = &chunk_data.blocks_of_interest.smokers;
|
||||
let mut smoke_properties: Vec<SmokeProperties> = 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 (strength, dry_chance) = {
|
||||
match smoker.kind {
|
||||
FireplaceType::House => {
|
||||
let prop = crate::scene::smoke_cycle::smoke_at_time(
|
||||
position,
|
||||
chunk_data.blocks_of_interest.temperature,
|
||||
time_of_day,
|
||||
);
|
||||
(
|
||||
prop.0,
|
||||
if prop.1 {
|
||||
// fire started, dark smoke
|
||||
0.8 - chunk_data.blocks_of_interest.humidity
|
||||
} else {
|
||||
// fire continues, light smoke
|
||||
1.2 - chunk_data.blocks_of_interest.humidity
|
||||
},
|
||||
)
|
||||
},
|
||||
FireplaceType::Workshop => (128.0, 1.0),
|
||||
}
|
||||
};
|
||||
sum += strength;
|
||||
smoke_properties.push(SmokeProperties {
|
||||
position,
|
||||
strength,
|
||||
dry_chance,
|
||||
});
|
||||
}
|
||||
let avg_particles = dt * sum as f32 * rate;
|
||||
|
||||
let particle_count = avg_particles.trunc() as usize
|
||||
+ (rng.gen::<f32>() < avg_particles.fract()) as usize;
|
||||
let chosen = smoke_properties.choose_multiple_weighted(
|
||||
&mut rng,
|
||||
particle_count,
|
||||
|smoker| smoker.strength,
|
||||
);
|
||||
if let Ok(chosen) = chosen {
|
||||
self.particles.extend(chosen.map(|smoker| {
|
||||
Particle::new(
|
||||
Duration::from_secs_f32(lifetime),
|
||||
time,
|
||||
if rng.gen::<f32>() > smoker.dry_chance {
|
||||
ParticleMode::BlackSmoke
|
||||
} else {
|
||||
ParticleMode::CampfireSmoke
|
||||
},
|
||||
smoker.position.map(|e: i32| e as f32 + rng.gen::<f32>()),
|
||||
)
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn maintain_shockwave_particles(&mut self, scene_data: &SceneData) {
|
||||
|
203
voxygen/src/scene/smoke_cycle.rs
Normal file
203
voxygen/src/scene/smoke_cycle.rs
Normal file
@ -0,0 +1,203 @@
|
||||
use rand::prelude::*;
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use vek::*;
|
||||
|
||||
// create pseudorandom from position
|
||||
fn seed_from_pos(pos: Vec3<i32>) -> [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,
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
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;
|
||||
|
||||
#[derive(Debug)]
|
||||
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::<f32>() * SMOKE_BREAKFAST_RANGE;
|
||||
let dinner: f32 = SMOKE_DINNER_START + rng.gen::<f32>() * SMOKE_DINNER_RANGE;
|
||||
let daily_cycle: f32 =
|
||||
SMOKE_DAILY_CYCLE_MIN + rng.gen::<f32>() * (SMOKE_DAILY_CYCLE_MAX - SMOKE_DAILY_CYCLE_MIN);
|
||||
FireplaceTiming {
|
||||
breakfast,
|
||||
dinner,
|
||||
daily_cycle,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_climate(temperature: f32) -> FireplaceClimate {
|
||||
// temp -1…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,
|
||||
}
|
||||
}
|
||||
|
||||
pub type Increasing = bool;
|
||||
|
||||
pub fn smoke_at_time(position: Vec3<i32>, temperature: 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);
|
||||
let after_breakfast = time_of_day - timing.breakfast;
|
||||
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 after_breakfast < climate.day_start {
|
||||
/* cooling */
|
||||
(
|
||||
(SMOKE_BREAKFAST_HALF_DURATION - after_breakfast)
|
||||
* (SMOKE_BREAKFAST_STRENGTH / SMOKE_BREAKFAST_HALF_DURATION),
|
||||
false,
|
||||
)
|
||||
} else if time_of_day < timing.dinner - climate.day_end {
|
||||
/* day cycle */
|
||||
let day_phase = ((after_breakfast - 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_DINNER_HALF_DURATION + time_of_day - timing.dinner)
|
||||
* (SMOKE_DINNER_STRENGTH / SMOKE_DINNER_HALF_DURATION),
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
/* cooling + night */
|
||||
(
|
||||
(SMOKE_DINNER_HALF_DURATION - time_of_day + timing.dinner).max(0.0)
|
||||
* (SMOKE_DINNER_STRENGTH / SMOKE_DINNER_HALF_DURATION),
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_conditions(position: Vec3<i32>, temperature: f32) {
|
||||
print!("{} T{:.1} ", position, temperature);
|
||||
let mut pseudorandom = ChaCha8Rng::from_seed(seed_from_pos(position));
|
||||
if true {
|
||||
let timing = create_timing(&mut pseudorandom);
|
||||
let climate = create_climate(temperature);
|
||||
print!(
|
||||
"B{:.1}+{:.1} D{:.1}-{:.1} C{:.0} S{:.0} ",
|
||||
timing.breakfast / 3600.0,
|
||||
climate.day_start / 3600.0,
|
||||
timing.dinner / 3600.0,
|
||||
climate.day_end / 3600.0,
|
||||
timing.daily_cycle / 60.0,
|
||||
climate.daily_strength
|
||||
);
|
||||
}
|
||||
for i in 0..24 {
|
||||
print!(" {}:", i);
|
||||
for j in 0..6 {
|
||||
let time_of_day = 60.0 * 60.0 * (i as f32) + 60.0 * 10.0 * (j as f32);
|
||||
let res = smoke_at_time(position, temperature, time_of_day);
|
||||
print!("{:.0}{} ", res.0, if res.1 { "^" } else { "" },);
|
||||
assert!(res.0 >= 0.0);
|
||||
assert!(res.0 <= SMOKE_DINNER_STRENGTH);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smoke() {
|
||||
test_conditions(Vec3::new(25_i32, 11, 33), -1.0);
|
||||
test_conditions(Vec3::new(22_i32, 11, 33), -0.5);
|
||||
test_conditions(Vec3::new(27_i32, 11, 33), 0.0);
|
||||
test_conditions(Vec3::new(24_i32, 11, 33), 0.5);
|
||||
test_conditions(Vec3::new(26_i32, 11, 33), 1.0);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
mod watcher;
|
||||
|
||||
pub use self::watcher::{BlocksOfInterest, Interaction};
|
||||
pub use self::watcher::{BlocksOfInterest, FireplaceType, Interaction};
|
||||
|
||||
use crate::{
|
||||
mesh::{
|
||||
|
@ -12,6 +12,20 @@ pub enum Interaction {
|
||||
Mine,
|
||||
}
|
||||
|
||||
pub enum FireplaceType {
|
||||
House,
|
||||
Workshop, // this also includes witch hut
|
||||
}
|
||||
|
||||
pub struct SmokerProperties {
|
||||
pub position: Vec3<i32>,
|
||||
pub kind: FireplaceType,
|
||||
}
|
||||
|
||||
impl SmokerProperties {
|
||||
fn new(position: Vec3<i32>, kind: FireplaceType) -> Self { Self { position, kind } }
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BlocksOfInterest {
|
||||
pub leaves: Vec<Vec3<i32>>,
|
||||
@ -20,7 +34,7 @@ pub struct BlocksOfInterest {
|
||||
pub slow_river: Vec<Vec3<i32>>,
|
||||
pub fast_river: Vec<Vec3<i32>>,
|
||||
pub fires: Vec<Vec3<i32>>,
|
||||
pub smokers: Vec<Vec3<i32>>,
|
||||
pub smokers: Vec<SmokerProperties>,
|
||||
pub beehives: Vec<Vec3<i32>>,
|
||||
pub reeds: Vec<Vec3<i32>>,
|
||||
pub fireflies: Vec<Vec3<i32>>,
|
||||
@ -36,6 +50,9 @@ pub struct BlocksOfInterest {
|
||||
// area for optimization
|
||||
pub interactables: Vec<(Vec3<i32>, Interaction)>,
|
||||
pub lights: Vec<(Vec3<i32>, u8)>,
|
||||
// needed for biome specific smoke variations
|
||||
pub temperature: f32,
|
||||
pub humidity: f32,
|
||||
}
|
||||
|
||||
impl BlocksOfInterest {
|
||||
@ -89,7 +106,7 @@ impl BlocksOfInterest {
|
||||
_ => match block.get_sprite() {
|
||||
Some(SpriteKind::Ember) => {
|
||||
fires.push(pos);
|
||||
smokers.push(pos);
|
||||
smokers.push(SmokerProperties::new(pos, FireplaceType::House));
|
||||
},
|
||||
// Offset positions to account for block height.
|
||||
// TODO: Is this a good idea?
|
||||
@ -117,7 +134,7 @@ impl BlocksOfInterest {
|
||||
interactables.push((pos, Interaction::Craft(CraftingTab::All)))
|
||||
},
|
||||
Some(SpriteKind::SmokeDummy) => {
|
||||
smokers.push(pos);
|
||||
smokers.push(SmokerProperties::new(pos, FireplaceType::Workshop));
|
||||
},
|
||||
Some(SpriteKind::Forge) => interactables
|
||||
.push((pos, Interaction::Craft(CraftingTab::ProcessedMaterial))),
|
||||
@ -175,6 +192,8 @@ impl BlocksOfInterest {
|
||||
frogs,
|
||||
interactables,
|
||||
lights,
|
||||
temperature: chunk.meta().temp(),
|
||||
humidity: chunk.meta().humidity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user