diff --git a/CHANGELOG.md b/CHANGELOG.md index 55245f8670..ee926c7103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New level of detail feature, letting you see all the world's terrain at any view distance. - Point and directional lights now cast realistic shadows, using shadow mapping. +- Added leaf and chimney particles ### Changed - Fixed a bug where leaving the Settings menu by pressing "N" in single player kept the game paused diff --git a/assets/voxygen/shaders/particle-frag.glsl b/assets/voxygen/shaders/particle-frag.glsl index 4dfbd6dbca..12a4ad2c3b 100644 --- a/assets/voxygen/shaders/particle-frag.glsl +++ b/assets/voxygen/shaders/particle-frag.glsl @@ -18,7 +18,7 @@ in vec3 f_pos; flat in vec3 f_norm; -in vec3 f_col; +in vec4 f_col; out vec4 tgt_color; @@ -50,7 +50,7 @@ void main() { DirectionalLight sun_info = get_sun_info(sun_dir, point_shadow * sun_shade_frac, f_pos); DirectionalLight moon_info = get_moon_info(moon_dir, point_shadow * moon_shade_frac); - vec3 surf_color = f_col; + vec3 surf_color = f_col.rgb; float alpha = 1.0; const float n2 = 1.5; const float R_s2s0 = pow((1.0 - n2) / (1.0 + n2), 2); @@ -82,5 +82,5 @@ void main() { vec3 color = surf_color; #endif - tgt_color = vec4(color, 0.3); + tgt_color = vec4(color, f_col.a); } diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index 082fc4e453..7170fbc6f8 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -21,12 +21,13 @@ in vec3 v_pos; in uint v_norm_ao; in vec3 inst_pos; in float inst_time; +in float inst_lifespan; in float inst_entropy; in int inst_mode; out vec3 f_pos; flat out vec3 f_norm; -out vec3 f_col; +out vec4 f_col; out float f_ao; out float f_light; @@ -43,6 +44,7 @@ const int FIREWORK_GREEN = 5; const int FIREWORK_PURPLE = 6; const int FIREWORK_RED = 7; const int FIREWORK_YELLOW = 8; +const int LEAF = 9; // meters per second squared (acceleration) const float earth_gravity = 9.807; @@ -50,7 +52,8 @@ const float earth_gravity = 9.807; struct Attr { vec3 offs; float scale; - vec3 col; + vec4 col; + mat4 rot; }; float lifetime = tick.x - inst_time; @@ -71,6 +74,23 @@ float linear_scale(float factor) { return lifetime * factor; } +float start_end(float from, float to) { + return mix(from, to, lifetime / inst_lifespan); +} + +mat4 spin_in_axis(vec3 axis, float angle) +{ + axis = normalize(axis); + float s = sin(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); +} + void main() { float rand0 = hash(vec4(inst_entropy + 0)); float rand1 = hash(vec4(inst_entropy + 1)); @@ -80,6 +100,8 @@ void main() { float rand5 = hash(vec4(inst_entropy + 5)); float rand6 = hash(vec4(inst_entropy + 6)); float rand7 = hash(vec4(inst_entropy + 7)); + float rand8 = hash(vec4(inst_entropy + 8)); + float rand9 = hash(vec4(inst_entropy + 9)); Attr attr; @@ -87,10 +109,11 @@ void main() { attr = Attr( linear_motion( vec3(0.0, 0.0, 0.0), - vec3(rand2 * 0.1, rand3 * 0.1, 1.0 + rand4 * 0.1)// + vec3(sin(lifetime), sin(lifetime + 1.5), sin(lifetime * 4) * 0.25) + vec3(rand2 * 0.02, rand3 * 0.02, 1.0 + rand4 * 0.1) ), linear_scale(0.5), - vec3(1) + vec4(1, 1, 1, start_end(1.0, 0.0)), + spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3 + lifetime * 0.5) ); } else if (inst_mode == FIRE) { attr = Attr( @@ -99,7 +122,8 @@ void main() { vec3(rand2 * 0.1, rand3 * 0.1, 2.0 + rand4 * 1.0) ), 1.0, - vec3(2, rand5 + 2, 0) + vec4(2, 0.8 + rand5 * 0.3, 0, 1), + spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3) ); } else if (inst_mode == GUN_POWDER_SPARK) { attr = Attr( @@ -108,7 +132,8 @@ void main() { vec3(rand4, rand5, rand6) * 2.0 + grav_vel(earth_gravity) ), 1.0, - vec3(3.5, 3 + rand7, 0) + vec4(3.5, 3 + rand7, 0, 1), + spin_in_axis(vec3(1,0,0),0) ); } else if (inst_mode == SHRAPNEL) { attr = Attr( @@ -117,7 +142,8 @@ void main() { vec3(rand4, rand5, rand6) * 40.0 + grav_vel(earth_gravity) ), 3.0 + rand0, - vec3(0.6 + rand7 * 0.4) + vec4(vec3(0.6 + rand7 * 0.4), 1), + spin_in_axis(vec3(1,0,0),0) ); } else if (inst_mode == FIREWORK_BLUE) { attr = Attr( @@ -126,7 +152,8 @@ void main() { vec3(rand4, rand5, rand6) * 40.0 + grav_vel(earth_gravity) ), 3.0 + rand0, - vec3(0.6 + rand7 * 0.4) + vec4(vec3(0.6 + rand7 * 0.4), 0.3), + spin_in_axis(vec3(1,0,0),0) ); } else if (inst_mode == FIREWORK_GREEN) { attr = Attr( @@ -135,7 +162,8 @@ void main() { vec3(rand4, rand5, rand6) * 40.0 + grav_vel(earth_gravity) ), 3.0 + rand0, - vec3(0.6 + rand7 * 0.4) + vec4(vec3(0.6 + rand7 * 0.4), 0.3), + spin_in_axis(vec3(1,0,0),0) ); } else if (inst_mode == FIREWORK_PURPLE) { attr = Attr( @@ -144,7 +172,8 @@ void main() { vec3(rand4, rand5, rand6) * 40.0 + grav_vel(earth_gravity) ), 3.0 + rand0, - vec3(0.6 + rand7 * 0.4) + vec4(vec3(0.6 + rand7 * 0.4), 0.3), + spin_in_axis(vec3(1,0,0),0) ); } else if (inst_mode == FIREWORK_RED) { attr = Attr( @@ -153,7 +182,8 @@ void main() { vec3(rand4, rand5, rand6) * 40.0 + grav_vel(earth_gravity) ), 3.0 + rand0, - vec3(0.6 + rand7 * 0.4) + vec4(vec3(0.6 + rand7 * 0.4), 0.3), + spin_in_axis(vec3(1,0,0),0) ); } else if (inst_mode == FIREWORK_YELLOW) { attr = Attr( @@ -162,7 +192,18 @@ void main() { vec3(rand4, rand5, rand6) * 40.0 + grav_vel(earth_gravity) ), 3.0 + rand0, - vec3(0.6 + rand7 * 0.4) + vec4(vec3(0.6 + rand7 * 0.4), 0.3), + spin_in_axis(vec3(1,0,0),0) + ); + } else if (inst_mode == LEAF) { + attr = Attr( + linear_motion( + vec3(0), + vec3(0, 0, -2) + ) + vec3(sin(lifetime), sin(lifetime + 0.7), sin(lifetime * 0.5)) * 2.0, + 4, + vec4(vec3(0.2 + rand7 * 0.2, 0.2 + (0.5 + rand6 * 0.5) * 0.6, 0), 1), + spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3 + lifetime * 5) ); } else { attr = Attr( @@ -171,22 +212,22 @@ void main() { vec3(rand2 * 0.1, rand3 * 0.1, 1.0 + rand4 * 0.5) ), exp_scale(-0.2), - vec3(1) + vec4(1), + spin_in_axis(vec3(1,0,0),0) ); } - f_pos = (inst_pos - focus_off.xyz) + (v_pos * attr.scale * SCALE + attr.offs); + f_pos = (inst_pos - focus_off.xyz) + (v_pos * attr.scale * SCALE * mat3(attr.rot) + attr.offs); // First 3 normals are negative, next 3 are positive - vec3 normals[6] = vec3[](vec3(-1,0,0), vec3(1,0,0), vec3(0,-1,0), vec3(0,1,0), vec3(0,0,-1), vec3(0,0,1)); - f_norm = + // 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)); + f_norm = // inst_pos * - normals[(v_norm_ao >> 0) & 0x7u]; + ((normals[(v_norm_ao >> 0) & 0x7u]) * attr.rot).xyz; //vec3 col = vec3((uvec3(v_col) >> uvec3(0, 8, 16)) & uvec3(0xFFu)) / 255.0; - f_col = - //srgb_to_linear(col) * - srgb_to_linear(attr.col); + f_col = vec4(srgb_to_linear(attr.col.rgb), attr.col.a); gl_Position = all_mat * diff --git a/voxygen/src/render/mesh.rs b/voxygen/src/render/mesh.rs index 4ddbe65843..cd38e88ae4 100644 --- a/voxygen/src/render/mesh.rs +++ b/voxygen/src/render/mesh.rs @@ -28,6 +28,9 @@ impl Mesh

{ /// Get a slice referencing the vertices of this mesh. pub fn vertices(&self) -> &[P::Vertex] { &self.verts } + /// Get a mutable slice referencing the vertices of this mesh. + pub fn vertices_mut(&mut self) -> &mut [P::Vertex] { &mut self.verts } + /// Push a new vertex onto the end of this mesh. pub fn push(&mut self, vert: P::Vertex) { self.verts.push(vert); } diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index b2af4921f2..3088bcb298 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -24,6 +24,9 @@ gfx_defines! { // can save 32 bits per instance, for particles that are not relatively animated. inst_time: f32 = "inst_time", + // The lifespan in seconds of the particle + inst_lifespan: f32 = "inst_lifespan", + // a seed value for randomness // can save 32 bits per instance, for particles that don't need randomness/uniqueness. inst_entropy: f32 = "inst_entropy", @@ -88,6 +91,7 @@ impl Vertex { } } +#[derive(Copy, Clone)] pub enum ParticleMode { CampfireSmoke = 0, CampfireFire = 1, @@ -98,6 +102,7 @@ pub enum ParticleMode { FireworkPurple = 6, FireworkRed = 7, FireworkYellow = 8, + Leaf = 9, } impl ParticleMode { @@ -105,10 +110,16 @@ impl ParticleMode { } impl Instance { - pub fn new(inst_time: f64, inst_mode: ParticleMode, inst_pos: Vec3) -> Self { + pub fn new( + inst_time: f64, + lifespan: f32, + inst_mode: ParticleMode, + inst_pos: Vec3, + ) -> Self { use rand::Rng; Self { inst_time: inst_time as f32, + inst_lifespan: lifespan, inst_entropy: rand::thread_rng().gen(), inst_mode: inst_mode as i32, inst_pos: inst_pos.into_array(), @@ -117,7 +128,7 @@ impl Instance { } impl Default for Instance { - fn default() -> Self { Self::new(0.0, ParticleMode::CampfireSmoke, Vec3::zero()) } + fn default() -> Self { Self::new(0.0, 0.0, ParticleMode::CampfireSmoke, Vec3::zero()) } } pub struct ParticlePipeline; diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index d69cf98c2c..f01ba9ec65 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -942,7 +942,8 @@ impl Scene { self.figure_mgr.clean(scene_data.tick); // Maintain the particles. - self.particle_mgr.maintain(renderer, &scene_data); + self.particle_mgr + .maintain(renderer, &scene_data, &self.terrain); // Maintain audio self.sfx_mgr.maintain( @@ -1018,12 +1019,12 @@ impl Scene { ); self.lod.render(renderer, global); - // Render particle effects. - self.particle_mgr.render(renderer, scene_data, global, lod); - // Render the skybox. renderer.render_skybox(&self.skybox.model, global, &self.skybox.locals, lod); + // Render particle effects. + self.particle_mgr.render(renderer, scene_data, global, lod); + self.terrain.render_translucent( renderer, global, diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 32f6ce5c49..be555c1468 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -1,4 +1,4 @@ -use super::SceneData; +use super::{terrain::BlocksOfInterest, SceneData, Terrain}; use crate::{ mesh::{greedy::GreedyMesh, Meshable}, render::{ @@ -11,10 +11,14 @@ use common::{ comp::{item::Reagent, object, Body, CharacterState, Pos}, figure::Segment, outcome::Outcome, + spiral::Spiral2d, + state::DeltaTime, + terrain::TerrainChunk, + vol::{RectRasterableVol, SizedVol}, }; use dot_vox::DotVoxData; use hashbrown::HashMap; -use rand::Rng; +use rand::prelude::*; use specs::{Join, WorldExt}; use std::time::Duration; use vek::*; @@ -83,7 +87,12 @@ impl ParticleMgr { } } - pub fn maintain(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { + pub fn maintain( + &mut self, + renderer: &mut Renderer, + scene_data: &SceneData, + terrain: &Terrain, + ) { if scene_data.particles_enabled { // update timings self.scheduler.maintain(scene_data.state.get_time()); @@ -95,6 +104,7 @@ impl ParticleMgr { // add new Particle self.maintain_body_particles(scene_data); self.maintain_boost_particles(scene_data); + self.maintain_block_particles(scene_data, terrain); } else { // remove all particle lifespans self.particles.clear(); @@ -147,7 +157,7 @@ impl ParticleMgr { Duration::from_secs(10), time, ParticleMode::CampfireSmoke, - pos.0, + pos.0.map(|e| e + thread_rng().gen_range(-0.25, 0.25)), )); } } @@ -245,6 +255,69 @@ impl ParticleMgr { } } + #[allow(clippy::same_item_push)] // TODO: Pending review in #587 + fn maintain_block_particles( + &mut self, + scene_data: &SceneData, + terrain: &Terrain, + ) { + let dt = scene_data.state.ecs().fetch::().0; + let time = scene_data.state.get_time(); + let player_pos = scene_data + .state + .read_component_cloned::(scene_data.player_entity) + .unwrap_or_default(); + let player_chunk = player_pos.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { + (e.floor() as i32).div_euclid(sz as i32) + }); + + type BoiFn<'a> = fn(&'a BlocksOfInterest) -> &'a [Vec3]; + // blocks, chunk range, emission density, lifetime, particle mode + // + // - blocks: the function to select the blocks of interest that we should emit + // from + // - chunk range: the range, in chunks, that the particles should be generated + // in from the player + // - emission density: the density, per block per second, of the generated + // particles + // - lifetime: the number of seconds that each particle should live for + // - particle mode: the visual mode of the generated particle + let particles: &[(BoiFn, usize, f32, f32, ParticleMode)] = &[ + (|boi| &boi.leaves, 4, 0.001, 30.0, ParticleMode::Leaf), + (|boi| &boi.embers, 2, 20.0, 0.25, ParticleMode::CampfireFire), + (|boi| &boi.embers, 8, 3.0, 40.0, ParticleMode::CampfireSmoke), + ]; + + let mut rng = thread_rng(); + for (get_blocks, range, rate, dur, mode) in particles.iter() { + 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 = get_blocks(&chunk_data.blocks_of_interest); + + let avg_particles = dt * blocks.len() as f32 * *rate; + let particle_count = avg_particles.trunc() as usize + + (rng.gen::() < avg_particles.fract()) as usize; + + self.particles + .resize_with(self.particles.len() + particle_count, || { + let block_pos = + Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32)) + + blocks.choose(&mut rng).copied().unwrap(); // Can't fail + + Particle::new( + Duration::from_secs_f32(*dur), + time, + *mode, + block_pos.map(|e: i32| e as f32 + rng.gen::()), + ) + }) + }); + } + } + } + fn upload_particles(&mut self, renderer: &mut Renderer) { let all_cpu_instances = self .particles @@ -305,11 +378,16 @@ fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model::generate_mesh( - Segment::from(vox.as_ref()), - &mut greedy, - ) - .0; + let segment = Segment::from(vox.as_ref()); + let segment_size = segment.size(); + let mut mesh = + Meshable::::generate_mesh(segment, &mut greedy).0; + // Center particle vertices around origin + for vert in mesh.vertices_mut() { + vert.pos[0] -= segment_size.x as f32 / 2.0; + vert.pos[1] -= segment_size.y as f32 / 2.0; + vert.pos[2] -= segment_size.z as f32 / 2.0; + } // NOTE: Ignoring coloring / lighting for now. drop(greedy); @@ -399,7 +477,7 @@ impl Particle { fn new(lifespan: Duration, time: f64, mode: ParticleMode, pos: Vec3) -> Self { Particle { alive_until: time + lifespan.as_secs_f64(), - instance: ParticleInstance::new(time, mode, pos), + instance: ParticleInstance::new(time, lifespan.as_secs_f32(), mode, pos), } } } diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index 06ec276b93..47cde9862b 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -1,3 +1,7 @@ +mod watcher; + +pub use self::watcher::BlocksOfInterest; + use crate::{ mesh::{greedy::GreedyMesh, Meshable}, render::{ @@ -35,7 +39,7 @@ enum Visibility { Visible = 2, } -struct TerrainChunkData { +pub struct TerrainChunkData { // GPU data load_time: f32, opaque_model: Model, @@ -43,6 +47,7 @@ struct TerrainChunkData { col_lights: guillotiere::AllocId, sprite_instances: HashMap<(BlockKind, usize), Instances>, locals: Consts, + pub blocks_of_interest: BlocksOfInterest, visible: Visibility, can_shadow_point: bool, @@ -51,6 +56,7 @@ struct TerrainChunkData { frustum_last_plane_index: u8, } +#[derive(Copy, Clone)] struct ChunkMeshState { pos: Vec2, started_tick: u64, @@ -67,6 +73,7 @@ struct MeshWorkerResponse { col_lights_info: ColLightInfo, sprite_instances: HashMap<(BlockKind, usize), Vec>, started_tick: u64, + blocks_of_interest: BlocksOfInterest, } struct SpriteConfig { @@ -392,6 +399,7 @@ fn mesh_worker + RectRasterableVol + ReadVol + Debug>( started_tick: u64, volume: as SampleVol>>::Sample, max_texture_size: u16, + chunk: Arc, range: Aabb, sprite_data: &HashMap<(BlockKind, usize), Vec>, ) -> MeshWorkerResponse { @@ -446,6 +454,7 @@ fn mesh_worker + RectRasterableVol + ReadVol + Debug>( instances }, + blocks_of_interest: BlocksOfInterest::from_chunk(&chunk), started_tick, } } @@ -2575,7 +2584,7 @@ impl Terrain { // Limit ourselves to u16::MAX even if larger textures are supported. let max_texture_size = renderer.max_texture_size(); - for todo in self + for (todo, chunk) in self .mesh_todo .values_mut() .filter(|todo| { @@ -2584,6 +2593,14 @@ impl Terrain { .unwrap_or(true) }) .min_by_key(|todo| todo.active_worker.unwrap_or(todo.started_tick)) + // Find a reference to the actual `TerrainChunk` we're meshing + .and_then(|todo| { + let pos = todo.pos; + Some((todo, scene_data.state + .terrain() + .get_key_arc(pos) + .cloned()?)) + }) { // TODO: find a alternative! if scene_data.thread_pool.queued_jobs() > 0 { @@ -2644,6 +2661,7 @@ impl Terrain { started_tick, volume, max_texture_size, + chunk, aabb, &sprite_data, )); @@ -2737,6 +2755,7 @@ impl Terrain { visible: Visibility::OutOfRange, can_shadow_point: false, can_shadow_sun: false, + blocks_of_interest: response.blocks_of_interest, z_bounds: response.z_bounds, frustum_last_plane_index: 0, }); @@ -2921,6 +2940,10 @@ impl Terrain { ) } + pub fn get(&self, chunk_key: Vec2) -> Option<&TerrainChunkData> { + self.chunks.get(&chunk_key) + } + pub fn chunk_count(&self) -> usize { self.chunks.len() } pub fn visible_chunk_count(&self) -> usize { diff --git a/voxygen/src/scene/terrain/watcher.rs b/voxygen/src/scene/terrain/watcher.rs new file mode 100644 index 0000000000..439462f12a --- /dev/null +++ b/voxygen/src/scene/terrain/watcher.rs @@ -0,0 +1,37 @@ +use common::{ + terrain::{BlockKind, TerrainChunk}, + vol::{IntoVolIterator, RectRasterableVol}, +}; +use rand::prelude::*; +use vek::*; + +pub struct BlocksOfInterest { + pub leaves: Vec>, + pub embers: Vec>, +} + +impl BlocksOfInterest { + pub fn from_chunk(chunk: &TerrainChunk) -> Self { + let mut leaves = Vec::new(); + let mut embers = Vec::new(); + + chunk + .vol_iter( + Vec3::new(0, 0, chunk.get_min_z()), + Vec3::new( + TerrainChunk::RECT_SIZE.x as i32, + TerrainChunk::RECT_SIZE.y as i32, + chunk.get_max_z(), + ), + ) + .for_each(|(pos, block)| { + if block.kind() == BlockKind::Leaves && thread_rng().gen_range(0, 16) == 0 { + leaves.push(pos); + } else if block.kind() == BlockKind::Ember { + embers.push(pos); + } + }); + + Self { leaves, embers } + } +}